public static WebAuthnAssertion VerifyAttestationResult(Fido2Configuration fido2Config, string requestJSON, string resultJSON)
        {
            var fido2 = new Fido2(fido2Config);
            IsCredentialIdUniqueToUserAsyncDelegate callback = (IsCredentialIdUniqueToUserParams args) =>
            {
                var id = args.CredentialId; // generated ID by authenticator(should be always unique for each authenticator, equivalent to client cert)
                var u  = args.User;         // user info, kind of useless as this can be changed by js at client side
                return(Task.FromResult(true));
            };
            var request = Newtonsoft.Json.JsonConvert.DeserializeObject <CredentialCreateOptions>(requestJSON);
            AuthenticatorAttestationRawResponse regResponse = Newtonsoft.Json.JsonConvert.DeserializeObject <AuthenticatorAttestationRawResponse>(resultJSON);
            var success        = fido2.MakeNewCredentialAsync(regResponse, request, callback).Result;
            var clientDataJson = Newtonsoft.Json.JsonConvert.DeserializeObject <AuthenticatorResponse>(System.Text.UTF8Encoding.UTF8.GetString(regResponse.Response.ClientDataJson));
            var challenge      = System.Convert.ToBase64String(clientDataJson.Challenge);
            var credentialId   = System.Convert.ToBase64String(success.Result.CredentialId);
            var publicKey      = System.Convert.ToBase64String(success.Result.PublicKey);
            var signingCounter = success.Result.Counter; // designed for replay attact prevention but useless for a multiple node situation
            var user           = success.Result.User;
            var aaguid         = success.Result.Aaguid;

            return(new WebAuthnAssertion
            {
                verified = true,
                challenge = challenge,
                credentialId = credentialId,
                publicKey = publicKey,
                signingCounter = signingCounter,
                aaguid = aaguid,
                userHandle = request.User.Id
            });
        }
Example #2
0
        public async Task <JsonResult> MakeCredentialResultTest([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
        {
            // 1. get the options we sent the client
            var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
            var options     = CredentialCreateOptions.FromJson(jsonOptions);

            // 2. Create callback so that lib can verify credential id is unique to this user
            IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
            {
                var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId);

                if (users.Count > 0)
                {
                    return(false);
                }

                return(true);
            };

            // 2. Verify and make the credentials
            var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback);

            // 3. Store the credentials in db
            DemoStorage.AddCredentialToUser(options.User, new StoredCredential
            {
                Descriptor       = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                PublicKey        = success.Result.PublicKey,
                UserHandle       = success.Result.User.Id,
                SignatureCounter = success.Result.Counter
            });

            // 4. return "ok" to the client
            return(Json(success));
        }
        public async Task <JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken)
        {
            try
            {
                // 1. get the options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var options     = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = static async(args, cancellationToken) =>
        public async Task <ActionResult <CredentialMakeResult> > MakeCredential(AuthenticatorAttestationRawResponse attestationResponse)
        {
            try
            {
                // 1. get the options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var options     = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
                {
                    var credentialIdString = Base64Url.Encode(args.CredentialId);
                    var cred = await context.StoredCredentials.Where(x => x.DescriptorJson.Contains(credentialIdString)).FirstOrDefaultAsync();

                    if (cred == null)
                    {
                        return(true);
                    }
                    var users = await context.Members.Where(x => x.UserId == cred.UserId).Select(u => new Fido2User
                    {
                        DisplayName = u.DisplayName,
                        Name        = u.UserName,
                        Id          = u.UserId
                    }).ToListAsync();

                    return(users.Count == 0);
                };

                // 2. Verify and make the credentials
                var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);

                context.StoredCredentials.Add(new StoredCredential
                {
                    Descriptor       = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                    UserId           = options.User.Id,
                    PublicKey        = success.Result.PublicKey,
                    UserHandle       = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType         = success.Result.CredType,
                    RegDate          = DateTime.Now,
                    AaGuid           = success.Result.Aaguid
                });
                await context.SaveChangesAsync();

                return(Ok(success));
            }
            catch (Exception e)
            {
                return(BadRequest(new CredentialMakeResult {
                    Status = "error", ErrorMessage = FormatException(e)
                }));
            }
        }
Example #5
0
        /// <summary>
        /// 注册:验证用户凭证
        /// <para>当客户端返回响应时,我们验证并注册凭据。</para>
        /// </summary>
        /// <param name="attestationResponse"></param>
        public async Task <CredentialMakeResult> RegisterCredentials(User user, string fido2Name, AuthenticatorAttestationRawResponse attestationResponse)
        {
            // 1. get the options we sent the client
            var jsonOptions = distributedCache.GetString(user.UserId.ToString() + "attestationOptions");

            if (string.IsNullOrEmpty(jsonOptions))
            {
                return(null);
            }
            var options = CredentialCreateOptions.FromJson(jsonOptions);

            // 2. Create callback so that lib can verify credential id is unique to this user
            IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
            {
                //var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId);
                //if (users.Count > 0)
                //    return false;
                var argUserId = args.CredentialId;
                if (this.dataContext.FIDO2Repository.Where(b => b.CredentialId.Equals(argUserId)).Any())
                {
                    return(false);
                }
                return(true);
            };
            // 2. Verify and make the credentials
            var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);

            // 3. Store the credentials in db
            //var user = dataContext.User.Where(b => b.UserId == userId).FirstOrDefault();
            if (user == null)
            {
                return(null);
            }
            var fido2 = new FIDO2Item()
            {
                Id               = IdGenerator.NextId(),
                UserId           = user.UserId,
                FIDO2Name        = fido2Name,
                CredentialId     = success.Result.CredentialId,
                PublicKey        = success.Result.PublicKey,
                UserHandle       = success.Result.User.Id,
                SignatureCounter = success.Result.Counter,
                CredType         = success.Result.CredType,
                RegDate          = DateTime.Now,
                AaGuid           = success.Result.Aaguid
            };

            dataContext.FIDO2Repository.Add(fido2);

            dataContext.SaveChanges();

            return(success);
        }
Example #6
0
        public async Task <ActionResult> MakeCredential(
            [FromBody] AuthenticatorAttestationRawResponse attestationResponse)
        {
            try
            {
                // 1. get the options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var options     = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
                {
                    var users = await fidoStore.GetUsersByCredentialIdAsync(args.CredentialId);

                    if (users.Count > 0)
                    {
                        return(false);
                    }

                    return(true);
                };

                // 2. Verify and make the credentials
                var success = await fido2.MakeNewCredentialAsync(attestationResponse, options, callback);

                // 3. Store the credentials in db
                fidoStore.AddCredentialToUser(options.User, new StoredCredential
                {
                    Descriptor       = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                    PublicKey        = success.Result.PublicKey,
                    UserHandle       = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType         = success.Result.CredType,
                    RegDate          = DateTime.Now,
                    AaGuid           = success.Result.Aaguid
                });

                // 4. return "ok" to the client
                return(Ok(success));
            }
            catch (Exception e)
            {
                return(BadRequest(new Fido2.CredentialMakeResult {
                    Status = "error", ErrorMessage = FormatException(e)
                }));
            }
        }
Example #7
0
        /// <summary>
        /// Verifies the response from the browser/authr after creating new credentials
        /// </summary>
        /// <param name="attestationResponse"></param>
        /// <param name="origChallenge"></param>
        /// <param name="isCredentialIdUniqueToUser"></param>
        /// <param name="requestTokenBindingId"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task <CredentialMakeResult> MakeNewCredentialAsync(
            AuthenticatorAttestationRawResponse attestationResponse,
            CredentialCreateOptions origChallenge,
            IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
            byte[]?requestTokenBindingId        = null,
            CancellationToken cancellationToken = default)
        {
            var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
            var success        = await parsedResponse.VerifyAsync(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId, cancellationToken);

            // todo: Set Errormessage etc.
            return(new CredentialMakeResult(
                       status: "ok",
                       errorMessage: string.Empty,
                       result: success
                       ));
        }
Example #8
0
        /// <summary>
        /// Verifies the response from the browser/authr after creating new credentials
        /// </summary>
        /// <param name="attestationResponse"></param>
        /// <param name="origChallenge"></param>
        /// <returns></returns>
        public async Task <CredentialMakeResult> MakeNewCredentialAsync(
            AuthenticatorAttestationRawResponse attestationResponse,
            CredentialCreateOptions origChallenge,
            IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
            byte[] requestTokenBindingId = null)
        {
            var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
            var success        = await parsedResponse.VerifyAsync(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId);

            // todo: Set Errormessage etc.
            return(new CredentialMakeResult
            {
                Status = "ok",
                ErrorMessage = string.Empty,
                Result = success
            });
        }
        public async Task <JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, [FromQuery] CredentialCreateOptions options)
        {
            var user = await GetCurrentLocalUserAsync();

            try
            {
                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
                {
                    var users = await _auth.GetUserIdsByCredentialIdAsync(args.CredentialId);

                    if (users.Count > 0)
                    {
                        return(false);
                    }

                    return(true);
                };

                // 2. Verify and make the credentials
                var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);

                // 3. Store the credentials in db
                await _auth.AddCredential(user, new StoredCredential
                {
                    Descriptor       = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                    PublicKey        = success.Result.PublicKey,
                    UserId           = BitConverter.GetBytes(user.Id),
                    UserHandle       = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType         = success.Result.CredType,
                    RegDate          = DateTime.Now,
                    AaGuid           = success.Result.Aaguid
                });

                // 4. return "ok" to the client
                return(Json(success));
            }
            catch (Exception e)
            {
                return(Json(new { Status = "error", ErrorMessage = e.Message }));
            }
        }
