Exemplo n.º 1
0
 public CredentialAssertion(byte[] credentialId, string credentialType, ClientData clientData, byte[] signature,
                            byte[] userHandle, AuthenticatorData authenticatorData) : base(credentialId, credentialType, clientData)
 {
     Signature         = signature;
     UserHandle        = userHandle;
     AuthenticatorData = authenticatorData;
 }
Exemplo n.º 2
0
        public static CredentialAssertion ToCredentialAssertion(this PublicKeyCredentialAssertion browserObject)
        {
            var credentialId      = WebEncoders.Base64UrlDecode(browserObject.Id);
            var clientData        = GetClientDataFromBase64UrlString(browserObject.Response.ClientDataJSON);
            var signature         = WebEncoders.Base64UrlDecode(browserObject.Response.Signature);
            var userHandle        = browserObject.Response.UserHandle == null ? null : WebEncoders.Base64UrlDecode(browserObject.Response.UserHandle);
            var authenticatorData = new AuthenticatorData(WebEncoders.Base64UrlDecode(browserObject.Response.AuthenticatorData));

            return(new CredentialAssertion(credentialId, browserObject.Type, clientData, signature, userHandle, authenticatorData));
        }
Exemplo n.º 3
0
        // Only API call
        public async Task <AuthorizationResponse> SignInAsync(SecurityKeySignInModel parameters)
        {
            var assertionRawResponse = parameters.AuthenticatorAssertionRawResponse;

            // Get the assertion options we sent the client
            var jsonOptions = _memoryCache.Get <string>(Convert.ToBase64String(assertionRawResponse.Id));
            var options     = AssertionOptions.FromJson(jsonOptions);

            // Get registered credential from database
            var creds = await GetCredentialById(assertionRawResponse.Id);

            if (creds == null)
            {
                throw new Exception("Unknown credentials");
            }

            // Get credential counter from database
            var storedCounter = creds.SignatureCounter;

            // Create callback to check if userhandle owns the credentialId
            IsUserHandleOwnerOfCredentialIdAsync callback = async(args) =>
            {
                var storedCreds = await GetCredentialsByUserHandleAsync(args.UserHandle);

                return(storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)));
            };

            // Make the assertion
            var res = await _lib.MakeAssertionAsync(assertionRawResponse, options, creds.PublicKey, storedCounter, callback);

            // Store the updated counter
            await UpdateCounter(res.CredentialId, res.Counter);

            // Get authenticator flags
            var authData = new AuthenticatorData(assertionRawResponse.Response.AuthenticatorData);

            if (authData.UserPresent && authData.UserVerified)
            {
                var user = await _userManager.FindByNameAsync(creds.Username);

                if (user == null)
                {
                    throw new HESException(HESCode.UserNotFound);
                }

                await _signInManager.SignInAsync(user, parameters.RememberMe);

                return(AuthorizationResponse.Success(user));
            }

            return(AuthorizationResponse.Error(HESCode.AuthenticatorNotFIDO2));
        }
Exemplo n.º 4
0
 /// <summary>
 /// Sync the current authenticator's time with its server
 /// </summary>
 public void Sync()
 {
     if (AuthenticatorData != null)
     {
         try
         {
             AuthenticatorData.Sync();
         }
         catch (EncrpytedSecretDataException)
         {
             // reset lastsync to force sync on next decryption
         }
     }
 }
Exemplo n.º 5
0
        public async Task TestPackedAttestationAsync()
        {
            var jsonPost = JsonConvert.DeserializeObject <AuthenticatorAttestationRawResponse>(File.ReadAllText("./attestationResultsPacked.json"));
            var options  = JsonConvert.DeserializeObject <CredentialCreateOptions>(File.ReadAllText("./attestationOptionsPacked.json"));
            var o        = AuthenticatorAttestationResponse.Parse(jsonPost);
            await o.VerifyAsync(options, _config, (x) => Task.FromResult(true), _metadataService, null);

            byte[] ad       = o.AttestationObject.AuthData;
            var    authData = new AuthenticatorData(ad);

            Assert.True(authData.ToByteArray().SequenceEqual(ad));
            var acdBytes = authData.AttestedCredentialData.ToByteArray();
            var acd      = new AttestedCredentialData(acdBytes);

            Assert.True(acd.ToByteArray().SequenceEqual(acdBytes));
        }
