Allow "rotating" to a YubiKey private key

This commit is contained in:
Jay Lee
2021-02-14 20:01:14 +00:00
parent 4f664df087
commit 7f0b286d8e
2 changed files with 98 additions and 55 deletions

View File

@@ -7694,32 +7694,14 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size):
return private_pem, publicKeyData return private_pem, publicKeyData
def _formatOAuth2ServiceData(project_id, client_email, client_id, private_key, def _formatOAuth2ServiceData(service_data):
private_key_id): quoted_email = quote(service_data.get('client_email', ''))
quoted_email = quote(client_email) service_data['auth_provider_x509_cert_url'] = 'https://www.googleapis.com/oauth2/v1/certs'
key_json = { service_data['auth_uri'] = 'https://accounts.google.com/o/oauth2/auth'
'auth_provider_x509_cert_url': service_data['client_x509_cert_url'] = f'https://www.googleapis.com/robot/v1/metadata/x509/{quoted_email}'
'https://www.googleapis.com/oauth2/v1/certs', service_data['token_uri'] = 'https://oauth2.googleapis.com/token'
'auth_uri': service_data['type'] = 'service_account'
'https://accounts.google.com/o/oauth2/auth', return json.dumps(service_data, indent=2, sort_keys=True)
'client_email':
client_email,
'client_id':
client_id,
'client_x509_cert_url':
f'https://www.googleapis.com/robot/v1/metadata/x509/{quoted_email}',
'private_key':
private_key,
'private_key_id':
private_key_id,
'project_id':
project_id,
'token_uri':
'https://oauth2.googleapis.com/token',
'type':
'service_account',
}
return json.dumps(key_json, indent=2, sort_keys=True)
def doShowServiceAccountKeys(): def doShowServiceAccountKeys():
@@ -7765,10 +7747,22 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
client_email=None, client_email=None,
client_id=None): client_id=None):
local_key_size = 2048 local_key_size = 2048
mode = 'retainexisting'
body = {} body = {}
if iam: if iam:
mode = 'retainexisting' new_data = {
'client_email': client_email,
'project_id': project_id,
'client_id': client_id,
'key_type': 'default'
}
else: else:
_getSvcAcctData()
# dict() ensures we have a real copy, not pointer
new_data = dict(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
oldPrivateKeyId = new_data.get('private_key_id')
# assume default key type unless we are told otherwise
new_data['key_type'] = 'default'
mode = 'retainnone' mode = 'retainnone'
i = 3 i = 3
iam = buildGAPIServiceObject('iam', None) iam = buildGAPIServiceObject('iam', None)
@@ -7793,38 +7787,59 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
'localkeysize must be 1024, 2048 or 4096. 1024 is weak and dangerous. 2048 is recommended. 4096 is slow.' 'localkeysize must be 1024, 2048 or 4096. 1024 is weak and dangerous. 2048 is recommended. 4096 is slow.'
) )
i += 2 i += 2
elif myarg == 'yubikey':
new_data['key_type'] = 'yubikey'
i += 1
elif myarg == 'yubikeyslot':
new_data['yubikey_slot'] = sys.argv[i+1].upper()
i =+ 2
elif myarg == 'yubikeypin':
new_data['yubikey_pin'] = input('Enter your YubiKey PIN: ')
i += 1
elif myarg == 'yubikeyserialnumber':
new_data['yubikey_serial_number'] = sys.argv[i+1]
i += 2
elif myarg in ['retainnone', 'retainexisting', 'replacecurrent']: elif myarg in ['retainnone', 'retainexisting', 'replacecurrent']:
mode = myarg mode = myarg
i += 1 i += 1
else: else:
controlflow.invalid_argument_exit(myarg, 'gam rotate sakeys') controlflow.invalid_argument_exit(myarg, 'gam rotate sakeys')
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA][ sa_name = f'projects/-/serviceAccounts/{new_data["client_id"]}'
'private_key_id'] if new_data.get('key_type') == 'yubikey':
project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id'] # Use yubikey private key
client_email = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_email'] new_data['yubikey_key_type'] = f'RSA{local_key_size}'
client_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['client_id'] new_data.pop('private_key', None)
clientId = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] yk = yubikey.YubiKey(new_data)
name = f'projects/-/serviceAccounts/{clientId}' publicKeyData = yk.get_certificate()
if mode != 'retainexisting': elif local_key_size:
keys = gapi.get_items(iam.projects().serviceAccounts().keys(), # Generate private key locally, store in file
'list', new_data['private_key'], publicKeyData = _generatePrivateKeyAndPublicCert(
'keys', sa_name, local_key_size)
name=name, new_data['key_type'] = 'default'
keyTypes='USER_MANAGED') for key in list(new_data):
if key.startswith('yubikey_'):
new_data.pop(key, None)
if local_key_size: if local_key_size:
private_key, publicKeyData = _generatePrivateKeyAndPublicCert( # Upload public cert for yubikey or local generated
name, local_key_size)
print(' Uploading new public certificate to Google...') print(' Uploading new public certificate to Google...')
throw_reasons = [
gapi_errors.ErrorReason.FOUR_O_O,
gapi_errors.ErrorReason.NOT_FOUND
]
max_retries = 10 max_retries = 10
for i in range(1, max_retries + 1): for i in range(1, max_retries + 1):
try: try:
result = gapi.call( result = gapi.call(
iam.projects().serviceAccounts().keys(), iam.projects().serviceAccounts().keys(),
'upload', 'upload',
throw_reasons=[gapi_errors.ErrorReason.NOT_FOUND], throw_reasons=throw_reasons,
name=name, name=sa_name,
body={'publicKeyData': publicKeyData}) body={'publicKeyData': publicKeyData})
break break
except googleapiclient.errors.HttpError:
print('WARNING: that key already exists.')
result = {'name': oldPrivateKeyId}
break
except gapi_errors.GapiNotFoundError as e: except gapi_errors.GapiNotFoundError as e:
if i == max_retries: if i == max_retries:
raise e raise e
@@ -7834,31 +7849,36 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
f'Waiting for Service Account creation to complete. Sleeping {sleep_time} seconds\n' f'Waiting for Service Account creation to complete. Sleeping {sleep_time} seconds\n'
) )
time.sleep(sleep_time) time.sleep(sleep_time)
private_key_id = result['name'].rsplit('/', 1)[-1] newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
oauth2service_data = _formatOAuth2ServiceData(project_id, client_email, new_data['private_key_id'] = newPrivateKeyId
client_id, private_key, new_data_str = _formatOAuth2ServiceData(new_data)
private_key_id)
else: else:
# Ask Google to generate private key, store locally
result = gapi.call(iam.projects().serviceAccounts().keys(), result = gapi.call(iam.projects().serviceAccounts().keys(),
'create', 'create',
name=name, name=name,
body=body) body=body)
oauth2service_data = base64.b64decode( new_data_str = base64.b64decode(
result['privateKeyData']).decode(UTF8) result['privateKeyData']).decode(UTF8)
private_key_id = result['name'].rsplit('/', 1)[-1] newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON], fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON],
oauth2service_data, new_data_str,
continue_on_error=False) continue_on_error=False)
print( print(
f' Wrote new private key {private_key_id} to {GC_Values[GC_OAUTH2SERVICE_JSON]}' f' Wrote new service account data for {newPrivateKeyId} to {GC_Values[GC_OAUTH2SERVICE_JSON]}'
) )
if mode != 'retainexisting': if mode != 'retainexisting':
keys = gapi.get_items(iam.projects().serviceAccounts().keys(),
'list',
'keys',
name=sa_name,
keyTypes='USER_MANAGED')
count = len(keys) if mode == 'retainnone' else 1 count = len(keys) if mode == 'retainnone' else 1
print( print(
f' Revoking {count} existing key(s) for Service Account {clientId}') f' Revoking {count} existing key(s) for Service Account {new_data["client_id"]}')
for key in keys: for key in keys:
keyName = key['name'].rsplit('/', 1)[-1] keyName = key['name'].rsplit('/', 1)[-1]
if mode == 'retainnone' or keyName == currentPrivateKeyId: if (mode == 'retainnone' or keyName == oldPrivateKeyId) and keyName != newPrivateKeyId:
print(f' Revoking existing key {keyName} for service account') print(f' Revoking existing key {keyName} for service account')
gapi.call(iam.projects().serviceAccounts().keys(), gapi.call(iam.projects().serviceAccounts().keys(),
'delete', 'delete',

View File

@@ -1,7 +1,8 @@
from base64 import b64encode
import sys import sys
from threading import Timer from threading import Timer
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
from ykman.device import connect_to_device from ykman.device import connect_to_device
from yubikit.piv import KEY_TYPE, SLOT, InvalidPinError, PivSession from yubikit.piv import KEY_TYPE, SLOT, InvalidPinError, PivSession
@@ -25,6 +26,28 @@ class YubiKey():
self.pin = service_account_info.get('yubikey_pin') self.pin = service_account_info.get('yubikey_pin')
self.key_id = service_account_info.get('private_key_id') self.key_id = service_account_info.get('private_key_id')
def get_certificate(self):
try:
conn, _, _ = connect_to_device(self.serial_number)
session = PivSession(conn)
if self.pin:
try:
session.verify_pin(self.pin)
except InvalidPinError as err:
controlflow.system_error_exit(7, f'YubiKey - {err}')
try:
cert = session.get_certificate(self.slot)
cert_pem = cert.public_bytes(
serialization.Encoding.PEM).decode()
publicKeyData = b64encode(cert_pem.encode())
if isinstance(publicKeyData, bytes):
publicKeyData = publicKeyData.decode()
return publicKeyData
except ApduError as err:
controlflow.system_error_exit(8, f'YubiKey - {err}')
except ValueError as err:
controlflow.system_error_exit(9, f'YubiKey - {err}')
def sign(self, message): def sign(self, message):
if 'mplock' in globals(): if 'mplock' in globals():
mplock.acquire() mplock.acquire()