Exemple #1
0
        public void AddConformanceTOC()
        {
            foreach (var tocURL in _endpoints)
            {
                var rawTOC             = DownloadString(tocURL).Result;
                MetadataTOCPayload toc = null;
                try { toc = ValidatedTOCFromJwtSecurityToken(rawTOC); }
                catch { continue; }

                foreach (var entry in toc.Entries)
                {
                    if (null != entry.AaGuid)
                    {
                        var rawStatement      = DownloadString(entry.Url).Result;
                        var statementBytes    = Base64Url.Decode(rawStatement);
                        var statement         = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
                        var metadataStatement = JsonConvert.DeserializeObject <MetadataStatement>(statement);
                        metadataStatement.Hash  = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawStatement)));
                        entry.MetadataStatement = metadataStatement;
                        payload.Add(new Guid(entry.AaGuid), entry);
                    }
                }
            }
            CustomTOCPayloadFromCache();
        }
Exemple #2
0
        private async Task ProcessConformanceTOC(string tocURL)
        {
            var rawTOC = await DownloadString(tocURL);

            MetadataTOCPayload toc = null;

            try { toc = await ValidatedTOCFromJwtSecurityToken(rawTOC); }

            catch { return; }

            foreach (var entry in toc.Entries)
            {
                if (null != entry.AaGuid)
                {
                    var rawStatement = await DownloadString(entry.Url);

                    var statementBytes    = Base64Url.Decode(rawStatement);
                    var statement         = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
                    var metadataStatement = JsonConvert.DeserializeObject <MetadataStatement>(statement);
                    metadataStatement.Hash  = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawStatement)));
                    entry.MetadataStatement = metadataStatement;
                    payload.Add(new Guid(entry.AaGuid), entry);
                }
            }
        }
Exemple #3
0
        private MetadataStatement GetMetadataStatement(MetadataTOCPayloadEntry entry, bool fromCache)
        {
            var rawStatement = "";

            if (false == fromCache)
            {
                rawStatement = DownloadString(entry.Url + tokenParamName + _accessToken).Result;
            }
            if (null != _cacheDir && 3 < _cacheDir.Length)
            {
                if (false == System.IO.Directory.Exists(_cacheDir))
                {
                    System.IO.Directory.CreateDirectory(_cacheDir);
                }
                var filename = _cacheDir + @"\" + entry.AaGuid + @".jwt";
                if (false == fromCache)
                {
                    System.IO.File.WriteAllText(filename, rawStatement, System.Text.Encoding.UTF8);
                }
                else
                {
                    rawStatement = System.IO.File.ReadAllText(filename);
                }
            }

            var statementBytes = Base64Url.Decode(rawStatement);
            var statement      = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
            var ret            = JsonConvert.DeserializeObject <MetadataStatement>(statement);

            ret.Hash = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawStatement)));
            return(ret);
        }
        public async Task <MetadataStatement> GetMetadataStatement(MetadataTOCPayloadEntry entry)
        {
            var statementBase64Url = await DownloadStringAsync(entry.Url);

            var tocAlg = await GetTocAlg();

            var statementBytes  = Base64Url.Decode(statementBase64Url);
            var statementString = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
            var statement       = JsonConvert.DeserializeObject <MetadataStatement>(statementString);

            statement.Hash = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(statementBase64Url)));

            return(statement);
        }