Exemplo n.º 6
0
        public void TestAuthenticatorData()
        {
            byte[]       rpId         = Encoding.UTF8.GetBytes("fido2.azurewebsites.net/");
            var          rpIdHash     = SHA256.Create().ComputeHash(rpId);
            var          flags        = AuthenticatorFlags.AT | AuthenticatorFlags.ED | AuthenticatorFlags.UP | AuthenticatorFlags.UV;
            const ushort signCount    = 0xf1d0;
            var          aaguid       = new Guid("F1D0F1D0-F1D0-F1D0-F1D0-F1D0F1D0F1D0");
            var          credentialID = new byte[] { 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, };
            var          ecdsa        = MakeECDsa(COSE.Algorithm.ES256, COSE.EllipticCurve.P256);
            var          ecparams     = ecdsa.ExportParameters(true);
            var          cpk          = MakeCredentialPublicKey(COSE.KeyType.EC2, COSE.Algorithm.ES256, COSE.EllipticCurve.P256, ecparams.Q.X, ecparams.Q.Y);

            var acd      = new AttestedCredentialData(aaguid, credentialID, cpk);
            var extBytes = CBORObject.NewMap().Add("testing", true).EncodeToBytes();
            var exts     = new Extensions(extBytes);

            var ad = new AuthenticatorData(rpIdHash, flags, signCount, acd, exts);

            Assert.True(ad.RpIdHash.SequenceEqual(rpIdHash));
            Assert.True(ad.HasAttestedCredentialData | ad.UserPresent | ad.UserVerified | ad.HasExtensionsData);
            Assert.True(ad.SignCount == signCount);
            Assert.True(ad.AttestedCredentialData.ToByteArray().SequenceEqual(acd.ToByteArray()));
            Assert.True(ad.Extensions.GetBytes().SequenceEqual(extBytes));
        }
        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.
        }
        public AttestationVerificationSuccess VerifyCredentialCreateOptions(RegisterCredentialOptions originalOptions, Fido2Configuration config, IsCredentialIdUniqueToUserDelegate 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 VerificationException("AttestationResponse is not type webauthn.create");
            }

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

            if (null == AttestationObject.AuthData || 0 == AttestationObject.AuthData.Length)
            {
                throw new VerificationException("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 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

            // 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.HasAttestedCredentialData)
            {
                throw new VerificationException("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, config.RequireValidAttestationRoot);
                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, config.RequireValidAttestationRoot);
                break;

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

            default: throw new VerificationException("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 == 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.
             * */
            // 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.AttestedCredentialData.CredentialID,
                PublicKey    = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(),
                User         = originalOptions.User,
                Counter      = authData.SignCount,
                CredType     = AttestationObject.Fmt,
                Aaguid       = authData.AttestedCredentialData.AaGuid,
            };

            return(result);
        }
Exemplo n.º 9
0
 public async Task<AuthenticatorData> RestoreAuthenticatorDataAsync(string serial, string restoreCode)
 {
     if (string.IsNullOrWhiteSpace(serial)) throw new ArgumentNullException(nameof(serial));
     if (string.IsNullOrWhiteSpace(restoreCode)) throw new ArgumentNullException(nameof(restoreCode));
     Validation.ValidateSerial(serial);
     Validation.ValidateRestoreCode(restoreCode);
     serial = serial.ToUpperInvariant();
     var result = new AuthenticatorData()
     {
         RegionCode = serial.Substring(0, 2),
         Serial = serial,
         RestoreCode = restoreCode
     };
     var serialBytes = Encoding.UTF8.GetBytes(serial.Replace("-", ""));
     LogVerbose($"Attempting to initiate restore for Serial '{serial}' was successful.");
     var initRestoreResponse = await InitiateRestore(result.RegionCode, serialBytes);
     LogVerbose($"Initiate restore for Serial '{serial}' was successful.");
     result.SecretKey = await VerifyRestoreAndGetSecretKeyAsync(result.RegionCode, restoreCode, serialBytes, initRestoreResponse);
     LogInformation($"Restore verification for Serial '{serial}' was successful and secret key was retrieved.");
     try
     {
         result.ServerTimeDifference = await GetServerTimeDifferenceAsync(result.RegionCode).ConfigureAwait(false);
     }
     catch (AuthenticatorException ex)
     {
         LogWarning($"Failed to Sync time while restoring authenticator data. Assuming zero time difference. Exception: {ex.Message}");
     }
     return result;
 }
Exemplo n.º 10
0
        public async Task<AuthenticatorData> EnrollAuthenticatorAsync(string regionCode)
        {
            if (string.IsNullOrWhiteSpace(regionCode)) throw new ArgumentNullException(nameof(regionCode));
            Validation.ValidateRegionCode(regionCode);

            LogVerbose($"Initiating enroll request for region code: '{regionCode}'.");
            var result = new AuthenticatorData()
            {
                RegionCode = regionCode
            };
            var enrollRequest = new byte[38];
            var otp = GenerateOneTypePad(20);
            otp.CopyTo(enrollRequest, 0);
            Encoding.UTF8.GetBytes(regionCode, 0, 2, enrollRequest, otp.Length);
            Encoding.UTF8.GetBytes(AuthenticatorDefaults.DeviceName, 0, AuthenticatorDefaults.DeviceName.Length, enrollRequest, otp.Length);

            using (var rsa = new RSACryptoServiceProvider())
            {
                rsa.ImportParameters(EnrollmentServiceRsaParameters);
                var encrypted = rsa.Encrypt(enrollRequest, false);
                using (var httpClient = new HttpClient())
                {
                    using (var postStream = new MemoryStream(encrypted))
                    {
                        var postContent = new StreamContent(postStream);
                        postContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                        var response = await httpClient.PostAsync(GetUri(regionCode, EnrollUrl), postContent).ConfigureAwait(false);
                        if (!response.IsSuccessStatusCode)
                        {
                            var error = $"Enroll request failed because server returned error code: {response.StatusCode}";
                            LogError(error);
                            throw new AuthenticatorException(error);
                        }
                        var enrollResult = await response.Content.ReadAsByteArrayAsync();
                        if (enrollResult.Length != 45)
                        {
                            var error = $"Enroll request failed because server returned invalid content length. Expected 45, Actual {enrollResult.Length}";
                            LogError(error);
                            throw new AuthenticatorException(error);
                        }
                        var serverTime = DateFromServerBytes(enrollResult);
                        var ourTime = DateTimeOffset.Now;
                        var difference = serverTime - ourTime;
                        LogInformation($"Enroll successfull got server time difference for region: '{regionCode}' was successful. Server time '{serverTime}', Our time '{ourTime}', and time difference is '{difference}'.");
                        result.ServerTimeDifference = difference;

                        result.Serial = Encoding.UTF8.GetString(enrollResult, 8, 17);
                        for (var i = 0; i < otp.Length; i++)
                        {
                            otp[i] ^= enrollResult[i + 25];
                        }
                        result.SecretKey = otp;
                        result.RestoreCode = GetRestoreCode(result.Serial, result.SecretKey);
                        
                        LogInformation($"Enroll new serial number is : '{result.Serial}' is complete.");
                    }
                }
            }
            return result;
        }
        public async Task UpdateAuthenticatorAsync(AuthenticatorData data)
        {
            if (data == null) throw new ArgumentNullException(nameof(data));

            ValidateAuthenticatorData(data);
            var file = PathHelper.GetAuthenticatorsSavePath(data.AccountName);
            if (!File.Exists(file))
            {
                var error = $"Data for an account with the same name {data.AccountName} doesn't exist.";
                _logger.LogWarning("Update authenticator failed. " + error);
                throw new ArgumentException(error, nameof(data.AccountName));
            }
            
            await GetEnrollmentDataAsync(data);
            await SaveAuthenticatorAsync(data, file).ConfigureAwait(false);
        }
Exemplo n.º 12
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 }));
            }
        }
        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);
        }
        /// <summary>
        /// SetLoginAssertionResult method implementation
        /// </summary>
        private int SetLoginAssertionResult(AuthenticationContext ctx, string jsonResponse, out string error)
        {
            bool isDeserialized = false;

            try
            {
                string jsonOptions = ctx.AssertionOptions;
                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)
                {
                    AssertionOptions options = AssertionOptions.FromJson(jsonOptions);
                    try
                    {
                        AuthenticatorAssertionRawResponse clientResponse = JsonConvert.DeserializeObject <AuthenticatorAssertionRawResponse>(jsonResponse);
                        isDeserialized = true;

                        MFAUserCredential creds = RuntimeRepository.GetCredentialById(Config, user, clientResponse.Id);
                        if (creds == null)
                        {
                            throw new Exception("Unknown credentials");
                        }

                        AuthenticatorData authData = new AuthenticatorData(clientResponse.Response.AuthenticatorData);
                        bool isnocount             = Utilities.IsNoCounterDevice(this.Config.WebAuthNProvider.Configuration, ctx);
                        uint authCounter           = 0;
                        uint storedCounter         = 0;

                        if (!isnocount)
                        {
                            authCounter   = authData.SignCount;
                            storedCounter = creds.SignatureCounter;
                        }
                        else
                        if ((authCounter > 0) && (authCounter <= storedCounter))
                        {
                            ResourcesLocale Resources = new ResourcesLocale(ctx.Lcid);
                            throw new Exception(Resources.GetString(ResourcesLocaleKind.FIDOHtml, "BIOERRORAUTHREPLAY"));
                        }

#pragma warning disable CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
                        IsUserHandleOwnerOfCredentialIdAsync 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 storedCreds = RuntimeRepository.GetCredentialsByUserHandle(Config, user, args.UserHandle);
                            return(storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)));
                        };

                        // Apple counter always 0
                        AssertionVerificationResult res = _webathn.SetAssertionResult(clientResponse, options, creds.PublicKey, storedCounter, callback).Result;
                        if (!isnocount)
                        {
                            RuntimeRepository.UpdateCounter(Config, user, res.CredentialId, res.Counter);
                        }
                        else
                        {
                            RuntimeRepository.UpdateCounter(Config, user, res.CredentialId, 0);
                        }

                        if (!authData.UserPresent || !authData.UserVerified)
                        {
                            switch (creds.CredType)
                            {
                            case "none":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.None));
                                break;

                            case "android-key":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.AndroidKey));
                                break;

                            case "android-safetynet":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.AndroidSafetyNet));
                                break;

                            case "fido-u2f":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.Fido2U2f));
                                break;

                            case "packed":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.Packed));
                                break;

                            case "tpm":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.TPM));
                                break;

                            case "apple":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.Apple));
                                break;

                            default:
                                ctx.PinRequirements = false;
                                break;
                            }
                        }
                        else
                        {
                            ctx.PinRequirements = false;
                        }
                        error = string.Empty;
                        return((int)AuthenticationResponseKind.Biometrics);
                    }
                    catch (Exception ex)
                    {
                        if (isDeserialized)
                        {
                            Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, ex.Message), EventLogEntryType.Error, 5000);
                        }
                        else
                        {
                            Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, jsonResponse), EventLogEntryType.Error, 5000);
                        }
                        error = ex.Message;
                        return((int)AuthenticationResponseKind.Error);
                    }
                }
                else
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), EventLogEntryType.Error, 5000);
                    error = string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !");
                    return((int)AuthenticationResponseKind.Error);
                }
            }
            catch (Exception e)
            {
                Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), EventLogEntryType.Error, 5000);
                error = e.Message;
                return((int)AuthenticationResponseKind.Error);
            }
        }
        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);
        }