Example #10
0
        public async Task <bool> CompleteWebAuthRegistrationAsync(User user, int id, string name, AuthenticatorAttestationRawResponse attestationResponse)
        {
            var keyId = $"Key{id}";

            var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);

            if (!provider?.MetaData?.ContainsKey("pending") ?? true)
            {
                return(false);
            }

            var options = CredentialCreateOptions.FromJson((string)provider.MetaData["pending"]);

            // Callback to ensure credential id is unique. Always return true since we don't care if another
            // account uses the same 2fa key.
            IsCredentialIdUniqueToUserAsyncDelegate callback = args => Task.FromResult(true);

            var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);

            provider.MetaData.Remove("pending");
            provider.MetaData[keyId] = new TwoFactorProvider.WebAuthnData
            {
                Name             = name,
                Descriptor       = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                PublicKey        = success.Result.PublicKey,
                UserHandle       = success.Result.User.Id,
                SignatureCounter = success.Result.Counter,
                CredType         = success.Result.CredType,
                RegDate          = DateTime.Now,
                AaGuid           = success.Result.Aaguid
            };

            var providers = user.GetTwoFactorProviders();

            providers[TwoFactorProviderType.WebAuthn] = provider;
            user.SetTwoFactorProviders(providers);
            await UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);

            return(true);
        }
        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 VerificationException("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 VerificationException("AttestationResponse is missing Id");
            }

            if (Raw.Type != PublicKeyCredentialType.PublicKey)
            {
                throw new VerificationException("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 VerificationException("Hash mismatch RPID");
            }

            // 10. Verify that the User Present bit of the flags in authData is set.
            if (false == authData.UserPresent)
            {
                throw new VerificationException("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 VerificationException("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 = default;

            switch (AttestationObject.Fmt)
            {
            case "none":
                verifier = new None();                  // https://www.w3.org/TR/webauthn/#none-attestation
                break;

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

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

            case "android-safetynet":
                verifier = new AndroidSafetyNet(metadataService.TimestampDriftTolerance);      // https://www.w3.org/TR/webauthn/#android-safetynet-attestation
                break;

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

            case "packed":
                verifier = new Packed();                // https://www.w3.org/TR/webauthn/#packed-attestation
                break;

            case "apple":
                verifier = new Apple();                 // https://www.w3.org/TR/webauthn/#apple-anonymous-attestation
                break;

            default:
                throw new VerificationException("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 VerificationException("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(MetadataAttestationType.ATTESTATION_BASIC_FULL.ToEnumMemberValue()) ?? false) ||
                    (entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_PRIVACY_CA.ToEnumMemberValue()) ?? false))
                {
                    var attestationRootCertificates = entry.MetadataStatement.AttestationRootCertificates
                                                      .Select(x => new X509Certificate2(Convert.FromBase64String(x)))
                                                      .ToArray();

                    if (false == CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates))
                    {
                        throw new VerificationException("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(MetadataAttestationType.ATTESTATION_BASIC_FULL.ToEnumMemberValue()) ?? false) &&
                    (!entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_PRIVACY_CA.ToEnumMemberValue()) ?? false) &&
                    (!entry?.MetadataStatement?.AttestationTypes.Contains(MetadataAttestationType.ATTESTATION_ANONCA.ToEnumMemberValue()) ?? false))
                {
                    if (trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer)
                    {
                        throw new VerificationException("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 VerificationException("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 VerificationException("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.
        }
Example #12
0
        public async Task <JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
        {
            try
            {
                // 1. get the options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var options     = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                // IsCredentialIdUniqueToUserAsyncDelegate callback = async (IsCredentialIdUniqueToUserParams args) =>
                // {
                //   // var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId);
                //   var users = await _fido2CredentialService.GetUsersByCredentialIdAsync(args.CredentialId);
                //   if (users.Count > 0)
                //     return false;
                //
                //   return true;
                // };
                IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
                {
                    // TODO Check if credentials are unique
                    return(true);
                };

                // 2. Verify and make the credentials
                var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);


                var applicationUser = await _userManager.GetUserAsync(HttpContext.User);

                // Schreibe die Credentials in die Datenbank
                var newFido2Credential = await _fido2CredentialService.AddCredentialToUser(new Fido2Credential()
                {
                    UserId           = applicationUser.Id,
                    Descriptor       = success.Result.CredentialId,
                    PublicKey        = success.Result.PublicKey,
                    UserHandle       = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType         = success.Result.CredType,
                    RegDate          = DateTime.Now,
                    AaGuid           = success.Result.Aaguid
                });

                // applicationUser.Fido2Credentials.Add(newFido2Credential);
                applicationUser.TwoFactorEnabled = true;
                applicationUser.TwoFactorMethod  = TwoFactorType.Fido2;
                await _userManager.UpdateAsync(applicationUser);

                // TODO Return Databse Entry not just JSON


                // 4. return "ok" to the client
                return(new JsonResult(success));
            }
            catch (Exception e)
            {
                return(new JsonResult(new Fido2.CredentialMakeResult {
                    Status = "error", ErrorMessage = FormatException(e)
                }));
            }
        }
        /// <summary>
        /// Verifies the response from the browser/authr after creating new credentials (MakeNewCredentialAsync)
        /// </summary>
        /// <param name="attestationResponse"></param>
        /// <param name="origChallenge"></param>
        /// <returns></returns>
        public async Task <CredentialMakeResult> SetRegisterCredentialResult(AuthenticatorAttestationRawResponse attestationResponse, CredentialCreateOptions origChallenge, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, byte[] requestTokenBindingId = null)
        {
            var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse);
            var success        = await parsedResponse.VerifyAsync(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId);

            try
            {
                return(new CredentialMakeResult
                {
                    Status = "ok",
                    ErrorMessage = string.Empty,
                    Result = success
                });
            }
            catch (Exception e)
            {
                return(new CredentialMakeResult
                {
                    Status = "error",
                    ErrorMessage = e.Message,
                    Result = success
                });
            }
        }
        public void TestAuthenticatorAttestationResponseInvalidRawType()
        {
            var challenge      = RandomGenerator.Default.GenerateBytes(128);
            var rp             = "fido2.azurewebsites.net";
            var clientDataJson = Encoding.UTF8.GetBytes(
                JsonConvert.SerializeObject
                (
                    new
            {
                Type      = "webauthn.create",
                Challenge = challenge,
                Origin    = rp,
            }
                )
                );
            var rawResponse = new AuthenticatorAttestationRawResponse
            {
                Type     = null,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
                Response = new AuthenticatorAttestationRawResponse.ResponseData()
                {
                    AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", new byte[0]).EncodeToBytes(),
                    ClientDataJson    = clientDataJson
                },
            };

            var origChallenge = new CredentialCreateOptions
            {
                Attestation            = AttestationConveyancePreference.Direct,
                AuthenticatorSelection = new AuthenticatorSelection
                {
                    AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                    RequireResidentKey      = true,
                    UserVerification        = UserVerificationRequirement.Required,
                },
                Challenge        = challenge,
                ErrorMessage     = "",
                PubKeyCredParams = new List <PubKeyCredParam>()
                {
                    new PubKeyCredParam
                    {
                        Alg  = -7,
                        Type = PublicKeyCredentialType.PublicKey,
                    }
                },
                Rp     = new PublicKeyCredentialRpEntity(rp, rp, ""),
                Status = "ok",
                User   = new Fido2User
                {
                    Name        = "testuser",
                    Id          = Encoding.UTF8.GetBytes("testuser"),
                    DisplayName = "Test User",
                },
                Timeout = 60000,
            };

            IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
            {
                return(Task.FromResult(true));
            };

            var lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = rp,
                ServerName   = rp,
                Origin       = rp,
            });

            var ex = Assert.ThrowsAsync <Fido2VerificationException>(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback));

            Assert.Equal("AttestationResponse is missing type with value 'public-key'", ex.Result.Message);
        }