Exemple #5
0
        public async Task <MetadataStatement> GetMetadataStatement(MetadataTOCPayload toc, MetadataTOCPayloadEntry entry)
        {
            var statementBase64Url = await DownloadStringAsync(entry.Url);

            var statementBytes  = Base64Url.Decode(statementBase64Url);
            var statementString = Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
            var statement       = Newtonsoft.Json.JsonConvert.DeserializeObject <MetadataStatement>(statementString);

            using (HashAlgorithm hasher = CryptoUtils.GetHasher(new HashAlgorithmName(toc.JwtAlg)))
            {
                statement.Hash = Base64Url.Encode(hasher.ComputeHash(Encoding.UTF8.GetBytes(statementBase64Url)));
            }

            return(statement);
        }
        public async Task <MetadataStatement> GetMetadataStatement(MetadataTOCPayloadEntry entry)
        {
            var statementBase64Url = await DownloadStringAsync(entry.Url + "/?token=" + _token);

            var tocAlg = await GetTocAlg();

            var statementBytes  = Base64Url.Decode(statementBase64Url);
            var statementString = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
            var statement       = JsonConvert.DeserializeObject <MetadataStatement>(statementString);

            statement.Hash = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(statementBase64Url)));

            if (!HashesAreEqual(entry.Hash, statement.Hash))
            {
                throw new Fido2VerificationException("TOC entry and statement hashes do not match");
            }

            return(statement);
        }
