diff --git a/Demo/Controller.cs b/Demo/Controller.cs index aa448f3be..fa0d52e61 100644 --- a/Demo/Controller.cs +++ b/Demo/Controller.cs @@ -71,10 +71,12 @@ public JsonResult MakeCredentialOptions([FromForm] string username, if (!string.IsNullOrEmpty(authType)) authenticatorSelection.AuthenticatorAttachment = authType.ToEnum(); - var exts = new AuthenticationExtensionsClientInputs() - { - Extensions = true, - UserVerificationMethod = true, + var exts = new AuthenticationExtensionsClientInputs() + { + Extensions = true, + UserVerificationMethod = true, + DevicePubKey = new AuthenticationExtensionsDevicePublicKeyInputs() { Attestation = attType }, + CredProps = true }; var options = _fido2.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum(), exts); @@ -117,19 +119,23 @@ public async Task MakeCredential([FromBody] AuthenticatorAttestation // 3. Store the credentials in db DemoStorage.AddCredentialToUser(options.User, new StoredCredential { + Type = success.Result.Type, + Id = success.Result.Id, Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), PublicKey = success.Result.PublicKey, UserHandle = success.Result.User.Id, - SignatureCounter = success.Result.Counter, + SignCount = success.Result.Counter, CredType = success.Result.CredType, RegDate = DateTime.Now, - AaGuid = success.Result.AaGuid + AaGuid = success.Result.AaGuid, + Transports = success.Result.Transports, + BE = success.Result.BE, + BS = success.Result.BS, + AttestationObject = success.Result.AttestationObject, + AttestationClientDataJSON = success.Result.AttestationClientDataJSON, + DevicePublicKeys = new List() { success.Result.DevicePublicKey } }); - // Remove Certificates from success because System.Text.Json cannot serialize them properly. See https://github.com/passwordless-lib/fido2-net-lib/issues/328 - success.Result.AttestationCertificate = null; - success.Result.AttestationCertificateChain = null; - // 4. return "ok" to the client return Json(success); } @@ -157,8 +163,10 @@ public ActionResult AssertionOptionsPost([FromForm] string username, [FromForm] } var exts = new AuthenticationExtensionsClientInputs() - { - UserVerificationMethod = true + { + Extensions = true, + UserVerificationMethod = true, + DevicePubKey = new AuthenticationExtensionsDevicePublicKeyInputs() }; // 3. Create options @@ -206,11 +214,14 @@ public async Task MakeAssertion([FromBody] AuthenticatorAssertionRaw }; // 5. Make the assertion - var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken); + var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, creds.DevicePublicKeys, storedCounter, callback, cancellationToken: cancellationToken); // 6. Store the updated counter DemoStorage.UpdateCounter(res.CredentialId, res.Counter); + if (res.DevicePublicKey is not null) + creds.DevicePublicKeys.Add(res.DevicePublicKey); + // 7. return OK to client return Json(res); } diff --git a/Demo/Startup.cs b/Demo/Startup.cs index 86e63f13d..32276ce6a 100644 --- a/Demo/Startup.cs +++ b/Demo/Startup.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; - +using Fido2NetLib; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -52,6 +52,8 @@ public void ConfigureServices(IServiceCollection services) options.Origins = Configuration.GetSection("fido2:origins").Get>(); options.TimestampDriftTolerance = Configuration.GetValue("fido2:timestampDriftTolerance"); options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"]; + options.BackupEligibleCredentialPolicy = Configuration.GetValue("fido2:backupEligibleCredentialPolicy"); + options.BackedUpCredentialPolicy = Configuration.GetValue("fido2:backedUpCredentialPolicy"); }) .AddCachedMetadataService(config => { diff --git a/Demo/TestController.cs b/Demo/TestController.cs index ce3c071aa..43fa481d6 100644 --- a/Demo/TestController.cs +++ b/Demo/TestController.cs @@ -18,7 +18,7 @@ namespace Fido2Demo; public class TestController : Controller { /* CONFORMANCE TESTING ENDPOINTS */ - private static readonly DevelopmentInMemoryStore DemoStorage = new (); + private static readonly DevelopmentInMemoryStore _demoStorage = new (); private readonly IFido2 _fido2; private readonly string _origin; @@ -56,7 +56,7 @@ public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams } // 1. Get user from DB by username (in our example, auto create missing users) - var user = DemoStorage.GetOrAddUser(opts.Username, () => new Fido2User + var user = _demoStorage.GetOrAddUser(opts.Username, () => new Fido2User { DisplayName = opts.DisplayName, Name = opts.Username, @@ -64,7 +64,7 @@ public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams }); // 2. Get user existing keys by username - var existingKeys = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); + var existingKeys = _demoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); //var exts = new AuthenticationExtensionsClientInputs() { Extensions = true, UserVerificationIndex = true, Location = true, UserVerificationMethod = true, BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds { FAR = float.MaxValue, FRR = float.MaxValue } }; var exts = new AuthenticationExtensionsClientInputs() { }; @@ -83,7 +83,7 @@ public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams [HttpPost] [Route("/attestation/result")] - public async Task MakeCredentialResultTest([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) + public async Task MakeCredentialResultTestAsync([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) { // 1. get the options we sent the client @@ -93,7 +93,7 @@ public async Task MakeCredentialResultTest([FromBody] AuthenticatorA // 2. Create callback so that lib can verify credential id is unique to this user IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) => { - var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); + var users = await _demoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); return users.Count <= 0; }; @@ -101,12 +101,12 @@ public async Task MakeCredentialResultTest([FromBody] AuthenticatorA var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken); // 3. Store the credentials in db - DemoStorage.AddCredentialToUser(options.User, new StoredCredential + _demoStorage.AddCredentialToUser(options.User, new StoredCredential { Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), PublicKey = success.Result.PublicKey, UserHandle = success.Result.User.Id, - SignatureCounter = success.Result.Counter + SignCount = success.Result.Counter }); // 4. return "ok" to the client @@ -119,12 +119,12 @@ public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams { var username = assertionClientParams.Username; // 1. Get user from DB - var user = DemoStorage.GetUser(username); + var user = _demoStorage.GetUser(username); if (user == null) return NotFound("username was not registered"); // 2. Get registered credentials from database - var existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); + var existingCredentials = _demoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); var uv = assertionClientParams.UserVerification; if (null != assertionClientParams.authenticatorSelection) @@ -154,14 +154,14 @@ public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams [HttpPost] [Route("/assertion/result")] - public async Task MakeAssertionTest([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) + public async Task MakeAssertionTestAsync([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) { // 1. Get the assertion options we sent the client var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); var options = AssertionOptions.FromJson(jsonOptions); // 2. Get registered credential from database - var creds = DemoStorage.GetCredentialById(clientResponse.Id); + var creds = _demoStorage.GetCredentialById(clientResponse.Id); // 3. Get credential counter from database var storedCounter = creds.SignatureCounter; @@ -169,15 +169,18 @@ public async Task MakeAssertionTest([FromBody] AuthenticatorAssertio // 4. Create callback to check if userhandle owns the credentialId IsUserHandleOwnerOfCredentialIdAsync callback = static async (args, cancellationToken) => { - var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken); + var storedCreds = await _demoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken); return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); }; // 5. Make the assertion - var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken); + var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, creds.DevicePublicKeys, storedCounter, callback, cancellationToken: cancellationToken); // 6. Store the updated counter - DemoStorage.UpdateCounter(res.CredentialId, res.Counter); + _demoStorage.UpdateCounter(res.CredentialId, res.Counter); + + if (res.DevicePublicKey is not null) + creds.DevicePublicKeys.Add(res.DevicePublicKey); var testRes = new { diff --git a/Demo/appsettings.json b/Demo/appsettings.json index fdd8de5d4..37c5f6e5d 100644 --- a/Demo/appsettings.json +++ b/Demo/appsettings.json @@ -2,7 +2,9 @@ "fido2": { "serverDomain": "localhost", "origins": [ "https://localhost:44329" ], - "timestampDriftTolerance": 300000 + "timestampDriftTolerance": 300000, + "backupEligibleCredentialPolicy": "allowed", + "backedUpCredentialPolicy": "allowed" }, "Logging": { "IncludeScopes": false, diff --git a/Demo/wwwroot/js/custom.register.js b/Demo/wwwroot/js/custom.register.js index 227b8ad92..c46586d8e 100644 --- a/Demo/wwwroot/js/custom.register.js +++ b/Demo/wwwroot/js/custom.register.js @@ -125,8 +125,9 @@ async function registerNewCredential(newCredential) { extensions: newCredential.getClientExtensionResults(), response: { AttestationObject: coerceToBase64Url(attestationObject), - clientDataJSON: coerceToBase64Url(clientDataJSON) - } + clientDataJson: coerceToBase64Url(clientDataJSON), + transports: newCredential.response.getTransports(), + }, }; let response; diff --git a/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs b/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs index 6fcb0782a..db202f7a2 100644 --- a/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs +++ b/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs @@ -42,9 +42,13 @@ public class AssertionResponse [JsonConverter(typeof(Base64UrlConverter))] [JsonPropertyName("clientDataJSON")] public byte[] ClientDataJson { get; set; } - +#nullable enable [JsonPropertyName("userHandle")] [JsonConverter(typeof(Base64UrlConverter))] - public byte[] UserHandle { get; set; } + public byte[]? UserHandle { get; set; } + + [JsonPropertyName("attestationObject")] + [JsonConverter(typeof(Base64UrlConverter))] + public byte[]? AttestationObject { get; set; } } } diff --git a/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs b/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs index 25d5e4564..ff05ee4db 100644 --- a/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs +++ b/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs @@ -15,7 +15,7 @@ public sealed class AuthenticatorAttestationRawResponse public byte[] RawId { get; set; } [JsonPropertyName("type")] - public PublicKeyCredentialType? Type { get; set; } + public PublicKeyCredentialType Type { get; set; } = PublicKeyCredentialType.PublicKey; [JsonPropertyName("response")] public ResponseData Response { get; set; } diff --git a/Src/Fido2.Models/Exceptions/Fido2ErrorCode.cs b/Src/Fido2.Models/Exceptions/Fido2ErrorCode.cs index 79f7c3e58..de5c99c42 100644 --- a/Src/Fido2.Models/Exceptions/Fido2ErrorCode.cs +++ b/Src/Fido2.Models/Exceptions/Fido2ErrorCode.cs @@ -32,5 +32,9 @@ public enum Fido2ErrorCode InvalidAuthenticatorResponseChallenge, NonUniqueCredentialId, AaGuidNotFound, - UnimplementedAlgorithm + UnimplementedAlgorithm, + BackupEligibilityRequirementNotMet, + BackupStateRequirementNotMet, + CredentialAlgorithmRequirementNotMet, + DevicePublicKeyAuthentication } diff --git a/Src/Fido2.Models/Fido2Configuration.cs b/Src/Fido2.Models/Fido2Configuration.cs index 78e39651a..a02ec4fb2 100644 --- a/Src/Fido2.Models/Fido2Configuration.cs +++ b/Src/Fido2.Models/Fido2Configuration.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; namespace Fido2NetLib; @@ -113,4 +114,35 @@ public ISet FullyQualifiedOrigins AuthenticatorStatus.USER_KEY_PHYSICAL_COMPROMISE, AuthenticatorStatus.REVOKED }; + + /// + /// Whether or not to accept a backup eligible credential + /// + public CredentialBackupPolicy BackupEligibleCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed; + + /// + /// Whether or not to accept a backed up credential + /// + public CredentialBackupPolicy BackedUpCredentialPolicy { get; set; } = CredentialBackupPolicy.Allowed; + + public enum CredentialBackupPolicy + { + /// + /// This value indicates that the Relying Party requires backup eligible or backed up credentials. + /// + [EnumMember(Value = "required")] + Required, + + /// + /// This value indicates that the Relying Party allows backup eligible or backed up credentials. + /// + [EnumMember(Value = "allowed")] + Allowed, + + /// + /// This value indicates that the Relying Party does not allow backup eligible or backed up credentials. + /// + [EnumMember(Value = "disallowed")] + Disallowed + } } diff --git a/Src/Fido2.Models/Objects/AssertionVerificationResult.cs b/Src/Fido2.Models/Objects/AssertionVerificationResult.cs index 7df970e10..4247ae1d9 100644 --- a/Src/Fido2.Models/Objects/AssertionVerificationResult.cs +++ b/Src/Fido2.Models/Objects/AssertionVerificationResult.cs @@ -8,4 +8,19 @@ public class AssertionVerificationResult : Fido2ResponseBase public byte[] CredentialId { get; set; } public uint Counter { get; set; } + + /// + /// The latest value of the signature counter in the authenticator data from any ceremony using the public key credential source. + /// + public uint SignCount { get; set; } + + /// + /// The latest value of the BS flag in the authenticator data from any ceremony using the public key credential source. + /// + public bool BS { get; set; } + + /// + /// The public key portion of a hardware-bound device key pair + /// + public byte[] DevicePublicKey { get; set; } } diff --git a/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs b/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs index 6c74fbe0e..8255f8ba5 100644 --- a/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs +++ b/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs @@ -9,18 +9,40 @@ namespace Fido2NetLib.Objects; /// public class AttestationVerificationSuccess : AssertionVerificationResult { - [JsonConverter(typeof(Base64UrlConverter))] - public byte[] PublicKey { get; set; } - public Fido2User User { get; set; } public string CredType { get; set; } - + public Guid AaGuid { get; set; } - -#nullable enable - public X509Certificate2? AttestationCertificate { get; set; } -#nullable disable - - public X509Certificate2[] AttestationCertificateChain { get; set; } + /// + /// The type of the public key credential source. + /// + public PublicKeyCredentialType Type { get; set; } = PublicKeyCredentialType.PublicKey; + /// + /// The Credential ID of the public key credential source. + /// + public byte[] Id { get; set; } + /// + /// The credential public key of the public key credential source. + /// + [JsonConverter(typeof(Base64UrlConverter))] + public byte[] PublicKey { get; set; } + /// + /// The value returned from getTransports() when the public key credential source was registered. + /// + public AuthenticatorTransport[] Transports { get; set; } + /// + /// The value of the BE flag when the public key credential source was created. + /// + public bool BE { get; set; } + /// + /// The value of the attestationObject attribute when the public key credential source was registered. + /// Storing this enables the Relying Party to reference the credential's attestation statement at a later time. + /// + public byte[] AttestationObject { get; set; } + /// + /// The value of the clientDataJSON attribute when the public key credential source was registered. + /// Storing this in combination with the above attestationObject item enables the Relying Party to re-verify the attestation signature at a later time. + /// + public byte[] AttestationClientDataJSON { get; set; } } diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs index 1aad9ef38..ca95b1732 100644 --- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs @@ -45,5 +45,21 @@ public sealed class AuthenticationExtensionsClientInputs [JsonPropertyName("uvm")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? UserVerificationMethod { get; set; } + +#nullable enable + /// + /// This extension enables use of a user verification method. + /// https://www.w3.org/TR/webauthn/#sctn-uvm-extension + /// + [JsonPropertyName("devicePubKey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AuthenticationExtensionsDevicePublicKeyInputs? DevicePubKey { get; set; } + + /// + /// This client registration extension facilitates reporting certain credential properties known by the client to the requesting WebAuthn Relying Party upon creation of a public key credential source as a result of a registration ceremony. + /// + [JsonPropertyName("credProps")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? CredProps { get; set; } } diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs index e2e7271f1..c3be3b51b 100644 --- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs @@ -42,4 +42,19 @@ public class AuthenticationExtensionsClientOutputs [JsonPropertyName("uvm")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ulong[][]? UserVerificationMethod { get; set; } + + /// + /// This authenticator registration extension and authentication extension provides a Relying Party with a "device continuity" signal for backup eligible credentials. + /// https://w3c.github.io/webauthn/#sctn-device-publickey-extension + /// + [JsonPropertyName("devicePubKey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AuthenticationExtensionsDevicePublicKeyOutputs? DevicePubKey { get; set; } + + /// + /// This client registration extension facilitates reporting certain credential properties known by the client to the requesting WebAuthn Relying Party upon creation of a public key credential source as a result of a registration ceremony. + /// + [JsonPropertyName("credProps")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? CredProps { get; set; } } diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsDevicePublicKeyInputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsDevicePublicKeyInputs.cs new file mode 100644 index 000000000..e0cfddca5 --- /dev/null +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsDevicePublicKeyInputs.cs @@ -0,0 +1,13 @@ +namespace Fido2NetLib.Objects; + +using System; +using System.Text.Json.Serialization; + +public sealed class AuthenticationExtensionsDevicePublicKeyInputs +{ + [JsonPropertyName("attestation")] + public string Attestation { get; set; } = "none"; + + [JsonPropertyName("attestationFormats")] + public string[] AttestationFormats { get; set; } = Array.Empty(); +} diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsDevicePublicKeyOutputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsDevicePublicKeyOutputs.cs new file mode 100644 index 000000000..63bb880c7 --- /dev/null +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsDevicePublicKeyOutputs.cs @@ -0,0 +1,14 @@ +namespace Fido2NetLib.Objects; + +using System.Text.Json.Serialization; + +public sealed class AuthenticationExtensionsDevicePublicKeyOutputs +{ + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("authenticatorOutput")] + public byte[] AuthenticatorOutput { get; set; } + + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("signature")] + public byte[] Signature { get; set; } +} diff --git a/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs b/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs index bd298d3a6..af94a293a 100644 --- a/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs +++ b/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs @@ -11,5 +11,8 @@ namespace Fido2NetLib.Objects; public enum PublicKeyCredentialType { [EnumMember(Value = "public-key")] - PublicKey + PublicKey, + + [EnumMember(Value = "invalid")] + Invalid } diff --git a/Src/Fido2/AuthenticatorAssertionResponse.cs b/Src/Fido2/AuthenticatorAssertionResponse.cs index 840d911e5..212e895c9 100644 --- a/Src/Fido2/AuthenticatorAssertionResponse.cs +++ b/Src/Fido2/AuthenticatorAssertionResponse.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; +using Fido2NetLib.Cbor; using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; @@ -29,7 +30,10 @@ private AuthenticatorAssertionResponse(byte[] clientDataJson) : base(clientDataJ public byte[] Signature { get; init; } - public byte[] UserHandle { get; init; } +#nullable enable + public byte[]? UserHandle { get; init; } + public byte[]? AttestationObject { get; init; } +#nullable disable public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResponse rawResponse) { @@ -38,7 +42,8 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp Raw = rawResponse, // accessed in Verify() AuthenticatorData = rawResponse.Response.AuthenticatorData, Signature = rawResponse.Response.Signature, - UserHandle = rawResponse.Response.UserHandle + UserHandle = rawResponse.Response.UserHandle, + AttestationObject = rawResponse.Response.AttestationObject }; return response; @@ -54,37 +59,37 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp /// The stored public key for this CredentialId /// The stored counter value for this CredentialId /// A function that returns if user handle is owned by the credential ID - /// /// public async Task VerifyAsync( AssertionOptions options, - ISet fullyQualifiedExpectedOrigins, + Fido2Configuration config, byte[] storedPublicKey, + List storedDevicePublicKeys, uint storedSignatureCounter, IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId, - byte[] requestTokenBindingId, + IMetadataService metadataService, CancellationToken cancellationToken = default) { - BaseVerify(fullyQualifiedExpectedOrigins, options.Challenge, requestTokenBindingId); + BaseVerify(config.FullyQualifiedOrigins, options.Challenge); if (Raw.Type != PublicKeyCredentialType.PublicKey) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse type must be public-key"); + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseNotPublicKey); if (Raw.Id is null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "Id is missing"); + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseIdMissing); if (Raw.RawId is null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "RawId is missing"); + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionResponseRawIdMissing); - // 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. + // 5. 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.Any()) { // 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 Fido2VerificationException("Invalid"); + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.CredentialIdNotInAllowedCredentials); } - // 2. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id + // 6. Identify the user being authenticated and verify that this user is the owner of the public key credential source credentialSource identified by credential.id if (UserHandle != null) { if (UserHandle.Length is 0) @@ -92,72 +97,77 @@ public async Task VerifyAsync( if (await isUserHandleOwnerOfCredId(new IsUserHandleOwnerOfCredentialIdParams(Raw.Id, UserHandle), cancellationToken) is false) { - throw new Fido2VerificationException("User is not owner of the public key identified by the credential id"); + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.UserHandleNotOwnerOfPublicKey); } } - // 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. - // Credential public key passed in via storePublicKey parameter - - // 4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively. + // 7. 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(AuthenticatorData); //var sig = Raw.Response.Signature; - // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData. + // 8. 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. + // 10. Verify that the value of C.type is the string webauthn.get. if (Type is not "webauthn.get") - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse must be webauthn.get"); - - // 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 + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, Fido2ErrorMessages.AssertionTypeNotWebAuthnGet); - // 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 value of C.challenge equals the base64url encoding of options.challenge. + // 12. Verify that the value of C.origin matches the Relying Party's origin. + // Both handled in BaseVerify - // 11. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party. + // 13. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party. // 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; byte[] hashedRpId = SHA256.HashData(Encoding.UTF8.GetBytes(rpid ?? string.Empty)); - byte[] hashedClientDataJson = SHA256.HashData(Raw.Response.ClientDataJson); + byte[] hash = SHA256.HashData(Raw.Response.ClientDataJson); if (!authData.RpIdHash.SequenceEqual(hashedRpId)) throw new Fido2VerificationException(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash); - // 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 ((!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 (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified) + // 14. Verify that the UP bit of the flags in authData is set. + if (!authData.UserPresent) + throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet); + + // 15. If the Relying Party requires user verification for this assertion, verify that the UV bit of the flags in authData is set. + if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified) throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet); - // 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) + // 16. If the credential backup state is used as part of Relying Party business logic or policy, let currentBe and currentBs be the values of the BE and BS bits, respectively, of the flags in authData. + // Compare currentBe and currentBs with credentialRecord.BE and credentialRecord.BS and apply Relying Party policy, if any. + if (authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Disallowed || + !authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Required) + throw new Fido2VerificationException(Fido2ErrorCode.BackupEligibilityRequirementNotMet, Fido2ErrorMessages.BackupEligibilityRequirementNotMet); + + if (authData.BackupState && config.BackedUpCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Disallowed || + !authData.BackupState && config.BackedUpCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Required) + throw new Fido2VerificationException(Fido2ErrorCode.BackupStateRequirementNotMet, Fido2ErrorMessages.BackupStateRequirementNotMet); + + // 17. 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 in options.extensions and any specific policy of the Relying Party regarding unsolicited extensions, + // i.e., those that were not specified as part of options.extensions. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use. + byte[] devicePublicKeyResult = null; + if (Raw.Extensions?.DevicePubKey is not null) + { + devicePublicKeyResult = DevicePublicKeyAuthentication(storedDevicePublicKeys, Raw.Extensions, AuthenticatorData, hash); + } + + // Pretty sure these conditions are not able to be met due to the AuthenticatorData constructor implementation if (authData.HasExtensionsData && (authData.Extensions is null || authData.Extensions.Length is 0)) throw new Fido2VerificationException(Fido2ErrorCode.MalformedExtensionsDetected, Fido2ErrorMessages.MalformedExtensionsDetected); if (!authData.HasExtensionsData && authData.Extensions != null) throw new Fido2VerificationException(Fido2ErrorCode.UnexpectedExtensionsDetected, Fido2ErrorMessages.UnexpectedExtensionsDetected); - // 15. - // Done earlier, hashedClientDataJson + // 18. Let hash be the result of computing a hash over the cData using SHA-256. + // done earlier in step 13 - // 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 = DataHelper.Concat(Raw.Response.AuthenticatorData, hashedClientDataJson); + // 19. Using credentialRecord.publicKey, verify that sig is a valid signature over the binary concatenation of authData and hash. + byte[] data = DataHelper.Concat(Raw.Response.AuthenticatorData, hash); if (storedPublicKey is null || storedPublicKey.Length is 0) throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey); @@ -167,16 +177,234 @@ public async Task VerifyAsync( if (!cpk.Verify(data, Signature)) throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignature, Fido2ErrorMessages.InvalidSignature); - // 17. + // 20. If authData.signCount is nonzero or credentialRecord.signCount is nonzero if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter) throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignCount, Fido2ErrorMessages.SignCountIsLessThanSignatureCounter); + // 21. If response.attestationObject is present and the Relying Party wishes to verify the attestation then... + if (AttestationObject is not null) + { + // ... perform CBOR decoding on attestationObject to obtain the attestation statement format fmt, and the attestation statement attStmt. + var cborAttestation = (CborMap)CborObject.Decode(AttestationObject); + string fmt = (string)cborAttestation["fmt"]; + var attStmt = (CborMap)cborAttestation["attStmt"]; + + // 1. Verify that the AT bit in the flags field of authData is set, indicating that attested credential data is included. + if (!authData.HasAttestedCredentialData) + throw new Fido2VerificationException(Fido2ErrorCode.AttestedCredentialDataFlagNotSet, Fido2ErrorMessages.AttestedCredentialDataFlagNotSet); + + // 2. Verify that the credentialPublicKey and credentialId fields of the attested credential data in authData match credentialRecord.publicKey and credentialRecord.id, respectively. + if (!Raw.Id.SequenceEqual(authData.AttestedCredentialData.CredentialID)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "Stored credential id does not match id in attested credential data"); + + if (!storedPublicKey.SequenceEqual(authData.AttestedCredentialData.CredentialPublicKey.GetBytes())) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "Stored public key does not match public key in attested credential data"); + + // 3. 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 "WebAuthn Attestation Statement Format Identifiers" registry [IANA-WebAuthn-Registries] established by [RFC8809]. + var verifier = AttestationVerifier.Create(fmt); + + // 4. 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 hash. + (var attType, var trustPath) = verifier.Verify(attStmt, AuthenticatorData, hash); + + // 5. 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. + + MetadataBLOBPayloadEntry metadataEntry = null; + if (metadataService != null) + metadataEntry = await metadataService.GetEntryAsync(authData.AttestedCredentialData.AaGuid, cancellationToken); + + // while conformance testing, we must reject any authenticator that we cannot get metadata for + if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && fmt is not "fido-u2f") + throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata"); + + AuthenticatorAttestationResponse.VerifyTrustAnchor(metadataEntry, trustPath); + } + return new AssertionVerificationResult { Status = "ok", ErrorMessage = string.Empty, CredentialId = Raw.Id, Counter = authData.SignCount, + BS = authData.BackupState, + DevicePublicKey = devicePublicKeyResult, }; } + + /// + /// If the devicePubKey extension was included on a navigator.credentials.get() call, then the below + /// verification steps are performed in the context of this step of § 7.2 Verifying an Authentication Assertion using + /// these variables established therein: credential, clientExtensionResults, authData, and hash. Relying Party policy + /// may specify whether a response without a devicePubKey is acceptable. + /// + /// + /// + /// + /// + private static byte[] DevicePublicKeyAuthentication( + List storedDevicePublicKeys, + AuthenticationExtensionsClientOutputs clientExtensionResults, + byte[] authData, + byte[] hash + ) + { + // 1. Let attObjForDevicePublicKey be the value of the devicePubKey member of clientExtensionResults. + var attObjForDevicePublicKey = clientExtensionResults.DevicePubKey; + + // 2. Verify that attObjForDevicePublicKey is valid CBOR conforming to the syntax defined above and + // perform CBOR decoding on it to extract the contained fields: aaguid, dpk, scope, nonce, fmt, attStmt. + DevicePublicKeyAuthenticatorOutput devicePublicKeyAuthenticatorOutput = new(attObjForDevicePublicKey.AuthenticatorOutput); + + // 3. Verify that signature is a valid signature over the assertion signature input (i.e. authData and hash) by the device public key dpk. + if (!devicePublicKeyAuthenticatorOutput.DevicePublicKey.Verify(DataHelper.Concat(authData, hash), attObjForDevicePublicKey.Signature)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignature, Fido2ErrorMessages.InvalidSignature); + + // 4. If the Relying Party's user account mapped to the credential.id in play (i.e., for the user being + // authenticated) holds aaguid, dpk and scope values corresponding to the extracted attObjForDevicePublicKey + // fields, then perform binary equality checks between the corresponding stored values and the extracted field + // values. The Relying Party MAY have more than one set of {aaguid, dpk, scope} values mapped to the user + // account and credential.id pair and each set MUST be checked. + if (storedDevicePublicKeys.Count > 0) + { + List matchedDpkRecords = new(); + storedDevicePublicKeys.ForEach(storedDevicePublicKey => + { + DevicePublicKeyAuthenticatorOutput dpkRecord = new(storedDevicePublicKey); + if (dpkRecord.AuthenticationMatcher.SequenceEqual(devicePublicKeyAuthenticatorOutput.AuthenticationMatcher) + && dpkRecord.Scope.Equals(devicePublicKeyAuthenticatorOutput.Scope)) + { + matchedDpkRecords.Add(dpkRecord); + } + }); + + // more than one match + if (matchedDpkRecords.Count > 1) + { + // Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps. + throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.NonUniqueDevicePublicKey); + } + // exactly one match + else if (matchedDpkRecords.Count is 1) + { + // This is likely a known device. + // If fmt's value is "none" then there is no attestation signature to verify and this is a known device public key with a valid signature and thus a known device. Terminate these verification steps. + if (devicePublicKeyAuthenticatorOutput.Fmt.Equals("none")) + return null; + // Otherwise, check attObjForDevicePublicKey's attStmt by performing a binary equality check between the corresponding stored and extracted attStmt values. + else if (devicePublicKeyAuthenticatorOutput.AttStmt.Encode().SequenceEqual(matchedDpkRecords.FirstOrDefault().AttStmt.Encode())) + { + // Note: This authenticator is not generating a fresh per-response random nonce. + return null; + } + else + { + // Optionally, if attestation was requested and the RP wishes to verify it, 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. See § 10.2.2.2.2 + // Attestation calculations. Relying Party policy may specifiy which attestations are acceptable. + // https://www.w3.org/TR/webauthn/#defined-attestation-formats + var verifier = AttestationVerifier.Create(devicePublicKeyAuthenticatorOutput.Fmt); + + // https://w3c.github.io/webauthn/#sctn-device-publickey-attestation-calculations + try + { + // This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps. + _ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash); + } + catch (Exception ex) + { + // Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps. + throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.InvalidDevicePublicKeyAttestation, ex); + } + } + } + // This is possibly a new device public key signifying a new device. + else if (matchedDpkRecords.Count == 0) + { + // Let matchedDpkKeys be a new empty set + List matchedDpkKeys = new(); + + // For each dpkRecord in credentialRecord.devicePubKeys + storedDevicePublicKeys.ForEach(storedDevicePublicKey => + { + DevicePublicKeyAuthenticatorOutput dpkRecord = new(storedDevicePublicKey); + + // If dpkRecord.dpk equals dpk + if (dpkRecord.DevicePublicKey.GetBytes().SequenceEqual(devicePublicKeyAuthenticatorOutput.DevicePublicKey.GetBytes())) + { + // Append dpkRecord to matchedDpkKeys. + matchedDpkKeys.Add(dpkRecord); + } + }); + + // If matchedDpkKeys is empty + if (matchedDpkKeys.Count == 0) + { + // If fmt’s value is "none" + if (devicePublicKeyAuthenticatorOutput.Fmt.Equals("none")) + // There is no attestation signature to verify and this is a new device. + // Unless Relying Party policy specifies that this attestation is unacceptable, Create a new device-bound key record and then terminate these verification steps. + return devicePublicKeyAuthenticatorOutput.GetBytes(); + + // Otherwise + else + { + // Optionally, if attestation was requested and the RP wishes to verify it, 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. See § 10.2.2.2.2 Attestation calculations. + // Relying Party policy may specifiy which attestations are acceptable. + var verifier = AttestationVerifier.Create(devicePublicKeyAuthenticatorOutput.Fmt); + // https://w3c.github.io/webauthn/#sctn-device-publickey-attestation-calculations + try + { + // This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps. + _ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash); + return devicePublicKeyAuthenticatorOutput.GetBytes(); + } + catch (Exception ex) + { + // Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps. + throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.InvalidDevicePublicKeyAttestation, ex); + } + } + } + else + { + // Otherwise there is some form of error: we recieved a known dpk value, but one or more of the + // accompanying aaguid, scope, or fmt values did not match what the Relying Party has stored + // along with that dpk value. Terminate these verification steps. + throw new Fido2VerificationException(Fido2ErrorCode.DevicePublicKeyAuthentication, Fido2ErrorMessages.MissingStoredPublicKey); + } + } + } + + // Otherwise, the Relying Party does not have attObjForDevicePublicKey fields presently mapped to this user account and credential.id pair: + else + { + // If fmt’s value is "none" there is no attestation signature to verify. + // Complete the steps in § 7.2 Verifying an Authentication Assertion and, if those steps are successful, store the extracted aaguid, dpk, scope, fmt, attStmt values indexed to the credential.id in the user account. + // Terminate these verification steps. + if (devicePublicKeyAuthenticatorOutput.Fmt.Equals("none")) + return devicePublicKeyAuthenticatorOutput.GetBytes(); + // Otherwise, 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. See § 10.2.2.2.2 Attestation calculations. + // Relying Party policy may specifiy which attestations are acceptable. + else + { + var verifier = AttestationVerifier.Create(devicePublicKeyAuthenticatorOutput.Fmt); + // https://w3c.github.io/webauthn/#sctn-device-publickey-attestation-calculations + try + { + // This is a known device public key with a valid signature and valid attestation and thus a known device. Terminate these verification steps. + _ = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash); + return devicePublicKeyAuthenticatorOutput.GetBytes(); + } + catch + { + // Some form of error has occurred. It is indeterminate whether this is a known device. Terminate these verification steps. + throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey); + } + } + } + + return null; + } } diff --git a/Src/Fido2/AuthenticatorAttestationResponse.cs b/Src/Fido2/AuthenticatorAttestationResponse.cs index 85ed9b8fa..6e18b32a4 100644 --- a/Src/Fido2/AuthenticatorAttestationResponse.cs +++ b/Src/Fido2/AuthenticatorAttestationResponse.cs @@ -2,6 +2,7 @@ using System; using System.Linq; +using System.Net.Http; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -22,6 +23,9 @@ namespace Fido2NetLib; /// public sealed class AuthenticatorAttestationResponse : AuthenticatorResponse { + private IMetadataService _metadataService; + private CancellationToken _cancellationToken; + private Fido2Configuration _config; private AuthenticatorAttestationResponse(byte[] clientDataJson) : base(clientDataJson) { @@ -74,24 +78,24 @@ public async Task VerifyAsync( Fido2Configuration config, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, IMetadataService metadataService, - byte[] requestTokenBindingId, CancellationToken cancellationToken = default) { + _metadataService = metadataService; + _cancellationToken = cancellationToken; + _config = config; // 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. + // 5. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON. + // 6. 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 + // 7. Verify that the value of C.type is webauthn.create if (Type is not "webauthn.create") throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse type must be 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.FullyQualifiedOrigins, originalOptions.Challenge, requestTokenBindingId); + // 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call. + // 9. Verify that the value of C.origin matches the Relying Party's origin. + BaseVerify(config.FullyQualifiedOrigins, originalOptions.Challenge); if (Raw.Id is null || Raw.Id.Length == 0) throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse is missing Id"); @@ -101,51 +105,59 @@ public async Task VerifyAsync( var authData = new AuthenticatorData(AttestationObject.AuthData); - // 7. Compute the hash of response.clientDataJSON using SHA-256. + // 10. Let hash be the result of computing a hash over response.clientDataJSON using SHA-256. byte[] clientDataHash = SHA256.HashData(Raw.Response.ClientDataJson); byte[] rpIdHash = SHA256.HashData(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, + // 11. 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 + // 12. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party if (!authData.RpIdHash.AsSpan().SequenceEqual(rpIdHash)) throw new Fido2VerificationException(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash); - // 10. Verify that the User Present bit of the flags in authData is set. + // 13. Verify that the User Present bit of the flags in authData is set. if (!authData.UserPresent) throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet); - // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. + // 14. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. if (originalOptions.AuthenticatorSelection?.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified) throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet); - // 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); + // 15. If the Relying Party uses the credential's backup eligibility to inform its user experience flows and/or policies, evaluate the BE bit of the flags in authData. + if (authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Disallowed || + !authData.IsBackupEligible && config.BackupEligibleCredentialPolicy is Fido2Configuration.CredentialBackupPolicy.Required) + throw new Fido2VerificationException(Fido2ErrorCode.BackupEligibilityRequirementNotMet, Fido2ErrorMessages.BackupEligibilityRequirementNotMet); if (!authData.HasAttestedCredentialData) throw new Fido2VerificationException(Fido2ErrorCode.AttestedCredentialDataFlagNotSet, Fido2ErrorMessages.AttestedCredentialDataFlagNotSet); - if (string.IsNullOrEmpty(AttestationObject.Fmt)) + // 17. Verify that the "alg" parameter in the credential public key in authData matches the alg attribute of one of the items in options.pubKeyCredParams. + if (!originalOptions.PubKeyCredParams.Any(a => authData.AttestedCredentialData.CredentialPublicKey.IsSameAlg(a.Alg))) + throw new Fido2VerificationException(Fido2ErrorCode.CredentialAlgorithmRequirementNotMet, Fido2ErrorMessages.CredentialAlgorithmRequirementNotMet); + + // 18. 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); + byte[] devicePublicKeyResult = null; + if (Raw.Extensions?.DevicePubKey is not null) { - throw new Fido2VerificationException(Fido2ErrorCode.MissingAttestationType, Fido2ErrorMessages.MissingAttestationType); + devicePublicKeyResult = await DevicePublicKeyRegistrationAsync(Raw.Extensions, AttestationObject.AuthData, clientDataHash); } - // 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt + // 19. 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. var verifier = AttestationVerifier.Create(AttestationObject.Fmt); - // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, + // 20. 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) + // 21. 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. @@ -157,6 +169,119 @@ public async Task VerifyAsync( if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && AttestationObject.Fmt is not "fido-u2f") throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata"); + VerifyTrustAnchor(metadataEntry, trustPath); + + // 22. 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. + + // Check status resports for authenticator with undesirable status + var latestStatusReport = metadataEntry?.GetLatestStatusReport(); + if (latestStatusReport != null && config.UndesiredAuthenticatorMetadataStatuses.Contains(latestStatusReport.Status)) + { + throw new UndesiredMetdatataStatusFido2VerificationException(latestStatusReport); + } + + // 23. Verify that the credentialId is ≤ 1023 bytes. + // Handled by AttestedCredentialData constructor + + // 24. 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 (await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User), cancellationToken) is false) + { + throw new Fido2VerificationException(Fido2ErrorCode.NonUniqueCredentialId, Fido2ErrorMessages.NonUniqueCredentialId); + } + + // 25. 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. + + // 26. 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. + + return new AttestationVerificationSuccess + { + Type = Raw.Type, + Id = authData.AttestedCredentialData.CredentialID, + PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(), + SignCount = authData.SignCount, + //Transports = result of response.getTransports(); + BE = authData.IsBackupEligible, + BS = authData.BackupState, + AttestationObject = Raw.Response.AttestationObject, + AttestationClientDataJSON = Raw.Response.ClientDataJson, + User = originalOptions.User, + CredType = AttestationObject.Fmt, + AaGuid = authData.AttestedCredentialData.AaGuid, + DevicePublicKey = devicePublicKeyResult + }; + } + + /// + /// If the devicePubKey extension was included on a navigator.credentials.create() call, + /// then the below verification steps are performed in the context of this step of § 7.1 + /// Registering a New Credential using these variables established therein: + /// credential, clientExtensionResults, authData, and hash. + /// Relying Party policy may specify whether a response without a devicePubKey is acceptable. + /// + /// + /// + /// + /// + private async Task DevicePublicKeyRegistrationAsync( + AuthenticationExtensionsClientOutputs clientExtensionResults, + byte[] authData, + byte[] hash + ) + { + // 1. Let attObjForDevicePublicKey be the value of the devicePubKey member of clientExtensionResults. + var attObjForDevicePublicKey = clientExtensionResults.DevicePubKey; + + // 2. Verify that attObjForDevicePublicKey is valid CBOR conforming to the syntax defined above and + // perform CBOR decoding on it to extract the contained fields: aaguid, dpk, scope, nonce, fmt, attStmt. + DevicePublicKeyAuthenticatorOutput devicePublicKeyAuthenticatorOutput = new(attObjForDevicePublicKey.AuthenticatorOutput); + + // 3. Verify that signature is a valid signature over the assertion signature input (i.e. authData and hash) by the device public key dpk. + if (!devicePublicKeyAuthenticatorOutput.DevicePublicKey.Verify(DataHelper.Concat(authData, hash), attObjForDevicePublicKey.Signature)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignature, Fido2ErrorMessages.InvalidSignature); + + // 4. Optionally, if attestation was requested and the Relying Party wishes to verify it, 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. + // https://www.w3.org/TR/webauthn/#defined-attestation-formats + var verifier = AttestationVerifier.Create(devicePublicKeyAuthenticatorOutput.Fmt); + + // https://w3c.github.io/webauthn/#sctn-device-publickey-attestation-calculations + (var attType, var trustPath) = verifier.Verify(devicePublicKeyAuthenticatorOutput.AttStmt, devicePublicKeyAuthenticatorOutput.AuthData, devicePublicKeyAuthenticatorOutput.Hash); + + // 5. Complete the steps from § 7.1 Registering a New Credential and, if those steps are successful, + // store the aaguid, dpk, scope, fmt, attStmt values indexed to the credential.id in the user account. + MetadataBLOBPayloadEntry metadataEntry = null; + if (_metadataService != null) + metadataEntry = await _metadataService.GetEntryAsync(devicePublicKeyAuthenticatorOutput.AaGuid, _cancellationToken); + + // while conformance testing, we must reject any authenticator that we cannot get metadata for + if (_metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && devicePublicKeyAuthenticatorOutput.Fmt is not "fido-u2f") + throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata"); + + VerifyTrustAnchor(metadataEntry, trustPath); + + // Check status resports for authenticator with undesirable status + var latestStatusReport = metadataEntry?.GetLatestStatusReport(); + if (latestStatusReport != null && _config.UndesiredAuthenticatorMetadataStatuses.Contains(latestStatusReport.Status)) + { + throw new UndesiredMetdatataStatusFido2VerificationException(latestStatusReport); + } + + return devicePublicKeyAuthenticatorOutput.GetBytes(); + } + + public static void VerifyTrustAnchor(MetadataBLOBPayloadEntry metadataEntry, X509Certificate2[] trustPath) + { if (trustPath != null && metadataEntry?.MetadataStatement?.AttestationTypes is not null) { static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAttestationType type) @@ -203,48 +328,6 @@ static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAtte // [ ] ATTESTATION_ANONCA "anonca" | currently not verified w/ no test coverage // [ ] ATTESTATION_NONE "none" | currently handled as self signed w/ no test coverage } - - // Check status resports for authenticator with undesirable status - var latestStatusReport = metadataEntry?.GetLatestStatusReport(); - if (latestStatusReport != null && config.UndesiredAuthenticatorMetadataStatuses.Contains(latestStatusReport.Status)) - { - throw new UndesiredMetdatataStatusFido2VerificationException(latestStatusReport); - } - - // 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 (await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User), cancellationToken) is false) - { - throw new Fido2VerificationException(Fido2ErrorCode.NonUniqueCredentialId, Fido2ErrorMessages.NonUniqueCredentialId); - } - - // 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. - - // 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. - - return new AttestationVerificationSuccess - { - CredentialId = authData.AttestedCredentialData.CredentialID, - PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(), - User = originalOptions.User, - Counter = authData.SignCount, - CredType = AttestationObject.Fmt, - AaGuid = authData.AttestedCredentialData.AaGuid, - AttestationCertificate = trustPath?.FirstOrDefault(), - AttestationCertificateChain = trustPath ?? Array.Empty(), - }; } /// diff --git a/Src/Fido2/AuthenticatorResponse.cs b/Src/Fido2/AuthenticatorResponse.cs index 855ef23b7..b415c5ed1 100644 --- a/Src/Fido2/AuthenticatorResponse.cs +++ b/Src/Fido2/AuthenticatorResponse.cs @@ -63,9 +63,7 @@ protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson) [JsonPropertyName("origin")] public string Origin { get; set; } - // todo: add TokenBinding https://www.w3.org/TR/webauthn/#dictdef-tokenbinding - - protected void BaseVerify(ISet fullyQualifiedExpectedOrigins, ReadOnlySpan originalChallenge, ReadOnlySpan requestTokenBindingId) + protected void BaseVerify(ISet fullyQualifiedExpectedOrigins, ReadOnlySpan originalChallenge) { if (Type is not "webauthn.create" && Type is not "webauthn.get") throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, $"Type must be 'webauthn.create' or 'webauthn.get'. Was '{Type}'"); @@ -73,13 +71,13 @@ protected void BaseVerify(ISet fullyQualifiedExpectedOrigins, ReadOnlySp if (Challenge is null) throw new Fido2VerificationException(Fido2ErrorCode.MissingAuthenticatorResponseChallenge, Fido2ErrorMessages.MissingAuthenticatorResponseChallange); - // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call + // 11. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call if (!Challenge.AsSpan().SequenceEqual(originalChallenge)) throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponseChallenge, Fido2ErrorMessages.InvalidAuthenticatorResponseChallenge); var fullyQualifiedOrigin = Origin.ToFullyQualifiedOrigin(); - // 5. Verify that the value of C.origin matches the Relying Party's origin. + // 12. Verify that the value of C.origin matches the Relying Party's origin. if (!fullyQualifiedExpectedOrigins.Contains(fullyQualifiedOrigin)) throw new Fido2VerificationException($"Fully qualified origin {fullyQualifiedOrigin} of {Origin} not equal to fully qualified original origin {string.Join(", ", fullyQualifiedExpectedOrigins.Take(MAX_ORIGINS_TO_PRINT))} ({fullyQualifiedExpectedOrigins.Count})"); diff --git a/Src/Fido2/DevelopmentInMemoryStore.cs b/Src/Fido2/DevelopmentInMemoryStore.cs index f67057ed6..72bd62ccf 100644 --- a/Src/Fido2/DevelopmentInMemoryStore.cs +++ b/Src/Fido2/DevelopmentInMemoryStore.cs @@ -43,7 +43,7 @@ public Task> GetCredentialsByUserHandleAsync(byte[] userH public void UpdateCounter(byte[] credentialId, uint counter) { var cred = _storedCredentials.First(c => c.Descriptor.Id.AsSpan().SequenceEqual(credentialId)); - cred.SignatureCounter = counter; + cred.SignCount = counter; } public void AddCredentialToUser(Fido2User user, StoredCredential credential) @@ -68,12 +68,70 @@ public Task> GetUsersByCredentialIdAsync(byte[] credentialId, Ca public class StoredCredential { + /// + /// The type of the public key credential source. + /// + public PublicKeyCredentialType Type { get; set; } = PublicKeyCredentialType.PublicKey; + + /// + /// The Credential ID of the public key credential source. + /// + public byte[] Id { get; set; } + + /// + /// The credential public key of the public key credential source. + /// + public byte[] PublicKey { get; set; } + + /// + /// The latest value of the signature counter in the authenticator data from any ceremony using the public key credential source. + /// + public uint SignCount { get; set; } + + /// + /// The value returned from getTransports() when the public key credential source was registered. + /// + public AuthenticatorTransport[] Transports { get; set; } + + /// + /// The value of the BE flag when the public key credential source was created. + /// + public bool BE { get; set; } + + /// + /// The latest value of the BS flag in the authenticator data from any ceremony using the public key credential source. + /// + public bool BS { get; set; } + + /// + /// The value of the attestationObject attribute when the public key credential source was registered. + /// Storing this enables the Relying Party to reference the credential's attestation statement at a later time. + /// + public byte[] AttestationObject { get; set; } + + /// + /// The value of the clientDataJSON attribute when the public key credential source was registered. + /// Storing this in combination with the above attestationObject item enables the Relying Party to re-verify the attestation signature at a later time. + /// + public byte[] AttestationClientDataJSON { get; set; } + + /// + /// + /// + public List DevicePublicKeys { get; set; } + public byte[] UserId { get; set; } + public PublicKeyCredentialDescriptor Descriptor { get; set; } - public byte[] PublicKey { get; set; } + public byte[] UserHandle { get; set; } - public uint SignatureCounter { get; set; } + + public uint SignatureCounter => SignCount; + public string CredType { get; set; } + public DateTime RegDate { get; set; } + public Guid AaGuid { get; set; } + } diff --git a/Src/Fido2/Fido2ErrorMessages.cs b/Src/Fido2/Fido2ErrorMessages.cs index 4ccf55d8c..ea4b95067 100644 --- a/Src/Fido2/Fido2ErrorMessages.cs +++ b/Src/Fido2/Fido2ErrorMessages.cs @@ -18,15 +18,27 @@ internal static class Fido2ErrorMessages public static readonly string InvalidCertificateChain = "Invalid certificate chain"; public static readonly string UserHandleIsEmpty = "UserHandle was empty DOMString. It should either be null or have a value."; public static readonly string InvalidAttestedCredentialData_TooShort = "Not enough bytes to be a valid AttestedCredentialData"; + public static readonly string InvalidAttestedCredentialData_CredentialIdTooLong = "CredentialId too many bytes"; public static readonly string MissingAuthenticatorResponseChallange = "Authenticator response challenge may not be null"; public static readonly string InvalidAuthenticatorResponseChallenge = "Authenticator response challenge does not match original challenge"; public static readonly string AttestedCredentialDataFlagNotSet = "Attestation flag not set on attestation data"; public static readonly string MissingAuthenticatorData = "Authenticator data may not be null"; public static readonly string InvalidAuthenticatorData_TooShort = $"Authenticator data is less than the minimum structure length of {AuthenticatorData.MinLength}"; public static readonly string InvalidCoseAlgorithmValue = "Unrecognized COSE algorithm value"; + public static readonly string BackupEligibilityRequirementNotMet = "Backup eligibility does not match policy requirement"; + public static readonly string BackupStateRequirementNotMet = "Backup state does not match policy requirement"; + public static readonly string CredentialAlgorithmRequirementNotMet = "Credential algorithm does not match policy requirement"; public static readonly string NonUniqueCredentialId = "CredentialId is not unique to this user"; public static readonly string MissingAttestationType = "Missing attestation type"; public static readonly string InvalidAttestationCertSubject = "Invalid attestation cert subject"; + public static readonly string InvalidDevicePublicKeyAttestation = "Invalid devicePublicKey attestation"; + public static readonly string NonUniqueDevicePublicKey = "More than one devicePublicKey match"; + public static readonly string AssertionResponseNotPublicKey = "AssertionResponse type must be public-key"; + public static readonly string AssertionResponseIdMissing = "AssertionResponse Id is missing"; + public static readonly string AssertionResponseRawIdMissing = "AssertionResponse RawId is missing"; + public static readonly string CredentialIdNotInAllowedCredentials = "Credential ID not in allowed credentials"; + public static readonly string UserHandleNotOwnerOfPublicKey = "User is not owner of the public key identified by the credential id"; + public static readonly string AssertionTypeNotWebAuthnGet = "AssertionResponse type must be webauthn.get"; public static readonly string UnimplementedAlgorithm_Ecdaa_Packed = "ECDAA support for packed attestation is not yet implemented"; public static readonly string UnimplementedAlgorithm_Ecdaa_Tpm = "ECDAA support for TPM attestation is not yet implemented"; diff --git a/Src/Fido2/Fido2NetLib.cs b/Src/Fido2/Fido2NetLib.cs index 6bba59159..2156b2780 100644 --- a/Src/Fido2/Fido2NetLib.cs +++ b/Src/Fido2/Fido2NetLib.cs @@ -59,18 +59,16 @@ public CredentialCreateOptions RequestNewCredential( /// /// /// - /// /// /// public async Task MakeNewCredentialAsync( AuthenticatorAttestationRawResponse attestationResponse, CredentialCreateOptions origChallenge, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, - byte[]? requestTokenBindingId = null, CancellationToken cancellationToken = default) { var parsedResponse = AuthenticatorAttestationResponse.Parse(attestationResponse); - var success = await parsedResponse.VerifyAsync(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, requestTokenBindingId, cancellationToken); + var success = await parsedResponse.VerifyAsync(origChallenge, _config, isCredentialIdUniqueToUser, _metadataService, cancellationToken); // todo: Set Errormessage etc. return new CredentialMakeResult( @@ -102,19 +100,20 @@ public async Task MakeAssertionAsync( AuthenticatorAssertionRawResponse assertionResponse, AssertionOptions originalOptions, byte[] storedPublicKey, + List storedDevicePublicKeys, uint storedSignatureCounter, IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback, - byte[]? requestTokenBindingId = null, CancellationToken cancellationToken = default) { var parsedResponse = AuthenticatorAssertionResponse.Parse(assertionResponse); var result = await parsedResponse.VerifyAsync(originalOptions, - _config.FullyQualifiedOrigins, + _config, storedPublicKey, + storedDevicePublicKeys, storedSignatureCounter, isUserHandleOwnerOfCredentialIdCallback, - requestTokenBindingId, + _metadataService, cancellationToken); return result; diff --git a/Src/Fido2/IFido2.cs b/Src/Fido2/IFido2.cs index 235d3277b..d9fd52ee2 100644 --- a/Src/Fido2/IFido2.cs +++ b/Src/Fido2/IFido2.cs @@ -16,16 +16,15 @@ Task MakeAssertionAsync( AuthenticatorAssertionRawResponse assertionResponse, AssertionOptions originalOptions, byte[] storedPublicKey, + List storedDevicePublicKeys, uint storedSignatureCounter, IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback, - byte[]? requestTokenBindingId = null, CancellationToken cancellationToken = default); Task MakeNewCredentialAsync( AuthenticatorAttestationRawResponse attestationResponse, CredentialCreateOptions origChallenge, IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, - byte[]? requestTokenBindingId = null, CancellationToken cancellationToken = default); CredentialCreateOptions RequestNewCredential( diff --git a/Src/Fido2/Objects/AttestedCredentialData.cs b/Src/Fido2/Objects/AttestedCredentialData.cs index a20c5f922..eb54f0c56 100644 --- a/Src/Fido2/Objects/AttestedCredentialData.cs +++ b/Src/Fido2/Objects/AttestedCredentialData.cs @@ -1,7 +1,6 @@ using System; using System.Buffers; using System.Buffers.Binary; -using System.Runtime.InteropServices; using Fido2NetLib.Exceptions; @@ -13,7 +12,9 @@ public sealed class AttestedCredentialData /// Minimum length of the attested credential data structure. AAGUID + credentialID length + credential ID + credential public key. /// /// - private static readonly int _minLength = Marshal.SizeOf() + sizeof(ushort) + sizeof(byte) + sizeof(byte); + private const int _minLength = 20; // Marshal.SizeOf(typeof(Guid)) + sizeof(ushort) + sizeof(byte) + sizeof(byte) + + private const int _maxCredentialIdLength = 1023; /// /// Instantiates an AttestedCredentialData object from an aaguid, credentialID, and CredentialPublicKey @@ -60,6 +61,8 @@ internal AttestedCredentialData(ReadOnlyMemory data, out int bytesRead) // Byte length of Credential ID, 16-bit unsigned big-endian integer. var credentialIDLen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(position, 2).Span); + if (credentialIDLen > _maxCredentialIdLength) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestedCredentialData, Fido2ErrorMessages.InvalidAttestedCredentialData_CredentialIdTooLong); position += 2; diff --git a/Src/Fido2/Objects/AuthenticatorData.cs b/Src/Fido2/Objects/AuthenticatorData.cs index 9427aafd5..e8ce27853 100644 --- a/Src/Fido2/Objects/AuthenticatorData.cs +++ b/Src/Fido2/Objects/AuthenticatorData.cs @@ -41,6 +41,22 @@ public sealed class AuthenticatorData /// public bool UserVerified => _flags.HasFlag(AuthenticatorFlags.UV); + /// + /// A Public Key Credential Source's generating authenticator determines at creation time whether the public key credential source is allowed to be backed up. + /// Backup eligibility is signaled in authenticator data's flags along with the current backup state. + /// Backup eligibility is a credential property and is permanent for a given public key credential source. + /// A backup eligible public key credential source is referred to as a multi-device credential whereas one that is not backup eligible is referred to as a single-device credential. + /// + /// + public bool IsBackupEligible => _flags.HasFlag(AuthenticatorFlags.BE); + + /// + /// The current backup state of a multi-device credential as determined by the current managing authenticator. + /// Backup state is signaled in authenticator data's flags and can change over time. + /// + /// + public bool BackupState => _flags.HasFlag(AuthenticatorFlags.BS); + /// /// HasAttestedCredentialData indicates that the authenticator added attested credential data to the authenticator data. /// diff --git a/Src/Fido2/Objects/AuthenticatorFlags.cs b/Src/Fido2/Objects/AuthenticatorFlags.cs index 642b02be5..ab26236e0 100644 --- a/Src/Fido2/Objects/AuthenticatorFlags.cs +++ b/Src/Fido2/Objects/AuthenticatorFlags.cs @@ -27,14 +27,20 @@ public enum AuthenticatorFlags : byte UV = 0x4, /// - /// Reserved for future use (RFU2) + /// A Public Key Credential Source's generating authenticator determines at creation time whether the public key credential source is allowed to be backed up. + /// Backup eligibility is signaled in authenticator data's flags along with the current backup state. + /// Backup eligibility is a credential property and is permanent for a given public key credential source. + /// A backup eligible public key credential source is referred to as a multi-device credential whereas one that is not backup eligible is referred to as a single-device credential. + /// /// - RFU2 = 0x8, + BE = 0x8, /// - /// Reserved for future use (RFU3) + /// The current backup state of a multi-device credential as determined by the current managing authenticator. + /// Backup state is signaled in authenticator data's flags and can change over time. + /// /// - RFU3 = 0x10, + BS = 0x10, /// /// Reserved for future use (RFU4) diff --git a/Src/Fido2/Objects/DevicePublicKeyAuthenticatorOutput.cs b/Src/Fido2/Objects/DevicePublicKeyAuthenticatorOutput.cs new file mode 100644 index 000000000..cbf4074ae --- /dev/null +++ b/Src/Fido2/Objects/DevicePublicKeyAuthenticatorOutput.cs @@ -0,0 +1,91 @@ +#nullable disable +namespace Fido2NetLib.Objects; + +using System; +using Fido2NetLib.Cbor; +using NSec.Cryptography; + + +public sealed class DevicePublicKeyAuthenticatorOutput +{ + // https://w3c.github.io/webauthn/#sctn-device-publickey-attestation-calculations + internal static ReadOnlySpan _dpkAuthDataPrefix => new byte[] { + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x20, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x20, + 0x6b, 0x65, 0x79, 0x20, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x73, 0x69, 0x67, 0x00, 0xff, 0xff, 0xff, 0xff }; + + internal CborMap _attObj; + + public byte[] AuthData => DataHelper.Concat(_dpkAuthDataPrefix, AaGuid.ToByteArray()); + + public byte[] Hash => DataHelper.Concat(DevicePublicKey.GetBytes(), Nonce); + + public byte[] GetBytes() => _attObj.Encode(); + + public byte[] AuthenticationMatcher => DataHelper.Concat(AaGuid.ToByteArray(), DevicePublicKey.GetBytes()); + + public ReadOnlySpan Nonce => _nonce; + + public DevicePublicKeyAuthenticatorOutput(CborMap attObjForDevicePublicKey) + { + AaGuid = new Guid((byte[])attObjForDevicePublicKey["aaguid"]); + DevicePublicKey = new CredentialPublicKey((byte[])attObjForDevicePublicKey["dpk"]); + Scope = (uint)attObjForDevicePublicKey["scope"]; + _nonce = (byte[])attObjForDevicePublicKey["nonce"]; + Fmt = (string)attObjForDevicePublicKey["fmt"]; + AttStmt = (CborMap)attObjForDevicePublicKey["attStmt"]; + EpAtt = false; + if ((Fmt == "enterprise") && attObjForDevicePublicKey["epAtt"] is not null) + EpAtt = (bool)attObjForDevicePublicKey["epAtt"]; + _attObj = attObjForDevicePublicKey; + } + + public DevicePublicKeyAuthenticatorOutput(byte[] attObjForDevicePublicKey) : this((CborMap)CborObject.Decode(attObjForDevicePublicKey)) { } + + /// + /// The AAGUID of the authenticator. Can be used to identify the make and model of the authenticator. + /// + /// + public Guid AaGuid { get; } + + /// + /// The credential public key encoded in COSE_Key format, as defined in + /// Section 7 of RFC8152, using the CTAP2 canonical CBOR encoding form. + /// + /// + public CredentialPublicKey DevicePublicKey { get; } + + /// + /// Whether this key is scoped to the entire device, or a loosely-defined, narrower scope called "app". + /// For example, a "device"-scoped key is expected to be the same between an app and a browser on the same device, while an "app"-scoped key would probably not be. + /// Whatever the scope, a device key is still specific to a given credential and does not provide any ability to link credentials. + /// Whether device-scoped or not, keys are still device-bound. I.e.an app-scoped key does not enjoy lesser protection from extraction. + /// A value of 0x00 means "entire device" ("all apps") scope. + /// 0x01 means "per-app" scope. Values other than 0x00 or 0x01 are reserved for future use. + /// + public uint Scope { get; } + + /// + /// An authenticator-generated random nonce for inclusion in the attestation signature. + /// If the authenticator chooses to not generate a nonce, it sets this to a zero-length byte string. + /// See the note below about "randomNonce" for a discussion on the nonce's purpose. + /// + public byte[] _nonce { get; } + + /// + /// Attestation statement formats are identified by a string, called an attestation statement format identifier, chosen by the author of the attestation statement format. + /// + /// + public string Fmt { get; } + + /// + /// A CborMap encoded attestation statement. + /// + public CborMap AttStmt { get; } + + /// + /// An optional boolean that indicates whether the attestation statement contains uniquely identifying information. + /// This can only be true when the `attestation` field of the extension input is "enterprise" and either the user-agent or the authenticator permits uniquely identifying attestation for the requested RP ID. + /// + public bool? EpAtt { get; } +} diff --git a/Test/Attestation/AndroidKey.cs b/Test/Attestation/AndroidKey.cs index 1233023c8..f3958900c 100644 --- a/Test/Attestation/AndroidKey.cs +++ b/Test/Attestation/AndroidKey.cs @@ -67,9 +67,9 @@ public async void TestAndroidKey() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("android-key", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); diff --git a/Test/Attestation/AndroidSafetyNet.cs b/Test/Attestation/AndroidSafetyNet.cs index 294929549..28619e2ae 100644 --- a/Test/Attestation/AndroidSafetyNet.cs +++ b/Test/Attestation/AndroidSafetyNet.cs @@ -104,9 +104,9 @@ public async void TestAndroidSafetyNet() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("android-safetynet", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); @@ -193,9 +193,9 @@ public async void TestAndroidSafetyNetRSA() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("android-safetynet", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); diff --git a/Test/Attestation/DevicePublicKey.cs b/Test/Attestation/DevicePublicKey.cs new file mode 100644 index 000000000..dbdbc8de3 --- /dev/null +++ b/Test/Attestation/DevicePublicKey.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using fido2_net_lib.Test; + +using Fido2NetLib.Objects; + +namespace Test.Attestation; +public class DevicePublicKey : Fido2Tests.Attestation +{ + public DevicePublicKey() + { + } + + [Fact] + public void TestDevicePublicKey() + { + string json = """{"authenticatorOutput":"pmNkcGtYTaUBAgMmIAEhWCBNwZidDC8QQNAffsFaxUKxTbVLxepdV-1_azg-u0-rsCJYIFtht9l1L8g2hqQOo8omnBd9fRj2byJzn1JQqnp19oVbY2ZtdGRub25lZW5vbmNlQGVzY29wZQBmYWFndWlkUAAAAAAAAAAAAAAAAAAAAABnYXR0U3RtdKA=","signature":"MEUCIQDTf2ImngEOi3qHws6gxf6CpquI97oDIl8m_4T2xQO-YwIgdWN7elqNuU-yMZtGpy8hQtL_E-qmZ1_rM2u2nhXYw7A="}"""; + + var model = JsonSerializer.Deserialize(json); + DevicePublicKeyAuthenticatorOutput devicePublicKeyAuthenticatorOutput = new(model.AuthenticatorOutput); + Assert.Equal("none", devicePublicKeyAuthenticatorOutput.Fmt); + } +} diff --git a/Test/Attestation/FidoU2f.cs b/Test/Attestation/FidoU2f.cs index b7c927ba9..f39259924 100644 --- a/Test/Attestation/FidoU2f.cs +++ b/Test/Attestation/FidoU2f.cs @@ -59,9 +59,9 @@ public async void TestU2f() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("fido-u2f", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); diff --git a/Test/Attestation/None.cs b/Test/Attestation/None.cs index 56884ba83..1a76494c0 100644 --- a/Test/Attestation/None.cs +++ b/Test/Attestation/None.cs @@ -33,9 +33,9 @@ public void TestNone() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("none", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); diff --git a/Test/Attestation/Packed.cs b/Test/Attestation/Packed.cs index f0c0604e7..95d7b245e 100644 --- a/Test/Attestation/Packed.cs +++ b/Test/Attestation/Packed.cs @@ -41,9 +41,9 @@ public void TestSelf() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("packed", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); @@ -330,9 +330,9 @@ public void TestFull() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("packed", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); diff --git a/Test/Attestation/Tpm.cs b/Test/Attestation/Tpm.cs index 667719525..9160294cc 100644 --- a/Test/Attestation/Tpm.cs +++ b/Test/Attestation/Tpm.cs @@ -304,9 +304,9 @@ public void TestTPM() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("tpm", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); @@ -426,9 +426,9 @@ public void TestTPMAikCertSANTCGConformant() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("tpm", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); @@ -5259,9 +5259,9 @@ public async void TestTPMAikCertMisingAAGUID() Assert.Equal(string.Empty, res.ErrorMessage); Assert.Equal("ok", res.Status); Assert.Equal(_aaguid, res.Result.AaGuid); - Assert.Equal(_signCount, res.Result.Counter); + Assert.Equal(_signCount, res.Result.SignCount); Assert.Equal("tpm", res.Result.CredType); - Assert.Equal(_credentialID, res.Result.CredentialId); + Assert.Equal(_credentialID, res.Result.Id); Assert.Null(res.Result.ErrorMessage); Assert.Equal(_credentialPublicKey.GetBytes(), res.Result.PublicKey); Assert.Null(res.Result.Status); diff --git a/Test/AuthenticatorResponse.cs b/Test/AuthenticatorResponse.cs index 989d24332..b3ee7bdf5 100644 --- a/Test/AuthenticatorResponse.cs +++ b/Test/AuthenticatorResponse.cs @@ -7,6 +7,9 @@ using Fido2NetLib.Cbor; using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; +using Fido2NetLib.Serialization; +using NSec.Cryptography; +using Test.Attestation; namespace Test; @@ -40,7 +43,7 @@ public class AuthenticatorResponse [InlineData("http://[0:0:0:0:0:0:0:1]", "http://[0:0:0:0:0:0:0:1]:80")] [InlineData("https://[0:0:0:0:0:0:0:1]", "https://[0:0:0:0:0:0:0:1]")] [InlineData("https://[0:0:0:0:0:0:0:1]", "https://[0:0:0:0:0:0:0:1]:443")] - public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin) + public async Task TestAuthenticatorOriginsAsync(string origin, string expectedOrigin) { var challenge = RandomNumberGenerator.GetBytes(128); var rp = origin; @@ -503,7 +506,7 @@ public void TestAuthenticatorAttestationResponseInvalidRawType() var rawResponse = new AuthenticatorAttestationRawResponse { - Type = null, + Type = PublicKeyCredentialType.Invalid, Id = new byte[] { 0xf1, 0xd0 }, RawId = new byte[] { 0xf1, 0xd0 }, Response = new AuthenticatorAttestationRawResponse.ResponseData() @@ -638,7 +641,7 @@ public void TestAuthenticatorAttestationResponseRpidMismatch() } [Fact] - public async Task TestAuthenticatorAttestationResponseNotUserPresent() + public async Task TestAuthenticatorAttestationResponseNotUserPresentAsync() { var challenge = RandomNumberGenerator.GetBytes(128); var rp = "https://www.passwordless.dev"; @@ -717,6 +720,162 @@ public async Task TestAuthenticatorAttestationResponseNotUserPresent() Assert.Equal(Fido2ErrorMessages.UserPresentFlagNotSet, ex.Message); } + [Fact] + public void TestAuthenticatorAttestationResponseBackupEligiblePolicyRequired() + { + var challenge = RandomNumberGenerator.GetBytes(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, + ResidentKey = ResidentKeyRequirement.Required, + UserVerification = UserVerificationRequirement.Discouraged, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam(COSE.Algorithm.ES256) + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = "testuser"u8.ToArray(), + 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 { rp }, + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal(Fido2ErrorMessages.BackupEligibilityRequirementNotMet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAttestationResponseBackupEligiblePolicyDisallowed() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + var authData = new AuthenticatorData( + SHA256.HashData(Encoding.UTF8.GetBytes(rp)), + AuthenticatorFlags.UP | AuthenticatorFlags.UV | AuthenticatorFlags.BE, + 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, + ResidentKey = ResidentKeyRequirement.Required, + UserVerification = UserVerificationRequirement.Discouraged, + }, + Challenge = challenge, + ErrorMessage = "", + PubKeyCredParams = new List() + { + new PubKeyCredParam(COSE.Algorithm.ES256) + }, + Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), + Status = "ok", + User = new Fido2User + { + Name = "testuser", + Id = "testuser"u8.ToArray(), + 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 { rp }, + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Disallowed, + }); + + var ex = Assert.ThrowsAsync(() => lib.MakeNewCredentialAsync(rawResponse, origChallenge, callback)); + Assert.Equal(Fido2ErrorMessages.BackupEligibilityRequirementNotMet, ex.Result.Message); + } + [Fact] public void TestAuthenticatorAttestationResponseNoAttestedCredentialData() { @@ -1080,4 +1239,1302 @@ public void TestAuthenticatorAssertionRawResponse() Assert.Equal("test", assertionResponse.Extensions.Example); Assert.Equal((ulong)4, assertionResponse.Extensions.UserVerificationMethod[0][0]); } + + [Fact] + public void TestAuthenticatorAssertionTypeNotPublicKey() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.Invalid, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.AssertionResponseNotPublicKey, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionIdMissing() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.AssertionResponseIdMissing, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionRawIdMissing() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.AssertionResponseRawIdMissing, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionUserHandleEmpty() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.UserHandleIsEmpty, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionUserHandleNotOwnerOfPublicKey() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(false); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.UserHandleNotOwnerOfPublicKey, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionTypeNotWebAuthnGet() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.create", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.AssertionTypeNotWebAuthnGet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionAppId() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Extensions = new() { AppID = "https://foo.bar" }, + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = true, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.InvalidRpidHash, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionInvalidRpIdHash() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes("https://foo.bar")), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.InvalidRpidHash, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionUPRequirementNotMet() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + UserVerification = UserVerificationRequirement.Required, + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), 0, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.UserPresentFlagNotSet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionUVPolicyNotMet() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + UserVerification = UserVerificationRequirement.Required, + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey, + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.UserVerificationRequirementNotMet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionBEPolicyRequired() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.BackupEligibilityRequirementNotMet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionBEPolicyDisallow() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV | AuthenticatorFlags.BE, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackupEligibleCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Disallowed, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.BackupEligibilityRequirementNotMet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionBSPolicyRequired() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackedUpCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Required, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.BackupStateRequirementNotMet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionBSPolicyDisallow() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV | AuthenticatorFlags.BS, 0, null).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + BackedUpCredentialPolicy = Fido2Configuration.CredentialBackupPolicy.Disallowed, + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.BackupStateRequirementNotMet, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionStoredPublicKeyMissing() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null, new Extensions(new byte[] { 0x42 })).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, null, null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.MissingStoredPublicKey, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionInvalidSignature() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey + } + } + }; + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 0, null, new Extensions(new byte[] { 0x42 })).ToByteArray(), + Signature = new byte[] { 0xf1, 0xd0 }, + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + fido2_net_lib.Test.Fido2Tests.MakeEdDSA(out _, out var publicKey, out var privateKey); + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, fido2_net_lib.Test.Fido2Tests.MakeCredentialPublicKey(COSE.KeyType.OKP, COSE.Algorithm.EdDSA, COSE.EllipticCurve.Ed25519, publicKey).GetBytes(), null, 0, callback)); + Assert.Equal(Fido2ErrorMessages.InvalidSignature, ex.Result.Message); + } + + [Fact] + public void TestAuthenticatorAssertionSignCountSignature() + { + var challenge = RandomNumberGenerator.GetBytes(128); + var rp = "https://www.passwordless.dev"; + + Fido2NetLib.AuthenticatorResponse authenticatorResponse = new() + { + + Type = "webauthn.get", + Challenge = challenge, + Origin = rp, + }; + + byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(authenticatorResponse, FidoSerializerContext.Default.AuthenticatorResponse); + + var options = new AssertionOptions + { + Challenge = challenge, + RpId = rp, + AllowCredentials = new[] + { + new PublicKeyCredentialDescriptor + { + Id = new byte[] { 0xf1, 0xd0 }, + Type = PublicKeyCredentialType.PublicKey + } + } + }; + + var authData = new AuthenticatorData(SHA256.HashData(Encoding.UTF8.GetBytes(rp)), AuthenticatorFlags.UP | AuthenticatorFlags.UV, 1, null, new Extensions(new byte[] { 0x42 })).ToByteArray(); + + fido2_net_lib.Test.Fido2Tests.MakeEdDSA(out _, out var publicKey, out var expandedPrivateKey); + Key privateKey = Key.Import(SignatureAlgorithm.Ed25519, expandedPrivateKey, KeyBlobFormat.RawPrivateKey); + var cpk = fido2_net_lib.Test.Fido2Tests.MakeCredentialPublicKey(COSE.KeyType.OKP, COSE.Algorithm.EdDSA, COSE.EllipticCurve.Ed25519, publicKey); + + + var assertion = new AuthenticatorAssertionRawResponse.AssertionResponse() + { + AuthenticatorData = authData, + Signature = SignatureAlgorithm.Ed25519.Sign(privateKey, DataHelper.Concat(authData, SHA256.HashData(clientDataJson))), + ClientDataJson = clientDataJson, + UserHandle = new byte[] { 0xf1, 0xd0 }, + }; + + var assertionResponse = new AuthenticatorAssertionRawResponse() + { + Response = assertion, + Type = PublicKeyCredentialType.PublicKey, + Id = new byte[] { 0xf1, 0xd0 }, + RawId = new byte[] { 0xf1, 0xd0 }, + Extensions = new AuthenticationExtensionsClientOutputs() + { + AppID = false, + AuthenticatorSelection = true, + Extensions = new string[] { "foo", "bar" }, + Example = "test", + UserVerificationMethod = new ulong[][] + { + new ulong[] + { + 4 // USER_VERIFY_PASSCODE_INTERNAL + }, + }, + } + }; + + IFido2 lib = new Fido2(new Fido2Configuration() + { + ServerDomain = rp, + ServerName = rp, + Origins = new HashSet { rp }, + }); + + IsUserHandleOwnerOfCredentialIdAsync callback = static (args, cancellationToken) => + { + return Task.FromResult(true); + }; + + var ex = Assert.ThrowsAsync(() => lib.MakeAssertionAsync(assertionResponse, options, cpk.GetBytes(), null, 2, callback)); + Assert.Equal(Fido2ErrorMessages.SignCountIsLessThanSignatureCounter, ex.Result.Message); + } } diff --git a/Test/ExistingU2fRegistrationDataTests.cs b/Test/ExistingU2fRegistrationDataTests.cs index 98da15c9e..4a98c9d5e 100644 --- a/Test/ExistingU2fRegistrationDataTests.cs +++ b/Test/ExistingU2fRegistrationDataTests.cs @@ -59,7 +59,7 @@ public async Task TestFido2AssertionWithExistingU2fRegistrationWithAppId() Origins = new HashSet { "https://localhost:44336" } //data was generated with this origin }); - var res = await fido2.MakeAssertionAsync(authResponse, options, publicKey.Encode(), 0, null); + var res = await fido2.MakeAssertionAsync(authResponse, options, publicKey.Encode(), null, 0, null); Assert.Equal("ok", res.Status); } diff --git a/Test/Fido2Tests.cs b/Test/Fido2Tests.cs index acd594266..3a312bec0 100644 --- a/Test/Fido2Tests.cs +++ b/Test/Fido2Tests.cs @@ -210,7 +210,18 @@ public Attestation() ErrorMessage = "", PubKeyCredParams = new List() { - new PubKeyCredParam(COSE.Algorithm.ES256) + new PubKeyCredParam(COSE.Algorithm.ES256), + new PubKeyCredParam(COSE.Algorithm.ES384), + new PubKeyCredParam(COSE.Algorithm.ES512), + new PubKeyCredParam(COSE.Algorithm.RS1), + new PubKeyCredParam(COSE.Algorithm.RS256), + new PubKeyCredParam(COSE.Algorithm.RS384), + new PubKeyCredParam(COSE.Algorithm.RS512), + new PubKeyCredParam(COSE.Algorithm.PS256), + new PubKeyCredParam(COSE.Algorithm.PS384), + new PubKeyCredParam(COSE.Algorithm.PS512), + new PubKeyCredParam(COSE.Algorithm.EdDSA), + new PubKeyCredParam(COSE.Algorithm.ES256K), }, Rp = new PublicKeyCredentialRpEntity(rp, rp, ""), Status = "ok", @@ -420,7 +431,7 @@ public async Task TestFido2AssertionAsync() var response = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationNoneResponse.json")); var o = AuthenticatorAttestationResponse.Parse(response); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); var credId = "F1-3C-7F-08-3C-A2-29-E0-B4-03-E8-87-34-6E-FC-7F-98-53-10-3A-30-91-75-67-39-7A-D1-D8-AF-87-04-61-87-EF-95-31-85-60-F3-5A-1A-2A-CF-7D-B0-1D-06-B9-69-F9-AB-F4-EC-F3-07-3E-CF-0F-71-E8-84-E8-41-20"; var allowedCreds = new List() { @@ -495,7 +506,7 @@ public async Task TestParsingAsync() Assert.NotNull(jsonPost); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); } [Fact] @@ -552,7 +563,7 @@ public async Task TestU2FAttestationAsync() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsU2F.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsU2F.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; // TODO : Why read ad ? Is the test finished ? } @@ -562,7 +573,8 @@ public async Task TestPackedAttestationAsync() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsPacked.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsPacked.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + options.PubKeyCredParams.Add(new PubKeyCredParam(COSE.Algorithm.RS1, PublicKeyCredentialType.PublicKey)); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; var authData = new AuthenticatorData(ad); Assert.True(authData.ToByteArray().SequenceEqual(ad)); @@ -577,7 +589,7 @@ public async Task TestNoneAttestationAsync() var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsNone.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); } [Fact] public async Task TestTPMSHA256AttestationAsync() @@ -585,7 +597,7 @@ public async Task TestTPMSHA256AttestationAsync() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA256Response.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA256Options.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; // TODO : Why read ad ? Is the test finished ? } @@ -595,7 +607,8 @@ public async Task TestTPMSHA1AttestationAsync() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA1Response.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA1Options.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + options.PubKeyCredParams.Add(new PubKeyCredParam(COSE.Algorithm.RS1, PublicKeyCredentialType.PublicKey)); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; // TODO : Why read ad ? Is the test finished ? } @@ -605,7 +618,7 @@ public async Task TestAndroidKeyAttestationAsync() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAndroidKeyResponse.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAndroidKeyOptions.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; // TODO : Why read ad ? Is the test finished ? } @@ -616,7 +629,7 @@ public async Task TaskPackedAttestation512() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsPacked512.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsPacked512.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; // TODO : Why read ad ? Is the test finished ? } @@ -627,7 +640,7 @@ public async Task TestTrustKeyAttestationAsync() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultTrustKeyT110.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsTrustKeyT110.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; var authData = new AuthenticatorData(ad); Assert.True(authData.ToByteArray().SequenceEqual(ad)); @@ -646,7 +659,7 @@ public async Task TestInvalidU2FAttestationAsync() var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsATKey.json")); var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsATKey.json")); var o = AuthenticatorAttestationResponse.Parse(jsonPost); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), _metadataService, CancellationToken.None); byte[] ad = o.AttestationObject.AuthData; var authData = new AuthenticatorData(ad); Assert.True(authData.ToByteArray().SequenceEqual(ad)); @@ -673,7 +686,7 @@ public async Task TestMdsStatusReportsSuccessAsync() mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false); var o = AuthenticatorAttestationResponse.Parse(response); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None); } [Fact] @@ -696,7 +709,7 @@ public async Task TestMdsStatusReportsUndesiredAsync() var o = AuthenticatorAttestationResponse.Parse(response); await Assert.ThrowsAsync(() => - o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None)); + o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None)); } [Fact] @@ -719,7 +732,7 @@ public async Task TestMdsStatusReportsUndesiredFixedAsync() mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false); var o = AuthenticatorAttestationResponse.Parse(response); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None); } [Fact] @@ -733,7 +746,7 @@ public async Task TestMdsStatusReportsNullAsync() mockMetadataService.Setup(m => m.ConformanceTesting()).Returns(false); var o = AuthenticatorAttestationResponse.Parse(response); - await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, null, CancellationToken.None); + await o.VerifyAsync(options, _config, (x, cancellationToken) => Task.FromResult(true), mockMetadataService.Object, CancellationToken.None); } //public void TestHasCorrentAAguid() @@ -1053,7 +1066,7 @@ internal static async Task MakeAssertionResponseAsy { return Task.FromResult(true); }; - return await lib.MakeAssertionAsync(response, options, cpk.GetBytes(), signCount, callback); + return await lib.MakeAssertionAsync(response, options, cpk.GetBytes(), null, signCount, callback); } internal static void MakeEdDSA(out byte[] privateKeySeed, out byte[] publicKey, out byte[] expandedPrivateKey)