Example #15
0
        public async Task TestApplePublicKeyMismatch()
        {
            var cpkBytes = new byte[] { 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0x79, 0xfe, 0x59, 0x08, 0xbb, 0x51, 0x29, 0xc8, 0x09, 0x38, 0xb7, 0x54, 0xc0, 0x4d, 0x2b, 0x34, 0x0e, 0xfa, 0x66, 0x15, 0xb9, 0x87, 0x69, 0x8b, 0xf5, 0x9d, 0xa4, 0xe5, 0x3e, 0xa3, 0xe6, 0xfe, 0x22, 0x58, 0x20, 0xfb, 0x03, 0xda, 0xa1, 0x27, 0x0d, 0x58, 0x04, 0xe8, 0xab, 0x61, 0xc1, 0x5a, 0xac, 0xa2, 0x43, 0x5c, 0x7d, 0xbf, 0x36, 0x9d, 0x71, 0xca, 0x15, 0xc5, 0x23, 0xb0, 0x00, 0x4a, 0x1b, 0x75, 0xb7 };

            _credentialPublicKey = new CredentialPublicKey(cpkBytes);

            var authData = new AuthenticatorData(_rpIdHash, _flags, _signCount, _acd, _exts).ToByteArray();

            _attestationObject.Set("authData", new CborByteString(authData));
            var clientData = new
            {
                type      = "webauthn.create",
                challenge = _challenge,
                origin    = "https://www.passwordless.dev",
            };
            var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(clientData);

            var invalidX5cStrings = StackAllocSha256(authData, clientDataJson);

            var trustPath = invalidX5cStrings
                            .Select(x => new X509Certificate2(Convert.FromBase64String(x)))
                            .ToArray();

            var X5c = new CborArray {
                { trustPath[0].RawData },
                { trustPath[1].RawData }
            };

            ((CborMap)_attestationObject["attStmt"]).Set("x5c", X5c);

            var attestationResponse = new AuthenticatorAttestationRawResponse
            {
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
                Response = new AuthenticatorAttestationRawResponse.ResponseData()
                {
                    AttestationObject = _attestationObject.Encode(),
                    ClientDataJson    = clientDataJson,
                }
            };

            var origChallenge = new CredentialCreateOptions
            {
                Attestation            = AttestationConveyancePreference.Direct,
                AuthenticatorSelection = new AuthenticatorSelection
                {
                    AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                    RequireResidentKey      = true,
                    UserVerification        = UserVerificationRequirement.Discouraged,
                },
                Challenge        = _challenge,
                ErrorMessage     = "",
                PubKeyCredParams = new List <PubKeyCredParam>()
                {
                    new PubKeyCredParam(COSE.Algorithm.ES256)
                },
                Rp     = new PublicKeyCredentialRpEntity("https://www.passwordless.dev", "6cc3c9e7967a.ngrok.io", ""),
                Status = "ok",
                User   = new Fido2User
                {
                    Name        = "testuser",
                    Id          = Encoding.UTF8.GetBytes("testuser"),
                    DisplayName = "Test User",
                },
                Timeout = 60000,
            };

            IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) =>
            {
                return(Task.FromResult(true));
            };

            IFido2 lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = "6cc3c9e7967a.ngrok.io",
                ServerName   = "6cc3c9e7967a.ngrok.io",
                Origins      = new HashSet <string> {
                    "https://www.passwordless.dev"
                },
            });

            var credentialMakeResult = await lib.MakeNewCredentialAsync(attestationResponse, origChallenge, callback);
        }
        public async Task <JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse authenticatorAttestationRawResponse)
        {
            try
            {
                var user = await _userManager.GetUserAsync(User);

                if (user == null)
                {
                    throw new Exception("Unable to retrieve user.");
                }

                var jsonOptions = await _distributedCache.GetStringAsync(UniqueId);

                if (string.IsNullOrEmpty(jsonOptions))
                {
                    throw new Exception("Cant get Credential options from cache.");
                }
                var options = CredentialCreateOptions.FromJson(jsonOptions);

                IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUserAsyncDelegate = async(IsCredentialIdUniqueToUserParams isCredentialIdUniqueToUserParams) =>
                {
                    var fido2Users = await _fido2Service.GetFido2UsersByCredentialIdAsync(isCredentialIdUniqueToUserParams.CredentialId);

                    if (fido2Users.Count > 0)
                    {
                        return(false);
                    }
                    return(true);
                };
                var result = await _fido2.MakeNewCredentialAsync(authenticatorAttestationRawResponse, options, isCredentialIdUniqueToUserAsyncDelegate);

                if (result.Status != "ok")
                {
                    throw new Exception("Unable to create credential.");
                }


                var newFido2StoredCredential = new Fido2StoredCredential {
                };
                newFido2StoredCredential.UserName         = options.User.Name;
                newFido2StoredCredential.UserId           = options.User.Id;
                newFido2StoredCredential.PublicKey        = result.Result.PublicKey;
                newFido2StoredCredential.UserHandle       = result.Result.User.Id;
                newFido2StoredCredential.SignatureCounter = result.Result.Counter;
                newFido2StoredCredential.CredType         = result.Result.CredType;
                newFido2StoredCredential.RegDate          = DateTime.Now;
                newFido2StoredCredential.AaGuid           = Guid.NewGuid();
                newFido2StoredCredential.Descriptor       = new PublicKeyCredentialDescriptor(result.Result.CredentialId);

                _fido2Service.AddFido2StoredCredential(newFido2StoredCredential);

                return(Json(result));
            }
            catch (Exception exception)
            {
                return(Json(new CredentialCreateOptions()
                {
                    Status = "error", ErrorMessage = CommonFunctions.FormatException(exception)
                }));
            }
        }
        public void TestAuthenticatorAttestationResponseUVRequired()
        {
            var challenge = RandomGenerator.Default.GenerateBytes(128);
            var rp        = "https://www.passwordless.dev";
            var acd       = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
            var authData  = new AuthenticatorData(
                SHA256.HashData(Encoding.UTF8.GetBytes(rp)),
                AuthenticatorFlags.AT | AuthenticatorFlags.UP,
                0,
                acd
                ).ToByteArray();
            var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new
            {
                type      = "webauthn.create",
                challenge = challenge,
                origin    = rp,
            });

            var rawResponse = new AuthenticatorAttestationRawResponse
            {
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
                Response = new AuthenticatorAttestationRawResponse.ResponseData()
                {
                    AttestationObject = new CborMap {
                        { "fmt", "none" },
                        { "attStmt", new CborMap() },
                        { "authData", authData }
                    }.Encode(),
                ClientDataJson = clientDataJson
                },
            };

            var origChallenge = new CredentialCreateOptions
            {
                Attestation            = AttestationConveyancePreference.Direct,
                AuthenticatorSelection = new AuthenticatorSelection
                {
                    AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                    RequireResidentKey      = true,
                    UserVerification        = UserVerificationRequirement.Required,
                },
                Challenge        = challenge,
                ErrorMessage     = "",
                PubKeyCredParams = new List <PubKeyCredParam>()
                {
                    new PubKeyCredParam(COSE.Algorithm.ES256)
                },
                Rp     = new PublicKeyCredentialRpEntity(rp, rp, ""),
                Status = "ok",
                User   = new Fido2User
                {
                    Name        = "testuser",
                    Id          = Encoding.UTF8.GetBytes("testuser"),
                    DisplayName = "Test User",
                },
                Timeout = 60000,
            };

            IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) =>
            {
                return(Task.FromResult(true));
            };

            IFido2 lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = rp,
                ServerName   = rp,
                Origins      = new HashSet <string> {
                    rp
                },
            });

            var ex = Assert.ThrowsAsync <Fido2VerificationException>(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback));

            Assert.Equal("User Verified flag not set in authenticator data and user verification was required", ex.Result.Message);
        }
        public void TestAuthenticatorAttestationResponseNotUserPresent()
        {
            var challenge = RandomGenerator.Default.GenerateBytes(128);
            var rp        = "https://www.passwordless.dev";
            var authData  = new AuthenticatorData(
                SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(rp)),
                AuthenticatorFlags.UV,
                0,
                null,
                null
                ).ToByteArray();
            var clientDataJson = Encoding.UTF8.GetBytes(
                JsonConvert.SerializeObject
                (
                    new
            {
                Type      = "webauthn.create",
                Challenge = challenge,
                Origin    = rp,
            }
                )
                );
            var rawResponse = new AuthenticatorAttestationRawResponse
            {
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
                Response = new AuthenticatorAttestationRawResponse.ResponseData()
                {
                    AttestationObject = CBORObject.NewMap().Add("fmt", "testing").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(),
                    ClientDataJson    = clientDataJson
                },
            };

            var origChallenge = new CredentialCreateOptions
            {
                Attestation            = AttestationConveyancePreference.Direct,
                AuthenticatorSelection = new AuthenticatorSelection
                {
                    AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                    RequireResidentKey      = true,
                    UserVerification        = UserVerificationRequirement.Required,
                },
                Challenge        = challenge,
                ErrorMessage     = "",
                PubKeyCredParams = new List <PubKeyCredParam>()
                {
                    new PubKeyCredParam
                    {
                        Alg  = COSE.Algorithm.ES256,
                        Type = PublicKeyCredentialType.PublicKey,
                    }
                },
                Rp     = new PublicKeyCredentialRpEntity(rp, rp, ""),
                Status = "ok",
                User   = new Fido2User
                {
                    Name        = "testuser",
                    Id          = Encoding.UTF8.GetBytes("testuser"),
                    DisplayName = "Test User",
                },
                Timeout = 60000,
            };

            IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
            {
                return(Task.FromResult(true));
            };

            var lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = rp,
                ServerName   = rp,
                Origin       = rp,
            });

            var ex = Assert.ThrowsAsync <Fido2VerificationException>(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback));

            Assert.Equal("User Present flag not set in authenticator data", ex.Result.Message);
        }
