/// <summary> /// GetCredentialCreateOptions method implementation (RequestNewCredential) /// </summary> /// <returns>CredentialCreateOptions including a challenge to be sent to the browser/authr to create new credentials</returns> /// <param name="attestationPreference">This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance. The default is none.</param> /// <param name="excludeCredentials">Recommended. This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.</param> public CredentialCreateOptions GetRegisterCredentialOptions(Fido2User user, List <PublicKeyCredentialDescriptor> excludeCredentials, AuthenticatorSelection authenticatorSelection, AttestationConveyancePreference attestationPreference, AuthenticationExtensionsClientInputs extensions = null) { var challenge = new byte[_config.ChallengeSize]; _crypto.GetBytes(challenge); var options = CredentialCreateOptions.Create(_config, challenge, user, authenticatorSelection, attestationPreference, excludeCredentials, extensions); return(options); }
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. }
/// <summary> /// SetRegisterCredentialResult method implementation /// </summary> private int SetRegisterCredentialResult(AuthenticationContext ctx, string jsonResponse, out string error) { bool isDeserialized = false; try { string jsonOptions = ctx.CredentialOptions; if (string.IsNullOrEmpty(jsonOptions)) { throw new ArgumentNullException(jsonOptions); } if (string.IsNullOrEmpty(jsonResponse)) { throw new ArgumentNullException(jsonResponse); } MFAWebAuthNUser user = RuntimeRepository.GetUser(Config, ctx.UPN); if (user != null) { CredentialCreateOptions options = CredentialCreateOptions.FromJson(jsonOptions); #pragma warning disable CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone IsCredentialIdUniqueToUserAsyncDelegate callback = async(args) => #pragma warning restore CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone { var users = RuntimeRepository.GetUsersByCredentialId(Config, user, args.CredentialId); if (users.Count > 0) { return(false); } return(true); }; AuthenticatorAttestationRawResponse attestationResponse = JsonConvert.DeserializeObject <AuthenticatorAttestationRawResponse>(jsonResponse); isDeserialized = true; CredentialMakeResult success = _webathn.SetRegisterCredentialResult(attestationResponse, options, callback).Result; RuntimeRepository.AddUserCredential(Config, user, new MFAUserCredential { Descriptor = new MFAPublicKeyCredentialDescriptor(success.Result.CredentialId), PublicKey = success.Result.PublicKey, UserHandle = success.Result.User.Id, SignatureCounter = success.Result.Counter, CredType = success.Result.CredType, RegDate = DateTime.Now, AaGuid = success.Result.Aaguid, NickName = ctx.NickName }); error = string.Empty; return((int)AuthenticationResponseKind.Biometrics); } else { Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), System.Diagnostics.EventLogEntryType.Error, 5000); error = string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"); return((int)AuthenticationResponseKind.Error); } } catch (Exception e) { if (isDeserialized) { Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), EventLogEntryType.Error, 5000); } else { Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, jsonResponse), EventLogEntryType.Error, 5000); } error = e.Message; return((int)AuthenticationResponseKind.Error); } }
/// <summary> /// GetRegisterCredentialOptions method implementation /// </summary> private string GetRegisterCredentialOptions(AuthenticationContext ctx) { try { if (string.IsNullOrEmpty(ctx.UPN)) { throw new ArgumentNullException(ctx.UPN); } string attType = this.ConveyancePreference; // none, direct, indirect string authType = this.Attachement; // <empty>, platform, cross-platform UserVerificationRequirement userVerification = this.UserVerificationRequirement; // preferred, required, discouraged bool requireResidentKey = this.RequireResidentKey; // true,false MFAWebAuthNUser user = RuntimeRepository.GetUser(Config, ctx.UPN); if (user != null) { List <MFAPublicKeyCredentialDescriptor> existingKeys = RuntimeRepository.GetCredentialsByUser(Config, user).Select(c => c.Descriptor).ToList(); // 3. Create options AuthenticatorSelection authenticatorSelection = new AuthenticatorSelection { RequireResidentKey = requireResidentKey, UserVerification = userVerification }; if (!string.IsNullOrEmpty(authType)) { authenticatorSelection.AuthenticatorAttachment = authType.ToEnum <AuthenticatorAttachment>(); } AuthenticationExtensionsClientInputs exts = new AuthenticationExtensionsClientInputs() { Extensions = this.Extentions, UserVerificationMethod = this.UserVerificationMethod }; CredentialCreateOptions options = null; if (existingKeys.Count > 0) { options = _webathn.GetRegisterCredentialOptions(user.ToCore(), existingKeys.ToCore(), authenticatorSelection, attType.ToEnum <AttestationConveyancePreference>(), exts); } else { options = _webathn.GetRegisterCredentialOptions(user.ToCore(), null, authenticatorSelection, attType.ToEnum <AttestationConveyancePreference>(), exts); } string result = options.ToJson(); ctx.CredentialOptions = result; return(result); } else { Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), EventLogEntryType.Error, 5000); string result = (new CredentialMakeResult { Status = "error", ErrorMessage = string.Format("{0}", "User does not exists !") }).ToJson(); ctx.CredentialOptions = result; return(result); } } catch (Exception e) { Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), System.Diagnostics.EventLogEntryType.Error, 5000); string result = (new CredentialMakeResult { Status = "error", ErrorMessage = string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "") }).ToJson(); ctx.CredentialOptions = result; return(result); } }
/// <summary> /// Verifies the response from the browser/authr after creating new credentials (MakeNewCredentialAsync) /// </summary> /// <param name="attestationResponse"></param> /// <param name="origChallenge"></param> /// <returns></returns> public async Task <CredentialMakeResult> SetRegisterCredentialResult(AuthenticatorAttestationRawResponse attestationResponse, CredentialCreateOptions origChallenge, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, byte[] requestTokenBindingId = null) { var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse); var success = await parsedResponse.VerifyAsync(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId); try { return(new CredentialMakeResult { Status = "ok", ErrorMessage = string.Empty, Result = success }); } catch (Exception e) { return(new CredentialMakeResult { Status = "error", ErrorMessage = e.Message, Result = success }); } }