def __init__(self, ui, okta_org_url, challenge, credential_id=None, timeout_ms=30_000): """ :param okta_org_url: Base URL string for Okta IDP. :param challenge: Challenge :param credential_id: FIDO credential ID """ self.ui = ui self._okta_org_url = okta_org_url self._clients = None self._has_prompted = False self._challenge = websafe_decode(challenge) self._timeout_ms = timeout_ms self._event = Event() self._assertions = None self._client_data = None self._rp = {'id': okta_org_url[8:], 'name': okta_org_url[8:]} if credential_id: self._allow_list = [ PublicKeyCredentialDescriptor( PublicKeyCredentialType.PUBLIC_KEY, websafe_decode(credential_id)) ]
def _get_assertion_request( rp_id: str = 'pasten.com', client_data: Optional[ClientData] = None, allowed_cred_ids: Optional[List[bytes]] = None, user_verification_required=False, ) -> dict: client_data = client_data or ClientData.build( typ=WEBAUTHN_TYPE.GET_ASSERTION, origin=rp_id, challenge=websafe_encode(b'pasten-challenge'), ) req = { CtapGetAssertionRequest.RP_ID_KEY: rp_id, CtapGetAssertionRequest.CLIENT_DATA_HASH_KEY: client_data.hash, } if user_verification_required: req[CtapGetAssertionRequest.OPTIONS_KEY] = { CtapOptions.USER_VERIFICATION: True } if allowed_cred_ids: req[CtapGetAssertionRequest.ALLOW_LIST_KEY] = [ PublicKeyCredentialDescriptor( PublicKeyCredentialType.PUBLIC_KEY, cred_id ) for cred_id in allowed_cred_ids ] return req
def cred(rp, user, credset=None, attset=None, prompt=None): assert set(['id', 'name']) <= set(rp.keys()) <= set(['id', 'name', 'icon']) assert set(['id', 'name']) <= set(user.keys()) assert set(user.keys()) <= set(['id', 'name', 'icon', 'display_name']) assert isinstance(user['id'], bytes) credset_dict = {} if credset is None else credset_decode(credset) attset_dict = {} if attset is None else attset_decode(attset) server = cred_server(rp) challenge = cred_challenge(rp, user) create_options, state = server.register_begin( user, credentials=[ PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=credential_id, ) for credential_id in sorted(credset_dict.keys()) ], challenge=challenge, ) lock = threading.Lock() prompted = [False] def on_keepalive(status): if status == STATUS.UPNEEDED: done = False with lock: done = prompted[0] prompted[0] = True if not done: prompt() def per_device(dev, cancel_ev=None): client = Fido2Client(dev, fidosig_origin(rp['id']), verify_origin) return client.make_credential( create_options['publicKey'], on_keepalive=on_keepalive if prompt is not None else None, **({} if cancel_ev is None else { 'event': cancel_ev })) attestation_object, client_data = iterdevs(per_device) auth_data = server.register_complete(state, client_data, attestation_object) credential_id = auth_data.credential_data.credential_id public_key = auth_data.credential_data.public_key # XXX Check rather than assert? Overwrite? if credential_id in credset_dict or credential_id in attset_dict: raise Exception('Duplicate credential id') credset_dict[credential_id] = public_key attset_dict[credential_id] = attestation_object return credset_encode(credset_dict), attset_encode(attset_dict)
def create(cls, get_assertion_req: dict): # noinspection PyProtectedMember return CtapGetAssertionRequest( rp_id=get_assertion_req.get(cls.RP_ID_KEY), client_data_hash=get_assertion_req.get(cls.CLIENT_DATA_HASH_KEY), allow_list=PublicKeyCredentialDescriptor._wrap_list( get_assertion_req.get(cls.ALLOW_LIST_KEY)), extensions=get_assertion_req.get(cls.EXTENSIONS_KEY), options=get_assertion_req.get(cls.OPTIONS_KEY), pin_auth=get_assertion_req.get(cls.PIN_AUTH_KEY), pin_protocol=get_assertion_req.get(cls.PIN_PROTOCOL_KEY), )
def test_descriptor(self): o = PublicKeyCredentialDescriptor("public-key", b"credential_id") self.assertEqual(o, {"type": "public-key", "id": b"credential_id"}) self.assertEqual(o.type, "public-key") self.assertEqual(o.id, b"credential_id") self.assertIsNone(o.transports) o = PublicKeyCredentialDescriptor( "public-key", b"credential_id", ["usb", "nfc"] ) self.assertEqual( o, { "type": "public-key", "id": b"credential_id", "transports": ["usb", "nfc"], }, ) self.assertEqual(o.transports, ["usb", "nfc"]) PublicKeyCredentialDescriptor("public-key", b"credential_id", ["valid_value"]) with self.assertRaises(ValueError): PublicKeyCredentialDescriptor("wrong-type", b"credential_id") with self.assertRaises(TypeError): PublicKeyCredentialDescriptor("wrong-type") with self.assertRaises(TypeError): PublicKeyCredentialDescriptor()
def softsign(softkey, rp, credset, msg, sigset=None, header=None, randomization=None, prompt=None): seed = softkey_decode(softkey) credset_dict = credset_decode(credset) sigset_dict = {} if sigset is None else sigset_decode(sigset) if header is None: header = b'' assert isinstance(header, bytes) # bytes, not Unicode text if randomization is None: randomization = os.urandom(24) credential_ids = [ credential_id for credential_id in sorted(credset_dict.keys()) if softkey_valid_credential_id(seed, credential_id) ] if len(credential_ids) == 0: raise FileNotFoundError server = sign_server(rp) challenge = sign_challenge(randomization, header, msg) descriptors = [ PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=credential_id, ) for credential_id in credential_ids ] request_options, state = server.authenticate_begin( credentials=descriptors, user_verification=UserVerificationRequirement.DISCOURAGED, challenge=challenge, ) assertions, client_data = \ _get_assertions(seed, rp, request_options['publicKey']) assert len(assertions) == len(credential_ids) for credential_id, assertion in zip(credential_ids, assertions): # XXX Warn if we're overwriting? sigset_dict[credential_id] = { SIGENTRY.RANDOMIZATION: randomization, SIGENTRY.AUTH_DATA: assertion.auth_data, SIGENTRY.SIGNATURE: assertion.signature, } return sigset_encode(sigset_dict)
def derivekey(pkconfs, rp, user, prompt=None): server = _fidokdf_server(rp) challenge = os.urandom(32) descriptors = [ PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=credential_id, ) for credential_id in sorted(pkconfs.keys()) ] request_options, state = server.authenticate_begin( credentials=descriptors, user_verification=UserVerificationRequirement.DISCOURAGED, challenge=challenge, ) lock = threading.Lock() prompted = [False] def on_keepalive(status): if status == STATUS.UPNEEDED: done = False with lock: done = prompted[0] prompted[0] = True if not done: prompt() def per_device(dev, cancel_ev=None): client = Fido2Client(dev, fidokdf_origin(rp['id']), verify_origin) return client.get_assertion( request_options['publicKey'], on_keepalive=on_keepalive if prompt is not None else None, **({} if cancel_ev is None else { 'event': cancel_ev })) assertions, client_data = iterdevs(per_device) return { assertion.credential['id']: server.authenticate_complete_kdf( state, pkconfs, assertion.credential['id'], client_data, assertion.auth_data, assertion.signature, ) for assertion in assertions }
def encrypt(payload, rp, user, exclude_credential_ids=set(), prompt=None): server = _fidocrypt_server(rp) challenge = os.urandom(32) create_options, state = server.register_begin( user, credentials=[ PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=credential_id, ) for credential_id in sorted(exclude_credential_ids) ], challenge=challenge, ) lock = threading.Lock() prompted = [False] def on_keepalive(status): if status == STATUS.UPNEEDED: done = False with lock: done = prompted[0] prompted[0] = True if not done: prompt() def per_device(dev, cancel_ev=None): client = Fido2Client(dev, fidocrypt_origin(rp['id']), verify_origin) return client.make_credential( create_options['publicKey'], on_keepalive=on_keepalive if prompt is not None else None, **({} if cancel_ev is None else { 'event': cancel_ev })) attestation_object, client_data = iterdevs(per_device) # Strip out the device attestation for privacy. attestation_object = AttestationObject.create( NoneAttestation.FORMAT, attestation_object.auth_data, {}, ) return server.register_complete_encrypt(state, payload, client_data, attestation_object)
def _get_assertion(self, request: CtapGetAssertionRequest, ctx: CtapGetNextAssertionContext) -> AssertionResponse: cred = ctx.get_next_cred() authenticator_data = self._make_authenticator_data( request.rp_id, attested_credential_data=None) signature = self._generate_signature(authenticator_data, request.client_data_hash, cred.private_key) response = AssertionResponse.create( PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, cred.id), authenticator_data, signature, user=PublicKeyCredentialUserEntity(cred.user_id, name=''), n_creds=len(ctx.creds), ) return response
def _get_make_credential_request( algorithm: int = cose.RS256.ALGORITHM, user_id: str = '*****@*****.**', user_name: str = 'pasten', rp_id: str = 'pasten.com', rp_name: str = 'Pasten LTD', client_data: Optional[ClientData] = None, excluded_cred_ids: Optional[List[bytes]] = None, ) -> dict: client_data = client_data or ClientData.build( typ=WEBAUTHN_TYPE.MAKE_CREDENTIAL, origin=rp_id, challenge=websafe_encode(b'pasten-challenge'), ) req = { CtapMakeCredentialRequest.CLIENT_DATA_HASH_KEY: hashlib.sha256( client_data ).digest(), CtapMakeCredentialRequest.RP_KEY: PublicKeyCredentialRpEntity( rp_id, rp_name ), CtapMakeCredentialRequest.USER_KEY: PublicKeyCredentialUserEntity( user_id, user_name ), CtapMakeCredentialRequest.PUBLIC_KEY_CREDENTIAL_PARAMS_KEY: [ PublicKeyCredentialParameters( PublicKeyCredentialType.PUBLIC_KEY, algorithm ) ], } if excluded_cred_ids: req[CtapMakeCredentialRequest.EXCLUDE_LIST_KEY] = [ PublicKeyCredentialDescriptor( PublicKeyCredentialType.PUBLIC_KEY, cred_id ) for cred_id in excluded_cred_ids ] return req
def __init__( self, challenge, timeout=None, rp_id=None, allow_credentials=None, user_verification=None, extensions=None, user_presence=None, ): super(PublicKeyCredentialRequestOptions, self).__init__( challenge=challenge, timeout=timeout, rp_id=rp_id, allow_credentials=PublicKeyCredentialDescriptor._wrap_list( allow_credentials), user_verification=UserVerificationRequirement._wrap( user_verification), extensions=extensions, user_presence=UserPresenceRequirement._wrap(user_presence))
def create(cls, make_credential_request: dict): # noinspection PyProtectedMember return CtapMakeCredentialRequest( client_data_hash=make_credential_request.get( cls.CLIENT_DATA_HASH_KEY), rp=PublicKeyCredentialRpEntity._wrap( make_credential_request.get(cls.RP_KEY)), user=PublicKeyCredentialUserEntity._wrap( make_credential_request.get(cls.USER_KEY)), public_key_credential_params=PublicKeyCredentialParameters. _wrap_list( make_credential_request.get( cls.PUBLIC_KEY_CREDENTIAL_PARAMS_KEY)), exclude_list=PublicKeyCredentialDescriptor._wrap_list( make_credential_request.get(cls.EXCLUDE_LIST_KEY)), extensions=make_credential_request.get(cls.EXTENSIONS_KEY), options=make_credential_request.get(cls.OPTIONS_KEY), pin_auth=make_credential_request.get(cls.PIN_AUTH_KEY), pin_protocol=make_credential_request.get(cls.PIN_PROTOCOL_KEY), )
def test_cryptserver_register(): server = Fido2CryptServer( RP, verify_origin=rp_origin_verifier(RP['id']), ) create_options, state = server.register_begin( USER, credentials=[PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=CREDENTIAL_ID, )], challenge=REGISTRATION['challenge'], ) registered_credentials = server.register_complete_encrypt( state, PAYLOAD, REGISTRATION['client_data'], REGISTRATION['attestation_object'], ) assert registered_credentials[CREDENTIAL_ID] == CIPHERTEXT
def test_cryptserver_authenticate(): server = Fido2CryptServer( RP, verify_origin=rp_origin_verifier(RP['id']), ) request_options, state = server.authenticate_begin( credentials=[PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=CREDENTIAL_ID, )], user_verification=UserVerificationRequirement.DISCOURAGED, challenge=AUTHENTICATION['challenge'], ) payload = server.authenticate_complete_decrypt( state, {CREDENTIAL_ID: CIPHERTEXT}, CREDENTIAL_ID, AUTHENTICATION['client_data'], AUTHENTICATION['auth_data'], AUTHENTICATION['signature'], ) assert payload == PAYLOAD
def test_kdfserver_register(): server = Fido2KDFServer( RP, verify_origin=rp_origin_verifier(RP['id']), ) create_options, state = server.register_begin( USER, credentials=[ PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=CREDENTIAL_ID, ) ], challenge=REGISTRATION['challenge'], ) registered_credentials, key = server.register_complete_kdf( state, REGISTRATION['client_data'], REGISTRATION['attestation_object'], ) assert registered_credentials == {CREDENTIAL_ID: PKCONF} assert key == KEY
def test_kdfserver_authenticate(): server = Fido2KDFServer( RP, verify_origin=rp_origin_verifier(RP['id']), ) request_options, state = server.authenticate_begin( credentials=[ PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=CREDENTIAL_ID, ) ], user_verification=UserVerificationRequirement.DISCOURAGED, challenge=AUTHENTICATION['challenge'], ) key = server.authenticate_complete_kdf( state, {CREDENTIAL_ID: PKCONF}, CREDENTIAL_ID, AUTHENTICATION['client_data'], AUTHENTICATION['auth_data'], AUTHENTICATION['signature'], ) assert key == KEY
def sign(rp, credset, msg, sigset=None, header=None, randomization=None, prompt=None): credset_dict = credset_decode(credset) sigset_dict = {} if sigset is None else sigset_decode(sigset) if header is None: header = b'' assert isinstance(header, bytes) # bytes, not Unicode text if randomization is None: randomization = os.urandom(24) server = sign_server(rp) challenge = sign_challenge(randomization, header, msg) descriptors = [ PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=credential_id, ) for credential_id in sorted(credset_dict.keys()) ] request_options, state = server.authenticate_begin( credentials=descriptors, user_verification=UserVerificationRequirement.DISCOURAGED, challenge=challenge, ) lock = threading.Lock() prompted = [False] def on_keepalive(status): if status == STATUS.UPNEEDED: done = False with lock: done = prompted[0] prompted[0] = True if not done: prompt() def per_device(dev, cancel_ev=None): client = Fido2Client(dev, fidosig_origin(rp['id']), verify_origin) return client.get_assertion( request_options['publicKey'], on_keepalive=on_keepalive if prompt is not None else None, **({} if cancel_ev is None else { 'event': cancel_ev })) assertions, client_data = iterdevs(per_device) assert len(assertions) >= 1 for assertion in assertions: credential_id = assertion.credential['id'] auth_data = assertion.auth_data signature = assertion.signature # Verify the signature before we return it. if credential_id not in credset_dict: raise Exception('unknown credential') credentials = [ AttestedCredentialData.create( b'\0' * 16, credential_id, credset_dict[credential_id], ) ] server.authenticate_complete( state, credentials, credential_id, client_data, auth_data, signature, ) # XXX Warn if we're overwriting? sigset_dict[credential_id] = { SIGENTRY.RANDOMIZATION: randomization, SIGENTRY.AUTH_DATA: assertions[0].auth_data, SIGENTRY.SIGNATURE: assertions[0].signature, } return sigset_encode(sigset_dict)
def sign_request(public_key, authn_select): """Signs a WebAuthn challenge and returns the data. :param public_key dict containing `rpId` the relying party and `challenge` the received challenge :param authn_select string, that contains the allowed public key of the user :return dict containing clientDataJSON, authenticatorData, signature, credentialId and userHandle if available. """ use_prompt = False pin = None uv = "discouraged" if WindowsClient.is_available( ) and not ctypes.windll.shell32.IsUserAnAdmin(): # Use the Windows WebAuthn API if available, and we're not running as admin client = WindowsClient("https://example.com") else: dev = next(CtapHidDevice.list_devices(), None) if dev is not None: print("Use USB HID channel.") use_prompt = True else: try: from fido2.pcsc import CtapPcscDevice dev = next(CtapPcscDevice.list_devices(), None) print("Use NFC channel.") except Exception as e: print("NFC channel search error:", e) if not dev: print("No FIDO device found") sys.exit(1) client = Fido2Client(dev, "http://localhost:8080", verify=lambda x, y: True) # Prefer UV if supported if client.info.options.get("uv"): uv = "preferred" print("Authenticator supports User Verification") elif client.info.options.get("clientPin"): # Prompt for PIN if needed pin = getpass("Please enter PIN: ") else: print("PIN not set, won't use") # the base64 library does not work when padding is missing, so append some allowed_key = base64.urlsafe_b64decode(authn_select + '===') pubKey = PublicKeyCredentialRequestOptions( public_key['challenge'], rp_id=public_key['rpId'], allow_credentials=[ PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY, allowed_key) ]) # Authenticate the credential if use_prompt: print("\nTouch your authenticator device now...\n") # Only one cred in allowCredentials, only one response. result = client.get_assertion(pubKey, pin=pin).get_response(0) data = { "clientDataJSON": b64encode(result.client_data), "authenticatorData": b64encode(result.authenticator_data), "signature": b64encode(result.signature), "credentialId": b64encode(result.credential_id), } if result.user_handle: data['userHandle'] = b64encode(result.user_handle) return data