Example #19
0
        public async Task <AttestationVerificationSuccess> VerifyAsync(CredentialCreateOptions originalOptions, string expectedOrigin, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, IMetadataService metadataService, byte[] requestTokenBindingId)
        {
            AttestationType attnType;

            X509Certificate2[] trustPath = null;
            BaseVerify(expectedOrigin, 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 != "public-key")
            {
                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");
            }
            AuthenticatorData 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 id done in BaseVerify.
            // todo: test that implmentation

            // 7
            // Compute the hash of response.clientDataJSON using SHA - 256.
            byte[] hashedClientDataJson;
            byte[] hashedRpId;
            using (var sha = SHA256.Create())
            {
                hashedClientDataJson = sha.ComputeHash(Raw.Response.ClientDataJson);
                hashedRpId           = 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(hashedRpId))
            {
                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.
            var userVerified = 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);

            // A COSEAlgorithmIdentifier containing the identifier of the algorithm used to generate the attestation signature
            var alg = AttestationObject.AttStmt["alg"];
            // A byte string containing the attestation signature
            var sig = AttestationObject.AttStmt["sig"];
            // The elements of this array contain attestnCert and its certificate chain, each encoded in X.509 format
            var x5c = AttestationObject.AttStmt["x5c"];
            // The identifier of the ECDAA-Issuer public key
            var ecdaaKeyId = AttestationObject.AttStmt["ecdaaKeyId"];

            if (false == authData.AttestedCredentialDataPresent)
            {
                throw new Fido2VerificationException("Attestation flag not set on attestation data");
            }
            var credentialId             = authData.AttData.CredentialID;
            var credentialPublicKeyBytes = authData.AttData.CredentialPublicKey.ToArray();

            PeterO.Cbor.CBORObject credentialPublicKey = null;
            var coseKty = 0;
            var coseAlg = 0;

            try
            {
                credentialPublicKey = PeterO.Cbor.CBORObject.DecodeFromBytes(authData.AttData.CredentialPublicKey);
                coseKty             = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(1)].AsInt32();
                coseAlg             = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(3)].AsInt32();
            }
            catch (PeterO.Cbor.CBORException)
            {
                throw new Fido2VerificationException("Malformed credentialPublicKey");
            }
            byte[] data = new byte[AttestationObject.AuthData.Length + hashedClientDataJson.Length];
            Buffer.BlockCopy(AttestationObject.AuthData, 0, data, 0, AttestationObject.AuthData.Length);
            Buffer.BlockCopy(hashedClientDataJson, 0, data, AttestationObject.AuthData.Length, hashedClientDataJson.Length);
            // 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. 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
            switch (AttestationObject.Fmt)
            {
            // 14
            // validate the attStmt

            case "none":
            {
                // https://www.w3.org/TR/webauthn/#none-attestation

                if (0 != AttestationObject.AttStmt.Keys.Count && 0 != AttestationObject.AttStmt.Values.Count)
                {
                    throw new Fido2VerificationException("Attestation format none should have no attestation statement");
                }
                attnType  = AttestationType.None;
                trustPath = null;
            }
            break;

            case "tpm":
            {
                // https://www.w3.org/TR/webauthn/#tpm-attestation

                if (null == sig || PeterO.Cbor.CBORType.ByteString != sig.Type || 0 == sig.GetByteString().Length)
                {
                    throw new Fido2VerificationException("Invalid TPM attestation signature");
                }
                if ("2.0" != AttestationObject.AttStmt["ver"].AsString())
                {
                    throw new Fido2VerificationException("FIDO2 only supports TPM 2.0");
                }

                // 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 != AttestationObject.AttStmt["pubArea"] && PeterO.Cbor.CBORType.ByteString == AttestationObject.AttStmt["pubArea"].Type && 0 != AttestationObject.AttStmt["pubArea"].GetByteString().Length)
                {
                    pubArea = new PubArea(AttestationObject.AttStmt["pubArea"].GetByteString());
                }
                if (null == pubArea || null == pubArea.Unique || 0 == pubArea.Unique.Length)
                {
                    throw new Fido2VerificationException("Missing or malformed pubArea");
                }
                if (3 == coseKty)                                                                             // RSA
                {
                    var coseMod = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(-1)].GetByteString(); // modulus
                    var coseExp = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(-2)].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[PeterO.Cbor.CBORObject.FromObject(-1)].AsInt32();
                    var X     = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(-2)].GetByteString();
                    var Y     = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(-3)].GetByteString();

                    if (pubArea.EccCurve != AuthDataHelper.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");
                    }
                }
                // Concatenate authenticatorData and clientDataHash to form attToBeSigned.
                // see data variable

                // Validate that certInfo is valid
                CertInfo certInfo = null;
                if (null != AttestationObject.AttStmt["certInfo"] && PeterO.Cbor.CBORType.ByteString == AttestationObject.AttStmt["certInfo"].Type && 0 != AttestationObject.AttStmt["certInfo"].GetByteString().Length)
                {
                    certInfo = new CertInfo(AttestationObject.AttStmt["certInfo"].GetByteString());
                }
                if (null == certInfo || null == certInfo.ExtraData || 0 == certInfo.ExtraData.Length)
                {
                    throw new Fido2VerificationException("CertInfo invalid parsing TPM format attStmt");
                }
                // Verify that magic is set to TPM_GENERATED_VALUE and type is set to TPM_ST_ATTEST_CERTIFY
                // handled in parser, see certInfo.Magic

                // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg"
                if (null == alg || PeterO.Cbor.CBORType.Number != alg.Type || false == AuthDataHelper.algMap.ContainsKey(alg.AsInt32()))
                {
                    throw new Fido2VerificationException("Invalid TPM attestation algorithm");
                }
                if (!AuthDataHelper.GetHasher(AuthDataHelper.algMap[alg.AsInt32()]).ComputeHash(data).SequenceEqual(certInfo.ExtraData))
                {
                    throw new Fido2VerificationException("Hash value mismatch extraData and attToBeSigned");
                }

                // 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
                if (false == AuthDataHelper.GetHasher(AuthDataHelper.algMap[(int)certInfo.Alg]).ComputeHash(pubArea.Raw).SequenceEqual(certInfo.AttestedName))
                {
                    throw new Fido2VerificationException("Hash value mismatch attested and pubArea");
                }

                // If x5c is present, this indicates that the attestation type is not ECDAA
                if (null != x5c && PeterO.Cbor.CBORType.Array == x5c.Type && 0 != x5c.Count)
                {
                    if (null == x5c.Values || 0 == x5c.Values.Count || PeterO.Cbor.CBORType.ByteString != x5c.Values.First().Type || 0 == x5c.Values.First().GetByteString().Length)
                    {
                        throw new Fido2VerificationException("Malformed x5c in TPM attestation");
                    }

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

                    PeterO.Cbor.CBORObject coseKey = AuthDataHelper.CoseKeyFromCertAndAlg(aikCert, alg.AsInt32());

                    if (true != AuthDataHelper.VerifySigWithCoseKey(certInfo.Raw, coseKey, sig.GetByteString()))
                    {
                        throw new Fido2VerificationException("Bad signature in TPM with aikCert");
                    }

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

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

                    // 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
                    var SAN = AuthDataHelper.SANFromAttnCertExts(aikCert.Extensions);
                    if (null == SAN || 0 == SAN.Length)
                    {
                        throw new Fido2VerificationException("SAN missing from TPM attestation certificate");
                    }
                    // 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 directoryNameform 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 (false == SAN.Contains("TPMManufacturer") || false == SAN.Contains("TPMModel") || false == SAN.Contains("TPMVersion"))
                    {
                        throw new Fido2VerificationException("SAN missing TPMManufacturer, TPMModel, or TPMVersopm from TPM attestation certificate");
                    }
                    // 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 = AuthDataHelper.EKUFromAttnCertExts(aikCert.Extensions);
                    if (null == EKU || 0 != EKU.CompareTo("Attestation Identity Key Certificate (2.23.133.8.3)"))
                    {
                        throw new Fido2VerificationException("Invalid EKU on AIK certificate");
                    }

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

                    // 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 = AuthDataHelper.AaguidFromAttnCertExts(aikCert.Extensions);
                    if ((null != aaguid) && (!aaguid.SequenceEqual(Guid.Empty.ToByteArray())) && (!aaguid.SequenceEqual(authData.AttData.Aaguid.ToArray())))
                    {
                        throw new Fido2VerificationException("aaguid malformed");
                    }

                    // If successful, return attestation type AttCA and attestation trust path x5c.
                    attnType  = AttestationType.AttCa;
                    trustPath = x5c.Values
                                .Select(x => new X509Certificate2(x.GetByteString()))
                                .ToArray();
                }
                // 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");
                }
            }
            break;

            case "android-key":
            {
                // https://www.w3.org/TR/webauthn/#android-key-attestation

                // Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields
                if (0 == AttestationObject.AttStmt.Keys.Count || 0 == AttestationObject.AttStmt.Values.Count)
                {
                    throw new Fido2VerificationException("Attestation format packed must have attestation statement");
                }
                if (null == sig || PeterO.Cbor.CBORType.ByteString != sig.Type || 0 == sig.GetByteString().Length)
                {
                    throw new Fido2VerificationException("Invalid packed attestation signature");
                }
                // 2a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
                // using the attestation public key in attestnCert with the algorithm specified in alg
                if (null == x5c && PeterO.Cbor.CBORType.Array != x5c.Type && 0 == x5c.Count)
                {
                    throw new Fido2VerificationException("Malformed x5c in android-key attestation");
                }
                if (null == x5c.Values || 0 == x5c.Values.Count || PeterO.Cbor.CBORType.ByteString != x5c.Values.First().Type || 0 == x5c.Values.First().GetByteString().Length)
                {
                    throw new Fido2VerificationException("Malformed x5c in android-key attestation");
                }
                X509Certificate2 androidKeyCert   = null;
                ECDsaCng         androidKeyPubKey = null;
                try
                {
                    androidKeyCert   = new X509Certificate2(x5c.Values.First().GetByteString());
                    androidKeyPubKey = (ECDsaCng)androidKeyCert.GetECDsaPublicKey();         // attestation public key
                }
                catch (Exception ex)
                {
                    throw new Fido2VerificationException("Failed to extract public key from android key" + ex.Message);
                }
                if (null == alg || PeterO.Cbor.CBORType.Number != alg.Type || false == AuthDataHelper.algMap.ContainsKey(alg.AsInt32()))
                {
                    throw new Fido2VerificationException("Invalid attestation algorithm");
                }
                if (true != androidKeyPubKey.VerifyData(data, AuthDataHelper.SigFromEcDsaSig(sig.GetByteString()), AuthDataHelper.algMap[alg.AsInt32()]))
                {
                    throw new Fido2VerificationException("Invalid android key signature");
                }
                var cng = ECDsaCng.Create(new ECParameters
                    {
                        Curve = ECCurve.NamedCurves.nistP256,
                        Q     = new ECPoint
                        {
                            X = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(-2)].GetByteString(),
                            Y = credentialPublicKey[PeterO.Cbor.CBORObject.FromObject(-3)].GetByteString()
                        }
                    });
                // Verify that the public key in the first certificate in in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
                if (true != cng.VerifyData(data, AuthDataHelper.SigFromEcDsaSig(sig.GetByteString()), AuthDataHelper.algMap[alg.AsInt32()]))
                {
                    throw new Fido2VerificationException("Invalid android key signature");
                }

                // Verify that in the attestation certificate extension data:
                var attExtBytes = AuthDataHelper.AttestationExtensionBytes(androidKeyCert.Extensions);

                // 1. The value of the attestationChallenge field is identical to clientDataHash.
                var attestationChallenge = AuthDataHelper.GetAttestationChallenge(attExtBytes);
                if (false == hashedClientDataJson.SequenceEqual(attestationChallenge))
                {
                    throw new Fido2VerificationException("Mismatched between attestationChallenge and hashedClientDataJson verifying android key attestation certificate extension");
                }

                // 2. The AuthorizationList.allApplications field is not present, since PublicKeyCredential MUST be bound to the RP ID.
                if (true == AuthDataHelper.FindAllApplicationsField(attExtBytes))
                {
                    throw new Fido2VerificationException("Found all applications field in android key attestation certificate extension");
                }

                // 3. The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED ( which == 0).
                if (false == AuthDataHelper.IsOriginGenerated(attExtBytes))
                {
                    throw new Fido2VerificationException("Found origin field not set to KM_ORIGIN_GENERATED in android key attestation certificate extension");
                }

                // 4. The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN (which == 2).
                if (false == AuthDataHelper.IsPurposeSign(attExtBytes))
                {
                    throw new Fido2VerificationException("Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension");
                }

                attnType  = AttestationType.Basic;
                trustPath = x5c.Values
                            .Select(x => new X509Certificate2(x.GetByteString()))
                            .ToArray();
            }
            break;

            case "android-safetynet":
            {
                // https://www.w3.org/TR/webauthn/#android-safetynet-attestation

                // Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields
                if ((PeterO.Cbor.CBORType.TextString != AttestationObject.AttStmt["ver"].Type) || (0 == AttestationObject.AttStmt["ver"].AsString().Length))
                {
                    throw new Fido2VerificationException("Invalid version in SafetyNet data");
                }

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

                if ((PeterO.Cbor.CBORType.ByteString != AttestationObject.AttStmt["response"].Type) || (0 == AttestationObject.AttStmt["response"].GetByteString().Length))
                {
                    throw new Fido2VerificationException("Invalid response in SafetyNet data");
                }
                var response = AttestationObject.AttStmt["response"].GetByteString();
                var signedAttestationStatement = Encoding.UTF8.GetString(response);
                var jwtToken           = new JwtSecurityToken(signedAttestationStatement);
                X509SecurityKey[] keys = (jwtToken.Header["x5c"] as JArray)
                                         .Values <string>()
                                         .Select(x => new X509SecurityKey(
                                                     new X509Certificate2(Convert.FromBase64String(x))))
                                         .ToArray();
                if ((null == keys) || (0 == keys.Count()))
                {
                    throw new Fido2VerificationException("SafetyNet attestation missing x5c");
                }
                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuer           = false,
                    ValidateAudience         = false,
                    ValidateLifetime         = false,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKeys        = keys
                };

                var           tokenHandler = new JwtSecurityTokenHandler();
                SecurityToken validatedToken;

                tokenHandler.ValidateToken(
                    signedAttestationStatement,
                    validationParameters,
                    out validatedToken);

                if (false == (validatedToken.SigningKey is X509SecurityKey))
                {
                    throw new Fido2VerificationException("Safetynet signing key invalid");
                }

                var nonce   = "";
                var payload = false;
                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))
                    {
                        payload = bool.Parse(claim.Value);
                    }
                    if (("timestampMs" == claim.Type) && ("http://www.w3.org/2001/XMLSchema#integer64" == claim.ValueType))
                    {
                        DateTime dt = DateTimeHelper.UnixEpoch.AddMilliseconds(double.Parse(claim.Value));
                        if ((DateTime.UtcNow < dt) || (DateTime.UtcNow.AddMinutes(-1) > dt))
                        {
                            throw new Fido2VerificationException("Android SafetyNet timestampMs must be between one minute ago and now");
                        }
                    }
                }

                // 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 Android SafetyNet attestation");
                }
                if (!AuthDataHelper.GetHasher(HashAlgorithmName.SHA256).ComputeHash(data).SequenceEqual(Convert.FromBase64String(nonce)))
                {
                    throw new Fido2VerificationException("Android SafetyNet hash value mismatch");
                }

                // Verify that the attestation certificate is issued to the hostname "attest.android.com"
                if (false == ("attest.android.com").Equals((validatedToken.SigningKey as X509SecurityKey).Certificate.GetNameInfo(X509NameType.DnsName, false)))
                {
                    throw new Fido2VerificationException("Safetynet DnsName is not attest.android.com");
                }

                // Verify that the ctsProfileMatch attribute in the payload of response is true
                if (true != payload)
                {
                    throw new Fido2VerificationException("Android SafetyNet ctsProfileMatch must be true");
                }

                attnType  = AttestationType.Basic;
                trustPath = (jwtToken.Header["x5c"] as JArray)
                            .Values <string>()
                            .Select(x => new X509Certificate2(Convert.FromBase64String(x)))
                            .ToArray();
            }
            break;

            case "fido-u2f":
            {
                // https://www.w3.org/TR/webauthn/#fido-u2f-attestation

                // verify that aaguid is 16 empty bytes (note: required by fido2 conformance testing, could not find this in spec?)
                if (false == authData.AttData.Aaguid.SequenceEqual(Guid.Empty.ToByteArray()))
                {
                    throw new Fido2VerificationException("Aaguid was not empty parsing fido-u2f atttestation statement");
                }

                // 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 || PeterO.Cbor.CBORType.Array != x5c.Type || x5c.Count != 1)
                {
                    throw new Fido2VerificationException("Malformed x5c in fido - u2f attestation");
                }

                // 2a. the attestation certificate attestnCert MUST be the first element in the array
                if (null == x5c.Values || 0 == x5c.Values.Count || PeterO.Cbor.CBORType.ByteString != x5c.Values.First().Type || 0 == x5c.Values.First().GetByteString().Length)
                {
                    throw new Fido2VerificationException("Malformed x5c in fido-u2f attestation");
                }
                var cert = new X509Certificate2(x5c.Values.First().GetByteString());

                // 2b. If certificate public key is not an Elliptic Curve (EC) public key over the P-256 curve, terminate this algorithm and return an appropriate error
                var pubKey = (ECDsaCng)cert.GetECDsaPublicKey();
                if (CngAlgorithm.ECDsaP256 != pubKey.Key.Algorithm)
                {
                    throw new Fido2VerificationException("attestation certificate public key is not an Elliptic Curve (EC) public key over the P-256 curve");
                }

                // 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey from authenticatorData
                // done above

                // 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of [RFC8152]) to CTAP1/U2F public Key format
                var publicKeyU2F = AuthDataHelper.U2FKeyFromCOSEKey(credentialPublicKey);

                // 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
                var verificationData = new byte[1] {
                    0x00
                };
                verificationData = verificationData.Concat(hashedRpId).Concat(hashedClientDataJson).Concat(credentialId).Concat(publicKeyU2F.ToArray()).ToArray();

                // 6. Verify the sig using verificationData and certificate public key
                if (null == sig || PeterO.Cbor.CBORType.ByteString != sig.Type || 0 == sig.GetByteString().Length)
                {
                    throw new Fido2VerificationException("Invalid fido-u2f attestation signature");
                }
                var ecsig = AuthDataHelper.SigFromEcDsaSig(sig.GetByteString());
                if (null == ecsig)
                {
                    throw new Fido2VerificationException("Failed to decode fido-u2f attestation signature from ASN.1 encoded form");
                }
                if (true != pubKey.VerifyData(verificationData, ecsig, AuthDataHelper.algMap[coseAlg]))
                {
                    throw new Fido2VerificationException("Invalid fido-u2f attestation signature");
                }
                attnType  = AttestationType.Basic;
                trustPath = x5c.Values
                            .Select(x => new X509Certificate2(x.GetByteString()))
                            .ToArray();
            }
            break;

            case "packed":
            {
                // https://www.w3.org/TR/webauthn/#packed-attestation

                // Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.
                if (0 == AttestationObject.AttStmt.Keys.Count || 0 == AttestationObject.AttStmt.Values.Count)
                {
                    throw new Fido2VerificationException("Attestation format packed must have attestation statement");
                }
                if (null == sig || PeterO.Cbor.CBORType.ByteString != sig.Type || 0 == sig.GetByteString().Length)
                {
                    throw new Fido2VerificationException("Invalid packed attestation signature");
                }
                if (null == alg || PeterO.Cbor.CBORType.Number != alg.Type)
                {
                    throw new Fido2VerificationException("Invalid packed attestation algorithm");
                }

                // If x5c is present, this indicates that the attestation type is not ECDAA
                if (null != x5c)
                {
                    if (PeterO.Cbor.CBORType.Array != x5c.Type || 0 == x5c.Count || null != ecdaaKeyId)
                    {
                        throw new Fido2VerificationException("Malformed x5c array in packed attestation statement");
                    }
                    IEnumerator <PeterO.Cbor.CBORObject> enumerator = x5c.Values.GetEnumerator();
                    while (enumerator.MoveNext())
                    {
                        if (null == enumerator || null == enumerator.Current || PeterO.Cbor.CBORType.ByteString != enumerator.Current.Type || 0 == enumerator.Current.GetByteString().Length)
                        {
                            throw new Fido2VerificationException("Malformed x5c cert found in packed attestation statement");
                        }
                        var x5ccert = new X509Certificate2(enumerator.Current.GetByteString());
                        if (DateTime.UtcNow < x5ccert.NotBefore || DateTime.UtcNow > x5ccert.NotAfter)
                        {
                            throw new Fido2VerificationException("Packed signing certificate expired or not yet valid");
                        }
                    }

                    // The attestation certificate attestnCert MUST be the first element in the array.
                    var attestnCert = new X509Certificate2(x5c.Values.First().GetByteString());

                    // 2a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
                    // using the attestation public key in attestnCert with the algorithm specified in alg
                    var packedPubKey = (ECDsaCng)attestnCert.GetECDsaPublicKey();         // attestation public key
                    if (false == AuthDataHelper.algMap.ContainsKey(alg.AsInt32()))
                    {
                        throw new Fido2VerificationException("Invalid attestation algorithm");
                    }

                    var coseKey = AuthDataHelper.CoseKeyFromCertAndAlg(attestnCert, alg.AsInt32());

                    if (true != AuthDataHelper.VerifySigWithCoseKey(data, coseKey, sig.GetByteString()))
                    {
                        throw new Fido2VerificationException("Invalid full packed signature");
                    }

                    // Verify that attestnCert meets the requirements in https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
                    // 2b. Version MUST be set to 3
                    if (3 != attestnCert.Version)
                    {
                        throw new Fido2VerificationException("Packed x5c attestation certificate not V3");
                    }

                    // Subject field MUST contain C, O, OU, CN
                    // OU must match "Authenticator Attestation"
                    if (true != AuthDataHelper.IsValidPackedAttnCertSubject(attestnCert.Subject))
                    {
                        throw new Fido2VerificationException("Invalid attestation cert subject");
                    }

                    // 2c. If the related attestation root certificate is used for multiple authenticator models,
                    // the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte OCTET STRING
                    // verify that the value of this extension matches the aaguid in authenticatorData
                    var aaguid = AuthDataHelper.AaguidFromAttnCertExts(attestnCert.Extensions);
                    if (aaguid != null && !aaguid.SequenceEqual(authData.AttData.Aaguid.ToArray()))
                    {
                        throw new Fido2VerificationException("aaguid present in packed attestation but does not match aaguid from authData");
                    }

                    // 2d. The Basic Constraints extension MUST have the CA component set to false
                    if (AuthDataHelper.IsAttnCertCACert(attestnCert.Extensions))
                    {
                        throw new Fido2VerificationException("Attestion certificate has CA cert flag present");
                    }

                    // id-fido-u2f-ce-transports
                    var u2ftransports = AuthDataHelper.U2FTransportsFromAttnCert(attestnCert.Extensions);
                    attnType  = AttestationType.Basic;
                    trustPath = x5c.Values
                                .Select(x => new X509Certificate2(x.GetByteString()))
                                .ToArray();
                }
                // If ecdaaKeyId is present, then the attestation type is ECDAA
                else if (null != ecdaaKeyId)
                {
                    // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
                    // using ECDAA-Verify with ECDAA-Issuer public key identified by ecdaaKeyId
                    // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm

                    throw new Fido2VerificationException("ECDAA is not yet implemented");
                    // If successful, return attestation type ECDAA and attestation trust path ecdaaKeyId.
                    //attnType = AttestationType.ECDAA;
                    //trustPath = ecdaaKeyId;
                }
                // If neither x5c nor ecdaaKeyId is present, self attestation is in use
                else
                {
                    // Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData
                    if (alg.AsInt32() != coseAlg)
                    {
                        throw new Fido2VerificationException("Algorithm mismatch between credential public key and authenticator data in self attestation statement");
                    }
                    // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the credential public key with alg
                    if (true != AuthDataHelper.VerifySigWithCoseKey(data, credentialPublicKey, sig.GetByteString()))
                    {
                        throw new Fido2VerificationException("Failed to validate signature");
                    }

                    // If successful, return attestation type Self and empty attestation trust path.
                    attnType  = AttestationType.Self;
                    trustPath = null;
                }
            }
            break;

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

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

            /*
             * 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
             * */
            // todo: implement (this is not for attfmt none)
            // use aaguid (authData.AttData.Aaguid) to find root certs in metadata
            // use root plus trustPath to build trust chain

            if (null != metadataService)
            {
                MetadataTOCPayloadEntry entry = metadataService.GetEntry(authData.AttData.GuidAaguid);

                if (null != entry)
                {
                    if (entry.Hash != entry.MetadataStatement.Hash)
                    {
                        throw new Fido2VerificationException("Authenticator metadata statement has invalid hash");
                    }
                    if (null != entry.MetadataStatement)
                    {
                        var hasBasicFull = entry.MetadataStatement.AttestationTypes.Contains((ushort)MetadataAttestationType.ATTESTATION_BASIC_FULL);
                        if (false == hasBasicFull &&
                            null != trustPath && trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer)
                        {
                            throw new Fido2VerificationException("Attestation with full attestation from authentictor that does not support full attestation");
                        }
                        if (true == hasBasicFull && null != trustPath && trustPath.FirstOrDefault().Subject != trustPath.FirstOrDefault().Issuer)
                        {
                            var root  = new X509Certificate2(Convert.FromBase64String(entry.MetadataStatement.AttestationRootCertificates.FirstOrDefault()));
                            var chain = new X509Chain();
                            chain.ChainPolicy.ExtraStore.Add(root);
                            if (trustPath.Length > 1)
                            {
                                foreach (X509Certificate2 cert in trustPath.Skip(1).Reverse())
                                {
                                    chain.ChainPolicy.ExtraStore.Add(cert);
                                }
                            }
                            var valid = chain.Build(trustPath[0]);
                            if (false == valid)
                            {
                            }
                        }
                    }

                    foreach (StatusReport report in entry.StatusReports)
                    {
                        if (true == Enum.IsDefined(typeof(UndesiredAuthenticatorStatus), (UndesiredAuthenticatorStatus)report.Status))
                        {
                            throw new Fido2VerificationException("Authenticator found with undesirable status");
                        }
                    }
                }
            }

            /*
             * 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(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.
             * */
            // todo: implement (this is not for attfmt none)

            var result = new AttestationVerificationSuccess()
            {
                CredentialId = credentialId,
                PublicKey    = credentialPublicKeyBytes,
                User         = originalOptions.User,
                Counter      = BitConverter.ToUInt32(authData.SignCount.ToArray().Reverse().ToArray(), 0)
            };

            return(result);
        }
        public void TestAuthenticatorAttestationResponseNoAttestedCredentialData()
        {
            var challenge = RandomGenerator.Default.GenerateBytes(128);
            var rp        = "https://www.passwordless.dev";
            var authData  = new AuthenticatorData(
                SHA256.HashData(Encoding.UTF8.GetBytes(rp)),
                AuthenticatorFlags.UP | AuthenticatorFlags.UV,
                0,
                null
                ).ToByteArray();

            var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new
            {
                type      = "webauthn.create",
                challenge = challenge,
                origin    = rp,
            });

            var rawResponse = new AuthenticatorAttestationRawResponse
            {
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
                Response = new AuthenticatorAttestationRawResponse.ResponseData()
                {
                    AttestationObject = new CborMap {
                        { "fmt", "testing" },
                        { "attStmt", new CborMap() },
                        { "authData", authData }
                    }.Encode(),
                ClientDataJson = clientDataJson
                },
            };

            var origChallenge = new CredentialCreateOptions
            {
                Attestation            = AttestationConveyancePreference.Direct,
                AuthenticatorSelection = new AuthenticatorSelection
                {
                    AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                    RequireResidentKey      = true,
                    UserVerification        = UserVerificationRequirement.Discouraged,
                },
                Challenge        = challenge,
                ErrorMessage     = "",
                PubKeyCredParams = new List <PubKeyCredParam>()
                {
                    new PubKeyCredParam(COSE.Algorithm.ES256)
                },
                Rp     = new PublicKeyCredentialRpEntity(rp, rp, ""),
                Status = "ok",
                User   = new Fido2User
                {
                    Name        = "testuser",
                    Id          = Encoding.UTF8.GetBytes("testuser"),
                    DisplayName = "Test User",
                },
                Timeout = 60000,
            };

            IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) =>
            {
                return(Task.FromResult(true));
            };

            IFido2 lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = rp,
                ServerName   = rp,
                Origins      = new HashSet <string> {
                    rp
                },
            });

            var ex = Assert.ThrowsAsync <Fido2VerificationException>(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback));

            Assert.Equal("Attestation flag not set on attestation data", ex.Result.Message);
        }
        public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin)
        {
            var challenge = RandomGenerator.Default.GenerateBytes(128);
            var rp        = origin;
            var acd       = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
            var authData  = new AuthenticatorData(
                SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(origin)),
                AuthenticatorFlags.UP | AuthenticatorFlags.AT,
                0,
                acd,
                null
                ).ToByteArray();
            var clientDataJson = Encoding.UTF8.GetBytes(
                JsonConvert.SerializeObject
                (
                    new
            {
                Type      = "webauthn.create",
                Challenge = challenge,
                Origin    = rp,
            }
                )
                );
            var rawResponse = new AuthenticatorAttestationRawResponse
            {
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
                Response = new AuthenticatorAttestationRawResponse.ResponseData()
                {
                    AttestationObject = CBORObject.NewMap().Add("fmt", "none").Add("attStmt", CBORObject.NewMap()).Add("authData", authData).EncodeToBytes(),
                    ClientDataJson    = clientDataJson
                },
            };

            var origChallenge = new CredentialCreateOptions
            {
                Attestation            = AttestationConveyancePreference.Direct,
                AuthenticatorSelection = new AuthenticatorSelection
                {
                    AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                    RequireResidentKey      = true,
                    UserVerification        = UserVerificationRequirement.Required,
                },
                Challenge        = challenge,
                ErrorMessage     = "",
                PubKeyCredParams = new List <PubKeyCredParam>()
                {
                    new PubKeyCredParam
                    {
                        Alg  = COSE.Algorithm.ES256,
                        Type = PublicKeyCredentialType.PublicKey,
                    }
                },
                Rp     = new PublicKeyCredentialRpEntity(rp, rp, ""),
                Status = "ok",
                User   = new Fido2User
                {
                    Name        = "testuser",
                    Id          = Encoding.UTF8.GetBytes("testuser"),
                    DisplayName = "Test User",
                },
                Timeout = 60000,
            };

            IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
            {
                return(Task.FromResult(true));
            };

            var lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = rp,
                ServerName   = rp,
                Origin       = expectedOrigin,
            });

            var result = await lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback);
        }
        public async Task <JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
        {
            try
            {
                // 1. get the options we sent the client
                var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var options     = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
                {
                    var users = await _fido2Storage.GetUsersByCredentialIdAsync(args.CredentialId);

                    if (users.Count > 0)
                    {
                        return(false);
                    }

                    return(true);
                };

                // 2. Verify and make the credentials
                var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback);

                // 3. Store the credentials in db
                await _fido2Storage.AddCredentialToUser(options.User, new FidoStoredCredential
                {
                    Username         = options.User.Name,
                    Descriptor       = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                    PublicKey        = success.Result.PublicKey,
                    UserHandle       = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType         = success.Result.CredType,
                    RegDate          = DateTime.Now,
                    AaGuid           = success.Result.Aaguid
                });

                // 4. return "ok" to the client

                var user = await _userManager.GetUserAsync(User);

                if (user == null)
                {
                    return(Json(new CredentialMakeResult {
                        Status = "error", ErrorMessage = _sharedLocalizer["FIDO2_USER_NOTFOUND", _userManager.GetUserId(User)]
                    }));
                }

                await _userManager.SetTwoFactorEnabledAsync(user, true);

                var userId = await _userManager.GetUserIdAsync(user);

                return(Json(success));
            }
            catch (Exception e)
            {
                return(Json(new CredentialMakeResult {
                    Status = "error", ErrorMessage = FormatException(e)
                }));
            }
        }
