@@ -74,7 +74,7 @@
- Device embedded authenticators aka platform authenticators , like Android Key and TPM)
+ Device embedded authenticators aka platform authenticators, like Android Key and TPM)
@@ -98,7 +98,7 @@
- All current attestation formats: "packed", "tpm", "android-key", "android-safetynet", "fido-u2f", and "none"
+ All current attestation formats: "packed", "tpm", "android-key", "android-safetynet", "fido-u2f", "apple", and "none"
Please note: Your browser does not seem to support WebAuthn yet. Supported browsers
-
-
- Please note: At the time of writing (July 2019) this is not supported on all browsers. Confirmed to work on: Windows Edge, Edge (OS X, pre-release), Firefox 69 (Windows), Chrome 76.0.3809.72 beta (OS X)
-
diff --git a/Demo/Pages/usernameless.cshtml.cs b/Demo/Pages/usernameless.cshtml.cs
index 2fee54ad..a56c9217 100644
--- a/Demo/Pages/usernameless.cshtml.cs
+++ b/Demo/Pages/usernameless.cshtml.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Fido2Demo
{
diff --git a/Demo/Properties/launchSettings.json b/Demo/Properties/launchSettings.json
index befd81ab..be223a77 100644
--- a/Demo/Properties/launchSettings.json
+++ b/Demo/Properties/launchSettings.json
@@ -3,24 +3,15 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
- "applicationUrl": "http://localhost:4728/",
+ "applicationUrl": "https://localhost:44329/",
"sslPort": 44329
}
},
"profiles": {
- "IIS Express": {
- "commandName": "IISExpress",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "Fido2Demo": {
+ "Demo": {
"commandName": "Project",
- "launchBrowser": true,
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- },
- "applicationUrl": "http://localhost:4729/"
+ "launchUrl": "https://localhost:44329/",
+ "applicationUrl": "https://localhost:44329/"
}
}
}
\ No newline at end of file
diff --git a/Demo/TestController.cs b/Demo/TestController.cs
index 0932a572..b15b579e 100644
--- a/Demo/TestController.cs
+++ b/Demo/TestController.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Text;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Fido2NetLib;
@@ -42,16 +43,7 @@ public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams
{
var attType = opts.Attestation;
- var username = Array.Empty();
-
- try
- {
- username = Base64Url.Decode(opts.Username);
- }
- catch (FormatException)
- {
- username = Encoding.UTF8.GetBytes(opts.Username);
- }
+ var username = Base64Url.Decode(opts.Username);
// 1. Get user from DB by username (in our example, auto create missing users)
var user = DemoStorage.GetOrAddUser(opts.Username, () => new Fido2User
@@ -64,13 +56,13 @@ public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams
// 2. Get user existing keys by username
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() { };
- if (opts.Extensions?.Example != null)
- exts.Example = opts.Extensions.Example;
-
// 3. Create options
- var options = _fido2.RequestNewCredential(user, existingKeys, opts.AuthenticatorSelection, opts.Attestation, exts);
+ var options = _fido2.RequestNewCredential(
+ user,
+ existingKeys,
+ opts.AuthenticatorSelection,
+ opts.Attestation,
+ (opts.Extensions?.Example == null) ? null : new() { Example = opts.Extensions.Example });
// 4. Temporarily store options, session/in-memory cache/redis/db
HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());
@@ -86,7 +78,7 @@ public async Task MakeCredentialResultTest([FromBody] AuthenticatorA
// 1. get the options we sent the client
var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions");
- var options = CredentialCreateOptions.FromJson(jsonOptions);
+ var options = PublicKeyCredentialCreationOptions.FromJson(jsonOptions);
// 2. Create callback so that lib can verify credential id is unique to this user
IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) =>
@@ -124,23 +116,11 @@ public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams
// 2. Get registered credentials from database
var existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList();
- var uv = assertionClientParams.UserVerification;
- if (null != assertionClientParams.authenticatorSelection)
- uv = assertionClientParams.authenticatorSelection.UserVerification;
-
- var exts = new AuthenticationExtensionsClientInputs
- {
- AppID = _origin,
- UserVerificationMethod = true
- };
- if (null != assertionClientParams.Extensions && null != assertionClientParams.Extensions.Example)
- exts.Example = assertionClientParams.Extensions.Example;
-
// 3. Create options
var options = _fido2.GetAssertionOptions(
existingCredentials,
- uv,
- exts
+ assertionClientParams.UserVerification,
+ (null == assertionClientParams.Extensions?.Example) ? null : new() { Example = assertionClientParams.Extensions.Example }
);
// 4. Temporarily store options, session/in-memory cache/redis/db
@@ -156,7 +136,7 @@ public async Task MakeAssertionTest([FromBody] AuthenticatorAssertio
{
// 1. Get the assertion options we sent the client
var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions");
- var options = AssertionOptions.FromJson(jsonOptions);
+ var options = PublicKeyCredentialRequestOptions.FromJson(jsonOptions);
// 2. Get registered credential from database
var creds = DemoStorage.GetCredentialById(clientResponse.Id);
@@ -193,8 +173,10 @@ public async Task MakeAssertionTest([FromBody] AuthenticatorAssertio
public class TEST_AssertionClientParams
{
public string Username { get; set; }
+
+ [JsonPropertyName("userVerification")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public UserVerificationRequirement? UserVerification { get; set; }
- public AuthenticatorSelection authenticatorSelection { get; set; }
public AuthenticationExtensionsClientOutputs Extensions { get; set; }
}
@@ -203,7 +185,7 @@ public class TEST_MakeCredentialParams
public string DisplayName { get; set; }
public string Username { get; set; }
public AttestationConveyancePreference Attestation { get; set; }
- public AuthenticatorSelection AuthenticatorSelection { get; set; }
+ public AuthenticatorSelectionCriteria AuthenticatorSelection { get; set; }
public AuthenticationExtensionsClientOutputs Extensions { get; set; }
}
}
diff --git a/Demo/UrlHelperExtensions.cs b/Demo/UrlHelperExtensions.cs
index 5f4489a2..db13ded4 100644
--- a/Demo/UrlHelperExtensions.cs
+++ b/Demo/UrlHelperExtensions.cs
@@ -6,7 +6,7 @@ public static class UrlHelperExtensions
{
public static string ToGithub(this IUrlHelper url, string path)
{
- return "https://github.com/abergs/fido2-net-lib/blob/design/" + path;
+ return "https://github.com/passwordless-lib/fido2-net-lib/blob/design/" + path;
}
}
}
diff --git a/Demo/wwwroot/js/custom.login.js b/Demo/wwwroot/js/custom.login.js
index e547c9dd..c9abd8a8 100644
--- a/Demo/wwwroot/js/custom.login.js
+++ b/Demo/wwwroot/js/custom.login.js
@@ -4,7 +4,11 @@ async function handleSignInSubmit(event) {
event.preventDefault();
let username = this.username.value;
- let user_verification = value("#option-userverification");
+ let user_verification = "discouraged";
+
+ if (value("#option-userverification") !== "undefined") {
+ user_verification = value("#option-userverification");
+ }
// prepare form post data
var formData = new FormData();
diff --git a/Demo/wwwroot/js/custom.register.js b/Demo/wwwroot/js/custom.register.js
index 85a07a80..57ab0f11 100644
--- a/Demo/wwwroot/js/custom.register.js
+++ b/Demo/wwwroot/js/custom.register.js
@@ -6,17 +6,25 @@ async function handleRegisterSubmit(event) {
let username = this.username.value;
let displayName = this.displayName.value;
- // possible values: none, direct, indirect
- let attestation_type = value("#option-attestation");
- // possible values: , platform, cross-platform
- let authenticator_attachment = value("#option-authenticator");
+ let attestation_type = "none";
- // possible values: preferred, required, discouraged
- let user_verification = value("#option-userverification");
+ if (value('#option-attestation') !== "undefined") {
+ attestation_type = value('#option-attestation');
+ }
- // possible values: true,false
- let require_resident_key = value("#option-residentkey");
+ let authenticator_attachment = "";
+
+ if (value("#option-authenticator") !== "undefined") {
+ authenticator_attachment = value("#option-authenticator");
+ }
+ let user_verification = "discouraged";
+
+ if (value("#option-userverification") !== "undefined") {
+ user_verification = value("#option-userverification");
+ }
+
+ let require_resident_key = value("#option-residentkey");
// prepare form post data
var data = new FormData();
@@ -27,8 +35,6 @@ async function handleRegisterSubmit(event) {
data.append('userVerification', user_verification);
data.append('requireResidentKey', require_resident_key);
- // send to server for registering
- let makeCredentialOptions;
try {
makeCredentialOptions = await fetchMakeCredentialOptions(data);
@@ -62,6 +68,156 @@ async function handleRegisterSubmit(event) {
console.log("Credential Options Formatted", makeCredentialOptions);
+ // send to server for registering
+ var makeCredentialOptions = {
+ rp: {
+ name: "WebAuthn Test Server",
+ icon: "https://example.com/rpIcon.png"
+ },
+ user: {
+ id: makeCredentialOptions.user.id,
+ name: displayName,
+ user: displayName,
+ displayName: displayName,
+ icon: "https://example.com/userIcon.png"
+ },
+ challenge: makeCredentialOptions.challenge,
+ pubKeyCredParams: [],
+ timeout: 90000,
+ excludeCredentials: [],
+ authenticatorSelection: {
+ userVerification: "discouraged"
+ },
+ attestation: undefined,
+ extensions: {}
+ };
+
+ switch (value('#option-rpinfo')) {
+ case "normal":
+ makeCredentialOptions.rp.id = window.location.hostname;
+ break;
+ case "suffix":
+ makeCredentialOptions.rp.id = "suffix." + window.location.hostname;
+ break;
+ case "securityerror":
+ makeCredentialOptions.rp.id = "foo.com";
+ break;
+ case "emptyrpid":
+ makeCredentialOptions.rp.id = "";
+ break;
+ case "emptyrpname":
+ makeCredentialOptions.rp.name = undefined;
+ break;
+ case "emptyrpicon":
+ makeCredentialOptions.rp.icon = undefined;
+ case "undefined":
+ default:
+ break;
+ }
+
+ if (value('#option-ES256')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -7
+ });
+ }
+ if (value('#option-ES384')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -35
+ });
+ }
+ if (value('#option-ES512')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -36
+ });
+ }
+ if (value('#option-RS256')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -257
+ });
+ }
+ if (value('#option-RS384')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -258
+ });
+ }
+ if (value('#option-RS512')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -259
+ });
+ }
+ if (value('#option-PS256')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -37
+ });
+ }
+ if (value('#option-PS384')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -38
+ });
+ }
+ if (value('#option-PS512')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -39
+ });
+ }
+ if (value('#option-EdDSA')) {
+ makeCredentialOptions.pubKeyCredParams.push({
+ type: "public-key",
+ alg: -8
+ });
+ }
+
+ if (value('#option-attestation') !== "undefined") {
+ makeCredentialOptions.attestation = value('#option-attestation');
+ }
+
+ if (value('#option-requireresidentkey') !== "undefined") {
+ var requireResidentKey = (value('#option-requireresidentkey') == "true");
+ makeCredentialOptions.authenticatorSelection.requireResidentKey = requireResidentKey;
+ }
+
+ if (value('#option-residentkey') !== "undefined") {
+ makeCredentialOptions.authenticatorSelection.residentKey = value('#option-residentkey');
+ }
+
+ if (value('#option-credprotect') !== "undefined") {
+ var credProtect = value('#option-credprotect');
+ makeCredentialOptions.extensions.credentialProtectionPolicy = credProtect;
+ }
+
+ if (value('#option-credprotectenforce') !== "undefined") {
+ var enforceCredProtect = (value('#coption-credprotectenforce') == "true");
+ makeCredentialOptions.extensions.enforceCredentialProtectionPolicy = enforceCredProtect;
+ }
+
+ if (value('#option-hmaccreate') !== "undefined") {
+ var hmacCreateSecret = (value('#option-hmaccreate') == "true");
+ makeCredentialOptions.extensions.hmacCreateSecret = hmacCreateSecret;
+ }
+
+ if (value('#option-minPinLength') !== "undefined") {
+ var minPinLength = (value('#option-minPinLength') == "true");
+ makeCredentialOptions.extensions.minPinLength = minPinLength;
+ }
+
+ if (value('#option-largeBlob') !== "undefined") {
+ makeCredentialOptions.extensions.largeBlob = {};
+ makeCredentialOptions.extensions.largeBlob.support = value('#option-largeBlob');
+ }
+
+ if (value("#option-userverification") !== "undefined") {
+ makeCredentialOptions.authenticatorSelection.userVerification = value("#option-userverification");
+ }
+
Swal.fire({
title: 'Registering...',
text: 'Tap your security key to finish registration.',
@@ -110,7 +266,6 @@ async function fetchMakeCredentialOptions(formData) {
return data;
}
-
// This should be used to verify the auth data with the server
async function registerNewCredential(newCredential) {
// Move data into Arrays incase it is super long
@@ -131,29 +286,29 @@ async function registerNewCredential(newCredential) {
let response;
try {
- response = await registerCredentialWithServer(data);
- } catch (e) {
- showErrorAlert(e);
+ response = await registerCredentialWithServer(data);
+ } catch (e) {
+ showErrorAlert(e);
}
- console.log("Credential Object", response);
-
- // show error
- if (response.status !== "ok") {
- console.log("Error creating credential");
- console.log(response.errorMessage);
- showErrorAlert(response.errorMessage);
- return;
- }
-
- // show success
- Swal.fire({
- title: 'Registration Successful!',
- text: 'You\'ve registered successfully.',
- type: 'success',
- timer: 2000
- });
-
+ console.log("Credential Object", response);
+
+ // show error
+ if (response.status !== "ok") {
+ console.log("Error creating credential");
+ console.log(response.errorMessage);
+ showErrorAlert(response.errorMessage);
+ return;
+ }
+
+ // show success
+ Swal.fire({
+ title: 'Registration Successful!',
+ text: 'You\'ve registered successfully.',
+ type: 'success',
+ timer: 2000
+ });
+
// redirect to dashboard?
//window.location.href = "/dashboard/" + state.user.displayName;
}
diff --git a/Demo/wwwroot/js/mfa.register.js b/Demo/wwwroot/js/mfa.register.js
index b333cf27..0e1fcb12 100644
--- a/Demo/wwwroot/js/mfa.register.js
+++ b/Demo/wwwroot/js/mfa.register.js
@@ -15,7 +15,7 @@ async function handleRegisterSubmit(event) {
let authenticator_attachment = "";
// possible values: preferred, required, discouraged
- let user_verification = "preferred";
+ let user_verification = "discouraged";
// possible values: true,false
let require_resident_key = false;
diff --git a/Demo/wwwroot/js/passwordless.register.js b/Demo/wwwroot/js/passwordless.register.js
index 327fb891..037105f3 100644
--- a/Demo/wwwroot/js/passwordless.register.js
+++ b/Demo/wwwroot/js/passwordless.register.js
@@ -8,17 +8,16 @@ async function handleRegisterSubmit(event) {
// possible values: none, direct, indirect
let attestation_type = "none";
+
// possible values: , platform, cross-platform
let authenticator_attachment = "";
// possible values: preferred, required, discouraged
- let user_verification = "preferred";
+ let user_verification = "discouraged";
// possible values: true,false
let require_resident_key = "false";
-
-
// prepare form post data
var data = new FormData();
data.append('username', username);
diff --git a/Demo/wwwroot/js/usernameless.register.js b/Demo/wwwroot/js/usernameless.register.js
index 44925285..1c0b86a0 100644
--- a/Demo/wwwroot/js/usernameless.register.js
+++ b/Demo/wwwroot/js/usernameless.register.js
@@ -12,14 +12,12 @@ async function handleRegisterSubmit(event) {
let authenticator_attachment = "";
// possible values: preferred, required, discouraged
- let user_verification = "preferred";
+ let user_verification = "discouraged";
// possible values: true,false
// NOTE: For usernameless scenarios, resident key must be set to true.
let require_resident_key = "true";
-
-
// prepare form post data
var data = new FormData();
//data.append('username', username);
diff --git a/Demo/wwwroot/login.html b/Demo/wwwroot/login.html
index 738bbcc8..ee74672c 100644
--- a/Demo/wwwroot/login.html
+++ b/Demo/wwwroot/login.html
@@ -105,7 +105,7 @@
FIDO2 .NET lib Demo
Currently, the WebAuthn spec supports credential creation and assertion best using U2F Token, like those provided by Yubico and Feitian.
-
The code for this demo can be found at the Fido2.net-lib repo and the original source here.
+
The code for this demo can be found at the Fido2.net-lib repo and the original source here.
To see what's happening under the hood when you create a test user and login using WebAuthn below, you can open your web browser's console and
see the output of the necessary credential objects being used.
@@ -187,7 +187,7 @@
FIDO2 .NET lib Demo
diff --git a/Src/Fido2.Models/AssertionOptions.cs b/Src/Fido2.Models/AssertionOptions.cs
deleted file mode 100644
index 184d0cd3..00000000
--- a/Src/Fido2.Models/AssertionOptions.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-using Fido2NetLib.Objects;
-
-namespace Fido2NetLib
-{
- ///
- /// Sent to the browser when we want to Assert credentials and authenticate a user
- ///
- public class AssertionOptions : Fido2ResponseBase
- {
- ///
- /// This member represents a challenge that the selected authenticator signs, along with other data, when producing an authentication assertion.See the §13.1 Cryptographic Challenges security consideration.
- ///
- [JsonPropertyName("challenge")]
- [JsonConverter(typeof(Base64UrlConverter))]
- public byte[] Challenge { get; set; }
-
- ///
- /// This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete. This is treated as a hint, and MAY be overridden by the client.
- ///
- [JsonPropertyName("timeout")]
- public uint Timeout { get; set; }
-
- ///
- /// This OPTIONAL member specifies the relying party identifier claimed by the caller.If omitted, its value will be the CredentialsContainer object’s relevant settings object's origin's effective domain
- ///
- [JsonPropertyName("rpId")]
- public string RpId { get; set; }
-
- ///
- /// This OPTIONAL member contains a list of PublicKeyCredentialDescriptor objects representing public key credentials acceptable to the caller, in descending order of the caller’s preference(the first item in the list is the most preferred credential, and so on down the list)
- ///
- [JsonPropertyName("allowCredentials")]
- public IEnumerable AllowCredentials { get; set; }
-
- ///
- /// This member describes the Relying Party's requirements regarding user verification for the get() operation. Eligible authenticators are filtered to only those capable of satisfying this requirement
- ///
- [JsonPropertyName("userVerification")]
- public UserVerificationRequirement? UserVerification { get; set; }
-
- ///
- /// This OPTIONAL member contains additional parameters requesting additional processing by the client and authenticator. For example, if transaction confirmation is sought from the user, then the prompt string might be included as an extension.
- ///
- [JsonPropertyName("extensions")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public AuthenticationExtensionsClientInputs Extensions { get; set; }
-
- public static AssertionOptions Create(Fido2Configuration config, byte[] challenge, IEnumerable allowedCredentials, UserVerificationRequirement? userVerification, AuthenticationExtensionsClientInputs extensions)
- {
- return new AssertionOptions()
- {
- Status = "ok",
- ErrorMessage = string.Empty,
- Challenge = challenge,
- Timeout = config.Timeout,
- RpId = config.ServerDomain,
- AllowCredentials = allowedCredentials ?? Array.Empty(),
- UserVerification = userVerification,
- Extensions = extensions
- };
- }
-
- public string ToJson()
- {
- return JsonSerializer.Serialize(this);
- }
-
- public static AssertionOptions FromJson(string json)
- {
- return JsonSerializer.Deserialize(json);
- }
- }
-}
diff --git a/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs b/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs
index 1108d5ee..efef279a 100644
--- a/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs
+++ b/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs
@@ -15,7 +15,6 @@ public class AuthenticatorAssertionRawResponse
[JsonPropertyName("id")]
public byte[] Id { get; set; }
- // might be wrong to base64url encode this...
[JsonConverter(typeof(Base64UrlConverter))]
[JsonPropertyName("rawId")]
public byte[] RawId { get; set; }
@@ -31,6 +30,10 @@ public class AuthenticatorAssertionRawResponse
public class AssertionResponse
{
+ [JsonConverter(typeof(Base64UrlConverter))]
+ [JsonPropertyName("clientDataJSON")]
+ public byte[] ClientDataJson { get; set; }
+
[JsonConverter(typeof(Base64UrlConverter))]
[JsonPropertyName("authenticatorData")]
public byte[] AuthenticatorData { get; set; }
@@ -39,10 +42,6 @@ public class AssertionResponse
[JsonPropertyName("signature")]
public byte[] Signature { get; set; }
- [JsonConverter(typeof(Base64UrlConverter))]
- [JsonPropertyName("clientDataJSON")]
- public byte[] ClientDataJson { get; set; }
-
[JsonPropertyName("userHandle")]
[JsonConverter(typeof(Base64UrlConverter))]
public byte[] UserHandle { get; set; }
diff --git a/Src/Fido2.Models/Base64Url.cs b/Src/Fido2.Models/Base64Url.cs
index 99967325..2736f741 100644
--- a/Src/Fido2.Models/Base64Url.cs
+++ b/Src/Fido2.Models/Base64Url.cs
@@ -119,6 +119,7 @@ public static byte[] DecodeUtf8(ReadOnlySpan text)
switch ((char)c)
{
+ case '+' or '/' or '=': throw new Fido2VerificationException("Invalid base64url encoding");
case '-': c = (byte)'+'; break;
case '_': c = (byte)'/'; break;
}
diff --git a/Src/Fido2.Models/Fido2ResponseBase.cs b/Src/Fido2.Models/Fido2ResponseBase.cs
index b69a1adc..2c19becd 100644
--- a/Src/Fido2.Models/Fido2ResponseBase.cs
+++ b/Src/Fido2.Models/Fido2ResponseBase.cs
@@ -5,9 +5,11 @@ namespace Fido2NetLib
public abstract class Fido2ResponseBase
{
[JsonPropertyName("status")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Status { get; set; }
[JsonPropertyName("errorMessage")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string ErrorMessage { get; set; }
}
}
diff --git a/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs b/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs
index 0d846b1d..3e93065f 100644
--- a/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs
+++ b/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs
@@ -27,6 +27,12 @@ public enum AttestationConveyancePreference
/// This value indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator.
///
[EnumMember(Value = "direct")]
- Direct
+ Direct,
+
+ ///
+ /// This value indicates that the Relying Party wants to receive an attestation statement that may include uniquely identifying information.
+ ///
+ [EnumMember(Value ="enterprise")]
+ Enterprise
}
}
diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs
index d99efca0..7328b2d7 100644
--- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs
+++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs
@@ -7,12 +7,13 @@ namespace Fido2NetLib.Objects
///
public sealed class AuthenticationExtensionsClientInputs
{
+#nullable enable
///
/// This extension allows for passing of conformance tests
///
- [JsonPropertyName("example.extension")]
+ [JsonPropertyName("example.extension.bool")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public object Example { get; set; }
+ public object? Example { get; set; }
///
/// This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO JavaScript APIs to request an assertion.
@@ -20,23 +21,7 @@ public sealed class AuthenticationExtensionsClientInputs
///
[JsonPropertyName("appid")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public string AppID { get; set; }
-
- ///
- /// This extension allows a WebAuthn Relying Party to guide the selection of the authenticator that will be leveraged when creating the credential. It is intended primarily for Relying Parties that wish to tightly control the experience around credential creation.
- /// https://www.w3.org/TR/webauthn/#sctn-authenticator-selection-extension
- ///
- [JsonPropertyName("authnSel")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public byte[][] AuthenticatorSelection { get; set; }
-
- ///
- /// This extension enables the WebAuthn Relying Party to determine which extensions the authenticator supports.
- /// https://www.w3.org/TR/webauthn/#sctn-supported-extensions-extension
- ///
- [JsonPropertyName("exts")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? Extensions { get; set; }
+ public string? AppID { get; set; }
///
/// This extension enables use of a user verification method.
@@ -45,6 +30,7 @@ public sealed class AuthenticationExtensionsClientInputs
[JsonPropertyName("uvm")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? UserVerificationMethod { get; set; }
+#nullable disable
}
}
diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs
index 1883465e..bbada6ee 100644
--- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs
+++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs
@@ -7,7 +7,7 @@ public class AuthenticationExtensionsClientOutputs
///
/// This extension allows for passing of conformance tests
///
- [JsonPropertyName("example.extension")]
+ [JsonPropertyName("example.extension.bool")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object Example { get; set; }
@@ -25,7 +25,7 @@ public class AuthenticationExtensionsClientOutputs
/// https://www.w3.org/TR/webauthn/#sctn-authenticator-selection-extension
///
[JsonPropertyName("authnSel")]
- public bool AuthenticatorSelection { get; set; }
+ public bool AuthenticatorSelectionCriteria { get; set; }
///
/// This extension enables the WebAuthn Relying Party to determine which extensions the authenticator supports.
diff --git a/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs b/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs
index 9853d2aa..f1fe5f6a 100644
--- a/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs
+++ b/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs
@@ -1,4 +1,5 @@
-using System.Text.Json.Serialization;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
namespace Fido2NetLib.Objects
{
@@ -22,14 +23,14 @@ public PublicKeyCredentialDescriptor()
///
/// This member contains the type of the public key credential the caller is referring to.
///
- [JsonPropertyName("type")]
+ [JsonPropertyName("type"), Required]
public PublicKeyCredentialType? Type { get; set; } = PublicKeyCredentialType.PublicKey;
///
/// This member contains the credential ID of the public key credential the caller is referring to.
///
[JsonConverter(typeof(Base64UrlConverter))]
- [JsonPropertyName("id")]
+ [JsonPropertyName("id"), Required]
public byte[] Id { get; set; }
#nullable enable
diff --git a/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs b/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs
new file mode 100644
index 00000000..a44c738f
--- /dev/null
+++ b/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs
@@ -0,0 +1,30 @@
+using System.Runtime.Serialization;
+using System.Text.Json.Serialization;
+
+namespace Fido2NetLib.Objects
+{
+ ///
+ /// This enumeration’s values describe the Relying Party's requirements for client-side discoverable credentials (formerly known as resident credentials or resident keys)
+ ///
+ [JsonConverter(typeof(FidoEnumConverter))]
+ public enum ResidentKeyRequirement
+ {
+ ///
+ /// The Relying Party prefers creating a server-side credential, but will accept a client-side discoverable credential. The client and authenticator SHOULD create a server-side credential if possible.
+ ///
+ [EnumMember(Value = "discouraged")]
+ Discouraged,
+
+ ///
+ /// The Relying Party strongly prefers creating a client-side discoverable credential, but will accept a server-side credential. The client and authenticator SHOULD create a discoverable credential if possible. For example, the client SHOULD guide the user through setting up user verification if needed to create a discoverable credential. This takes precedence over the setting of userVerification.
+ ///
+ [EnumMember(Value = "preferred")]
+ Preferred,
+
+ ///
+ /// The Relying Party requires a client-side discoverable credential. The client MUST return an error if a client-side discoverable credential cannot be created.
+ ///
+ [EnumMember(Value = "required")]
+ Required,
+ }
+}
diff --git a/Src/Fido2.Models/CredentialCreateOptions.cs b/Src/Fido2.Models/PublicKeyCredentialCreationOptions.cs
similarity index 61%
rename from Src/Fido2.Models/CredentialCreateOptions.cs
rename to Src/Fido2.Models/PublicKeyCredentialCreationOptions.cs
index ad26e2eb..ac60d7ab 100644
--- a/Src/Fido2.Models/CredentialCreateOptions.cs
+++ b/Src/Fido2.Models/PublicKeyCredentialCreationOptions.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -6,7 +7,7 @@
namespace Fido2NetLib
{
- public sealed class CredentialCreateOptions : Fido2ResponseBase
+ public sealed class PublicKeyCredentialCreationOptions : Fido2ResponseBase
{
///
///
@@ -14,73 +15,77 @@ public sealed class CredentialCreateOptions : Fido2ResponseBase
/// Its value’s name member is required.
/// Its value’s id member specifies the relying party identifier with which the credential should be associated.If omitted, its value will be the CredentialsContainer object’s relevant settings object's origin's effective domain.
///
- [JsonPropertyName("rp")]
+ [JsonPropertyName("rp"), Required]
public PublicKeyCredentialRpEntity Rp { get; set; }
///
/// This member contains data about the user account for which the Relying Party is requesting attestation.
/// Its value’s name, displayName and id members are required.
///
- [JsonPropertyName("user")]
+ [JsonPropertyName("user"), Required]
public Fido2User User { get; set; }
///
/// Must be generated by the Server (Relying Party)
///
- [JsonPropertyName("challenge")]
+ [JsonPropertyName("challenge"), Required]
[JsonConverter(typeof(Base64UrlConverter))]
public byte[] Challenge { get; set; }
///
/// This member contains information about the desired properties of the credential to be created. The sequence is ordered from most preferred to least preferred. The platform makes a best-effort to create the most preferred credential that it can.
///
- [JsonPropertyName("pubKeyCredParams")]
- public List PubKeyCredParams { get; set; }
+ [JsonPropertyName("pubKeyCredParams"), Required]
+ public List PublicKeyCredentialParameters { get; set; }
///
/// This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete. This is treated as a hint, and MAY be overridden by the platform.
///
- [JsonPropertyName("timeout")]
- public long Timeout { get; set; }
-
+ [JsonPropertyName("timeout")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public long? Timeout { get; set; }
+
///
- /// This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance.The default is none.
+ /// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.
///
- [JsonPropertyName("attestation")]
- public AttestationConveyancePreference Attestation { get; set; } = AttestationConveyancePreference.None;
-
+ [JsonPropertyName("excludeCredentials")]
+ public List ExcludeCredentials { get; set; }
+#nullable enable
///
/// This member is intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the create() operation.
///
- [JsonPropertyName("authenticatorSelection")]
- public AuthenticatorSelection AuthenticatorSelection { get; set; }
+ [JsonPropertyName("authenticatorSelection")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public AuthenticatorSelectionCriteria? AuthenticatorSelectionCriteria { get; set; }
///
- /// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.
+ /// This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance.The default is none.
///
- [JsonPropertyName("excludeCredentials")]
- public List ExcludeCredentials { get; set; }
+ [JsonPropertyName("attestation")]
+ public AttestationConveyancePreference Attestation { get; set; } = AttestationConveyancePreference.None;
///
/// This OPTIONAL member contains additional parameters requesting additional processing by the client and authenticator. For example, if transaction confirmation is sought from the user, then the prompt string might be included as an extension.
///
[JsonPropertyName("extensions")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public AuthenticationExtensionsClientInputs Extensions { get; set; }
-
- public static CredentialCreateOptions Create(Fido2Configuration config, byte[] challenge, Fido2User user, AuthenticatorSelection authenticatorSelection, AttestationConveyancePreference attestationConveyancePreference, List excludeCredentials, AuthenticationExtensionsClientInputs extensions)
+ public AuthenticationExtensionsClientInputs? Extensions { get; set; }
+#nullable disable
+ public static PublicKeyCredentialCreationOptions Create(Fido2Configuration config, byte[] challenge, Fido2User user, AuthenticatorSelectionCriteria authenticatorSelection, AttestationConveyancePreference attestationConveyancePreference, List excludeCredentials, AuthenticationExtensionsClientInputs extensions)
{
- return new CredentialCreateOptions
+ return new PublicKeyCredentialCreationOptions
{
Status = "ok",
ErrorMessage = string.Empty,
Challenge = challenge,
- Rp = new PublicKeyCredentialRpEntity(config.ServerDomain, config.ServerName, config.ServerIcon),
+ Rp = new PublicKeyCredentialRpEntity(config.ServerDomain, config.ServerName),
Timeout = config.Timeout,
User = user,
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
// Add additional as appropriate
+ // TODO: Make this a configuration parameter with reasonable defaults
+ Ed25519,
ES256,
RS256,
PS256,
@@ -90,9 +95,9 @@ public static CredentialCreateOptions Create(Fido2Configuration config, byte[] c
ES512,
RS512,
PS512,
- Ed25519,
+ RS1,
},
- AuthenticatorSelection = authenticatorSelection,
+ AuthenticatorSelectionCriteria = authenticatorSelection,
Attestation = attestationConveyancePreference,
ExcludeCredentials = excludeCredentials ?? new List(),
Extensions = extensions
@@ -104,30 +109,31 @@ public string ToJson()
return JsonSerializer.Serialize(this);
}
- public static CredentialCreateOptions FromJson(string json)
+ public static PublicKeyCredentialCreationOptions FromJson(string json)
{
- return JsonSerializer.Deserialize(json);
+ return JsonSerializer.Deserialize(json);
}
- private static readonly PubKeyCredParam ES256 = new(COSE.Algorithm.ES256); // External authenticators support the ES256 algorithm
- private static readonly PubKeyCredParam ES384 = new(COSE.Algorithm.ES384);
- private static readonly PubKeyCredParam ES512 = new(COSE.Algorithm.ES512);
- private static readonly PubKeyCredParam RS256 = new(COSE.Algorithm.RS256); // Supported by windows hello
- private static readonly PubKeyCredParam RS384 = new(COSE.Algorithm.RS384);
- private static readonly PubKeyCredParam RS512 = new(COSE.Algorithm.RS512);
- private static readonly PubKeyCredParam PS256 = new(COSE.Algorithm.PS256);
- private static readonly PubKeyCredParam PS384 = new(COSE.Algorithm.PS384);
- private static readonly PubKeyCredParam PS512 = new(COSE.Algorithm.PS512);
- private static readonly PubKeyCredParam Ed25519 = new(COSE.Algorithm.EdDSA);
+ private static readonly PublicKeyCredentialParameters ES256 = new(COSE.Algorithm.ES256); // External authenticators support the ES256 algorithm
+ private static readonly PublicKeyCredentialParameters ES384 = new(COSE.Algorithm.ES384);
+ private static readonly PublicKeyCredentialParameters ES512 = new(COSE.Algorithm.ES512);
+ private static readonly PublicKeyCredentialParameters RS256 = new(COSE.Algorithm.RS256); // Supported by windows hello
+ private static readonly PublicKeyCredentialParameters RS384 = new(COSE.Algorithm.RS384);
+ private static readonly PublicKeyCredentialParameters RS512 = new(COSE.Algorithm.RS512);
+ private static readonly PublicKeyCredentialParameters PS256 = new(COSE.Algorithm.PS256);
+ private static readonly PublicKeyCredentialParameters PS384 = new(COSE.Algorithm.PS384);
+ private static readonly PublicKeyCredentialParameters PS512 = new(COSE.Algorithm.PS512);
+ private static readonly PublicKeyCredentialParameters Ed25519 = new(COSE.Algorithm.EdDSA);
+ private static readonly PublicKeyCredentialParameters RS1 = new(COSE.Algorithm.RS1);
}
- public sealed class PubKeyCredParam
+ public sealed class PublicKeyCredentialParameters
{
///
- /// Constructs a PubKeyCredParam instance
+ /// Constructs a PublicKeyCredentialParameters instance
///
[JsonConstructor]
- public PubKeyCredParam(COSE.Algorithm alg, PublicKeyCredentialType type = PublicKeyCredentialType.PublicKey)
+ public PublicKeyCredentialParameters(COSE.Algorithm alg, PublicKeyCredentialType type = PublicKeyCredentialType.PublicKey)
{
Type = type;
Alg = alg;
@@ -147,85 +153,96 @@ public PubKeyCredParam(COSE.Algorithm alg, PublicKeyCredentialType type = Public
}
///
- /// PublicKeyCredentialRpEntity
+ /// https://w3c.github.io/webauthn/#dictionary-rp-credential-params
+ /// PublicKeyCredentialRpEntity is used to supply additional Relying Party attributes when creating a new credential.
///
- public class PublicKeyCredentialRpEntity
+ public class PublicKeyCredentialRpEntity : PublicKeyCredentialEntity
{
- public PublicKeyCredentialRpEntity(string id, string name, string icon)
+ public PublicKeyCredentialRpEntity(string id, string name = "")
{
- Name = name;
Id = id;
- Icon = icon;
+ Name = name;
}
///
/// A unique identifier for the Relying Party entity, which sets the RP ID.
///
- [JsonPropertyName("id")]
+ [JsonPropertyName("id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Id { get; set; }
-
- ///
- /// A human-readable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents:
- ///
- [JsonPropertyName("name")]
- public string Name { get; set; }
-
- [JsonPropertyName("icon")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public string Icon { get; set; }
}
///
/// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements regarding authenticator attributes.
///
- public class AuthenticatorSelection
+ public class AuthenticatorSelectionCriteria
{
///
/// If this member is present, eligible authenticators are filtered to only authenticators attached with the specified §5.4.5 Authenticator Attachment enumeration (enum AuthenticatorAttachment).
///
[JsonPropertyName("authenticatorAttachment")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public AuthenticatorAttachment? AuthenticatorAttachment { get; set; }
+ ///
+ /// Specifies the extent to which the Relying Party desires to create a client-side discoverable credential.
+ /// For historical reasons the naming retains the deprecated “resident” terminology.
+ /// The value SHOULD be a member of ResidentKeyRequirement but client platforms MUST ignore unknown values, treating an unknown value as if the member does not exist.
+ /// If no value is given then the effective value is required if requireResidentKey is true or discouraged if it is false or absent.
+ ///
+ [JsonPropertyName("residentKey")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public ResidentKeyRequirement? ResidentKey { get; set; }
+
///
/// This member describes the Relying Parties' requirements regarding resident credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident public key credential source when creating a public key credential.
///
[JsonPropertyName("requireResidentKey")]
- public bool RequireResidentKey { get; set; }
-
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? RequireResidentKey { get; set; }
+
///
/// This member describes the Relying Party's requirements regarding user verification for the create() operation. Eligible authenticators are filtered to only those capable of satisfying this requirement.
///
[JsonPropertyName("userVerification")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public UserVerificationRequirement UserVerification { get; set; }
- public static AuthenticatorSelection Default => new AuthenticatorSelection
+ public static AuthenticatorSelectionCriteria Default => new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = null,
+ ResidentKey = null,
RequireResidentKey = false,
UserVerification = UserVerificationRequirement.Preferred
};
}
- public class Fido2User
- {
+ public class PublicKeyCredentialEntity
+ {
///
- /// Required. A human-friendly identifier for a user account. It is intended only for display, i.e., aiding the user in determining the difference between user accounts with similar displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234". https://w3c.github.io/webauthn/#dictdef-publickeycredentialentity
+ /// A human-palatable name for the entity.
///
- [JsonPropertyName("name")]
- public string Name { get; set; }
+ [JsonPropertyName("name"), Required]
+ public string Name { get; set; }
+ }
+ public class PublicKeyCredentialUserEntity : PublicKeyCredentialEntity
+ {
///
/// The user handle of the user account entity. To ensure secure operation, authentication and authorization decisions MUST be made on the basis of this id member, not the displayName nor name members
///
- [JsonPropertyName("id")]
+ [JsonPropertyName("id"), Required]
[JsonConverter(typeof(Base64UrlConverter))]
public byte[] Id { get; set; }
///
/// A human-friendly name for the user account, intended only for display. For example, "Alex P. Müller" or "田中 倫". The Relying Party SHOULD let the user choose this, and SHOULD NOT restrict the choice more than necessary.
///
- [JsonPropertyName("displayName")]
- public string DisplayName { get; set; }
+ [JsonPropertyName("displayName"), Required]
+ public string DisplayName { get; set; }
+ }
+
+ public class Fido2User : PublicKeyCredentialUserEntity
+ {
}
}
diff --git a/Src/Fido2.Models/PublicKeyCredentialRequestOptions.cs b/Src/Fido2.Models/PublicKeyCredentialRequestOptions.cs
new file mode 100644
index 00000000..156508fa
--- /dev/null
+++ b/Src/Fido2.Models/PublicKeyCredentialRequestOptions.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using Fido2NetLib.Objects;
+
+namespace Fido2NetLib
+{
+ ///
+ /// https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptions
+ /// The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion.
+ /// Its challenge member MUST be present, while its other members are OPTIONAL.
+ ///
+ public class PublicKeyCredentialRequestOptions : Fido2ResponseBase
+ {
+ ///
+ /// This member specifies a challenge that the authenticator signs, along with other data, when producing an authentication assertion.
+ ///
+ [JsonPropertyName("challenge"), Required]
+ [JsonConverter(typeof(Base64UrlConverter))]
+ public byte[] Challenge { get; set; }
+
+ ///
+ /// This OPTIONAL member specifies a time, in milliseconds, that the Relying Party is willing to wait for the call to complete. The value is treated as a hint, and MAY be overridden by the client.
+ ///
+ [JsonPropertyName("timeout")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public uint? Timeout { get; set; }
+#nullable enable
+ ///
+ /// This OPTIONAL member specifies the RP ID claimed by the Relying Party. The client MUST verify that the Relying Party's origin matches the scope of this RP ID. The authenticator MUST verify that this RP ID exactly equals the rpId of the credential to be used for the authentication ceremony.
+ /// If not specified, its value will be the CredentialsContainer object’s relevant settings object's origin's effective domain.
+ ///
+ [JsonPropertyName("rpId")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? RpId { get; set; }
+
+ ///
+ /// This OPTIONAL member is used by the client to find authenticators eligible for this authentication ceremony.
+ ///
+ [JsonPropertyName("allowCredentials")]
+ public IEnumerable? AllowCredentials { get; set; }
+
+ ///
+ /// This OPTIONAL member specifies the Relying Party's requirements regarding user verification for the get() operation. The value SHOULD be a member of UserVerificationRequirement but client platforms MUST ignore unknown values, treating an unknown value as if the member does not exist. Eligible authenticators are filtered to only those capable of satisfying this requirement.
+ ///
+ [JsonPropertyName("userVerification")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public UserVerificationRequirement? UserVerification { get; set; }
+
+ ///
+ /// The Relying Party MAY use this OPTIONAL member to provide client extension inputs requesting additional processing by the client and authenticator.
+ ///
+ [JsonPropertyName("extensions")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public AuthenticationExtensionsClientInputs? Extensions { get; set; }
+#nullable disable
+ public static PublicKeyCredentialRequestOptions Create(Fido2Configuration config, byte[] challenge, IEnumerable allowedCredentials, AuthenticationExtensionsClientInputs extensions, UserVerificationRequirement? userVerification = UserVerificationRequirement.Preferred)
+ {
+ return new PublicKeyCredentialRequestOptions()
+ {
+ Status = "ok",
+ ErrorMessage = string.Empty,
+ Challenge = challenge,
+ Timeout = config.Timeout,
+ RpId = config.ServerDomain,
+ AllowCredentials = allowedCredentials ?? Array.Empty(),
+ UserVerification = userVerification,
+ Extensions = extensions
+ };
+ }
+
+ public string ToJson()
+ {
+ return JsonSerializer.Serialize(this);
+ }
+
+ public static PublicKeyCredentialRequestOptions FromJson(string json)
+ {
+ return JsonSerializer.Deserialize(json);
+ }
+ }
+}
diff --git a/Src/Fido2/AuthenticatorAssertionResponse.cs b/Src/Fido2/AuthenticatorAssertionResponse.cs
index 2b9e5b39..e32ea3ac 100644
--- a/Src/Fido2/AuthenticatorAssertionResponse.cs
+++ b/Src/Fido2/AuthenticatorAssertionResponse.cs
@@ -15,7 +15,7 @@ namespace Fido2NetLib
/// The AuthenticatorAssertionResponse interface represents an authenticator's response to a client’s request for generation of a new authentication assertion given the Relying Party's challenge and optional list of credentials it is aware of.
/// This response contains a cryptographic signature proving possession of the credential private key, and optionally evidence of user consent to a specific transaction.
///
- public sealed class AuthenticatorAssertionResponse : AuthenticatorResponse
+ public sealed class AuthenticatorAssertionResponse : CollectedClientData
{
private AuthenticatorAssertionResponse(byte[] clientDataJson) : base(clientDataJson)
{
@@ -45,25 +45,23 @@ public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResp
///
/// Implements alghoritm from https://www.w3.org/TR/webauthn/#verifying-assertion
///
- /// The assertionoptions that was sent to the client
+ /// The assertion options that was sent to the client
///
/// The expected fully qualified server origins, used to verify that the signature is sent to the expected server
///
/// 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,
+ PublicKeyCredentialRequestOptions options,
HashSet fullyQualifiedExpectedOrigins,
byte[] storedPublicKey,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId,
- byte[] requestTokenBindingId,
CancellationToken cancellationToken = default)
{
- BaseVerify(fullyQualifiedExpectedOrigins, options.Challenge, requestTokenBindingId);
+ BaseVerify(fullyQualifiedExpectedOrigins, options.Challenge);
if (Raw.Type != PublicKeyCredentialType.PublicKey)
throw new Fido2VerificationException("AssertionResponse Type is not set to public-key");
@@ -74,7 +72,7 @@ public async Task VerifyAsync(
if (Raw.RawId is null)
throw new Fido2VerificationException("RawId is missing");
- // 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
+ // 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
@@ -82,7 +80,7 @@ public async Task VerifyAsync(
throw new Fido2VerificationException("Invalid");
}
- // 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)
@@ -94,57 +92,53 @@ public async Task VerifyAsync(
}
}
- // 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.
+ // 7. 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.
+ // 8. 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.
+ // 9. Let JSONtext be the result of running UTF-8 decode on the value of cData.
// var JSONtext = Encoding.UTF8.GetBytes(cData.ToString());
+ // 10. Let C, the client data claimed as used for the signature, be the result of running an implementation-specific JSON parser on JSONtext
- // 7. Verify that the value of C.type is the string webauthn.get.
+ // 11. Verify that the value of C.type is the string webauthn.get.
if (Type is not "webauthn.get")
throw new Fido2VerificationException("AssertionResponse is not type 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.
+ // 12. Verify that the value of C.challenge equals the base64url encoding of options.challenge
+ // 13. Verify that the value of C.origin matches the Relying Party's origin.
// done in base class
- // 10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
- // Validated in BaseVerify.
- // todo: Needs testing
-
- // 11. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party.
+ // 14. Verify that the rpIdHash in authData 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);
if (!authData.RpIdHash.SequenceEqual(hashedRpId))
throw new Fido2VerificationException("Hash mismatch RPID");
- // 12. Verify that the User Present bit of the flags in authData is set.
- // UNLESS...userVerification is set to preferred or discouraged?
- // See Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
- // P-5 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "preferred", and check that server succeeds
- // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds
- // if ((!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
+ // 15. Verify that the User Present bit of the flags in authData is set.
+ if (!authData.UserPresent &&
+ // Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData
+ // P-5 Send a valid ServerAuthenticatorAssertionResponse with authenticatorData.flags.UP is set, despite requested userVerification set to "discouraged", and check that server succeeds
+ // P-7 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeed
+ ((options.UserVerification is not UserVerificationRequirement.Discouraged) &&
+ // P-4 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
+ (!(options.UserVerification is UserVerificationRequirement.Preferred && !authData.UserVerified))))
+ throw new Fido2VerificationException("User Present flag not set in authenticator data");
+
+ // 16. If the Relying Party requires user verification for this assertion, verify that the User Verified bit of the flags in authData is set
if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
throw new Fido2VerificationException("User verification is required");
- // 14. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the get() call.In particular, any extension identifier values in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use.
+ // 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 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.
// todo: Verify this (and implement extensions on options)
if (authData.HasExtensionsData && (authData.Extensions is null || authData.Extensions.Length is 0))
throw new Fido2VerificationException("Extensions flag present, malformed extensions detected");
@@ -152,11 +146,11 @@ public async Task VerifyAsync(
if (!authData.HasExtensionsData && authData.Extensions != null)
throw new Fido2VerificationException("Extensions flag not present, but extensions detected");
- // 15.
- // Done earlier, hashedClientDataJson
+ // 19. Let hash be the result of computing a hash over the cData using SHA-256.
+ byte[] hash = SHA256.HashData(Raw.Response.ClientDataJson);
- // 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);
+ // 20. Using credentialPublicKey, 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("Stored public key is null or empty");
@@ -166,10 +160,21 @@ public async Task VerifyAsync(
if (!cpk.Verify(data, Signature))
throw new Fido2VerificationException("Signature did not match");
- // 17.
- if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter)
- throw new Fido2VerificationException("SignatureCounter was not greater than stored SignatureCounter");
+ // 21. Let storedSignCount be the stored signature counter value associated with credential.id.
+ // If authData.signCount is nonzero or storedSignCount is nonzero, then run the following sub-step
+ if (authData.SignCount != 0 || storedSignatureCounter != 0)
+ {
+ // If authData.signCount is greater than storedSignCount, update storedSignCount to be the value of authData.signCount
+ if (authData.SignCount > storedSignatureCounter)
+ storedSignatureCounter = authData.SignCount;
+
+ // If authData.signCount is less than or equal to storedSignCount, This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential private key may exist and are being used in parallel.
+ // Relying Parties should incorporate this information into their risk scoring. Whether the Relying Party updates storedSignCount in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific
+ else if (authData.SignCount <= storedSignatureCounter)
+ throw new Fido2VerificationException("SignatureCounter was not greater than stored SignatureCounter");
+ }
+ // 22. If all the above steps are successful, continue with the authentication ceremony as appropriate. Otherwise, fail the authentication ceremony.
return new AssertionVerificationResult()
{
Status = "ok",
diff --git a/Src/Fido2/AuthenticatorAttestationResponse.cs b/Src/Fido2/AuthenticatorAttestationResponse.cs
index 189c2665..f39e9e6f 100644
--- a/Src/Fido2/AuthenticatorAttestationResponse.cs
+++ b/Src/Fido2/AuthenticatorAttestationResponse.cs
@@ -19,7 +19,7 @@ namespace Fido2NetLib
/// It contains information about the new credential that can be used to identify it for later use,
/// and metadata that can be used by the Relying Party to assess the characteristics of the credential during registration.
///
- public sealed class AuthenticatorAttestationResponse : AuthenticatorResponse
+ public sealed class AuthenticatorAttestationResponse : CollectedClientData
{
private AuthenticatorAttestationResponse(byte[] clientDataJson)
: base(clientDataJson)
@@ -72,28 +72,26 @@ public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRaw
}
public async Task VerifyAsync(
- CredentialCreateOptions originalOptions,
+ PublicKeyCredentialCreationOptions originalOptions,
Fido2Configuration config,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
IMetadataService metadataService,
- byte[] requestTokenBindingId,
CancellationToken cancellationToken = default)
{
+
// 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("AttestationResponse is not type webauthn.create");
- // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
- // 5. Verify that the value of C.origin matches the Relying Party's origin.
- // 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained.
- // If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
- BaseVerify(config.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("AttestationResponse is missing Id");
@@ -103,36 +101,41 @@ public async Task VerifyAsync(
var authData = new AuthenticatorData(AttestationObject.AuthData);
- // 7. Compute the hash of response.clientDataJSON using SHA-256.
- byte[] clientDataHash = SHA256.HashData(Raw.Response.ClientDataJson);
+ // 10. Let hash be the result of computing a hash over response.clientDataJSON using SHA-256.
+ byte[] hash = 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, the authenticator data authData, and the attestation statement attStmt.
+ // 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("Hash mismatch RPID");
- // 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("User Present flag not set in authenticator data");
- // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
- if (originalOptions.AuthenticatorSelection?.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
+ // 14. If the Relying Party requires user verification for this registration, verify that the User Verified bit of the flags in authData is set.
+ if (originalOptions.AuthenticatorSelectionCriteria?.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified)
throw new Fido2VerificationException("User Verified flag not set in authenticator data and user verification was required");
- // 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,
+ if (!authData.HasAttestedCredentialData)
+ throw new Fido2VerificationException("Attestation flag not set on attestation data");
+
+ // 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.PublicKeyCredentialParameters.Any(a => authData.AttestedCredentialData.CredentialPublicKey.IsSameAlg(a.Alg)))
+ throw new Fido2VerificationException("Alg in credential public key does not match any alg from pubKeyCredParams");
+
+ // 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);
- if (!authData.HasAttestedCredentialData)
- throw new Fido2VerificationException("Attestation flag not set on attestation data");
- // 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values.
+ // 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.
// An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same name
// https://www.w3.org/TR/webauthn/#defined-attestation-formats
AttestationVerifier verifier = AttestationObject.Fmt switch
@@ -148,11 +151,11 @@ public async Task VerifyAsync(
_ => throw new Fido2VerificationException("Missing or unknown attestation type")
};
- // 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);
+ (var attType, var trustPath) = verifier.Verify(AttestationObject.AttStmt, AttestationObject.AuthData, hash);
- // 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement format fmt, from a trusted source or from policy.
+ // 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.
MetadataBLOBPayloadEntry metadataEntry = null;
@@ -182,6 +185,18 @@ static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAtte
attestationRootCertificates[i] = new X509Certificate2(Convert.FromBase64String(certStrings[i]));
}
+ // FIDO conformance Server-ServerAuthenticatorAttestationResponse-Resp-5 Test server processing "packed" FULL attestation
+ // Test F-10 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c containing full chain, and check that server returns an error
+ if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_BASIC_FULL))
+ {
+ foreach (X509Certificate2 attestationRootCertificate in attestationRootCertificates)
+ {
+ // test for cert in x5c that should only be in metadata
+ if (trustPath.Any(x => x.Thumbprint == attestationRootCertificate.Thumbprint && x.Subject.Equals(x.Issuer, StringComparison.Ordinal)))
+ throw new Fido2VerificationException("Full packed attestation with x5c containing full chain detected");
+ }
+ }
+
if (!CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates))
{
throw new Fido2VerificationException("Invalid certificate chain");
@@ -219,19 +234,23 @@ static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAtte
}
}
- // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows:
+ // 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.
- // 17. Check that the credentialId is not yet registered to any other user.
+ // 23. Verify that the credentialId is ≤ 1023 bytes. Credential IDs larger than this many bytes SHOULD cause the RP to fail this registration ceremony.
+ if (!(authData.AttestedCredentialData.CredentialID.Length <= 1023))
+ throw new Fido2VerificationException($"Credential ID must not be larger than 1023 bytes, was {authData.AttestedCredentialData.CredentialID.Length} bytes");
+
+ // 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 (false == await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User), cancellationToken))
{
throw new Fido2VerificationException("CredentialId is not unique to this user");
}
- // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, then register the new credential with the account that was denoted in the options.user passed to create(),
+ // 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.
var result = new AttestationVerificationSuccess()
{
@@ -244,7 +263,7 @@ static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAtte
};
return result;
- // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, the Relying Party SHOULD fail the registration ceremony.
+ // 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.
}
diff --git a/Src/Fido2/AuthenticatorResponse.cs b/Src/Fido2/CollectedClientData.cs
similarity index 75%
rename from Src/Fido2/AuthenticatorResponse.cs
rename to Src/Fido2/CollectedClientData.cs
index 78e1a4ce..7cfe8bb1 100644
--- a/Src/Fido2/AuthenticatorResponse.cs
+++ b/Src/Fido2/CollectedClientData.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -9,22 +10,22 @@ namespace Fido2NetLib
///
/// Base class for responses sent by the Authenticator Client
///
- public class AuthenticatorResponse
+ public class CollectedClientData
{
- protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson)
+ protected CollectedClientData(ReadOnlySpan utf8EncodedJson)
{
if (utf8EncodedJson.Length is 0)
throw new Fido2VerificationException("utf8EncodedJson may not be empty");
// 1. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON
-
+
// 2. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext
// Note: C may be any implementation-specific data structure representation, as long as C’s components are referenceable, as required by this algorithm.
// We call this AuthenticatorResponse
- AuthenticatorResponse response;
+ CollectedClientData response;
try
{
- response = JsonSerializer.Deserialize(utf8EncodedJson)!;
+ response = JsonSerializer.Deserialize(utf8EncodedJson)!;
}
catch (Exception e) when (e is JsonException)
{
@@ -36,10 +37,11 @@ protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson)
Type = response.Type;
Challenge = response.Challenge;
Origin = response.Origin;
+ CrossOrigin = response.CrossOrigin;
}
#nullable disable
- public AuthenticatorResponse() // for deserialization
+ public CollectedClientData() // for deserialization
{
}
@@ -47,19 +49,21 @@ protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson)
public const int MAX_ORIGINS_TO_PRINT = 5;
- [JsonPropertyName("type")]
+ [JsonPropertyName("type"), Required]
public string Type { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]
- [JsonPropertyName("challenge")]
+ [JsonPropertyName("challenge"), Required]
public byte[] Challenge { get; set; }
- [JsonPropertyName("origin")]
+ [JsonPropertyName("origin"), Required]
public string Origin { get; set; }
- // todo: add TokenBinding https://www.w3.org/TR/webauthn/#dictdef-tokenbinding
+ [JsonPropertyName("crossOrigin")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? CrossOrigin { get; set; }
- protected void BaseVerify(HashSet fullyQualifiedExpectedOrigins, ReadOnlySpan originalChallenge, ReadOnlySpan requestTokenBindingId)
+ protected void BaseVerify(HashSet fullyQualifiedExpectedOrigins, ReadOnlySpan originalChallenge)
{
if (Type is not "webauthn.create" && Type is not "webauthn.get")
throw new Fido2VerificationException($"Type not equal to 'webauthn.create' or 'webauthn.get'. Was: '{Type}'");
@@ -76,17 +80,6 @@ protected void BaseVerify(HashSet fullyQualifiedExpectedOrigins, ReadOnl
// 5. 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})");
-
- }
-
- private static string FullyQualifiedOrigin(string origin)
- {
- var uri = new Uri(origin);
-
- if (UriHostNameType.Unknown != uri.HostNameType)
- return uri.IsDefaultPort ? $"{uri.Scheme}://{uri.Host}" : $"{uri.Scheme}://{uri.Host}:{uri.Port}";
-
- return origin;
}
}
}
diff --git a/Src/Fido2/CryptoUtils.cs b/Src/Fido2/CryptoUtils.cs
index 27c5b0fc..8a4bb5f6 100644
--- a/Src/Fido2/CryptoUtils.cs
+++ b/Src/Fido2/CryptoUtils.cs
@@ -55,7 +55,7 @@ public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certific
// A trust anchor can be a root certificate, an intermediate CA certificate or even the attestation certificate itself.
// Let's check the simplest case first. If subject and issuer are the same, and the attestation cert is in the list, that's all the validation we need
- if (trustPath.Length == 1 && trustPath[0].Subject.Equals(trustPath[0].Issuer, StringComparison.Ordinal))
+ if (trustPath.Length == 1 && (trustPath[0].Subject.Equals(trustPath[0].Issuer, StringComparison.Ordinal)))
{
foreach (X509Certificate2? cert in attestationRootCertificates)
{
diff --git a/Src/Fido2/Fido2NetLib.cs b/Src/Fido2/Fido2NetLib.cs
index 49cd5464..17211952 100644
--- a/Src/Fido2/Fido2NetLib.cs
+++ b/Src/Fido2/Fido2NetLib.cs
@@ -23,35 +23,35 @@ public Fido2(
}
///
- /// Returns CredentialCreateOptions including a challenge to be sent to the browser/authr to create new credentials
+ /// Returns PublicKeyCredentialCreationOptions including a challenge to be sent to the browser/authr to create new credentials
///
///
/// Recommended. This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.
- public CredentialCreateOptions RequestNewCredential(
+ public PublicKeyCredentialCreationOptions RequestNewCredential(
Fido2User user,
List excludeCredentials,
AuthenticationExtensionsClientInputs? extensions = null)
{
- return RequestNewCredential(user, excludeCredentials, AuthenticatorSelection.Default, AttestationConveyancePreference.None, extensions);
+ return RequestNewCredential(user, excludeCredentials, AuthenticatorSelectionCriteria.Default, AttestationConveyancePreference.None, extensions);
}
///
- /// Returns CredentialCreateOptions including a challenge to be sent to the browser/authr to create new credentials
+ /// Returns PublicKeyCredentialCreationOptions including a challenge to be sent to the browser/authr to create new credentials
///
///
/// This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance. The default is none.
/// Recommended. This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for the same account on a single authenticator.The client is requested to return an error if the new credential would be created on an authenticator that also contains one of the credentials enumerated in this parameter.
- public CredentialCreateOptions RequestNewCredential(
+ public PublicKeyCredentialCreationOptions RequestNewCredential(
Fido2User user,
List excludeCredentials,
- AuthenticatorSelection authenticatorSelection,
+ AuthenticatorSelectionCriteria authenticatorSelection,
AttestationConveyancePreference attestationPreference,
AuthenticationExtensionsClientInputs? extensions = null)
{
var challenge = new byte[_config.ChallengeSize];
RandomNumberGenerator.Fill(challenge);
- var options = CredentialCreateOptions.Create(_config, challenge, user, authenticatorSelection, attestationPreference, excludeCredentials, extensions);
+ var options = PublicKeyCredentialCreationOptions.Create(_config, challenge, user, authenticatorSelection, attestationPreference, excludeCredentials, extensions);
return options;
}
@@ -61,18 +61,16 @@ public CredentialCreateOptions RequestNewCredential(
///
///
///
- ///
///
///
public async Task MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
- CredentialCreateOptions origChallenge,
+ PublicKeyCredentialCreationOptions 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(
@@ -86,7 +84,7 @@ public async Task MakeNewCredentialAsync(
/// Returns AssertionOptions including a challenge to the browser/authr to assert existing credentials and authenticate a user.
///
///
- public AssertionOptions GetAssertionOptions(
+ public PublicKeyCredentialRequestOptions GetAssertionOptions(
IEnumerable allowedCredentials,
UserVerificationRequirement? userVerification,
AuthenticationExtensionsClientInputs? extensions = null)
@@ -94,7 +92,7 @@ public AssertionOptions GetAssertionOptions(
var challenge = new byte[_config.ChallengeSize];
RandomNumberGenerator.Fill(challenge);
- var options = AssertionOptions.Create(_config, challenge, allowedCredentials, userVerification, extensions);
+ var options = PublicKeyCredentialRequestOptions.Create(_config, challenge, allowedCredentials, extensions, userVerification);
return options;
}
@@ -104,11 +102,10 @@ public AssertionOptions GetAssertionOptions(
///
public async Task MakeAssertionAsync(
AuthenticatorAssertionRawResponse assertionResponse,
- AssertionOptions originalOptions,
+ PublicKeyCredentialRequestOptions originalOptions,
byte[] storedPublicKey,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
- byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default)
{
var parsedResponse = AuthenticatorAssertionResponse.Parse(assertionResponse);
@@ -118,7 +115,6 @@ public async Task MakeAssertionAsync(
storedPublicKey,
storedSignatureCounter,
isUserHandleOwnerOfCredentialIdCallback,
- requestTokenBindingId,
cancellationToken);
return result;
diff --git a/Src/Fido2/IFido2.cs b/Src/Fido2/IFido2.cs
index 51af9551..7fcbe02a 100644
--- a/Src/Fido2/IFido2.cs
+++ b/Src/Fido2/IFido2.cs
@@ -7,36 +7,34 @@ namespace Fido2NetLib
{
public interface IFido2
{
- AssertionOptions GetAssertionOptions(
+ PublicKeyCredentialRequestOptions GetAssertionOptions(
IEnumerable allowedCredentials,
- UserVerificationRequirement? userVerification,
+ UserVerificationRequirement? userVerification = UserVerificationRequirement.Preferred,
AuthenticationExtensionsClientInputs? extensions = null);
Task MakeAssertionAsync(
AuthenticatorAssertionRawResponse assertionResponse,
- AssertionOptions originalOptions,
+ PublicKeyCredentialRequestOptions originalOptions,
byte[] storedPublicKey,
uint storedSignatureCounter,
IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback,
- byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default);
Task MakeNewCredentialAsync(
AuthenticatorAttestationRawResponse attestationResponse,
- CredentialCreateOptions origChallenge,
+ PublicKeyCredentialCreationOptions origChallenge,
IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser,
- byte[]? requestTokenBindingId = null,
CancellationToken cancellationToken = default);
- CredentialCreateOptions RequestNewCredential(
+ PublicKeyCredentialCreationOptions RequestNewCredential(
Fido2User user,
List excludeCredentials,
AuthenticationExtensionsClientInputs? extensions = null);
- CredentialCreateOptions RequestNewCredential(
+ PublicKeyCredentialCreationOptions RequestNewCredential(
Fido2User user,
List excludeCredentials,
- AuthenticatorSelection authenticatorSelection,
+ AuthenticatorSelectionCriteria authenticatorSelection,
AttestationConveyancePreference attestationPreference,
AuthenticationExtensionsClientInputs? extensions = null);
}
diff --git a/Test/Attestation/Apple.cs b/Test/Attestation/Apple.cs
index 6782bf1d..c6f4bbd7 100644
--- a/Test/Attestation/Apple.cs
+++ b/Test/Attestation/Apple.cs
@@ -10,7 +10,6 @@
using System.Threading.Tasks;
using System.Text;
using System.Text.Json;
-using System.Threading;
using Fido2NetLib.Cbor;
namespace Test.Attestation
@@ -231,10 +230,10 @@ public async Task TestApplePublicKeyMismatch()
}
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -242,11 +241,11 @@ public async Task TestApplePublicKeyMismatch()
},
Challenge = _challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity("https://www.passwordless.dev/", "6cc3c9e7967a.ngrok.io", ""),
+ Rp = new PublicKeyCredentialRpEntity("https://www.passwordless.dev/"),
Status = "ok",
User = new Fido2User
{
diff --git a/Test/AuthenticatorResponse.cs b/Test/AuthenticatorResponse.cs
index 5f54fbf4..995483a2 100644
--- a/Test/AuthenticatorResponse.cs
+++ b/Test/AuthenticatorResponse.cs
@@ -50,6 +50,7 @@ public class AuthenticatorResponse
public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin)
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = origin;
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
var authData = new AuthenticatorData(
@@ -81,10 +82,10 @@ public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin)
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -92,11 +93,11 @@ public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin)
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -154,6 +155,7 @@ public async Task TestAuthenticatorOrigins(string origin, string expectedOrigin)
public void TestAuthenticatorOriginsFail(string origin, string expectedOrigin)
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = origin;
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
var authData = new AuthenticatorData(
@@ -185,10 +187,10 @@ public void TestAuthenticatorOriginsFail(string origin, string expectedOrigin)
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -196,11 +198,11 @@ public void TestAuthenticatorOriginsFail(string origin, string expectedOrigin)
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -251,7 +253,7 @@ public void TestAuthenticatorAttestationRawResponse()
Extensions = new AuthenticationExtensionsClientOutputs()
{
AppID = true,
- AuthenticatorSelection = true,
+ AuthenticatorSelectionCriteria = true,
Extensions = new string[] { "foo", "bar" },
Example = "test",
UserVerificationMethod = new ulong[][]
@@ -269,7 +271,7 @@ public void TestAuthenticatorAttestationRawResponse()
Assert.True(rawResponse.Response.AttestationObject.SequenceEqual(new byte[] { 0xa0 }));
Assert.True(rawResponse.Response.ClientDataJson.SequenceEqual(clientDataJson));
Assert.True(rawResponse.Extensions.AppID);
- Assert.True(rawResponse.Extensions.AuthenticatorSelection);
+ Assert.True(rawResponse.Extensions.AuthenticatorSelectionCriteria);
Assert.Equal(rawResponse.Extensions.Extensions, new string[] { "foo", "bar" });
Assert.Equal("test", rawResponse.Extensions.Example);
Assert.Equal((ulong)4, rawResponse.Extensions.UserVerificationMethod[0][0]);
@@ -381,10 +383,10 @@ public void TestAuthenticatorAttestationResponseInvalidType()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -392,11 +394,11 @@ public void TestAuthenticatorAttestationResponseInvalidType()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -429,6 +431,7 @@ public void TestAuthenticatorAttestationResponseInvalidType()
public void TestAuthenticatorAttestationResponseInvalidRawId(byte[] value)
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
byte[] clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new {
type = "webauthn.create",
@@ -452,10 +455,10 @@ public void TestAuthenticatorAttestationResponseInvalidRawId(byte[] value)
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -463,11 +466,11 @@ public void TestAuthenticatorAttestationResponseInvalidRawId(byte[] value)
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -498,6 +501,7 @@ public void TestAuthenticatorAttestationResponseInvalidRawId(byte[] value)
public void TestAuthenticatorAttestationResponseInvalidRawType()
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
var clientDataJson = JsonSerializer.SerializeToUtf8Bytes(new {
type = "webauthn.create",
@@ -521,10 +525,10 @@ public void TestAuthenticatorAttestationResponseInvalidRawType()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -532,11 +536,11 @@ public void TestAuthenticatorAttestationResponseInvalidRawType()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -567,6 +571,7 @@ public void TestAuthenticatorAttestationResponseInvalidRawType()
public void TestAuthenticatorAttestationResponseRpidMismatch()
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
var authData = new AuthenticatorData(
SHA256.HashData(Encoding.UTF8.GetBytes("passwordless.dev")),
@@ -598,10 +603,10 @@ public void TestAuthenticatorAttestationResponseRpidMismatch()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -609,11 +614,11 @@ public void TestAuthenticatorAttestationResponseRpidMismatch()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -644,6 +649,7 @@ public void TestAuthenticatorAttestationResponseRpidMismatch()
public void TestAuthenticatorAttestationResponseNotUserPresent()
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
var authData = new AuthenticatorData(
SHA256.HashData(Encoding.UTF8.GetBytes(rp)),
@@ -676,10 +682,10 @@ public void TestAuthenticatorAttestationResponseNotUserPresent()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -687,11 +693,11 @@ public void TestAuthenticatorAttestationResponseNotUserPresent()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -722,6 +728,7 @@ public void TestAuthenticatorAttestationResponseNotUserPresent()
public void TestAuthenticatorAttestationResponseNoAttestedCredentialData()
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
var authData = new AuthenticatorData(
SHA256.HashData(Encoding.UTF8.GetBytes(rp)),
@@ -753,10 +760,10 @@ public void TestAuthenticatorAttestationResponseNoAttestedCredentialData()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -764,11 +771,11 @@ public void TestAuthenticatorAttestationResponseNoAttestedCredentialData()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -799,6 +806,7 @@ public void TestAuthenticatorAttestationResponseNoAttestedCredentialData()
public void TestAuthenticatorAttestationResponseUnknownAttestationType()
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
var authData = new AuthenticatorData(
@@ -831,10 +839,10 @@ public void TestAuthenticatorAttestationResponseUnknownAttestationType()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -842,11 +850,11 @@ public void TestAuthenticatorAttestationResponseUnknownAttestationType()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -877,6 +885,7 @@ public void TestAuthenticatorAttestationResponseUnknownAttestationType()
public void TestAuthenticatorAttestationResponseNotUniqueCredId()
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
var authData = new AuthenticatorData(
@@ -908,10 +917,10 @@ public void TestAuthenticatorAttestationResponseNotUniqueCredId()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -919,11 +928,11 @@ public void TestAuthenticatorAttestationResponseNotUniqueCredId()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -954,6 +963,7 @@ public void TestAuthenticatorAttestationResponseNotUniqueCredId()
public void TestAuthenticatorAttestationResponseUVRequired()
{
var challenge = RandomNumberGenerator.GetBytes(128);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var rp = "https://www.passwordless.dev/";
var acd = new AttestedCredentialData(("00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-40-FE-6A-32-63-BE-37-D1-01-B1-2E-57-CA-96-6C-00-22-93-E4-19-C8-CD-01-06-23-0B-C6-92-E8-CC-77-12-21-F1-DB-11-5D-41-0F-82-6B-DB-98-AC-64-2E-B1-AE-B5-A8-03-D1-DB-C1-47-EF-37-1C-FD-B1-CE-B0-48-CB-2C-A5-01-02-03-26-20-01-21-58-20-A6-D1-09-38-5A-C7-8E-5B-F0-3D-1C-2E-08-74-BE-6D-BB-A4-0B-4F-2A-5F-2F-11-82-45-65-65-53-4F-67-28-22-58-20-43-E1-08-2A-F3-13-5B-40-60-93-79-AC-47-42-58-AA-B3-97-B8-86-1D-E4-41-B4-4E-83-08-5D-1C-6B-E0-D0").Split('-').Select(c => Convert.ToByte(c, 16)).ToArray());
var authData = new AuthenticatorData(
@@ -985,10 +995,10 @@ public void TestAuthenticatorAttestationResponseUVRequired()
},
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -996,11 +1006,11 @@ public void TestAuthenticatorAttestationResponseUVRequired()
},
Challenge = challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(COSE.Algorithm.ES256)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -1055,7 +1065,7 @@ public void TestAuthenticatorAssertionRawResponse()
Extensions = new AuthenticationExtensionsClientOutputs()
{
AppID = true,
- AuthenticatorSelection = true,
+ AuthenticatorSelectionCriteria = true,
Extensions = new string[] { "foo", "bar" },
Example = "test",
UserVerificationMethod = new ulong[][]
@@ -1075,7 +1085,7 @@ public void TestAuthenticatorAssertionRawResponse()
Assert.True(assertionResponse.Response.ClientDataJson.SequenceEqual(clientDataJson));
Assert.True(assertionResponse.Response.UserHandle.SequenceEqual(new byte[] { 0xf1, 0xd0 }));
Assert.True(assertionResponse.Extensions.AppID);
- Assert.True(assertionResponse.Extensions.AuthenticatorSelection);
+ Assert.True(assertionResponse.Extensions.AuthenticatorSelectionCriteria);
Assert.Equal(assertionResponse.Extensions.Extensions, new string[] { "foo", "bar" });
Assert.Equal("test", assertionResponse.Extensions.Example);
Assert.Equal((ulong)4, assertionResponse.Extensions.UserVerificationMethod[0][0]);
diff --git a/Test/Base64UrlTest.cs b/Test/Base64UrlTest.cs
index 80339478..a3116175 100644
--- a/Test/Base64UrlTest.cs
+++ b/Test/Base64UrlTest.cs
@@ -37,5 +37,17 @@ public TestDataGenerator()
Add(Array.Empty());
}
}
+
+ [Fact]
+ public void TestBase64EncodedString()
+ {
+ var ex = Assert.Throws(() => Base64Url.DecodeUtf8(Encoding.UTF8.GetBytes("Xvg6kAhrcnvASuXpCDw38DcIn0waPdQE8dBHDumc===")));
+ Assert.True("Invalid base64url encoding" == ex.Message);
+ ex = Assert.Throws(() => Base64Url.DecodeUtf8(Encoding.UTF8.GetBytes("Xvg6kAhrcnvASuX+pCDw38DcIn0waPdQE8dBHDumc")));
+ Assert.True("Invalid base64url encoding" == ex.Message);
+ ex = Assert.Throws(() => Base64Url.DecodeUtf8(Encoding.UTF8.GetBytes("Xvg6kAhrcnvASuX/pCDw38DcIn0waPdQE8dBHDumc")));
+ Assert.True("Invalid base64url encoding" == ex.Message);
+ Assert.NotNull(Base64Url.DecodeUtf8(Encoding.UTF8.GetBytes("Xvg6kAhrcnvASuX-pCDw38DcIn0w_aPdQ_E8dBHDumc")));
+ }
}
}
diff --git a/Test/ExistingU2fRegistrationDataTests.cs b/Test/ExistingU2fRegistrationDataTests.cs
index c7878cf7..7247b6a9 100644
--- a/Test/ExistingU2fRegistrationDataTests.cs
+++ b/Test/ExistingU2fRegistrationDataTests.cs
@@ -23,7 +23,7 @@ public async Task TestFido2AssertionWithExistingU2fRegistrationWithAppId()
//key as cbor
var publicKey = CreatePublicKeyFromU2fRegistrationData(keyHandleData, publicKeyData);
- var options = new AssertionOptions
+ var options = new PublicKeyCredentialRequestOptions
{
Challenge = Base64Url.Decode("mNxQVDWI8+ahBXeQJ8iS4jk5pDUd5KetZGVOwSkw2X0"),
RpId = "localhost",
@@ -35,6 +35,7 @@ public async Task TestFido2AssertionWithExistingU2fRegistrationWithAppId()
Type = PublicKeyCredentialType.PublicKey
}
},
+ UserVerification = UserVerificationRequirement.Discouraged,
Extensions = new AuthenticationExtensionsClientInputs
{
AppID = appId
diff --git a/Test/Fido2Tests.cs b/Test/Fido2Tests.cs
index b134c55f..a45cf4df 100644
--- a/Test/Fido2Tests.cs
+++ b/Test/Fido2Tests.cs
@@ -168,7 +168,8 @@ public Attestation()
RandomNumberGenerator.Fill(_credentialID);
_challenge = new byte[128];
- RandomNumberGenerator.Fill(_credentialID);
+ RandomNumberGenerator.Fill(_challenge);
+ _challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(_challenge));
var signCount = new byte[2];
RandomNumberGenerator.Fill(signCount);
@@ -189,7 +190,7 @@ public Attestation()
var attestationResponse = new AuthenticatorAttestationRawResponse
{
Type = PublicKeyCredentialType.PublicKey,
- Id = new byte[] { 0xf1, 0xd0 },
+ Id = Encoding.UTF8.GetBytes(Base64Url.Encode(new byte[] { 0xf1, 0xd0 })),
RawId = new byte[] { 0xf1, 0xd0 },
Response = new AuthenticatorAttestationRawResponse.ResponseData()
{
@@ -199,7 +200,7 @@ public Attestation()
Extensions = new AuthenticationExtensionsClientOutputs()
{
AppID = true,
- AuthenticatorSelection = true,
+ AuthenticatorSelectionCriteria = true,
Extensions = new string[] { "foo", "bar" },
Example = "test",
UserVerificationMethod = new ulong[][]
@@ -212,10 +213,10 @@ public Attestation()
}
};
- var origChallenge = new CredentialCreateOptions
+ var origChallenge = new PublicKeyCredentialCreationOptions
{
Attestation = AttestationConveyancePreference.Direct,
- AuthenticatorSelection = new AuthenticatorSelection
+ AuthenticatorSelectionCriteria = new AuthenticatorSelectionCriteria
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
RequireResidentKey = true,
@@ -223,11 +224,11 @@ public Attestation()
},
Challenge = _challenge,
ErrorMessage = "",
- PubKeyCredParams = new List()
+ PublicKeyCredentialParameters = new List()
{
- new PubKeyCredParam(COSE.Algorithm.ES256)
+ new PublicKeyCredentialParameters(_credentialPublicKey._alg)
},
- Rp = new PublicKeyCredentialRpEntity(rp, rp, ""),
+ Rp = new PublicKeyCredentialRpEntity(rp),
Status = "ok",
User = new Fido2User
{
@@ -381,10 +382,10 @@ internal static byte[] SignData(COSE.KeyType kty, COSE.Algorithm alg, byte[] dat
[Fact]
public void TestStringIsSerializable()
{
- var x2 = new AuthenticatorSelection();
+ var x2 = new AuthenticatorSelectionCriteria();
x2.UserVerification = UserVerificationRequirement.Discouraged;
var json = JsonSerializer.Serialize(x2);
- var c3 = JsonSerializer.Deserialize(json);
+ var c3 = JsonSerializer.Deserialize(json);
Assert.Equal(UserVerificationRequirement.Discouraged, c3.UserVerification);
@@ -429,11 +430,11 @@ public async Task TestFido2AssertionAsync()
//var key2 = "45-43-53-31-20-00-00-00-1D-60-44-D7-92-A0-0C-1E-3B-F9-58-5A-28-43-92-FD-F6-4F-BB-7F-8E-86-33-38-30-A4-30-5D-4E-2C-71-E3-53-3C-7B-98-81-99-FE-A9-DA-D9-24-8E-04-BD-C7-86-40-D3-03-1E-6E-00-81-7D-85-C3-A2-19-C9-21-85-8D";
//var key2 = "45-43-53-31-20-00-00-00-A9-E9-12-2A-37-8A-F0-74-E7-BA-52-54-B0-91-55-46-DB-21-E5-2C-01-B8-FB-69-CD-E5-ED-02-B6-C3-16-E3-1A-59-16-C1-43-87-0D-04-B9-94-7F-CF-56-E5-AA-5E-96-8C-5B-27-8F-83-F4-E2-50-AB-B3-F6-28-A1-F8-9E";
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationNoneOptions.json"));
+ var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationNoneOptions.json"));
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() {
@@ -446,7 +447,7 @@ public async Task TestFido2AssertionAsync()
// assertion
- var aoptions = await GetAsync("./assertionNoneOptions.json");
+ var aoptions = await GetAsync("./assertionNoneOptions.json");
var aresponse = await GetAsync("./assertionNoneResponse.json");
// signed assertion?
@@ -457,12 +458,12 @@ public async Task TestFido2AssertionAsync()
public async Task TestParsingAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./json1.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./options1.json"));
+ var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./options1.json"));
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]
@@ -517,9 +518,9 @@ public void TestAuthenticatorDataPa2rsing()
public async Task TestU2FAttestationAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsU2F.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsU2F.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 ?
}
@@ -527,9 +528,9 @@ public async Task TestU2FAttestationAsync()
public async Task TestPackedAttestationAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsPacked.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsPacked.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);
+ 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));
@@ -541,18 +542,18 @@ public async Task TestPackedAttestationAsync()
public async Task TestNoneAttestationAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsNone.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsNone.json"));
+ 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()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA256Response.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA256Options.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 ?
}
@@ -560,9 +561,9 @@ public async Task TestTPMSHA256AttestationAsync()
public async Task TestTPMSHA1AttestationAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA1Response.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationTPMSHA1Options.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);
+ 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 ?
}
@@ -570,9 +571,9 @@ public async Task TestTPMSHA1AttestationAsync()
public async Task TestAndroidKeyAttestationAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAndroidKeyResponse.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAndroidKeyOptions.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 ?
}
@@ -581,10 +582,10 @@ public async Task TestAndroidKeyAttestationAsync()
public async Task TestAppleAttestationAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAppleResponse.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAppleOptions.json"));
+ var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationAppleOptions.json"));
var o = AuthenticatorAttestationResponse.Parse(jsonPost);
var config = new Fido2Configuration { Origins = new HashSet { "https://6cc3c9e7967a.ngrok.io/" } };
- 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 ?
}
@@ -593,9 +594,9 @@ public async Task TestAppleAttestationAsync()
public async Task TaskPackedAttestation512()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsPacked512.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsPacked512.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 ?
}
@@ -604,9 +605,9 @@ public async Task TaskPackedAttestation512()
public async Task TestTrustKeyAttestationAsync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultTrustKeyT110.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsTrustKeyT110.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));
@@ -619,9 +620,9 @@ public async Task TestTrustKeyAttestationAsync()
public async Task TestInvalidU2FAttestationASync()
{
var jsonPost = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationResultsATKey.json"));
- var options = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./attestationOptionsATKey.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));
@@ -882,6 +883,7 @@ internal static async Task MakeAssertionResponse(CO
var challenge = new byte[128];
RandomNumberGenerator.Fill(challenge);
+ challenge = Encoding.UTF8.GetBytes(Base64Url.Encode(challenge));
var clientData = new
{
@@ -922,7 +924,7 @@ internal static async Task MakeAssertionResponse(CO
};
existingCredentials.Add(cred);
- var options = lib.GetAssertionOptions(existingCredentials, null, null);
+ var options = lib.GetAssertionOptions(existingCredentials);
options.Challenge = challenge;
var response = new AuthenticatorAssertionRawResponse()
{
diff --git a/Test/PubKeyCredParamTests.cs b/Test/PubKeyCredParamTests.cs
index 9da1b214..ab1d7c95 100644
--- a/Test/PubKeyCredParamTests.cs
+++ b/Test/PubKeyCredParamTests.cs
@@ -7,14 +7,14 @@
namespace fido2_net_lib.Test
{
- public class PubKeyCredParamTests
+ public class PublicKeyCredentialParametersTests
{
[Fact]
public void CanDeserializeES256()
{
string json = @"{""type"":""public-key"",""alg"":-7}";
- var model = JsonSerializer.Deserialize(json);
+ var model = JsonSerializer.Deserialize(json);
Assert.Equal(PublicKeyCredentialType.PublicKey, model.Type);
Assert.Equal(COSE.Algorithm.ES256, model.Alg);
@@ -25,7 +25,7 @@ public void CanDeserializeES256K()
{
string json = @"{""type"":""public-key"",""alg"":-47}";
- var model = JsonSerializer.Deserialize(json);
+ var model = JsonSerializer.Deserialize(json);
Assert.Equal(PublicKeyCredentialType.PublicKey, model.Type);
Assert.Equal(COSE.Algorithm.ES256K, model.Alg);
diff --git a/Test/TestFiles/attestationTPMSHA1Options.json b/Test/TestFiles/attestationTPMSHA1Options.json
index d7673cef..9c02ee46 100644
--- a/Test/TestFiles/attestationTPMSHA1Options.json
+++ b/Test/TestFiles/attestationTPMSHA1Options.json
@@ -14,7 +14,7 @@
"pubKeyCredParams": [
{
"type": "public-key",
- "alg": -7
+ "alg": -65535
},
{
"type": "public-key",