예제 #1
0
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
예제 #2
0
파일: tpm.py 프로젝트: duo-labs/py_webauthn
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
예제 #3
0
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
예제 #4
0
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
예제 #5
0
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
예제 #6
0
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