Support for YubiKey private key storage

This commit is contained in:
Jay Lee
2021-02-11 16:38:19 +00:00
parent bf4a6e6cde
commit f74168e2c7
4 changed files with 92 additions and 8 deletions

View File

@@ -27,6 +27,7 @@ import webbrowser
import zipfile import zipfile
import http.client as http_client import http.client as http_client
from multiprocessing import Pool as mp_pool from multiprocessing import Pool as mp_pool
from multiprocessing import Lock as mp_lock
from urllib.parse import quote, urlencode, urlparse from urllib.parse import quote, urlencode, urlparse
import dateutil.parser import dateutil.parser
@@ -45,6 +46,7 @@ from cryptography.x509.oid import NameOID
import gam.auth.oauth import gam.auth.oauth
from gam import auth from gam import auth
from gam.auth import yubikey
from gam import controlflow from gam import controlflow
from gam import display from gam import display
from gam import fileutils from gam import fileutils
@@ -822,8 +824,14 @@ def _getSvcAcctData():
def getSvcAcctCredentials(scopes, act_as): def getSvcAcctCredentials(scopes, act_as):
try: try:
_getSvcAcctData() _getSvcAcctData()
sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if sign_method == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info( credentials = google.oauth2.service_account.Credentials.from_service_account_info(
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]) GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
elif sign_method == 'yubikey':
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = credentials.with_scopes(scopes) credentials = credentials.with_scopes(scopes)
if act_as: if act_as:
credentials = credentials.with_subject(act_as) credentials = credentials.with_subject(act_as)
@@ -10849,7 +10857,9 @@ Append an 'r' to grant read-only access or an 'a' to grant action-only access.
) )
def init_gam_worker(): def init_gam_worker(l):
global mplock
mplock = l
signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN)
@@ -10857,7 +10867,8 @@ def run_batch(items):
if not items: if not items:
return return
num_worker_threads = min(len(items), GC_Values[GC_NUM_THREADS]) num_worker_threads = min(len(items), GC_Values[GC_NUM_THREADS])
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200) l = mp_lock()
pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(l,))
sys.stderr.write(f'Using {num_worker_threads} processes...\n') sys.stderr.write(f'Using {num_worker_threads} processes...\n')
try: try:
results = [] results = []
@@ -10868,7 +10879,7 @@ def run_batch(items):
) )
pool.close() pool.close()
pool.join() pool.join()
pool = mp_pool(num_worker_threads, init_gam_worker) pool = mp_pool(num_worker_threads, init_gam_worker, maxtasksperchild=200, initargs=(1,))
sys.stderr.write( sys.stderr.write(
'commit-batch - running processes finished, proceeding\n') 'commit-batch - running processes finished, proceeding\n')
continue continue

View File

@@ -5,7 +5,9 @@ import os
from google.auth.jwt import Credentials as JWTCredentials from google.auth.jwt import Credentials as JWTCredentials
import gam
from gam.auth import oauth from gam.auth import oauth
from gam.auth import yubikey
from gam.var import _FN_OAUTH2_TXT from gam.var import _FN_OAUTH2_TXT
from gam.var import _FN_OAUTH2SERVICE_JSON from gam.var import _FN_OAUTH2SERVICE_JSON
from gam.var import GC_OAUTH2_TXT from gam.var import GC_OAUTH2_TXT
@@ -36,10 +38,17 @@ def get_admin_credentials(api=None):
with open(credential_file, 'r') as f: with open(credential_file, 'r') as f:
creds_data = json.load(f) creds_data = json.load(f)
# Validate that enable DASA matches content of authorization file # Validate that enable DASA matches content of authorization file
if GC_Values[GC_ENABLE_DASA] and 'private_key' in creds_data: if GC_Values[GC_ENABLE_DASA] and 'private_key_id' in creds_data:
audience = f'https://{api}.googleapis.com/' audience = f'https://{api}.googleapis.com/'
key_type = creds_data.get('key_type', 'default')
if key_type == 'default':
return JWTCredentials.from_service_account_info(creds_data, return JWTCredentials.from_service_account_info(creds_data,
audience=audience) audience=audience)
elif key_type == 'yubikey':
yksigner = yubikey.YubiKey(creds_data)
return JWTCredentials._from_signer_and_info(yksigner,
creds_data,
audience=audience)
elif not GC_Values[GC_ENABLE_DASA] and 'token' in creds_data: elif not GC_Values[GC_ENABLE_DASA] and 'token' in creds_data:
return oauth.Credentials.from_credentials_file(credential_file) return oauth.Credentials.from_credentials_file(credential_file)
else: else:

63
src/gam/auth/yubikey.py Normal file
View File

@@ -0,0 +1,63 @@
import sys
from threading import Timer
# hack to avoid ImportError on unneccessary libraries
class fake_open():
open_devices = None
sys.modules['ykman.driver_otp'] = fake_open
import ykman.descriptor
from ykman.piv import SLOT, ALGO, PivController, DEFAULT_MANAGEMENT_KEY
from ykman.util import TRANSPORT
from gam import controlflow
class YubiKey():
def __init__(self, service_account_info):
algo = service_account_info.get('yubikey_algo', 'RSA2048')
try:
self.algo = getattr(ALGO, algo.upper())
except AttributeError:
controlflow.system_error_exit(6, f'{algo} is not a valid value for yubikey_algo')
slot = service_account_info.get('yubikey_slot', 'AUTHENTICATION')
try:
self.slot = getattr(SLOT, slot.upper())
except AttributeError:
controlflow.system_error_exit(6, f'{slot} is not a valid value for yubikey_slot')
self.pin = service_account_info.get('yubikey_pin')
self.key_id = service_account_info.get('private_key_id')
def touch_callback(self):
sys.stderr.write('\nTouch your YubiKey...\n')
def sign(self, message):
timer = Timer(0.5, self.touch_callback)
if 'mplock' in globals():
mplock.acquire()
try:
with ykman.descriptor.open_device(transports=TRANSPORT.CCID) as yk:
controller = PivController(yk.driver)
if self.pin:
controller.verify(self.pin)
timer.start() # if sign() takes more than .5 sec we need touch
try:
signed = controller.sign(self.slot, self.algo, message)
except ykman.driver_ccid.APDUError: # We need PIN
timer.cancel() # reset timer while user enters PIN
sys.stderr.write('\nEnter your YubiKey PIN:\n')
self.pin = input()
timer = Timer(0.5, self.touch_callback)
controller.verify(self.pin)
timer.start()
signed = controller.sign(self.slot, self.algo, message)
timer.cancel()
except ykman.descriptor.FailedOpeningDeviceException:
controlflow.system_error_exit(5, 'No YubiKey found. Is it plugged in?')
except ykman.piv.WrongPin:
controlflow.system_error_exit(7, 'Wrong PIN for YubiKey.')
except ykman.piv.AuthenticationBlocked:
controlflow.system_error_exit(8, 'YubiKey PIN is blocked.')
if 'mplock' in globals():
mplock.release()
return signed

View File

@@ -8,3 +8,4 @@ google-auth>=1.11.2
httplib2>=0.17.0 httplib2>=0.17.0
passlib>=1.7.2; sys_platform == 'win32' passlib>=1.7.2; sys_platform == 'win32'
python-dateutil python-dateutil
yubikey-manager