/// <summary> /// SetRegisterCredentialResult method implementation /// </summary> private int SetRegisterCredentialResult(AuthenticationContext ctx, string jsonResponse) { 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) { RegisterCredentialOptions options = RegisterCredentialOptions.FromJson(jsonOptions); bool callback(IsCredentialIdUniqueToUserParams args) { var users = RuntimeRepository.GetUsersByCredentialId(Config, user, args.CredentialId); if (users.Count > 0) { return(false); } return(true); } AuthenticatorAttestationRawResponse attestationResponse = JsonConvert.DeserializeObject <AuthenticatorAttestationRawResponse>(jsonResponse); RegisterCredentialResult success = _webathn.SetRegisterCredentialResult(attestationResponse, options, callback); RuntimeRepository.AddUserCredential(Config, options.User.FromCore(), 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 }); return((int)AuthenticationResponseKind.Biometrics); } else { Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, "User does not exists !"), System.Diagnostics.EventLogEntryType.Error, 5000); return((int)AuthenticationResponseKind.Error); } } catch (Exception e) { Log.WriteEntry(string.Format("{0}\r\n{1}", ctx.UPN, e.Message), System.Diagnostics.EventLogEntryType.Error, 5000); return((int)AuthenticationResponseKind.Error); } }
/// <summary> /// GetCredentialCreateOptions method implementation /// </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 RegisterCredentialOptions GetRegisterCredentialOptions(Fido2User user, List <PublicKeyCredentialDescriptor> excludeCredentials, AuthenticatorSelection authenticatorSelection, AttestationConveyancePreference attestationPreference, AuthenticationExtensionsClientInputs extensions = null) { var challenge = new byte[_config.ChallengeSize]; _crypto.GetBytes(challenge); var options = RegisterCredentialOptions.Create(_config, challenge, user, authenticatorSelection, attestationPreference, excludeCredentials, extensions); return(options); }
/// <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 string 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.ToEnum <UserVerificationRequirement>() }; if (!string.IsNullOrEmpty(authType)) { authenticatorSelection.AuthenticatorAttachment = authType.ToEnum <AuthenticatorAttachment>(); } AuthenticationExtensionsClientInputs exts = new AuthenticationExtensionsClientInputs() { Extensions = this.Extentions, UserVerificationIndex = this.UserVerificationIndex, Location = this.Location, UserVerificationMethod = this.UserVerificationMethod, EnforceCredProtect = this.EnforceCredProtect, CredProtect = this.CredProtect, HmacSecret = this.HmacSecret, BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds { FAR = float.MaxValue, FRR = float.MaxValue } }; RegisterCredentialOptions options = _webathn.GetRegisterCredentialOptions(user.ToCore(), existingKeys.ToCore(), 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 RegisterCredentialOptions { 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 RegisterCredentialOptions { Status = "error", ErrorMessage = string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : "") }).ToJson(); ctx.CredentialOptions = result; return(result); } }
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); }
/// <summary> /// Verifies the response from the browser/authr after creating new credentials /// </summary> /// <param name="attestationResponse"></param> /// <param name="origChallenge"></param> /// <returns></returns> public RegisterCredentialResult SetRegisterCredentialResult(AuthenticatorAttestationRawResponse attestationResponse, RegisterCredentialOptions origChallenge, IsCredentialIdUniqueToUserDelegate isCredentialIdUniqueToUser, byte[] requestTokenBindingId = null) { var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse); var success = parsedResponse.VerifyCredentialCreateOptions(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId); try { return(new RegisterCredentialResult { Status = "ok", ErrorMessage = string.Empty, Result = success }); } catch (Exception e) { return(new RegisterCredentialResult { Status = "error", ErrorMessage = e.Message, Result = success }); } }