public override (AttestationType, X509Certificate2[]) Verify() { // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields. if (null == X5c || CBORType.Array != X5c.Type || X5c.Count < 2 || null == X5c.Values || 0 == X5c.Values.Count || CBORType.ByteString != X5c.Values.First().Type || 0 == X5c.Values.First().GetByteString().Length) { throw new Fido2VerificationException("Malformed x5c in Apple attestation"); } // 2. Verify x5c is a valid certificate chain starting from the credCert to the Apple WebAuthn root certificate. // TODO: Pull this in instead of hard coding? // https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem var appleWebAuthnRoots = new string[] { "MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w" + "HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ" + "bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx" + "NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG" + "A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49" + "AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k" + "xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/" + "pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk" + "2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA" + "MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3" + "jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B" + "1bWeT0vT" }; var trustPath = X5c.Values .Select(x => new X509Certificate2(x.GetByteString())) .ToArray(); var appleWebAuthnRootCerts = appleWebAuthnRoots .Select(x => new X509Certificate2(Convert.FromBase64String(x))) .ToArray(); if (!CryptoUtils.ValidateTrustChain(trustPath, appleWebAuthnRootCerts)) { throw new Fido2VerificationException("Invalid certificate chain in Apple attestation"); } // credCert is the first certificate in the trust path var credCert = trustPath[0]; // 3. Concatenate authenticatorData and clientDataHash to form nonceToHash. var nonceToHash = Data; // 4. Perform SHA-256 hash of nonceToHash to produce nonce. var nonce = CryptoUtils.GetHasher(HashAlgorithmName.SHA256).ComputeHash(nonceToHash); // 5. Verify nonce matches the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert. var appleExtensionBytes = GetAppleAttestationExtensionValue(credCert.Extensions); if (!nonce.SequenceEqual(appleExtensionBytes)) { throw new Fido2VerificationException("Mismatch between nonce and credCert attestation extension in Apple attestation"); } // 6. Verify credential public key matches the Subject Public Key of credCert. // First, obtain COSE algorithm being used from credential public key var coseAlg = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.Alg)].AsInt32(); // Next, build temporary CredentialPublicKey for comparison from credCert and COSE algorithm var cpk = new CredentialPublicKey(credCert, coseAlg); // Finally, compare byte sequence of CredentialPublicKey built from credCert with byte sequence of CredentialPublicKey from AttestedCredentialData from authData if (!cpk.GetBytes().SequenceEqual(AuthData.AttestedCredentialData.CredentialPublicKey.GetBytes())) { throw new Fido2VerificationException("Credential public key in Apple attestation does not match subject public key of credCert"); } // 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c. return(AttestationType.Basic, trustPath); }
public async Task <AttestationVerificationSuccess> VerifyAsync(CredentialCreateOptions originalOptions, Fido2Configuration config, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, IMetadataService metadataService, byte[] requestTokenBindingId) { // https://www.w3.org/TR/webauthn/#registering-a-new-credential // 1. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON. // 2. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext. // Note: C may be any implementation-specific data structure representation, as long as C’s components are referenceable, as required by this algorithm. // Above handled in base class constructor // 3. Verify that the value of C.type is webauthn.create if (Type != "webauthn.create") { throw new Fido2VerificationException("AttestationResponse is not type webauthn.create"); } // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call. // 5. Verify that the value of C.origin matches the Relying Party's origin. // 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained. // If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection. BaseVerify(config.Origin, originalOptions.Challenge, requestTokenBindingId); if (Raw.Id == null || Raw.Id.Length == 0) { throw new Fido2VerificationException("AttestationResponse is missing Id"); } if (Raw.Type != PublicKeyCredentialType.PublicKey) { throw new Fido2VerificationException("AttestationResponse is missing type with value 'public-key'"); } var authData = new AuthenticatorData(AttestationObject.AuthData); // 7. Compute the hash of response.clientDataJSON using SHA-256. byte[] clientDataHash, rpIdHash; using (var sha = CryptoUtils.GetHasher(HashAlgorithmName.SHA256)) { clientDataHash = sha.ComputeHash(Raw.Response.ClientDataJson); rpIdHash = sha.ComputeHash(Encoding.UTF8.GetBytes(originalOptions.Rp.Id)); } // 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, the authenticator data authData, and the attestation statement attStmt. // Handled in AuthenticatorAttestationResponse::Parse() // 9. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party if (false == authData.RpIdHash.SequenceEqual(rpIdHash)) { throw new Fido2VerificationException("Hash mismatch RPID"); } // 10. Verify that the User Present bit of the flags in authData is set. if (false == authData.UserPresent) { throw new Fido2VerificationException("User Present flag not set in authenticator data"); } // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. // see authData.UserVerified // TODO: Make this a configurable option and add check to require // 12. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, // considering the client extension input values that were given as the extensions option in the create() call. In particular, any extension identifier values // in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., // no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use. // TODO?: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x); if (false == authData.HasAttestedCredentialData) { throw new Fido2VerificationException("Attestation flag not set on attestation data"); } // 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. // An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same name // https://www.w3.org/TR/webauthn/#defined-attestation-formats AttestationVerifier verifier = AttestationObject.Fmt switch { // TODO: Better way to build these mappings? "none" => new None(), // https://www.w3.org/TR/webauthn/#none-attestation "tpm" => new Tpm(), // https://www.w3.org/TR/webauthn/#tpm-attestation "android-key" => new AndroidKey(), // https://www.w3.org/TR/webauthn/#android-key-attestation "android-safetynet" => new AndroidSafetyNet(), // https://www.w3.org/TR/webauthn/#android-safetynet-attestation "fido-u2f" => new FidoU2f(), // https://www.w3.org/TR/webauthn/#fido-u2f-attestation "packed" => new Packed(), // https://www.w3.org/TR/webauthn/#packed-attestation "apple" => new Apple(), // https://www.w3.org/TR/webauthn/#apple-anonymous-attestation _ => throw new Fido2VerificationException("Missing or unknown attestation type"), }; // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, // by using the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized client data computed in step 7 (var attType, var trustPath) = verifier.Verify(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash); // 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement format fmt, from a trusted source or from policy. // For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData. var entry = metadataService?.GetEntry(authData.AttestedCredentialData.AaGuid); // while conformance testing, we must reject any authenticator that we cannot get metadata for if (metadataService?.ConformanceTesting() == true && null == entry && AttestationType.None != attType && "fido-u2f" != AttestationObject.Fmt) { throw new Fido2VerificationException("AAGUID not found in MDS test metadata"); } if (null != trustPath) { // If the authenticator is listed as in the metadata as one that should produce a basic full attestation, build and verify the chain if ((entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL) ?? false) || (entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_ATTCA) ?? false)) { var attestationRootCertificates = entry.MetadataStatement.AttestationRootCertificates .Select(x => new X509Certificate2(Convert.FromBase64String(x))) .ToArray(); if (false == CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates)) { throw new Fido2VerificationException("Invalid certificate chain"); } } // If the authenticator is not listed as one that should produce a basic full attestation, the certificate should be self signed if ((!entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL) ?? false) && (!entry?.MetadataStatement?.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_ATTCA) ?? false)) { if (trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer) { throw new Fido2VerificationException("Attestation with full attestation from authenticator that does not support full attestation"); } } } // Check status resports for authenticator with undesirable status foreach (var report in entry?.StatusReports ?? Enumerable.Empty <StatusReport>()) { if (true == Enum.IsDefined(typeof(UndesiredAuthenticatorStatus), (UndesiredAuthenticatorStatus)report.Status)) { throw new Fido2VerificationException("Authenticator found with undesirable status"); } } // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows: // If self attestation was used, check if self attestation is acceptable under Relying Party policy. // If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in the set of acceptable trust anchors obtained in step 15. // Otherwise, use the X.509 certificates returned by the verification procedure to verify that the attestation public key correctly chains up to an acceptable root certificate. // 17. Check that the credentialId is not yet registered to any other user. // If registration is requested for a credential that is already registered to a different user, the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration if (false == await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User))) { throw new Fido2VerificationException("CredentialId is not unique to this user"); } // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, then register the new credential with the account that was denoted in the options.user passed to create(), // by associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as appropriate for the Relying Party's system. var result = new AttestationVerificationSuccess() { CredentialId = authData.AttestedCredentialData.CredentialID, PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(), User = originalOptions.User, Counter = authData.SignCount, CredType = AttestationObject.Fmt, Aaguid = authData.AttestedCredentialData.AaGuid, }; return(result); // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, the Relying Party SHOULD fail the registration ceremony. // This implementation throws if the outputs are not trustworthy for a particular attestation type. }