Exemple #7
0
        public async Task <MetadataStatement> GetMetadataStatement(MetadataTOCPayload toc, MetadataTOCPayloadEntry entry)
        {
            var statementBase64Url = await DownloadStringAsync(entry.Url + "/?token=" + WebUtility.UrlEncode(_token));

            var statementBytes  = Base64Url.Decode(statementBase64Url);
            var statementString = Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
            var statement       = Newtonsoft.Json.JsonConvert.DeserializeObject <MetadataStatement>(statementString);

            using (HashAlgorithm hasher = CryptoUtils.GetHasher(new HashAlgorithmName(toc.JwtAlg)))
            {
                statement.Hash = Base64Url.Encode(hasher.ComputeHash(Encoding.UTF8.GetBytes(statementBase64Url)));
            }

            if (!HashesAreEqual(entry.Hash, statement.Hash))
            {
                throw new Fido2VerificationException("TOC entry and statement hashes do not match");
            }

            return(statement);
        }
        public void AddConformanceTOC()
        {
            var endpoints = new string[] {
                "https://fidoalliance.co.nz/mds/execute/20c027c091eba81d2e92c6581bf42c68776dc3910cf48840b73a035e5d70f956",
                "https://fidoalliance.co.nz/mds/execute/3e0be36ab70cdf5f32ae858b8610fcb7bf6e4f1aa47c7e53afcda5c822f5a346",
                "https://fidoalliance.co.nz/mds/execute/55a6301b9d7a7a45dc27dceeddc9b0ae4396c7d9ea8f46757018dd865dda24c5",
                "https://fidoalliance.co.nz/mds/execute/62c8ba89cf4f991e6890f442a606bb0b6f31f9a05946031846c4af1113046900",
                "https://fidoalliance.co.nz/mds/execute/d352b77e801de7b0d7d9842b02721c3e708c82405353235d2c04081fff8a302a"
            };

            var client = new System.Net.WebClient();

            foreach (var tocURL in endpoints)
            {
                var rawTOC = client.DownloadString(tocURL);
                //var jwtToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(rawTOC);
                //var tocPayload = (jwtToken).Payload.SerializeToJson();
                MetadataTOCPayload toc = null;
                try { toc = ValidatedTOCFromJwtSecurityToken(rawTOC, true); }
                catch { Exception ex; continue; }

                foreach (var entry in toc.Entries)
                {
                    if (null != entry.AaGuid)
                    {
                        var rawStatement      = client.DownloadString(entry.Url);
                        var statementBytes    = Base64Url.Decode(rawStatement);
                        var statement         = System.Text.Encoding.UTF8.GetString(statementBytes, 0, statementBytes.Length);
                        var metadataStatement = JsonConvert.DeserializeObject <MetadataStatement>(statement);
                        metadataStatement.Hash  = Base64Url.Encode(CryptoUtils.GetHasher(new HashAlgorithmName(tocAlg)).ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawStatement)));
                        entry.MetadataStatement = metadataStatement;
                        payload.Add(new Guid(entry.AaGuid), entry);
                    }
                }
            }
        }
        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
            AttestationFormat.AttestationFormat verifier;
            switch (AttestationObject.Fmt)
            {
            // 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
            case "none":
                // https://www.w3.org/TR/webauthn/#none-attestation
                verifier = new None(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash);
                break;

            case "tpm":
                // https://www.w3.org/TR/webauthn/#tpm-attestation
                verifier = new Tpm(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService);
                break;

            case "android-key":
                // https://www.w3.org/TR/webauthn/#android-key-attestation
                verifier = new AndroidKey(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash);
                break;

            case "android-safetynet":
                // https://www.w3.org/TR/webauthn/#android-safetynet-attestation
                verifier = new AndroidSafetyNet(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, config.TimestampDriftTolerance);
                break;

            case "fido-u2f":
                // https://www.w3.org/TR/webauthn/#fido-u2f-attestation
                verifier = new FidoU2f(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService);
                break;

            case "packed":
                // https://www.w3.org/TR/webauthn/#packed-attestation
                verifier = new Packed(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService);
                break;

            default: throw new Fido2VerificationException("Missing or unknown attestation type");
            }

            verifier.Verify();
            // 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.
            // Done for "fido-u2f", "packed", and "tpm" inside format-specific verifier above

            // 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.
        }
        public async Task <AttestationVerificationSuccess> VerifyAsync(CredentialCreateOptions originalOptions, Fido2NetLib.Fido2.Configuration config, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, IMetadataService metadataService, byte[] requestTokenBindingId)
        {
            BaseVerify(config.Origin, originalOptions.Challenge, requestTokenBindingId);
            // verify challenge is same as we expected
            // verify origin
            // done in baseclass

            if (Type != "webauthn.create")
            {
                throw new Fido2VerificationException("AttestationResponse is not type webauthn.create");
            }

            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'");
            }

            if (null == AttestationObject.AuthData || 0 == AttestationObject.AuthData.Length)
            {
                throw new Fido2VerificationException("Missing or malformed authData");
            }

            var authData = new AuthenticatorData(AttestationObject.AuthData);

            // 6
            //todo:  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.
            // This is done in BaseVerify.
            // TODO: test that implmentation

            // 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));
            }

            // 9
            // Verify that the RP ID hash in authData is indeed the SHA - 256 hash of the RP ID expected by the RP.
            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

            // 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
            // todo: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x);

            if (false == authData.AttestedCredentialDataPresent)
            {
                throw new Fido2VerificationException("Attestation flag not set on attestation data");
            }

            // 13
            // Determine the attestation statement format by performing a US ASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. The up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the in the IANA registry of the same name [WebAuthn-Registries].
            // https://www.w3.org/TR/webauthn/#defined-attestation-formats
            AttestationFormat.AttestationFormat verifier;
            switch (AttestationObject.Fmt)
            {
            // 14
            // validate the attStmt
            case "none":
                // https://www.w3.org/TR/webauthn/#none-attestation
                verifier = new None(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash);
                break;

            case "tpm":
                // https://www.w3.org/TR/webauthn/#tpm-attestation
                verifier = new Tpm(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash);
                break;

            case "android-key":
                // https://www.w3.org/TR/webauthn/#android-key-attestation
                verifier = new AndroidKey(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash);
                break;

            case "android-safetynet":
                // https://www.w3.org/TR/webauthn/#android-safetynet-attestation
                verifier = new AndroidSafetyNet(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, config.TimestampDriftTolerance);
                break;

            case "fido-u2f":
                // https://www.w3.org/TR/webauthn/#fido-u2f-attestation
                verifier = new FidoU2f(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService);
                break;

            case "packed":
                // https://www.w3.org/TR/webauthn/#packed-attestation
                verifier = new Packed(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash, metadataService);
                break;

            default: throw new Fido2VerificationException("Missing or unknown attestation type");
            }

            verifier.Verify();

            /*
             * 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.
             * */

            /*
             * 16
             * Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows: https://www.w3.org/TR/webauthn/#registering-a-new-credential
             * */
            // use aaguid (authData.AttData.Aaguid) to find root certs in metadata
            // use root plus trustPath to build trust chain
            // implemented for AttestationObject.Fmt == "packed" in packed specific verifier

            /*
             * 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.AttData.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.
             * */
            // This is handled by code att call site and result object.


            /*
             * 19
             * If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, the Relying Party SHOULD fail the registration ceremony.
             * NOTE: However, if permitted by policy, the Relying Party MAY register the credential ID and credential public key but treat the credential as one with self attestation (see §6.3.3 Attestation Types). If doing so, the Relying Party is asserting there is no cryptographic proof that the public key credential has been generated by a particular authenticator model. See [FIDOSecRef] and [UAFProtocol] for a more detailed discussion.
             * */

            var result = new AttestationVerificationSuccess()
            {
                CredentialId = authData.AttData.CredentialID,
                PublicKey    = authData.AttData.CredentialPublicKey,
                User         = originalOptions.User,
                Counter      = BitConverter.ToUInt32(authData.SignCount.Reverse().ToArray(), 0),
                CredType     = AttestationObject.Fmt,
                Aaguid       = authData.AttData.GuidAaguid
            };

            return(result);
        }