Example #23
0
        public async Task <IActionResult> RegisterCallback([FromBody] AuthenticatorAttestationRawResponse model)
        {
            var sub = HttpContext.User.Claims.FirstOrDefault(x => x.Type == "sub")?.Value;

            if (string.IsNullOrEmpty(sub))
            {
                return(RedirectToAction("Index", "Home"));
            }
            var user = await _users.FindByIdAsync(sub);

            if (user == null)
            {
                return(RedirectToAction("Index", "Home"));
            }

            try
            {
                // 1. get the options we sent the client
                var jsonOptions       = HttpContext.Session.GetString("fido2.attestationOptions");
                var options           = CredentialCreateOptions.FromJson(jsonOptions);
                var authenticatorName = HttpContext.Session.GetString("fido2.attestationOptions.authenticatorType");
                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
                {
                    var users = _authContext.FidoLogins.Where(l => l.PublicKeyIdBytes.SequenceEqual(args.CredentialId));
                    if (users.Count() > 0)
                    {
                        return(false);
                    }

                    return(true);
                };

                // 2. Verify and make the credentials
                var success = await _lib.MakeNewCredentialAsync(model, options, callback);

                var parsedResponse = AuthenticatorAttestationResponse.Parse(model);;
                var authData       = new AuthenticatorData(parsedResponse.AttestationObject.AuthData);
                var dbUser         = _authContext.Users.First(x => x.Id == user.Id);
                dbUser.TwoFactorEnabled = true;
                var login = new FidoLogin()
                {
                    PublicKeyIdBytes  = success.Result.CredentialId,
                    PublicKeyId       = Fido2NetLib.Base64Url.Encode(success.Result.CredentialId),
                    AaGuid            = success.Result.Aaguid.ToString(),
                    PublicKey         = success.Result.PublicKey,
                    SignatureCounter  = success.Result.Counter,
                    CredType          = success.Result.CredType,
                    RegistrationDate  = DateTime.Now,
                    User              = dbUser,
                    UserHandle        = success.Result.User.Id,
                    AuthenticatorName = authenticatorName
                };
                _authContext.FidoLogins.Add(login);
                _authContext.SaveChanges();


                // 4. return "ok" to the client
                return(Json(new { success = true }));
            }
            catch (Exception e)
            {
                return(Json(new { error = true }));
            }
        }