Exemplo n.º 16
0
        internal async void MakeAssertionResponse(COSE.KeyType kty, COSE.Algorithm alg, COSE.EllipticCurve crv = COSE.EllipticCurve.P256)
        {
            const string rp = "fido2.azurewebsites.net";

            byte[]       rpId         = Encoding.UTF8.GetBytes(rp);
            var          rpIdHash     = SHA256.Create().ComputeHash(rpId);
            var          flags        = AuthenticatorFlags.AT | AuthenticatorFlags.ED | AuthenticatorFlags.UP | AuthenticatorFlags.UV;
            const ushort signCount    = 0xf1d0;
            var          aaguid       = new Guid("F1D0F1D0-F1D0-F1D0-F1D0-F1D0F1D0F1D0");
            var          credentialID = new byte[] { 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, };

            CredentialPublicKey cpk = null;
            ECDsa ecdsa             = null;
            RSA   rsa = null;

            byte[] expandedPrivateKey = null;
            switch (kty)
            {
            case COSE.KeyType.EC2:
            {
                ecdsa = MakeECDsa(alg, crv);
                var ecparams = ecdsa.ExportParameters(true);
                cpk = MakeCredentialPublicKey(kty, alg, crv, ecparams.Q.X, ecparams.Q.Y);
                break;
            }

            case COSE.KeyType.RSA:
            {
                rsa = RSA.Create();
                var rsaparams = rsa.ExportParameters(true);
                cpk = MakeCredentialPublicKey(kty, alg, rsaparams.Modulus, rsaparams.Exponent);
                break;
            }

            case COSE.KeyType.OKP:
            {
                MakeEdDSA(out var privateKeySeed, out var publicKey, out expandedPrivateKey);
                cpk = MakeCredentialPublicKey(kty, alg, COSE.EllipticCurve.Ed25519, publicKey);
                break;
            }
                throw new ArgumentOutOfRangeException(nameof(kty), $"Missing or unknown kty {kty}");
            }

            var acd      = new AttestedCredentialData(aaguid, credentialID, cpk);
            var extBytes = CBORObject.NewMap().Add("testing", true).EncodeToBytes();
            var exts     = new Extensions(extBytes);

            var ad       = new AuthenticatorData(rpIdHash, flags, signCount, acd, exts);
            var authData = ad.ToByteArray();

            var challenge = new byte[128];
            var rng       = RandomNumberGenerator.Create();

            rng.GetBytes(challenge);


            var clientData = new
            {
                Type      = "webauthn.get",
                Challenge = challenge,
                Origin    = rp,
            };
            var clientDataJson = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(clientData));

            var sha = SHA256.Create();
            var hashedClientDataJson = sha.ComputeHash(clientDataJson);

            byte[] data = new byte[authData.Length + hashedClientDataJson.Length];
            Buffer.BlockCopy(authData, 0, data, 0, authData.Length);
            Buffer.BlockCopy(hashedClientDataJson, 0, data, authData.Length, hashedClientDataJson.Length);
            byte[] signature = null;
            switch (kty)
            {
            case COSE.KeyType.EC2:
            {
                signature = ecdsa.SignData(data, CryptoUtils.algMap[(int)alg]);
                break;
            }

            case COSE.KeyType.RSA:
            {
                RSASignaturePadding padding;
                switch (alg)         // https://www.iana.org/assignments/cose/cose.xhtml#algorithms
                {
                case COSE.Algorithm.PS256:
                case COSE.Algorithm.PS384:
                case COSE.Algorithm.PS512:
                    padding = RSASignaturePadding.Pss;
                    break;

                case COSE.Algorithm.RS1:
                case COSE.Algorithm.RS256:
                case COSE.Algorithm.RS384:
                case COSE.Algorithm.RS512:
                    padding = RSASignaturePadding.Pkcs1;
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(alg), $"Missing or unknown alg {alg}");
                }
                signature = rsa.SignData(data, CryptoUtils.algMap[(int)alg], padding);
                break;
            }

            case COSE.KeyType.OKP:
            {
                signature = Ed25519.Sign(data, expandedPrivateKey);
                break;
            }

            default:
                throw new ArgumentOutOfRangeException(nameof(kty), $"Missing or unknown kty {kty}");
            }

            if (kty == COSE.KeyType.EC2)
            {
                signature = EcDsaSigFromSig(signature, ecdsa.KeySize);
            }

            var userHandle = new byte[16];

            rng.GetBytes(userHandle);

            var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse()
            {
                AuthenticatorData = authData,
                Signature         = signature,
                ClientDataJson    = clientDataJson,
                UserHandle        = userHandle,
            };

            var lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = rp,
                ServerName   = rp,
                Origin       = rp,
            });
            var existingCredentials = new List <PublicKeyCredentialDescriptor>();
            var cred = new PublicKeyCredentialDescriptor
            {
                Type = PublicKeyCredentialType.PublicKey,
                Id   = new byte[] { 0xf1, 0xd0 }
            };

            existingCredentials.Add(cred);

            var options = lib.GetAssertionOptions(existingCredentials, null, null);

            options.Challenge = challenge;
            var response = new AuthenticatorAssertionRawResponse()
            {
                Response = assertion,
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
            };
            IsUserHandleOwnerOfCredentialIdAsync callback = (args) =>
            {
                return(Task.FromResult(true));
            };
            var res = await lib.MakeAssertionAsync(response, options, cpk.GetBytes(), signCount - 1, callback);
        }
 private async Task GetEnrollmentDataAsync(AuthenticatorData data)
 {
     if (string.IsNullOrWhiteSpace(data.Serial))
     {
         var result = await _enrollmentservice.EnrollAuthenticatorAsync(data.RegionCode);
         data.RestoreCode = result.RestoreCode;
         data.Serial = result.Serial;
         data.ServerTimeDifference = result.ServerTimeDifference;
         data.SecretKey = result.SecretKey;
     }
     else
     {
         if (data.SecretKey == null)
         {
             var result = await _enrollmentservice.RestoreAuthenticatorDataAsync(data.Serial, data.RestoreCode);
             data.SecretKey = result.SecretKey;
             if (result.ServerTimeDifference != TimeSpan.Zero)
             {
                 data.ServerTimeDifference = result.ServerTimeDifference;
             }
         }
         else if (data.RestoreCode == null)
         {
             data.RestoreCode = _enrollmentservice.GetRestoreCode(data.Serial, data.SecretKey);
             if (data.ServerTimeDifference == TimeSpan.Zero)
             {
                 data.ServerTimeDifference = await _enrollmentservice.GetServerTimeDifferenceAsync(data.RegionCode);
             }
         }
         else
         {
             if (data.ServerTimeDifference == TimeSpan.Zero)
             {
                 data.ServerTimeDifference = await _enrollmentservice.GetServerTimeDifferenceAsync(data.RegionCode);
             }
         }
     }
 }
 private async Task SaveAuthenticatorAsync(AuthenticatorData data, string fileName)
 {
     var json = JsonConvert.SerializeObject(data);
     var protectedData = _dataProtector.Protect(Encoding.UTF8.GetBytes(json));
     using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 8000, FileOptions.Asynchronous | FileOptions.WriteThrough))
     {
         await fs.WriteAsync(protectedData, 0, protectedData.Length).ConfigureAwait(false);
     }
 }
 private static void ValidateAuthenticatorData(AuthenticatorData data)
 {
     if (string.IsNullOrEmpty(data.AccountName))
     {
         throw new ArgumentNullException(nameof(data.AccountName));
     }
     if (!string.IsNullOrEmpty(data.Serial))
     {
         Validation.ValidateSerial(data.Serial);
     }
     if (!string.IsNullOrEmpty(data.RestoreCode))
     {
         Validation.ValidateRestoreCode(data.RestoreCode);
     }
     if (data.SecretKey != null)
     {
         Validation.ValidateSecretKey(data.SecretKey);
     }
     if (data.Serial != null)
     {
         if (data.RestoreCode == null && data.SecretKey == null)
             throw new ArgumentException($"Either {nameof(data.SecretKey)} or {nameof(data.RestoreCode)} must be set.");
     }
     if (string.IsNullOrEmpty(data.RegionCode))
     {
         if (!string.IsNullOrEmpty(data.Serial))
         {
             data.RegionCode = data.Serial.Substring(0, 2).ToUpperInvariant();
         }
         else
         {
             throw new ArgumentNullException(nameof(data.RegionCode));
         }
     }
     else
     {
         Validation.ValidateRegionCode(data.RegionCode);
     }
 }
