async def add_resident_credential( ctx: wire.Context, msg: WebAuthnAddResidentCredential ) -> Success: if not msg.credential_id: raise wire.ProcessError("Missing credential ID parameter.") try: cred = Fido2Credential.from_cred_id(bytes(msg.credential_id), None) except Exception: text = Text("Import credential", ui.ICON_WRONG, ui.RED) text.normal( "The credential you are", "trying to import does", "not belong to this", "authenticator.", ) await require_confirm(ctx, text, confirm=None, cancel="Close") raise wire.ActionCancelled("Cancelled") from None content = ConfirmContent(ConfirmAddCredential(cred)) await require_confirm(ctx, content) if store_resident_credential(cred): return Success(message="Credential added") else: raise wire.ProcessError("Internal credential storage is full.")
def store_resident_credential(cred: Fido2Credential) -> bool: slot = None for i in range( _RESIDENT_CREDENTIAL_START_KEY, _RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS, ): stored_cred_data = common._get(common._APP_FIDO2, i) if stored_cred_data is None: if slot is None: slot = i continue stored_rp_id_hash = stored_cred_data[:32] stored_cred_id = stored_cred_data[32:] if cred.rp_id_hash != stored_rp_id_hash: # Stored credential is not for this RP ID. continue stored_cred = Fido2Credential.from_cred_id(stored_cred_id, stored_rp_id_hash) if stored_cred is None: # Stored credential is not for this RP ID. continue # If a credential for the same RP ID and user ID already exists, then overwrite it. if stored_cred.user_id == cred.user_id: slot = i break if slot is None: return False common._set(common._APP_FIDO2, slot, cred.rp_id_hash + cred.id) return True
def get_resident_credential( index: int, rp_id_hash: Optional[bytes] = None) -> Optional[Credential]: if not (0 <= index < _MAX_RESIDENT_CREDENTIALS): return None stored_cred_data = common.get(common.APP_WEBAUTHN, index + _RESIDENT_CREDENTIAL_START_KEY) if stored_cred_data is None: return None stored_rp_id_hash = stored_cred_data[:32] stored_cred_id = stored_cred_data[32:] if rp_id_hash is not None and rp_id_hash != stored_rp_id_hash: # Stored credential is not for this RP ID. return None stored_cred = Fido2Credential.from_cred_id(stored_cred_id, stored_rp_id_hash) if stored_cred is None: return None stored_cred.index = index return stored_cred
def test_fido2_credential_decode(self): mnemonic_secret = b"all all all all all all all all all all all all" mnemonic.get_secret = lambda: mnemonic_secret storage.is_initialized = lambda: True cred_id = ( b"f1d0020013e65c865634ad8abddf7a66df56ae7d8c3afd356f76426801508b2e" b"579bcb3496fe6396a6002e3cd6d80f6359dfa9961e24c544bfc2f26acec1b8d8" b"78ba56727e1f6a7b5176c607552aea63a5abe5d826d69fab3063edfa0201d9a5" b"1013d69eddb2eff37acdd5963f" ) rp_id = "example.com" rp_id_hash = sha256(rp_id).digest() user_id = ( b"3082019330820138a0030201023082019330820138a003020102308201933082" ) user_name = "*****@*****.**" creation_time = 2 public_key = ( b"0451f0d4c307bc737c90ac605c6279f7d01e451798aa7b74df550fdb43a7760c" b"7c02b5107fef42094d00f52a9b1e90afb90e1b9decbf15a6f13d4f882de857e2" b"f4" ) cred_random = ( b"36a9b5d71c13ed54594474b54073af1fb03ea91cd056588909dae43ae2f35dbf" ) # Load credential. cred = Fido2Credential.from_cred_id(unhexlify(cred_id), rp_id_hash) self.assertIsNotNone(cred) # Check credential data. self.assertEqual(hexlify(cred.id), cred_id) self.assertEqual(cred.rp_id, rp_id) self.assertEqual(cred.rp_id_hash, rp_id_hash) self.assertEqual(hexlify(cred.user_id), user_id) self.assertEqual(cred.user_name, user_name) self.assertEqual(cred.creation_time, creation_time) self.assertTrue(cred.hmac_secret) self.assertIsNone(cred.rp_name) self.assertIsNone(cred.user_display_name) # Check credential keys. self.assertEqual(hexlify(cred.hmac_secret_key()), cred_random) cred_public_key = nist256p1.publickey(cred.private_key(), False) self.assertEqual(hexlify(cred_public_key), public_key)
def test_truncation(self): cred = Fido2Credential() cred.truncate_names() self.assertIsNone(cred.rp_name) self.assertIsNone(cred.user_name) self.assertIsNone(cred.user_display_name) cred.rp_name = "a" * (NAME_MAX_LENGTH - 2) + "\u0123" cred.user_name = "a" * (NAME_MAX_LENGTH - 1) + "\u0123" cred.user_display_name = "a" * NAME_MAX_LENGTH + "\u0123" cred.truncate_names() self.assertEqual(cred.rp_name, "a" * (NAME_MAX_LENGTH - 2) + "\u0123") self.assertEqual(cred.user_name, "a" * (NAME_MAX_LENGTH - 1)) self.assertEqual(cred.user_display_name, "a" * NAME_MAX_LENGTH)
def test_allow_list_processing(self): a1 = Fido2Credential() a1.user_id = b"user-a" a1.user_name = "user-a" a1.creation_time = 1 a2 = Fido2Credential() a2.user_id = b"user-a" a2.user_display_name = "User A" a2.creation_time = 3 a3 = Fido2Credential() a3.user_id = b"user-a" a3.user_name = "User A" a3.creation_time = 4 b1 = Fido2Credential() b1.user_id = b"user-b" b1.creation_time = 2 b2 = Fido2Credential() b2.user_id = b"user-b" b2.creation_time = 5 b3 = Fido2Credential() b3.user_id = b"user-b" b3.creation_time = 5 c1 = U2fCredential() c2 = U2fCredential() self.assertEqual( sorted(distinguishable_cred_list([a1, a2, a3, b1, b2, c1, c2])), [b2, a3, a1, c1]) self.assertEqual( sorted(distinguishable_cred_list([c2, c1, b2, b1, a3, a2, a1])), [b2, a3, a1, c2]) # Test input by creation time. self.assertEqual( sorted(distinguishable_cred_list([b2, a3, c1, a2, b1, a1, c2])), [b2, a3, a1, c1]) self.assertEqual( sorted(distinguishable_cred_list([c2, a1, b1, a2, c1, a3, b2])), [b2, a3, a1, c2]) # Test duplicities. self.assertEqual( sorted(distinguishable_cred_list([c1, a1, a1, c2, c1])), [a1, c1]) self.assertEqual(sorted(distinguishable_cred_list([b2, b3])), [b2]) self.assertEqual(sorted(distinguishable_cred_list([b3, b2])), [b3])
def get_resident_credentials(rp_id_hash: Optional[bytes]) -> List[Credential]: creds = [] # type: List[Credential] for i in range( _RESIDENT_CREDENTIAL_START_KEY, _RESIDENT_CREDENTIAL_START_KEY + _MAX_RESIDENT_CREDENTIALS, ): stored_cred_data = common._get(common._APP_FIDO2, i) if stored_cred_data is None: continue stored_rp_id_hash = stored_cred_data[:32] stored_cred_id = stored_cred_data[32:] if rp_id_hash is not None and rp_id_hash != stored_rp_id_hash: # Stored credential is not for this RP ID. continue stored_cred = Fido2Credential.from_cred_id(stored_cred_id, stored_rp_id_hash) if stored_cred is not None: creds.append(stored_cred) return creds
def _credential_from_data(index: int, data: bytes) -> Fido2Credential: rp_id_hash = data[:RP_ID_HASH_LENGTH] cred_id = data[RP_ID_HASH_LENGTH:] cred = Fido2Credential.from_cred_id(cred_id, rp_id_hash) cred.index = index return cred