Example #24
0
        public async Task <FidoStoredCredential> AddSecurityKeyAsync(string userEmail, IJSRuntime jsRuntime)
        {
            if (string.IsNullOrWhiteSpace(userEmail))
            {
                throw new ArgumentNullException(nameof(userEmail));
            }

            if (jsRuntime == null)
            {
                throw new ArgumentNullException(nameof(jsRuntime));
            }

            var fidoUser = new Fido2User
            {
                Id          = Encoding.UTF8.GetBytes(userEmail),
                Name        = userEmail,
                DisplayName = userEmail
            };

            var authenticatorSelection = new AuthenticatorSelection
            {
                RequireResidentKey      = RequireResidentKey,
                UserVerification        = UserVerification,
                AuthenticatorAttachment = null // platform and cross-platform
            };

            var exts = new AuthenticationExtensionsClientInputs
            {
                Extensions            = true,
                UserVerificationIndex = true,
                Location = true,
                UserVerificationMethod = true,
                BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds
                {
                    FAR = float.MaxValue,
                    FRR = float.MaxValue
                }
            };

            var credentials = await GetCredentialsByUserEmail(userEmail);

            var options = _lib.RequestNewCredential(fidoUser, credentials.Select(x => x.Descriptor).ToList(), authenticatorSelection, AttestationType, exts);

            var attestationResponseJson = await jsRuntime.InvokeAsync <string>("createCredentials", TimeSpan.FromMinutes(3), JsonConvert.SerializeObject(options, new JsonSerializerSettings {
                Converters = new List <JsonConverter> {
                    new StringEnumConverter()
                }
            }));

            var attestationResponse = JsonConvert.DeserializeObject <AuthenticatorAttestationRawResponse>(attestationResponseJson);

            // Create callback so that lib can verify credential id is unique to this user
            IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
            {
                var users = await GetUsersByCredentialIdAsync(args.CredentialId);

                if (users.Count > 0)
                {
                    return(false);
                }

                return(true);
            };

            // Verify and make the credentials
            var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback);

            // Store the credentials in db
            return(await _fidoCredentialsRepository.AddAsync(new FidoStoredCredential
            {
                UserId = options.User.Id,
                Username = options.User.Name,
                SecurityKeyName = $"Security Key",
                Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                PublicKey = success.Result.PublicKey,
                UserHandle = success.Result.User.Id,
                SignatureCounter = success.Result.Counter,
                CredType = success.Result.CredType,
                RegDate = DateTime.UtcNow,
                AaGuid = success.Result.Aaguid
            }));
        }
        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);
        }
            public async Task <Fido2.CredentialMakeResult> MakeAttestationResponse()
            {
                _attestationObject.Set("authData", _authData);

                var attestationResponse = new AuthenticatorAttestationRawResponse
                {
                    Type     = PublicKeyCredentialType.PublicKey,
                    Id       = new byte[] { 0xf1, 0xd0 },
                    RawId    = new byte[] { 0xf1, 0xd0 },
                    Response = new AuthenticatorAttestationRawResponse.ResponseData()
                    {
                        AttestationObject = _attestationObject.EncodeToBytes(),
                        ClientDataJson    = _clientDataJson,
                    },
                    Extensions = new AuthenticationExtensionsClientOutputs()
                    {
                        AppID = true,
                        AuthenticatorSelection = true,
                        BiometricAuthenticatorPerformanceBounds = true,
                        GenericTransactionAuthorization         = new byte[] { 0xf1, 0xd0 },
                        SimpleTransactionAuthorization          = "test",
                        Extensions             = new string[] { "foo", "bar" },
                        Example                = "test",
                        Location               = new GeoCoordinatePortable.GeoCoordinate(42.523714, -71.040860),
                        UserVerificationIndex  = new byte[] { 0xf1, 0xd0 },
                        UserVerificationMethod = new ulong[][]
                        {
                            new ulong[]
                            {
                                4 // USER_VERIFY_PASSCODE_INTERNAL
                            },
                        },
                    }
                };

                var origChallenge = new CredentialCreateOptions
                {
                    Attestation            = AttestationConveyancePreference.Direct,
                    AuthenticatorSelection = new AuthenticatorSelection
                    {
                        AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                        RequireResidentKey      = true,
                        UserVerification        = UserVerificationRequirement.Required,
                    },
                    Challenge        = _challenge,
                    ErrorMessage     = "",
                    PubKeyCredParams = new List <PubKeyCredParam>()
                    {
                        new PubKeyCredParam
                        {
                            Alg  = COSE.Algorithm.ES256,
                            Type = PublicKeyCredentialType.PublicKey,
                        }
                    },
                    Rp     = new PublicKeyCredentialRpEntity(rp, rp, ""),
                    Status = "ok",
                    User   = new Fido2User
                    {
                        Name        = "testuser",
                        Id          = Encoding.UTF8.GetBytes("testuser"),
                        DisplayName = "Test User",
                    },
                    Timeout = 60000,
                };

                IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
                {
                    return(Task.FromResult(true));
                };

                var lib = new Fido2(new Fido2Configuration()
                {
                    ServerDomain = rp,
                    ServerName   = rp,
                    Origin       = rp,
                });

                var credentialMakeResult = await lib.MakeNewCredentialAsync(attestationResponse, origChallenge, callback);

                return(credentialMakeResult);
            }
        /// <summary>
        /// SetRegisterCredentialResult method implementation
        /// </summary>
        private int SetRegisterCredentialResult(AuthenticationContext ctx, string jsonResponse, out string error)
        {
            bool isDeserialized = false;

            try
            {
                string jsonOptions = ctx.CredentialOptions;
                if (string.IsNullOrEmpty(jsonOptions))
                {
                    throw new ArgumentNullException(jsonOptions);
                }
                if (string.IsNullOrEmpty(jsonResponse))
                {
                    throw new ArgumentNullException(jsonResponse);
                }

                MFAWebAuthNUser user = RuntimeRepository.GetUser(Config, ctx.UPN);
                if (user != null)
                {
                    CredentialCreateOptions options = CredentialCreateOptions.FromJson(jsonOptions);

#pragma warning disable CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
                    IsCredentialIdUniqueToUserAsyncDelegate callback = async(args) =>
#pragma warning restore CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
                    {
                        var users = RuntimeRepository.GetUsersByCredentialId(Config, user, args.CredentialId);
                        if (users.Count > 0)
                        {
                            return(false);
                        }
                        return(true);
                    };
                    AuthenticatorAttestationRawResponse attestationResponse = JsonConvert.DeserializeObject <AuthenticatorAttestationRawResponse>(jsonResponse);
                    isDeserialized = true;
                    CredentialMakeResult success = _webathn.SetRegisterCredentialResult(attestationResponse, options, callback).Result;

                    RuntimeRepository.AddUserCredential(Config, user, new MFAUserCredential
                    {
                        Descriptor       = new MFAPublicKeyCredentialDescriptor(success.Result.CredentialId),
                        PublicKey        = success.Result.PublicKey,
                        UserHandle       = success.Result.User.Id,
                        SignatureCounter = success.Result.Counter,
                        CredType         = success.Result.CredType,
                        RegDate          = DateTime.Now,
                        AaGuid           = success.Result.Aaguid,
                        NickName         = ctx.NickName
                    });
                    error = string.Empty;
                    return((int)AuthenticationResponseKind.Biometrics);
                }
                else
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), System.Diagnostics.EventLogEntryType.Error, 5000);
                    error = string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !");
                    return((int)AuthenticationResponseKind.Error);
                }
            }
            catch (Exception e)
            {
                if (isDeserialized)
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), EventLogEntryType.Error, 5000);
                }
                else
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, jsonResponse), EventLogEntryType.Error, 5000);
                }
                error = e.Message;
                return((int)AuthenticationResponseKind.Error);
            }
        }
