def verify_authentication_response( *, credential: AuthenticationCredential, expected_challenge: bytes, expected_rp_id: str, expected_origin: Union[str, List[str]], credential_public_key: bytes, credential_current_sign_count: int, require_user_verification: bool = False, ) -> VerifiedAuthentication: """Verify a response from navigator.credentials.get() Args: `credential`: The value returned from `navigator.credentials.get()`. `expected_challenge`: The challenge passed to the authenticator within the preceding authentication options. `expected_rp_id`: The Relying Party's unique identifier as specified in the precending authentication options. `expected_origin`: The domain, with HTTP protocol (e.g. "https://domain.here"), on which the authentication ceremony should have occurred. `credential_public_key`: The public key for the credential's ID as provided in a preceding authenticator registration ceremony. `credential_current_sign_count`: The current known number of times the authenticator was used. (optional) `require_user_verification`: Whether or not to require that the authenticator verified the user. Returns: Information about the authenticator Raises: `helpers.exceptions.InvalidAuthenticationResponse` if the response cannot be verified """ # FIDO-specific check if bytes_to_base64url(credential.raw_id) != credential.id: raise InvalidAuthenticationResponse( "id and raw_id were not equivalent") # FIDO-specific check if credential.type != PublicKeyCredentialType.PUBLIC_KEY: raise InvalidAuthenticationResponse( f'Unexpected credential type "{credential.type}", expected "public-key"' ) response = credential.response client_data = parse_client_data_json(response.client_data_json) if client_data.type != ClientDataType.WEBAUTHN_GET: raise InvalidAuthenticationResponse( f'Unexpected client data type "{client_data.type}", expected "{ClientDataType.WEBAUTHN_GET}"' ) if expected_challenge != client_data.challenge: raise InvalidAuthenticationResponse( "Client data challenge was not expected challenge") if isinstance(expected_origin, str): if expected_origin != client_data.origin: raise InvalidAuthenticationResponse( f'Unexpected client data origin "{client_data.origin}", expected "{expected_origin}"' ) else: try: expected_origin.index(client_data.origin) except ValueError: raise InvalidAuthenticationResponse( f'Unexpected client data origin "{client_data.origin}", expected one of {expected_origin}' ) if client_data.token_binding: status = client_data.token_binding.status if status not in expected_token_binding_statuses: raise InvalidAuthenticationResponse( f'Unexpected token_binding status of "{status}", expected one of "{",".join(expected_token_binding_statuses)}"' ) auth_data = parse_authenticator_data(response.authenticator_data) # Generate a hash of the expected RP ID for comparison expected_rp_id_hash = hashlib.sha256() expected_rp_id_hash.update(expected_rp_id.encode("utf-8")) expected_rp_id_hash_bytes = expected_rp_id_hash.digest() if auth_data.rp_id_hash != expected_rp_id_hash_bytes: raise InvalidAuthenticationResponse("Unexpected RP ID hash") if not auth_data.flags.up: raise InvalidAuthenticationResponse( "User was not present during authentication") if require_user_verification and not auth_data.flags.uv: raise InvalidAuthenticationResponse( "User verification is required but user was not verified during authentication" ) if (auth_data.sign_count > 0 or credential_current_sign_count > 0 ) and auth_data.sign_count <= credential_current_sign_count: # Require the sign count to have been incremented over what was reported by the # authenticator the last time this credential was used, otherwise this might be # a replay attack raise InvalidAuthenticationResponse( f"Response sign count of {auth_data.sign_count} was not greater than current count of {credential_current_sign_count}" ) client_data_hash = hashlib.sha256() client_data_hash.update(response.client_data_json) client_data_hash_bytes = client_data_hash.digest() signature_base = response.authenticator_data + client_data_hash_bytes try: decoded_public_key = decode_credential_public_key( credential_public_key) crypto_public_key = decoded_public_key_to_cryptography( decoded_public_key) verify_signature( public_key=crypto_public_key, signature_alg=decoded_public_key.alg, signature=response.signature, data=signature_base, ) except InvalidSignature: raise InvalidAuthenticationResponse( "Could not verify authentication signature") return VerifiedAuthentication( credential_id=credential.raw_id, new_sign_count=auth_data.sign_count, )
def verify_registration_response( *, credential: RegistrationCredential, expected_challenge: bytes, expected_rp_id: str, expected_origin: Union[str, List[str]], require_user_verification: bool = False, supported_pub_key_algs: List[ COSEAlgorithmIdentifier] = default_supported_pub_key_algs, pem_root_certs_bytes_by_fmt: Optional[Mapping[AttestationFormat, List[bytes]]] = None, ) -> VerifiedRegistration: """Verify an authenticator's response to navigator.credentials.create() Args: `credential`: The value returned from `navigator.credentials.create()`. `expected_challenge`: The challenge passed to the authenticator within the preceding registration options. `expected_rp_id`: The Relying Party's unique identifier as specified in the precending registration options. `expected_origin`: The domain, with HTTP protocol (e.g. "https://domain.here"), on which the registration should have occurred. Can also be a list of expected origins. (optional) `require_user_verification`: Whether or not to require that the authenticator verified the user. (optional) `supported_pub_key_algs`: A list of public key algorithm IDs the RP chooses to restrict support to. Defaults to all supported algorithm IDs. Returns: Information about the authenticator and registration Raises: `helpers.exceptions.InvalidRegistrationResponse` if the response cannot be verified """ verified = False # FIDO-specific check if bytes_to_base64url(credential.raw_id) != credential.id: raise InvalidRegistrationResponse("id and raw_id were not equivalent") # FIDO-specific check if credential.type != PublicKeyCredentialType.PUBLIC_KEY: raise InvalidRegistrationResponse( f'Unexpected credential type "{credential.type}", expected "public-key"' ) response = credential.response client_data = parse_client_data_json(response.client_data_json) if client_data.type != ClientDataType.WEBAUTHN_CREATE: raise InvalidRegistrationResponse( f'Unexpected client data type "{client_data.type}", expected "{ClientDataType.WEBAUTHN_CREATE}"' ) if expected_challenge != client_data.challenge: raise InvalidRegistrationResponse( "Client data challenge was not expected challenge") if isinstance(expected_origin, str): if expected_origin != client_data.origin: raise InvalidRegistrationResponse( f'Unexpected client data origin "{client_data.origin}", expected "{expected_origin}"' ) else: try: expected_origin.index(client_data.origin) except ValueError: raise InvalidRegistrationResponse( f'Unexpected client data origin "{client_data.origin}", expected one of {expected_origin}' ) if client_data.token_binding: status = client_data.token_binding.status if status not in expected_token_binding_statuses: raise InvalidRegistrationResponse( f'Unexpected token_binding status of "{status}", expected one of "{",".join(expected_token_binding_statuses)}"' ) attestation_object = parse_attestation_object(response.attestation_object) auth_data = attestation_object.auth_data # Generate a hash of the expected RP ID for comparison expected_rp_id_hash = hashlib.sha256() expected_rp_id_hash.update(expected_rp_id.encode("utf-8")) expected_rp_id_hash_bytes = expected_rp_id_hash.digest() if auth_data.rp_id_hash != expected_rp_id_hash_bytes: raise InvalidRegistrationResponse("Unexpected RP ID hash") if not auth_data.flags.up: raise InvalidRegistrationResponse( "User was not present during attestation") if require_user_verification and not auth_data.flags.uv: raise InvalidRegistrationResponse( "User verification is required but user was not verified during attestation" ) if not auth_data.attested_credential_data: raise InvalidRegistrationResponse( "Authenticator did not provide attested credential data") attested_credential_data = auth_data.attested_credential_data if not attested_credential_data.credential_id: raise InvalidRegistrationResponse( "Authenticator did not provide a credential ID") if not attested_credential_data.credential_public_key: raise InvalidRegistrationResponse( "Authenticator did not provide a credential public key") if not attested_credential_data.aaguid: raise InvalidRegistrationResponse( "Authenticator did not provide an AAGUID") decoded_credential_public_key = decode_credential_public_key( attested_credential_data.credential_public_key) if decoded_credential_public_key.alg not in supported_pub_key_algs: raise InvalidRegistrationResponse( f'Unsupported credential public key alg "{decoded_credential_public_key.alg}", expected one of: {supported_pub_key_algs}' ) # Prepare a list of possible root certificates for certificate chain validation pem_root_certs_bytes: List[bytes] = [] if pem_root_certs_bytes_by_fmt: custom_certs = pem_root_certs_bytes_by_fmt.get(attestation_object.fmt) if custom_certs: # Load any provided custom root certs pem_root_certs_bytes.extend(custom_certs) if attestation_object.fmt == AttestationFormat.NONE: # A "none" attestation should not contain _anything_ in its attestation # statement num_att_stmt_fields_set = [ val for _, val in asdict(attestation_object.att_stmt).items() if val is not None ] if len(num_att_stmt_fields_set) > 0: raise InvalidRegistrationResponse( "None attestation had unexpected attestation statement") # There's nothing else to verify, so mark the verification successful verified = True elif attestation_object.fmt == AttestationFormat.FIDO_U2F: verified = verify_fido_u2f( attestation_statement=attestation_object.att_stmt, client_data_json=response.client_data_json, rp_id_hash=auth_data.rp_id_hash, credential_id=attested_credential_data.credential_id, credential_public_key=attested_credential_data. credential_public_key, aaguid=attested_credential_data.aaguid, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.PACKED: verified = verify_packed( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.TPM: verified = verify_tpm( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.APPLE: verified = verify_apple( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.ANDROID_SAFETYNET: verified = verify_android_safetynet( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, pem_root_certs_bytes=pem_root_certs_bytes, ) elif attestation_object.fmt == AttestationFormat.ANDROID_KEY: verified = verify_android_key( attestation_statement=attestation_object.att_stmt, attestation_object=response.attestation_object, client_data_json=response.client_data_json, credential_public_key=attested_credential_data. credential_public_key, pem_root_certs_bytes=pem_root_certs_bytes, ) else: # Raise exception on an attestation format we're not prepared to verify raise InvalidRegistrationResponse( f'Unsupported attestation type "{attestation_object.fmt}"') # If we got this far and still couldn't verify things then raise an error instead # of simply returning False if not verified: raise InvalidRegistrationResponse( "Attestation statement could not be verified") return VerifiedRegistration( credential_id=attested_credential_data.credential_id, credential_public_key=attested_credential_data.credential_public_key, sign_count=auth_data.sign_count, aaguid=aaguid_to_string(attested_credential_data.aaguid), fmt=attestation_object.fmt, credential_type=credential.type, user_verified=auth_data.flags.uv, attestation_object=response.attestation_object, )
def verify_tpm( *, attestation_statement: AttestationStatement, attestation_object: bytes, client_data_json: bytes, credential_public_key: bytes, pem_root_certs_bytes: List[bytes], ) -> bool: """Verify a "tpm" attestation statement See https://www.w3.org/TR/webauthn-2/#sctn-tpm-attestation """ if not attestation_statement.cert_info: raise InvalidRegistrationResponse( "Attestation statement was missing certInfo (TPM)") if not attestation_statement.pub_area: raise InvalidRegistrationResponse( "Attestation statement was missing pubArea (TPM)") if not attestation_statement.alg: raise InvalidRegistrationResponse( "Attestation statement was missing alg (TPM)") if not attestation_statement.x5c: raise InvalidRegistrationResponse( "Attestation statement was missing x5c (TPM)") if not attestation_statement.sig: raise InvalidRegistrationResponse( "Attestation statement was missing sig (TPM)") att_stmt_ver = attestation_statement.ver if att_stmt_ver != "2.0": raise InvalidRegistrationResponse( f'Attestation statement ver "{att_stmt_ver}" was not "2.0" (TPM)') # Validate the certificate chain try: validate_certificate_chain( x5c=attestation_statement.x5c, pem_root_certs_bytes=pem_root_certs_bytes, ) except InvalidCertificateChain as err: raise InvalidRegistrationResponse(f"{err} (TPM)") # Verify that the public key specified by the parameters and unique fields of # pubArea is identical to the credentialPublicKey in the attestedCredentialData # in authenticatorData. pub_area = parse_pub_area(attestation_statement.pub_area) decoded_public_key = decode_credential_public_key(credential_public_key) if isinstance(pub_area.parameters, TPMPubAreaParametersRSA): if not isinstance(decoded_public_key, DecodedRSAPublicKey): raise InvalidRegistrationResponse( "Public key was not RSA key as indicated in pubArea (TPM)") if pub_area.unique.value != decoded_public_key.n: unique_hex = pub_area.unique.value.hex() pub_key_n_hex = decoded_public_key.n.hex() raise InvalidRegistrationResponse( f'PubArea unique "{unique_hex}" was not same as public key modulus "{pub_key_n_hex}" (TPM)' ) pub_area_exponent = int.from_bytes(pub_area.parameters.exponent, "big") if pub_area_exponent == 0: # "When zero, indicates that the exponent is the default of 2^16 + 1" pub_area_exponent = 65537 pub_key_exponent = int.from_bytes(decoded_public_key.e, "big") if pub_area_exponent != pub_key_exponent: raise InvalidRegistrationResponse( f'PubArea exponent "{pub_area_exponent}" was not same as public key exponent "{pub_key_exponent}" (TPM)' ) elif isinstance(pub_area.parameters, TPMPubAreaParametersECC): if not isinstance(decoded_public_key, DecodedEC2PublicKey): raise InvalidRegistrationResponse( "Public key was not ECC key as indicated in pubArea (TPM)") pubKeyCoords = b"".join([decoded_public_key.x, decoded_public_key.y]) if pub_area.unique.value != pubKeyCoords: unique_hex = pub_area.unique.value.hex() pub_key_xy_hex = pubKeyCoords.hex() raise InvalidRegistrationResponse( f'Unique "{unique_hex}" was not same as public key [x,y] "{pub_key_xy_hex}" (TPM)' ) pub_area_crv = TPM_ECC_CURVE_COSE_CRV_MAP[pub_area.parameters.curve_id] if pub_area_crv != decoded_public_key.crv: raise InvalidRegistrationResponse( f'PubArea curve ID "{pub_area_crv}" was not same as public key crv "{decoded_public_key.crv}" (TPM)' ) else: pub_area_param_type = type(pub_area.parameters) raise InvalidRegistrationResponse( f'Unsupported pub_area.parameters "{pub_area_param_type}" (TPM)') # Validate that certInfo is valid: cert_info = parse_cert_info(attestation_statement.cert_info) # Verify that magic is set to TPM_GENERATED_VALUE. # a.k.a. 0xff544347 magic_int = int.from_bytes(cert_info.magic, "big") if magic_int != int(0xFF544347): raise InvalidRegistrationResponse( f'CertInfo magic "{magic_int}" was not TPM_GENERATED_VALUE 4283712327 (0xff544347) (TPM)' ) # Concatenate authenticatorData and clientDataHash to form attToBeSigned. attestation_dict = cbor2.loads(attestation_object) authenticator_data_bytes: bytes = attestation_dict["authData"] client_data_hash = hash_by_alg(client_data_json) att_to_be_signed = b"".join([ authenticator_data_bytes, client_data_hash, ]) # Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg". att_to_be_signed_hash = hash_by_alg(att_to_be_signed, attestation_statement.alg) if cert_info.extra_data != att_to_be_signed_hash: raise InvalidRegistrationResponse( "PubArea extra data did not match hash of auth data and client data (TPM)" ) # Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in # [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for # pubArea, as computed using the algorithm in the nameAlg field of pubArea using # the procedure specified in [TPMv2-Part1] section 16. pub_area_hash = hash_by_alg( attestation_statement.pub_area, TPM_ALG_COSE_ALG_MAP[pub_area.name_alg], ) attested_name = b"".join([ cert_info.attested.name_alg_bytes, pub_area_hash, ]) if attested_name != cert_info.attested.name: raise InvalidRegistrationResponse( "CertInfo attested name did not match PubArea hash (TPM)") # Verify the sig is a valid signature over certInfo using the attestation # public key in aikCert with the algorithm specified in alg. attestation_cert_bytes = attestation_statement.x5c[0] attestation_cert = x509.load_der_x509_certificate(attestation_cert_bytes, default_backend()) attestation_cert_pub_key = attestation_cert.public_key() try: verify_signature( public_key=attestation_cert_pub_key, signature_alg=attestation_statement.alg, signature=attestation_statement.sig, data=attestation_statement.cert_info, ) except InvalidSignature: raise InvalidRegistrationResponse( "Could not verify attestation statement signature (TPM)") # Verify that aikCert meets the requirements in § 8.3.1 TPM Attestation Statement # Certificate Requirements. # https://w3c.github.io/webauthn/#sctn-tpm-cert-requirements # Version MUST be set to 3. if attestation_cert.version != Version.v3: raise InvalidRegistrationResponse( f'Certificate Version "{attestation_cert.version}" was not "{Version.v3}"" Constraints CA was not False (TPM)' ) # Subject field MUST be set to empty. if len(attestation_cert.subject) > 0: raise InvalidRegistrationResponse( f'Certificate Subject "{attestation_cert.subject}" was not empty (TPM)' ) # Start extensions analysis cert_extensions = attestation_cert.extensions # The Subject Alternative Name extension MUST be set as defined in # [TPMv2-EK-Profile] section 3.2.9. try: ext_subject_alt_name: SubjectAlternativeName = ( cert_extensions.get_extension_for_oid( ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value) except ExtensionNotFound: raise InvalidRegistrationResponse( f"Certificate missing extension {ExtensionOID.SUBJECT_ALTERNATIVE_NAME} (TPM)" ) tcg_at_tpm_values: Name = ext_subject_alt_name.get_values_for_type( GeneralName)[0] tcg_at_tpm_manufacturer = None tcg_at_tpm_model = None tcg_at_tpm_version = None for obj in tcg_at_tpm_values: oid = obj.oid.dotted_string if oid == "2.23.133.2.1": tcg_at_tpm_manufacturer = obj.value elif oid == "2.23.133.2.2": tcg_at_tpm_model = obj.value elif oid == "2.23.133.2.3": tcg_at_tpm_version = obj.value if not tcg_at_tpm_manufacturer or not tcg_at_tpm_model or not tcg_at_tpm_version: raise InvalidRegistrationResponse( f"Certificate Subject Alt Name was invalid value {tcg_at_tpm_values} (TPM)", ) try: TPM_MANUFACTURERS[tcg_at_tpm_manufacturer] except KeyError: raise InvalidRegistrationResponse( f'Unrecognized TPM Manufacturer "{tcg_at_tpm_manufacturer}" (TPM)') # The Extended Key Usage extension MUST contain the OID 2.23.133.8.3 # ("joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) # tcg-kp-AIKCertificate(3)"). try: ext_extended_key_usage: ExtendedKeyUsage = ( cert_extensions.get_extension_for_oid( ExtensionOID.EXTENDED_KEY_USAGE).value) except ExtensionNotFound: raise InvalidRegistrationResponse( f"Certificate missing extension {ExtensionOID.EXTENDED_KEY_USAGE} (TPM)" ) ext_key_usage_oid = ext_extended_key_usage[0].dotted_string if ext_key_usage_oid != "2.23.133.8.3": raise InvalidRegistrationResponse( f'Certificate Extended Key Usage OID "{ext_key_usage_oid}" was not "2.23.133.8.3" (TPM)' ) # The Basic Constraints extension MUST have the CA component set to false. try: ext_basic_constraints = cert_extensions.get_extension_for_oid( ExtensionOID.BASIC_CONSTRAINTS) except ExtensionNotFound: raise InvalidRegistrationResponse( f"Certificate missing extension {ExtensionOID.BASIC_CONSTRAINTS} (TPM)" ) if ext_basic_constraints.value.ca is not False: raise InvalidRegistrationResponse( "Certificate Basic Constraints CA was not False (TPM)") # If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 # (id-fido-gen-ce-aaguid) verify that the value of this extension matches the # aaguid in authenticatorData. # TODO: Implement this later if we can find a TPM that returns something here # try: # fido_gen_ce_aaguid = cert_extensions.get_extension_for_oid( # ObjectIdentifier("1.3.6.1.4.1.45724.1.1.4") # ) # except ExtensionNotFound: # pass return True
def verify_android_key( *, attestation_statement: AttestationStatement, attestation_object: bytes, client_data_json: bytes, credential_public_key: bytes, pem_root_certs_bytes: List[bytes], ) -> bool: """Verify an "android-key" attestation statement See https://www.w3.org/TR/webauthn-2/#sctn-android-key-attestation Also referenced: https://source.android.com/security/keystore/attestation """ if not attestation_statement.sig: raise InvalidRegistrationResponse( "Attestation statement was missing signature (Android Key)") if not attestation_statement.alg: raise InvalidRegistrationResponse( "Attestation statement was missing algorithm (Android Key)") if not attestation_statement.x5c: raise InvalidRegistrationResponse( "Attestation statement was missing x5c (Android Key)") # Validate certificate chain try: # Include known root certificates for this attestation format pem_root_certs_bytes.append(google_hardware_attestation_root_1) pem_root_certs_bytes.append(google_hardware_attestation_root_2) validate_certificate_chain( x5c=attestation_statement.x5c, pem_root_certs_bytes=pem_root_certs_bytes, ) except InvalidCertificateChain as err: raise InvalidRegistrationResponse(f"{err} (Android Key)") # Extract attStmt bytes from attestation_object attestation_dict = cbor2.loads(attestation_object) authenticator_data_bytes = attestation_dict["authData"] # Generate a hash of client_data_json client_data_hash = hashlib.sha256() client_data_hash.update(client_data_json) client_data_hash_bytes = client_data_hash.digest() verification_data = b"".join([ authenticator_data_bytes, client_data_hash_bytes, ]) # Verify that sig is a valid signature over the concatenation of authenticatorData # and clientDataHash using the public key in the first certificate in x5c with the # algorithm specified in alg. attestation_cert_bytes = attestation_statement.x5c[0] attestation_cert = x509.load_der_x509_certificate(attestation_cert_bytes, default_backend()) attestation_cert_pub_key = attestation_cert.public_key() try: verify_signature( public_key=attestation_cert_pub_key, signature_alg=attestation_statement.alg, signature=attestation_statement.sig, data=verification_data, ) except InvalidSignature: raise InvalidRegistrationResponse( "Could not verify attestation statement signature (Android Key)") # Verify that the public key in the first certificate in x5c matches the # credentialPublicKey in the attestedCredentialData in authenticatorData. attestation_cert_pub_key_bytes = attestation_cert_pub_key.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) # Convert our raw public key bytes into the same format cryptography generates for # the cert subject key decoded_pub_key = decode_credential_public_key(credential_public_key) pub_key_crypto = decoded_public_key_to_cryptography(decoded_pub_key) pub_key_crypto_bytes = pub_key_crypto.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) if attestation_cert_pub_key_bytes != pub_key_crypto_bytes: raise InvalidRegistrationResponse( "Certificate public key did not match credential public key (Android Key)" ) # Verify that the attestationChallenge field in the attestation certificate # extension data is identical to clientDataHash. ext_key_description_oid = "1.3.6.1.4.1.11129.2.1.17" try: cert_extensions = attestation_cert.extensions ext_key_description: Extension = cert_extensions.get_extension_for_oid( ObjectIdentifier(ext_key_description_oid)) except ExtensionNotFound: raise InvalidRegistrationResponse( f"Certificate missing extension {ext_key_description_oid} (Android Key)" ) # Peel apart the Extension into an UnrecognizedExtension, then the bytes we actually # want ext_value_wrapper: UnrecognizedExtension = ext_key_description.value ext_value: bytes = ext_value_wrapper.value parsed_ext = KeyDescription.load(ext_value) # Verify the following using the appropriate authorization list from the attestation # certificate extension data: software_enforced: AuthorizationList = parsed_ext["softwareEnforced"] tee_enforced: AuthorizationList = parsed_ext["teeEnforced"] # The AuthorizationList.allApplications field is not present on either authorization # list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped # to the RP ID. if software_enforced["allApplications"].native is not None: raise InvalidRegistrationResponse( "allApplications field was present in softwareEnforced (Android Key)" ) if tee_enforced["allApplications"].native is not None: raise InvalidRegistrationResponse( "allApplications field was present in teeEnforced (Android Key)") # The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED. origin = tee_enforced["origin"].native if origin != KeyOrigin.GENERATED: raise InvalidRegistrationResponse( f"teeEnforced.origin {origin} was not {KeyOrigin.GENERATED}") # The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN. purpose = tee_enforced["purpose"].native if purpose != [KeyPurpose.SIGN]: raise InvalidRegistrationResponse( f"teeEnforced.purpose {purpose} was not [{KeyPurpose.SIGN}]") return True
def verify_packed( *, attestation_statement: AttestationStatement, attestation_object: bytes, client_data_json: bytes, credential_public_key: bytes, pem_root_certs_bytes: List[bytes], ) -> bool: """Verify a "packed" attestation statement See https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation """ if not attestation_statement.sig: raise InvalidRegistrationResponse( "Attestation statement was missing signature (Packed)") if not attestation_statement.alg: raise InvalidRegistrationResponse( "Attestation statement was missing algorithm (Packed)") # Extract attStmt bytes from attestation_object attestation_dict = cbor2.loads(attestation_object) authenticator_data_bytes = attestation_dict["authData"] # Generate a hash of client_data_json client_data_hash = hashlib.sha256() client_data_hash.update(client_data_json) client_data_hash_bytes = client_data_hash.digest() verification_data = b"".join([ authenticator_data_bytes, client_data_hash_bytes, ]) if attestation_statement.x5c: # Validate the certificate chain try: validate_certificate_chain( x5c=attestation_statement.x5c, pem_root_certs_bytes=pem_root_certs_bytes, ) except InvalidCertificateChain as err: raise InvalidRegistrationResponse(f"{err} (Packed)") attestation_cert_bytes = attestation_statement.x5c[0] attestation_cert = x509.load_der_x509_certificate( attestation_cert_bytes, default_backend()) attestation_cert_pub_key = attestation_cert.public_key() try: verify_signature( public_key=attestation_cert_pub_key, signature_alg=attestation_statement.alg, signature=attestation_statement.sig, data=verification_data, ) except InvalidSignature: raise InvalidRegistrationResponse( "Could not verify attestation statement signature (Packed)") else: # Self Attestation decoded_pub_key = decode_credential_public_key(credential_public_key) if decoded_pub_key.alg != attestation_statement.alg: raise InvalidRegistrationResponse( f"Credential public key alg {decoded_pub_key.alg} did not equal attestation statement alg {attestation_statement.alg}" ) public_key = decoded_public_key_to_cryptography(decoded_pub_key) try: verify_signature( public_key=public_key, signature_alg=attestation_statement.alg, signature=attestation_statement.sig, data=verification_data, ) except InvalidSignature: raise InvalidRegistrationResponse( "Could not verify attestation statement signature (Packed|Self)" ) return True
def verify_apple( *, attestation_statement: AttestationStatement, attestation_object: bytes, client_data_json: bytes, credential_public_key: bytes, pem_root_certs_bytes: List[bytes], ) -> bool: """ https://www.w3.org/TR/webauthn-2/#sctn-apple-anonymous-attestation """ if not attestation_statement.x5c: raise InvalidRegistrationResponse( "Attestation statement was missing x5c (Apple)") # Validate the certificate chain try: # Include known root certificates for this attestation format pem_root_certs_bytes.append(apple_webauthn_root_ca) validate_certificate_chain( x5c=attestation_statement.x5c, pem_root_certs_bytes=pem_root_certs_bytes, ) except InvalidCertificateChain as err: raise InvalidRegistrationResponse(f"{err} (Apple)") # Concatenate authenticatorData and clientDataHash to form nonceToHash. attestation_dict = cbor2.loads(attestation_object) authenticator_data_bytes = attestation_dict["authData"] client_data_hash = hashlib.sha256() client_data_hash.update(client_data_json) client_data_hash_bytes = client_data_hash.digest() nonce_to_hash = b"".join([ authenticator_data_bytes, client_data_hash_bytes, ]) # Perform SHA-256 hash of nonceToHash to produce nonce. nonce = hashlib.sha256() nonce.update(nonce_to_hash) nonce_bytes = nonce.digest() # Verify that nonce equals the value of the extension with # OID 1.2.840.113635.100.8.2 in credCert. attestation_cert_bytes = attestation_statement.x5c[0] attestation_cert = x509.load_der_x509_certificate(attestation_cert_bytes, default_backend()) cert_extensions = attestation_cert.extensions # Still no documented name for this OID... ext_1_2_840_113635_100_8_2_oid = "1.2.840.113635.100.8.2" try: ext_1_2_840_113635_100_8_2: Extension = cert_extensions.get_extension_for_oid( ObjectIdentifier(ext_1_2_840_113635_100_8_2_oid)) except ExtensionNotFound: raise InvalidRegistrationResponse( f"Certificate missing extension {ext_1_2_840_113635_100_8_2_oid} (Apple)" ) # Peel apart the Extension into an UnrecognizedExtension, then the bytes we actually # want ext_value_wrapper: UnrecognizedExtension = ext_1_2_840_113635_100_8_2.value # Ignore the first six ASN.1 structure bytes that define the nonce as an # OCTET STRING. Should trim off '0$\xa1"\x04' ext_value: bytes = ext_value_wrapper.value[6:] if ext_value != nonce_bytes: raise InvalidRegistrationResponse( "Certificate nonce was not expected value (Apple)") # Verify that the credential public key equals the Subject Public Key of credCert. attestation_cert_pub_key = attestation_cert.public_key() attestation_cert_pub_key_bytes = attestation_cert_pub_key.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) # Convert our raw public key bytes into the same format cryptography generates for # the cert subject key decoded_pub_key = decode_credential_public_key(credential_public_key) pub_key_crypto = decoded_public_key_to_cryptography(decoded_pub_key) pub_key_crypto_bytes = pub_key_crypto.public_bytes( Encoding.DER, PublicFormat.SubjectPublicKeyInfo, ) if attestation_cert_pub_key_bytes != pub_key_crypto_bytes: raise InvalidRegistrationResponse( "Certificate public key did not match credential public key (Apple)" ) return True