Exemplo n.º 20
0
 /// <summary>
 ///     Determines whether this instance holds enough information to be considered as a valid representation of a
 ///     registered authenticator device or software.
 /// </summary>
 /// <returns>
 ///     <c>true</c> if this instance holds enough information; otherwise, <c>false</c>.
 /// </returns>
 public bool HasEnoughInfo()
 {
     return(!string.IsNullOrWhiteSpace(DeviceId) &&
            Session.HasEnoughInfo() &&
            AuthenticatorData.HasEnoughInfo());
 }
Exemplo n.º 21
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);
        }
Exemplo n.º 22
0
        /// <summary>
        /// SetLoginAssertionResult method implementation
        /// </summary>
        private int SetLoginAssertionResult(AuthenticationContext ctx, string jsonResponse, out string error)
        {
            bool isDeserialized = false;

            try
            {
                string jsonOptions = ctx.AssertionOptions;
                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)
                {
                    AssertionOptions options = AssertionOptions.FromJson(jsonOptions);
                    try
                    {
                        AuthenticatorAssertionRawResponse clientResponse = JsonConvert.DeserializeObject <AuthenticatorAssertionRawResponse>(jsonResponse);
                        isDeserialized = true;

                        MFAUserCredential creds = RuntimeRepository.GetCredentialById(Config, user, clientResponse.Id);
                        if (creds == null)
                        {
                            throw new Exception("Unknown credentials");
                        }

                        // Check Replay
                        AuthenticatorData authData = new AuthenticatorData(clientResponse.Response.AuthenticatorData);
                        uint authCounter           = authData.SignCount;
                        uint storedCounter         = creds.SignatureCounter;
                        if ((authCounter <= 0) || (authCounter <= storedCounter))
                        {
                            ResourcesLocale Resources = new ResourcesLocale(ctx.Lcid);
                            throw new Exception(Resources.GetString(ResourcesLocaleKind.Html, "BIOERRORAUTHREPLAY"));
                        }

                        bool callback(IsUserHandleOwnerOfCredentialIdParams args)
                        {
                            var storedCreds = RuntimeRepository.GetCredentialsByUserHandle(Config, user, args.UserHandle);

                            return(storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)));
                        }
                        AssertionVerificationResult res = _webathn.SetAssertionResult(clientResponse, options, creds.PublicKey, storedCounter, callback);
                        RuntimeRepository.UpdateCounter(Config, user, res.CredentialId, res.Counter);

                        if (!authData.UserPresent || !authData.UserVerified)
                        {
                            switch (creds.CredType)
                            {
                            case "none":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.None));
                                break;

                            case "android-key":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.AndroidKey));
                                break;

                            case "android-safetynet":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.AndroidSafetyNet));
                                break;

                            case "fido-u2f":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.Fido2U2f));
                                break;

                            case "packed":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.Packed));
                                break;

                            case "tpm":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.TPM));
                                break;

                            case "apple":
                                ctx.PinRequirements = (this.PinRequirements.HasFlag(WebAuthNPinRequirements.Apple));
                                break;

                            default:
                                ctx.PinRequirements = false;
                                break;
                            }
                        }
                        else
                        {
                            ctx.PinRequirements = false;
                        }
                        error = string.Empty;
                        return((int)AuthenticationResponseKind.Biometrics);
                    }
                    catch (Exception ex)
                    {
                        if (isDeserialized)
                        {
                            Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, ex.Message), EventLogEntryType.Error, 5000);
                        }
                        else
                        {
                            Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, jsonResponse), EventLogEntryType.Error, 5000);
                        }
                        error = ex.Message;
                        return((int)AuthenticationResponseKind.Error);
                    }
                }
                else
                {
                    Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), EventLogEntryType.Error, 5000);
                    error = string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !");
                    return((int)AuthenticationResponseKind.Error);
                }
            }
            catch (Exception e)
            {
                Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), EventLogEntryType.Error, 5000);
                error = e.Message;
                return((int)AuthenticationResponseKind.Error);
            }
        }
        internal static async Task <AssertionVerificationResult> MakeAssertionResponse(COSE.KeyType kty, COSE.Algorithm alg, COSE.EllipticCurve crv = COSE.EllipticCurve.P256, CredentialPublicKey cpk = null, ushort signCount = 0, ECDsa ecdsa = null, RSA rsa = null, byte[] expandedPrivateKey = null)
        {
            const string rp = "https://www.passwordless.dev";

            byte[] rpId         = Encoding.UTF8.GetBytes(rp);
            var    rpIdHash     = SHA256.Create().ComputeHash(rpId);
            var    flags        = AuthenticatorFlags.AT | AuthenticatorFlags.ED | AuthenticatorFlags.UP | AuthenticatorFlags.UV;
            var    aaguid       = new Guid("F1D0F1D0-F1D0-F1D0-F1D0-F1D0F1D0F1D0");
            var    credentialID = new byte[] { 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, 0xf1, 0xd0, };

            if (cpk == null)
            {
                switch (kty)
                {
                case COSE.KeyType.EC2:
                {
                    if (ecdsa == null)
                    {
                        ecdsa = MakeECDsa(alg, crv);
                    }
                    var ecparams = ecdsa.ExportParameters(true);
                    cpk = MakeCredentialPublicKey(kty, alg, crv, ecparams.Q.X, ecparams.Q.Y);
                    break;
                }

                case COSE.KeyType.RSA:
                {
                    if (rsa == null)
                    {
                        rsa = RSA.Create();
                    }
                    var rsaparams = rsa.ExportParameters(true);
                    cpk = MakeCredentialPublicKey(kty, alg, rsaparams.Modulus, rsaparams.Exponent);
                    break;
                }

                case COSE.KeyType.OKP:
                {
                    byte[] publicKey = null;
                    if (expandedPrivateKey == null)
                    {
                        MakeEdDSA(out var privateKeySeed, out publicKey, out expandedPrivateKey);
                    }

                    cpk = MakeCredentialPublicKey(kty, alg, COSE.EllipticCurve.Ed25519, publicKey);
                    break;
                }
                    throw new ArgumentOutOfRangeException(nameof(kty), $"Missing or unknown kty {kty}");
                }
            }
            var acd      = new AttestedCredentialData(aaguid, credentialID, cpk);
            var extBytes = CBORObject.NewMap().Add("testing", true).EncodeToBytes();
            var exts     = new Extensions(extBytes);

            var ad       = new AuthenticatorData(rpIdHash, flags, (uint)(signCount + 1), acd, exts);
            var authData = ad.ToByteArray();

            var challenge = new byte[128];
            var rng       = RandomNumberGenerator.Create();

            rng.GetBytes(challenge);

            var clientData = new
            {
                Type      = "webauthn.get",
                Challenge = challenge,
                Origin    = rp,
            };
            var clientDataJson = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(clientData));

            var sha = SHA256.Create();
            var hashedClientDataJson = sha.ComputeHash(clientDataJson);

            byte[] data = new byte[authData.Length + hashedClientDataJson.Length];
            Buffer.BlockCopy(authData, 0, data, 0, authData.Length);
            Buffer.BlockCopy(hashedClientDataJson, 0, data, authData.Length, hashedClientDataJson.Length);
            byte[] signature = SignData(kty, alg, data, ecdsa, rsa, expandedPrivateKey);

            var userHandle = new byte[16];

            rng.GetBytes(userHandle);

            var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse()
            {
                AuthenticatorData = authData,
                Signature         = signature,
                ClientDataJson    = clientDataJson,
                UserHandle        = userHandle,
            };

            var lib = new Fido2(new Fido2Configuration()
            {
                ServerDomain = rp,
                ServerName   = rp,
                Origin       = rp,
            });
            var existingCredentials = new List <PublicKeyCredentialDescriptor>();
            var cred = new PublicKeyCredentialDescriptor
            {
                Type = PublicKeyCredentialType.PublicKey,
                Id   = new byte[] { 0xf1, 0xd0 }
            };

            existingCredentials.Add(cred);

            var options = lib.GetAssertionOptions(existingCredentials, null, null);

            options.Challenge = challenge;
            var response = new AuthenticatorAssertionRawResponse()
            {
                Response = assertion,
                Type     = PublicKeyCredentialType.PublicKey,
                Id       = new byte[] { 0xf1, 0xd0 },
                RawId    = new byte[] { 0xf1, 0xd0 },
            };
            IsUserHandleOwnerOfCredentialIdAsync callback = (args) =>
            {
                return(Task.FromResult(true));
            };

            return(await lib.MakeAssertionAsync(response, options, cpk.GetBytes(), signCount, callback));
        }
        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 async Task AddAuthenticatorAsync(AuthenticatorData data)
        {
            if (data == null) throw new ArgumentNullException(nameof(data));

            ValidateAuthenticatorData(data);
            var file = PathHelper.GetAuthenticatorsSavePath(data.AccountName);
            Debug.Assert(file != null, "Authenticator Save Path cannot be null.");
            if (File.Exists(file))
            {
                var error = $"Data for an account with the same name {data.AccountName} already exist";
                _logger.LogWarning("Add authenticator failed. " + error);
                throw new ArgumentException(error, nameof(data));
            }
            // ReSharper disable once AssignNullToNotNullAttribute
            var dir = new DirectoryInfo(Path.GetDirectoryName(file));
            if (!dir.Exists)
            {
                dir.Create();
                _logger.LogWarning($"Authenticators store directory created at {dir.FullName}");
            }
            await GetEnrollmentDataAsync(data);
            await SaveAuthenticatorAsync(data, file).ConfigureAwait(false);
        }
        /// <summary>
        /// Implements alghoritm from https://www.w3.org/TR/webauthn/#verifying-assertion
        /// </summary>
        /// <param name="options">The assertionoptions that was sent to the client</param>
        /// <param name="expectedOrigin">
        /// The expected server origin, used to verify that the signature is sent to the expected server
        /// </param>
        /// <param name="storedPublicKey">The stored public key for this CredentialId</param>
        /// <param name="storedSignatureCounter">The stored counter value for this CredentialId</param>
        /// <param name="isUserHandleOwnerOfCredId">A function that returns <see langword="true"/> if user handle is owned by the credential ID</param>
        /// <param name="requestTokenBindingId"></param>
        public AssertionVerificationResult Verify(
            AssertionOptions options,
            string expectedOrigin,
            byte[] storedPublicKey,
            uint storedSignatureCounter,
            IsUserHandleOwnerOfCredentialId isUserHandleOwnerOfCredId,
            byte[] requestTokenBindingId)
        {
            BaseVerify(expectedOrigin, options.Challenge, requestTokenBindingId);

            if (Raw.Type != PublicKeyCredentialType.PublicKey)
            {
                throw new VerificationException("AssertionResponse Type is not set to public-key");
            }

            if (Raw.Id == null)
            {
                throw new VerificationException("Id is missing");
            }
            if (Raw.RawId == null)
            {
                throw new VerificationException("RawId is missing");
            }

            // 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
            if (options.AllowCredentials != null && options.AllowCredentials.Count() > 0)
            {
                // might need to transform x.Id and raw.id as described in https://www.w3.org/TR/webauthn/#publickeycredential
                if (!options.AllowCredentials.Any(x => x.Id.SequenceEqual(Raw.Id)))
                {
                    throw new VerificationException("Invalid");
                }
            }

            // 2. If credential.response.userHandle is present, verify that the user identified by this value is the owner of the public key credential identified by credential.id.
            if (UserHandle != null)
            {
                if (UserHandle.Length == 0)
                {
                    throw new VerificationException("Userhandle was empty DOMString. It should either be null or have a value.");
                }

                if (false == isUserHandleOwnerOfCredId(new IsUserHandleOwnerOfCredentialIdParams(Raw.Id, UserHandle)))
                {
                    throw new VerificationException("User is not owner of the public key identitief by the credential id");
                }
            }

            // 3. Using credential’s id attribute(or the corresponding rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key.
            // public key inserted via parameter.

            // 4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively.
            //var cData = Raw.Response.ClientDataJson;
            var authData = new AuthenticatorData(Raw.Response.AuthenticatorData);

            //var sig = Raw.Response.Signature;

            // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
            //var JSONtext = Encoding.UTF8.GetBytes(cData.ToString());


            // 7. Verify that the value of C.type is the string webauthn.get.
            if (Type != "webauthn.get")
            {
                throw new VerificationException();
            }

            // 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
            // 9. Verify that the value of C.origin matches the Relying Party's origin.
            // done in base class

            //10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation 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.
            // Validated in BaseVerify.
            // todo: Needs testing

            // 11. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party.

            byte[] hashedClientDataJson;
            byte[] hashedRpId;
            using (var sha = SHA256.Create())
            {
                // 11
                // https://www.w3.org/TR/webauthn/#sctn-appid-extension
                // FIDO AppID Extension:
                // If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the rpIdHash to be the hash of the AppID, not the RP ID.
                var rpid = Raw.Extensions?.AppID ?? false ? options.Extensions?.AppID : options.RpId;
                hashedRpId = sha.ComputeHash(Encoding.UTF8.GetBytes(rpid ?? string.Empty));
                // 15
                hashedClientDataJson = sha.ComputeHash(Raw.Response.ClientDataJson);
            }

            if (false == authData.RpIdHash.SequenceEqual(hashedRpId))
            {
                throw new VerificationException("Hash mismatch RPID");
            }
            // 12. Verify that the User Present bit of the flags in authData is set.
            // UNLESS...userVerification is set to preferred or discouraged?
            // See Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
            // P-5 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "preferred", and check that server succeeds
            // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds
            //if ((false == authData.UserPresent) && (options.UserVerification != UserVerificationRequirement.Discouraged && options.UserVerification != UserVerificationRequirement.Preferred)) throw new Fido2VerificationException("User Present flag not set in authenticator data");

            // 13 If user verification is required for this assertion, verify that the User Verified bit of the flags in aData is set.
            // UNLESS...userPresent is true?
            // see ee Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
            // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds
            if (UserVerificationRequirement.Required == options.UserVerification && false == authData.UserVerified)
            {
                throw new VerificationException("User verification is required");
            }

            // 14. 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 get() 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: Verify this (and implement extensions on options)
            if (true == authData.HasExtensionsData && ((null == authData.Extensions) || (0 == authData.Extensions.Length)))
            {
                throw new VerificationException("Extensions flag present, malformed extensions detected");
            }
            if (false == authData.HasExtensionsData && (null != authData.Extensions))
            {
                throw new VerificationException("Extensions flag not present, but extensions detected");
            }

            // 15.
            // Done earlier, hashedClientDataJson

            // 16. Using the credential public key looked up in step 3, verify that sig is a valid signature over the binary concatenation of aData and hash.
            byte[] data = new byte[Raw.Response.AuthenticatorData.Length + hashedClientDataJson.Length];
            Buffer.BlockCopy(Raw.Response.AuthenticatorData, 0, data, 0, Raw.Response.AuthenticatorData.Length);
            Buffer.BlockCopy(hashedClientDataJson, 0, data, Raw.Response.AuthenticatorData.Length, hashedClientDataJson.Length);

            if (null == storedPublicKey || 0 == storedPublicKey.Length)
            {
                throw new VerificationException("Stored public key is null or empty");
            }
            var cpk = new CredentialPublicKey(storedPublicKey);

            if (true != cpk.Verify(data, Signature))
            {
                throw new VerificationException("Signature did not match");
            }

            // 17.
            if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter)
            {
                throw new VerificationException("SignatureCounter was not greater than stored SignatureCounter");
            }

            return(new AssertionVerificationResult()
            {
                Status = "ok",
                ErrorMessage = string.Empty,
                CredentialId = Raw.Id,
                Counter = authData.SignCount,
            });
        }
        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);
        }