Exemple #11
0
        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);
        }
Exemple #12
0
        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
            // (handled in base class)
            if ((CBORType.TextString != attStmt["ver"].Type) ||
                (0 == attStmt["ver"].AsString().Length))
            {
                throw new Fido2VerificationException("Invalid version in SafetyNet data");
            }

            // 2. Verify that response is a valid SafetyNet response of version ver
            var ver = attStmt["ver"].AsString();

            if ((CBORType.ByteString != attStmt["response"].Type) ||
                (0 == attStmt["response"].GetByteString().Length))
            {
                throw new Fido2VerificationException("Invalid response in SafetyNet data");
            }

            var response    = attStmt["response"].GetByteString();
            var responseJWT = Encoding.UTF8.GetString(response);

            if (string.IsNullOrWhiteSpace(responseJWT))
            {
                throw new Fido2VerificationException("SafetyNet response null or whitespace");
            }

            var jwtParts = responseJWT.Split('.');

            if (jwtParts.Length != 3)
            {
                throw new Fido2VerificationException("SafetyNet response JWT does not have the 3 expected components");
            }

            var jwtHeaderString = jwtParts.First();
            var jwtHeaderJSON   = JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(jwtHeaderString)));

            var x5cArray = jwtHeaderJSON["x5c"] as JArray;

            if (x5cArray == null)
            {
                throw new Fido2VerificationException("SafetyNet response JWT header missing x5c");
            }
            var x5cStrings = x5cArray.Values <string>().ToList();

            if (x5cStrings.Count == 0)
            {
                throw new Fido2VerificationException("No keys were present in the TOC header in SafetyNet response JWT");
            }

            var certs = new List <X509Certificate2>();
            var keys  = new List <SecurityKey>();

            foreach (var certString in x5cStrings)
            {
                var cert = GetX509Certificate(certString);
                certs.Add(cert);

                var ecdsaPublicKey = cert.GetECDsaPublicKey();
                if (ecdsaPublicKey != null)
                {
                    keys.Add(new ECDsaSecurityKey(ecdsaPublicKey));
                }

                var rsaPublicKey = cert.GetRSAPublicKey();
                if (rsaPublicKey != null)
                {
                    keys.Add(new RsaSecurityKey(rsaPublicKey));
                }
            }

            var validationParameters = new TokenValidationParameters
            {
                ValidateIssuer           = false,
                ValidateAudience         = false,
                ValidateLifetime         = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKeys        = keys
            };

            var           tokenHandler   = new JwtSecurityTokenHandler();
            SecurityToken validatedToken = null;

            try
            {
                tokenHandler.ValidateToken(
                    responseJWT,
                    validationParameters,
                    out validatedToken);
            }
            catch (SecurityTokenException ex)
            {
                throw new Fido2VerificationException("SafetyNet response security token validation failed", ex);
            }

            var  nonce           = "";
            bool?ctsProfileMatch = null;
            var  timestampMs     = DateTimeHelper.UnixEpoch;

            var jwtToken = validatedToken as JwtSecurityToken;

            foreach (var claim in jwtToken.Claims)
            {
                if (("nonce" == claim.Type) && ("http://www.w3.org/2001/XMLSchema#string" == claim.ValueType) && (0 != claim.Value.Length))
                {
                    nonce = claim.Value;
                }
                if (("ctsProfileMatch" == claim.Type) && ("http://www.w3.org/2001/XMLSchema#boolean" == claim.ValueType))
                {
                    ctsProfileMatch = bool.Parse(claim.Value);
                }
                if (("timestampMs" == claim.Type) && ("http://www.w3.org/2001/XMLSchema#integer64" == claim.ValueType))
                {
                    timestampMs = DateTimeHelper.UnixEpoch.AddMilliseconds(double.Parse(claim.Value));
                }
            }

            var notAfter  = DateTime.UtcNow.AddMilliseconds(_driftTolerance);
            var notBefore = DateTime.UtcNow.AddMinutes(-1).AddMilliseconds(-(_driftTolerance));

            if ((notAfter < timestampMs) || ((notBefore) > timestampMs))
            {
                throw new Fido2VerificationException(string.Format("SafetyNet timestampMs must be present and between one minute ago and now, got: {0}", timestampMs.ToString()));
            }

            // 3. Verify that the nonce in the response is identical to the SHA-256 hash of the concatenation of authenticatorData and clientDataHash
            if ("" == nonce)
            {
                throw new Fido2VerificationException("Nonce value not found in SafetyNet attestation");
            }

            byte[] nonceHash = null;
            try
            {
                nonceHash = Convert.FromBase64String(nonce);
            }
            catch (Exception ex)
            {
                throw new Fido2VerificationException("Nonce value not base64string in SafetyNet attestation", ex);
            }

            using (var hasher = CryptoUtils.GetHasher(HashAlgorithmName.SHA256))
            {
                var dataHash = hasher.ComputeHash(Data);
                if (false == dataHash.SequenceEqual(nonceHash))
                {
                    throw new Fido2VerificationException(
                              string.Format(
                                  "SafetyNet response nonce / hash value mismatch, nonce {0}, hash {1}",
                                  BitConverter.ToString(nonceHash).Replace("-", ""),
                                  BitConverter.ToString(dataHash).Replace("-", "")
                                  )
                              );
                }
            }

            // 4. Let attestationCert be the attestation certificate
            var attestationCert = certs[0];
            var subject         = attestationCert.GetNameInfo(X509NameType.DnsName, false);

            // 5. Verify that the attestation certificate is issued to the hostname "attest.android.com"
            if (false == ("attest.android.com").Equals(subject))
            {
                throw new Fido2VerificationException(string.Format("SafetyNet attestation cert DnsName invalid, want {0}, got {1}", "attest.android.com", subject));
            }

            // 6. Verify that the ctsProfileMatch attribute in the payload of response is true
            if (null == ctsProfileMatch)
            {
                throw new Fido2VerificationException("SafetyNet response ctsProfileMatch missing");
            }

            if (true != ctsProfileMatch)
            {
                throw new Fido2VerificationException("SafetyNet response ctsProfileMatch false");
            }

            return(AttestationType.Basic, new X509Certificate2[] { attestationCert });
        }
