def verify_android_safetynet( *, attestation_statement: AttestationStatement, attestation_object: bytes, client_data_json: bytes, pem_root_certs_bytes: List[bytes], verify_timestamp_ms: bool = True, ) -> bool: """Verify an "android-safetynet" attestation statement See https://www.w3.org/TR/webauthn-2/#sctn-android-safetynet-attestation Notes: - `verify_timestamp_ms` is a kind of escape hatch specifically for enabling testing of this method. Without this we can't use static responses in unit tests because they'll always evaluate as expired. This flag can be removed from this method if we ever figure out how to dynamically create safetynet-formatted responses that can be immediately tested. """ if not attestation_statement.ver: # As of this writing, there is only one format of the SafetyNet response and # ver is reserved for future use (so for now just make sure it's present) raise InvalidRegistrationResponse( "Attestation statement was missing version (SafetyNet)") if not attestation_statement.response: raise InvalidRegistrationResponse( "Attestation statement was missing response (SafetyNet)") # Begin peeling apart the JWS in the attestation statement response jws = attestation_statement.response.decode("ascii") jws_parts = jws.split(".") if len(jws_parts) != 3: raise InvalidRegistrationResponse( "Response JWS did not have three parts (SafetyNet)") header = SafetyNetJWSHeader.parse_raw(jws_parts[0]) payload = SafetyNetJWSPayload.parse_raw(jws_parts[1]) signature_bytes_str: str = jws_parts[2] # Verify that the nonce attribute in the payload of response is identical to the # Base64 encoding of the SHA-256 hash of the concatenation of authenticatorData and # clientDataHash. # 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() nonce_data = b"".join([ authenticator_data_bytes, client_data_hash_bytes, ]) # Start with a sha256 hash nonce_data_hash = hashlib.sha256() nonce_data_hash.update(nonce_data) nonce_data_hash_bytes = nonce_data_hash.digest() # Encode to base64 nonce_data_hash_bytes = base64.b64encode(nonce_data_hash_bytes) # Finish by decoding to string nonce_data_str = nonce_data_hash_bytes.decode("utf-8") if payload.nonce != nonce_data_str: raise InvalidRegistrationResponse( "Payload nonce was not expected value (SafetyNet)") # Verify that the SafetyNet response actually came from the SafetyNet service # by following the steps in the SafetyNet online documentation. x5c = [base64url_to_bytes(cert) for cert in header.x5c] if not payload.cts_profile_match: raise InvalidRegistrationResponse( "Could not verify device integrity (SafetyNet)") if verify_timestamp_ms: try: verify_safetynet_timestamp(payload.timestamp_ms) except ValueError as err: raise InvalidRegistrationResponse(f"{err} (SafetyNet)") # Verify that the leaf certificate was issued to the hostname attest.android.com attestation_cert = x509.load_der_x509_certificate(x5c[0], default_backend()) cert_common_name = attestation_cert.subject.get_attributes_for_oid( NameOID.COMMON_NAME, )[0] if cert_common_name.value != "attest.android.com": raise InvalidRegistrationResponse( 'Certificate common name was not "attest.android.com" (SafetyNet)') # Validate certificate chain try: # Include known root certificates for this attestation format with whatever # other certs were provided pem_root_certs_bytes.append(globalsign_r2) pem_root_certs_bytes.append(globalsign_root_ca) validate_certificate_chain( x5c=x5c, pem_root_certs_bytes=pem_root_certs_bytes, ) except InvalidCertificateChain as err: raise InvalidRegistrationResponse(f"{err} (SafetyNet)") # Verify signature verification_data = f"{jws_parts[0]}.{jws_parts[1]}".encode("utf-8") signature_bytes = base64url_to_bytes(signature_bytes_str) if header.alg != "RS256": raise InvalidRegistrationResponse( f"JWS header alg was not RS256: {header.alg} (SafetyNet") # Get cert public key bytes attestation_cert_pub_key = attestation_cert.public_key() try: verify_signature( public_key=attestation_cert_pub_key, signature_alg=COSEAlgorithmIdentifier.RSASSA_PKCS1_v1_5_SHA_256, signature=signature_bytes, data=verification_data, ) except InvalidSignature: raise InvalidRegistrationResponse( "Could not verify attestation statement signature (Packed)") return True
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_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_fido_u2f( *, attestation_statement: AttestationStatement, client_data_json: bytes, rp_id_hash: bytes, credential_id: bytes, credential_public_key: bytes, aaguid: bytes, pem_root_certs_bytes: List[bytes], ) -> bool: """Verify a "fido-u2f" attestation statement See https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation """ if not attestation_statement.sig: raise InvalidRegistrationResponse( "Attestation statement was missing signature (FIDO-U2F)") if not attestation_statement.x5c: raise InvalidRegistrationResponse( "Attestation statement was missing certificate (FIDO-U2F)") if len(attestation_statement.x5c) > 1: raise InvalidRegistrationResponse( "Attestation statement contained too many certificates (FIDO-U2F)") # 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} (FIDO-U2F)") # FIDO spec requires AAGUID in U2F attestations to be all zeroes # See https://fidoalliance.org/specs/fido-v2.1-rd-20191217/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html#u2f-authenticatorMakeCredential-interoperability actual_aaguid = aaguid_to_string(aaguid) expected_aaguid = "00000000-0000-0000-0000-000000000000" if actual_aaguid != expected_aaguid: raise InvalidRegistrationResponse( f"AAGUID {actual_aaguid} was not expected {expected_aaguid} (FIDO-U2F)" ) # Get the public key from the leaf certificate leaf_cert_bytes = attestation_statement.x5c[0] leaf_cert = x509.load_der_x509_certificate(leaf_cert_bytes, default_backend()) leaf_cert_pub_key = leaf_cert.public_key() # We need the cert's x and y points so make sure they exist if not isinstance(leaf_cert_pub_key, EllipticCurvePublicKey): raise InvalidRegistrationResponse( "Leaf cert was not an EC2 certificate (FIDO-U2F)") if not isinstance(leaf_cert_pub_key.curve, SECP256R1): raise InvalidRegistrationResponse( "Leaf cert did not use P-256 curve (FIDO-U2F)") decoded_public_key = decode_credential_public_key(credential_public_key) if not isinstance(decoded_public_key, DecodedEC2PublicKey): raise InvalidRegistrationResponse( "Credential public key was not EC2 (FIDO-U2F)") # Convert the public key to "Raw ANSI X9.62 public key format" public_key_u2f = b"".join([ bytes([0x04]), decoded_public_key.x, decoded_public_key.y, ]) # 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() # Prepare the signature base (called "verificationData" in the WebAuthn spec) verification_data = b"".join([ bytes([0x00]), rp_id_hash, client_data_hash_bytes, credential_id, public_key_u2f, ]) try: verify_signature( public_key=leaf_cert_pub_key, signature_alg=COSEAlgorithmIdentifier.ECDSA_SHA_256, signature=attestation_statement.sig, data=verification_data, ) except InvalidSignature: raise InvalidRegistrationResponse( "Could not verify attestation statement signature (FIDO-U2F)") # If we make it to here we're all good return True