Example #28
0
        public async Task <JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
        {
            try
            {
                // 1. get the options we sent the client
                //var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
                var jsonOptions = await _distributedCache.GetStringAsync(UniqueId);

                if (string.IsNullOrEmpty(jsonOptions))
                {
                    throw new Exception("Can't get CredentialOptions from cache");
                }
                var options = CredentialCreateOptions.FromJson(jsonOptions);

                // 2. Create callback so that lib can verify credential id is unique to this user
                IsCredentialIdUniqueToUserAsyncDelegate callback = async(IsCredentialIdUniqueToUserParams args) =>
                {
                    var users = await _fido2Storage.GetUsersByCredentialIdAsync(args.CredentialId);

                    if (users.Count > 0)
                    {
                        return(false);
                    }

                    return(true);
                };

                // 2. Verify and make the credentials
                var success = await _lib.MakeNewCredentialAsync(attestationResponse, options, callback);

                // 3. Store the credentials in db
                await _fido2Storage.AddCredentialToUser(options.User, new FidoStoredCredential
                {
                    Username         = options.User.Name,
                    Descriptor       = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
                    PublicKey        = success.Result.PublicKey,
                    UserHandle       = success.Result.User.Id,
                    SignatureCounter = success.Result.Counter,
                    CredType         = success.Result.CredType,
                    RegDate          = DateTime.Now,
                    AaGuid           = success.Result.Aaguid
                });

                // 4. return "ok" to the client
                var user = await _userManager.GetUserAsync(User);

                if (user == null)
                {
                    return(Json(new CredentialMakeResult {
                        Status = "error", ErrorMessage = $"Unable to load user with ID '{_userManager.GetUserId(User)}'."
                    }));
                }

                await _userManager.SetTwoFactorEnabledAsync(user, true);

                if (await _userManager.CountRecoveryCodesAsync(user) == 0)
                {
                    var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);

                    RecoveryCodes = recoveryCodes.ToArray();
                }

                return(Json(success));
            }
            catch (Exception e)
            {
                return(Json(new CredentialMakeResult {
                    Status = "error", ErrorMessage = FormatException(e)
                }));
            }
        }