Exemple #13
0
        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.
        }
Exemple #14
0
        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.
            // (handled in base class)
            if (null == Sig || CBORType.ByteString != Sig.Type || 0 == Sig.GetByteString().Length)
            {
                throw new Fido2VerificationException("Invalid TPM attestation signature");
            }

            if ("2.0" != attStmt["ver"].AsString())
            {
                throw new Fido2VerificationException("FIDO2 only supports TPM 2.0");
            }

            // 2. Verify that the public key specified by the parameters and unique fields of pubArea
            // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData
            PubArea pubArea = null;

            if (null != attStmt["pubArea"] &&
                CBORType.ByteString == attStmt["pubArea"].Type &&
                0 != attStmt["pubArea"].GetByteString().Length)
            {
                pubArea = new PubArea(attStmt["pubArea"].GetByteString());
            }

            if (null == pubArea || null == pubArea.Unique || 0 == pubArea.Unique.Length)
            {
                throw new Fido2VerificationException("Missing or malformed pubArea");
            }

            var coseKty = CredentialPublicKey[CBORObject.FromObject(COSE.KeyCommonParameter.KeyType)].AsInt32();

            if (3 == coseKty)                                                                                      // RSA
            {
                var coseMod = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.N)].GetByteString(); // modulus
                var coseExp = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.E)].GetByteString(); // exponent

                if (!coseMod.ToArray().SequenceEqual(pubArea.Unique.ToArray()))
                {
                    throw new Fido2VerificationException("Public key mismatch between pubArea and credentialPublicKey");
                }
                if ((coseExp[0] + (coseExp[1] << 8) + (coseExp[2] << 16)) != pubArea.Exponent)
                {
                    throw new Fido2VerificationException("Public key exponent mismatch between pubArea and credentialPublicKey");
                }
            }
            else if (2 == coseKty) // ECC
            {
                var curve = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.Crv)].AsInt32();
                var X     = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.X)].GetByteString();
                var Y     = CredentialPublicKey[CBORObject.FromObject(COSE.KeyTypeParameter.Y)].GetByteString();

                if (pubArea.EccCurve != CoseCurveToTpm[curve])
                {
                    throw new Fido2VerificationException("Curve mismatch between pubArea and credentialPublicKey");
                }
                if (!pubArea.ECPoint.X.SequenceEqual(X))
                {
                    throw new Fido2VerificationException("X-coordinate mismatch between pubArea and credentialPublicKey");
                }
                if (!pubArea.ECPoint.Y.SequenceEqual(Y))
                {
                    throw new Fido2VerificationException("Y-coordinate mismatch between pubArea and credentialPublicKey");
                }
            }
            // 3. Concatenate authenticatorData and clientDataHash to form attToBeSigned
            // See Data field of base class

            // 4. Validate that certInfo is valid
            CertInfo certInfo = null;

            if (null != attStmt["certInfo"] &&
                CBORType.ByteString == attStmt["certInfo"].Type &&
                0 != attStmt["certInfo"].GetByteString().Length)
            {
                certInfo = new CertInfo(attStmt["certInfo"].GetByteString());
            }

            if (null == certInfo)
            {
                throw new Fido2VerificationException("CertInfo invalid parsing TPM format attStmt");
            }

            // 4a. Verify that magic is set to TPM_GENERATED_VALUE
            // Handled in CertInfo constructor, see CertInfo.Magic

            // 4b. Verify that type is set to TPM_ST_ATTEST_CERTIFY
            // Handled in CertInfo constructor, see CertInfo.Type

            // 4c. Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg"
            if (null == Alg || true != Alg.IsNumber)
            {
                throw new Fido2VerificationException("Invalid TPM attestation algorithm");
            }

            using (var hasher = CryptoUtils.GetHasher(CryptoUtils.HashAlgFromCOSEAlg(Alg.AsInt32())))
            {
                if (!hasher.ComputeHash(Data).SequenceEqual(certInfo.ExtraData))
                {
                    throw new Fido2VerificationException("Hash value mismatch extraData and attToBeSigned");
                }
            }

            // 4d. Verify that attested contains a TPMS_CERTIFY_INFO structure, whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea
            using (var hasher = CryptoUtils.GetHasher(CryptoUtils.HashAlgFromCOSEAlg(certInfo.Alg)))
            {
                if (false == hasher.ComputeHash(pubArea.Raw).SequenceEqual(certInfo.AttestedName))
                {
                    throw new Fido2VerificationException("Hash value mismatch attested and pubArea");
                }
            }

            // 4e. Note that the remaining fields in the "Standard Attestation Structure" [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion are ignored. These fields MAY be used as an input to risk engines.

            // 5. If x5c is present, this indicates that the attestation type is not ECDAA
            if (null != X5c && CBORType.Array == X5c.Type && 0 != X5c.Count)
            {
                if (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 TPM attestation");
                }

                // 5a. Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.
                var aikCert = new X509Certificate2(X5c.Values.First().GetByteString());

                var cpk = new CredentialPublicKey(aikCert, Alg.AsInt32());
                if (true != cpk.Verify(certInfo.Raw, Sig.GetByteString()))
                {
                    throw new Fido2VerificationException("Bad signature in TPM with aikCert");
                }

                // 5b. Verify that aikCert meets the TPM attestation statement certificate requirements
                // https://www.w3.org/TR/webauthn/#tpm-cert-requirements
                // 5bi. Version MUST be set to 3
                if (3 != aikCert.Version)
                {
                    throw new Fido2VerificationException("aikCert must be V3");
                }

                // 5bii. Subject field MUST be set to empty - they actually mean subject name
                if (0 != aikCert.SubjectName.Name.Length)
                {
                    throw new Fido2VerificationException("aikCert subject must be empty");
                }

                // 5biii. The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.
                // https://www.w3.org/TR/webauthn/#tpm-cert-requirements
                (string tpmManufacturer, string tpmModel, string tpmVersion) = SANFromAttnCertExts(aikCert.Extensions);

                // From https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf
                // "The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryName
                // form within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device
                // Attributes. In accordance with RFC 5280[11], this extension MUST be critical if subject is empty
                // and SHOULD be non-critical if subject is non-empty"

                // Best I can figure to do for now?
                if (string.Empty == tpmManufacturer ||
                    string.Empty == tpmModel ||
                    string.Empty == tpmVersion)
                {
                    throw new Fido2VerificationException("SAN missing TPMManufacturer, TPMModel, or TPMVersion from TPM attestation certificate");
                }

                if (false == TPMManufacturers.Contains(tpmManufacturer))
                {
                    throw new Fido2VerificationException("Invalid TPM manufacturer found parsing TPM attestation");
                }

                // 5biiii. The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
                // OID is 2.23.133.8.3
                var EKU = EKUFromAttnCertExts(aikCert.Extensions, "2.23.133.8.3");
                if (!EKU)
                {
                    throw new Fido2VerificationException("aikCert EKU missing tcg-kp-AIKCertificate OID");
                }

                // 5biiiii. The Basic Constraints extension MUST have the CA component set to false.
                if (IsAttnCertCACert(aikCert.Extensions))
                {
                    throw new Fido2VerificationException("aikCert Basic Constraints extension CA component must be false");
                }

                // 5biiiiii. An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280]
                // are both OPTIONAL as the status of many attestation certificates is available through metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService].
                var trustPath = X5c.Values
                                .Select(x => new X509Certificate2(x.GetByteString()))
                                .ToArray();

                // 5c. 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
                var aaguid = AaguidFromAttnCertExts(aikCert.Extensions);
                if ((null != aaguid) &&
                    (!aaguid.SequenceEqual(Guid.Empty.ToByteArray())) &&
                    (0 != AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid)))
                {
                    throw new Fido2VerificationException(string.Format("aaguid malformed, expected {0}, got {1}", AuthData.AttestedCredentialData.AaGuid, new Guid(aaguid)));
                }

                return(AttestationType.AttCa, trustPath);
            }
            // If ecdaaKeyId is present, then the attestation type is ECDAA
            else if (null != EcdaaKeyId)
            {
                // Perform ECDAA-Verify on sig to verify that it is a valid signature over certInfo
                // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm
                throw new Fido2VerificationException("ECDAA support for TPM attestation is not yet implemented");
                // If successful, return attestation type ECDAA and the identifier of the ECDAA-Issuer public key ecdaaKeyId.
                //attnType = AttestationType.ECDAA;
                //trustPath = ecdaaKeyId;
            }
            else
            {
                throw new Fido2VerificationException("Neither x5c nor ECDAA were found in the TPM attestation statement");
            }
        }