Example #29
0
            public async Task <Fido2.CredentialMakeResult> MakeAttestationResponse()
            {
                _attestationObject.Set("authData", _authData);

                var attestationResponse = new AuthenticatorAttestationRawResponse
                {
                    Type     = PublicKeyCredentialType.PublicKey,
                    Id       = new byte[] { 0xf1, 0xd0 },
                    RawId    = new byte[] { 0xf1, 0xd0 },
                    Response = new AuthenticatorAttestationRawResponse.ResponseData()
                    {
                        AttestationObject = _attestationObject.EncodeToBytes(),
                        ClientDataJson    = _clientDataJson,
                    }
                };

                var origChallenge = new CredentialCreateOptions
                {
                    Attestation            = AttestationConveyancePreference.Direct,
                    AuthenticatorSelection = new AuthenticatorSelection
                    {
                        AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                        RequireResidentKey      = true,
                        UserVerification        = UserVerificationRequirement.Required,
                    },
                    Challenge        = _challenge,
                    ErrorMessage     = "",
                    PubKeyCredParams = new List <PubKeyCredParam>()
                    {
                        new PubKeyCredParam
                        {
                            Alg  = -7,
                            Type = PublicKeyCredentialType.PublicKey,
                        }
                    },
                    Rp     = new PublicKeyCredentialRpEntity(rp, rp, ""),
                    Status = "ok",
                    User   = new Fido2User
                    {
                        Name        = "testuser",
                        Id          = Encoding.UTF8.GetBytes("testuser"),
                        DisplayName = "Test User",
                    },
                    Timeout = 60000,
                };

                IsCredentialIdUniqueToUserAsyncDelegate callback = (args) =>
                {
                    return(Task.FromResult(true));
                };

                var lib = new Fido2(new Fido2Configuration()
                {
                    ServerDomain = rp,
                    ServerName   = rp,
                    Origin       = rp,
                });

                var credentialMakeResult = await lib.MakeNewCredentialAsync(attestationResponse, origChallenge, callback);

                return(credentialMakeResult);
            }