From 61a3590ab7537d55bd5ceae10469c2e179c0aced Mon Sep 17 00:00:00 2001 From: Jason Nelson Date: Wed, 23 Nov 2022 21:25:16 -0800 Subject: [PATCH] Use file scoped namespaces --- Demo/ConformanceTesting.cs | 38 +- Demo/Controller.cs | 351 +++--- Demo/Program.cs | 27 +- Demo/RouteHelperExtensions.cs | 43 +- Demo/Startup.cs | 124 +- Demo/TestController.cs | 341 +++--- Demo/UrlHelperExtensions.cs | 11 +- Src/Fido2.AspNet/DateTimeUtilities.cs | 47 +- .../DistributedCacheMetadataService.cs | 290 ++--- .../Fido2NetLibBuilderExtensions.cs | 167 +-- Src/Fido2.AspNet/NullMetadataService.cs | 19 +- Src/Fido2.Models/AssertionOptions.cs | 113 +- .../AuthenticatorAssertionRawResponse.cs | 67 +- .../AuthenticatorAttestationRawResponse.cs | 47 +- Src/Fido2.Models/Base64Url.cs | 224 ++-- Src/Fido2.Models/COSETypes.cs | 368 +++--- .../Converters/Base64Converter.cs | 33 +- Src/Fido2.Models/Converters/EnumNameMapper.cs | 99 +- .../Converters/FidoEnumConverter.cs | 34 +- Src/Fido2.Models/CredentialCreateOptions.cs | 495 ++++---- Src/Fido2.Models/Fido2Configuration.cs | 187 ++- Src/Fido2.Models/Fido2ResponseBase.cs | 15 +- .../Metadata/AlternativeDescriptions.cs | 39 +- .../Metadata/AuthenticatorStatus.cs | 163 ++- .../Metadata/BiometricAccuracyDescriptor.cs | 89 +- .../Metadata/BiometricStatusReport.cs | 103 +- .../Metadata/CodeAccuracyDescriptor.cs | 67 +- .../DisplayPNGCharacteristicsDescriptor.cs | 111 +- Src/Fido2.Models/Metadata/EcdaaTrustAnchor.cs | 79 +- .../Metadata/ExtensionDescriptor.cs | 81 +- .../Metadata/MetadataBLOBPayload.cs | 75 +- .../Metadata/MetadataBLOBPayloadEntry.cs | 147 ++- .../Metadata/MetadataStatement.cs | 383 +++--- .../Metadata/PatternAccuracyDescriptor.cs | 55 +- Src/Fido2.Models/Metadata/RgbPaletteEntry.cs | 45 +- Src/Fido2.Models/Metadata/StatusReport.cs | 111 +- Src/Fido2.Models/Metadata/UafVersion.cs | 35 +- .../Metadata/UserVerificationMethods.cs | 153 ++- .../Metadata/VerificationMethodDescriptor.cs | 57 +- .../Objects/AssertionVerificationResult.cs | 18 +- .../AttestationConveyancePreference.cs | 45 +- .../Objects/AttestationVerificationSuccess.cs | 27 +- .../AuthenticationExtensionsClientInputs.cs | 77 +- .../AuthenticationExtensionsClientOutputs.cs | 73 +- .../Objects/AuthenticatorAttachment.cs | 43 +- .../Objects/AuthenticatorTransport.cs | 53 +- Src/Fido2.Models/Objects/KeyProtection.cs | 75 +- .../Objects/PublicKeyCredentialDescriptor.cs | 65 +- .../Objects/PublicKeyCredentialType.cs | 21 +- .../Objects/PublicKeyCredentialUserEntity.cs | 17 +- .../Objects/ResidentKeyRequirement.cs | 43 +- .../Objects/UserVerificationRequirement.cs | 43 +- Src/Fido2.Models/Objects/Version.cs | 37 +- Src/Fido2.Models/StringExtensions.cs | 17 +- ...tdatataStatusFido2VerificationException.cs | 31 +- Src/Fido2/Asn1Element.cs | 261 +++-- Src/Fido2/AttestationFormat/AndroidKey.cs | 327 +++--- .../AttestationFormat/AndroidSafetyNet.cs | 257 ++-- Src/Fido2/AttestationFormat/Apple.cs | 123 +- Src/Fido2/AttestationFormat/AppleAppAttest.cs | 183 ++- .../AttestationFormat/AttestationFormat.cs | 125 +- .../MetadataAttestationType.cs | 81 +- Src/Fido2/AttestationFormat/None.cs | 15 +- Src/Fido2/AttestationFormat/Packed.cs | 223 ++-- Src/Fido2/AttestationFormat/Tpm.cs | 1035 ++++++++--------- .../Attributes/Fido2StandardAttribute.cs | 11 +- Src/Fido2/AuthDataHelper.cs | 35 +- Src/Fido2/AuthenticatorAssertionResponse.cs | 263 +++-- Src/Fido2/AuthenticatorAttestationResponse.cs | 429 ++++--- Src/Fido2/AuthenticatorResponse.cs | 129 +- Src/Fido2/ConformanceMetadataService.cs | 125 +- Src/Fido2/CryptoUtils.cs | 333 +++--- Src/Fido2/DevelopmentInMemoryStore.cs | 110 +- Src/Fido2/IFido2.cs | 63 +- Src/Fido2/IMetadataRepository.cs | 11 +- Src/Fido2/IMetadataService.cs | 33 +- .../Metadata/ConformanceMetadataRepository.cs | 361 +++--- .../Fido2MetadataServiceRepository.cs | 329 +++--- .../Metadata/FileSystemMetadataRepository.cs | 95 +- Src/Fido2/Metadata/MDSGetEndpointResponse.cs | 17 +- Src/Fido2/Objects/AttestedCredentialData.cs | 265 +++-- Src/Fido2/Objects/AuthenticatorData.cs | 235 ++-- Src/Fido2/Objects/AuthenticatorFlags.cs | 101 +- .../Objects/CredentialIdUserHandleParams.cs | 29 +- Src/Fido2/Objects/CredentialIdUserParams.cs | 25 +- Src/Fido2/Objects/CredentialPublicKey.cs | 353 +++--- Src/Fido2/Objects/Extensions.cs | 29 +- 87 files changed, 5773 insertions(+), 5856 deletions(-) diff --git a/Demo/ConformanceTesting.cs b/Demo/ConformanceTesting.cs index dd54a638d..c16dc57ce 100644 --- a/Demo/ConformanceTesting.cs +++ b/Demo/ConformanceTesting.cs @@ -1,34 +1,34 @@ using System.Collections.Generic; + using Fido2NetLib; -namespace Fido2Demo +namespace Fido2Demo; + +public static class ConformanceTesting { - public static class ConformanceTesting - { - private static readonly object _syncRoot = new (); + private static readonly object _syncRoot = new (); - private static IMetadataService _instance; + private static IMetadataService _instance; - public static IMetadataService MetadataServiceInstance(string cacheDir, string origin) + public static IMetadataService MetadataServiceInstance(string cacheDir, string origin) + { + if (_instance == null) { - if (_instance == null) + lock (_syncRoot) { - lock (_syncRoot) + if (_instance == null) { - if (_instance == null) + var repos = new List { - var repos = new List - { - new ConformanceMetadataRepository(null, origin), - new FileSystemMetadataRepository(cacheDir) - }; - var simpleService = new ConformanceMetadataService(repos); - simpleService.InitializeAsync().Wait(); - _instance = simpleService; - } + new ConformanceMetadataRepository(null, origin), + new FileSystemMetadataRepository(cacheDir) + }; + var simpleService = new ConformanceMetadataService(repos); + simpleService.InitializeAsync().Wait(); + _instance = simpleService; } } - return _instance; } + return _instance; } } diff --git a/Demo/Controller.cs b/Demo/Controller.cs index 80792db4c..0c18c1f36 100644 --- a/Demo/Controller.cs +++ b/Demo/Controller.cs @@ -4,218 +4,219 @@ using System.Text; using System.Threading; using System.Threading.Tasks; + using Fido2NetLib; using Fido2NetLib.Development; using Fido2NetLib.Objects; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using static Fido2NetLib.Fido2; -namespace Fido2Demo +namespace Fido2Demo; + +[Route("api/[controller]")] +public class MyController : Controller { - [Route("api/[controller]")] - public class MyController : Controller + private IFido2 _fido2; + public static IMetadataService _mds; + public static readonly DevelopmentInMemoryStore DemoStorage = new DevelopmentInMemoryStore(); + + public MyController(IFido2 fido2) { - private IFido2 _fido2; - public static IMetadataService _mds; - public static readonly DevelopmentInMemoryStore DemoStorage = new DevelopmentInMemoryStore(); + _fido2 = fido2; + } - public MyController(IFido2 fido2) - { - _fido2 = fido2; - } + private string FormatException(Exception e) + { + return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : ""); + } - private string FormatException(Exception e) + [HttpPost] + [Route("/makeCredentialOptions")] + public JsonResult MakeCredentialOptions([FromForm] string username, + [FromForm] string displayName, + [FromForm] string attType, + [FromForm] string authType, + [FromForm] string residentKey, + [FromForm] string userVerification) + { + try { - return string.Format("{0}{1}", e.Message, e.InnerException != null ? " (" + e.InnerException.Message + ")" : ""); - } - [HttpPost] - [Route("/makeCredentialOptions")] - public JsonResult MakeCredentialOptions([FromForm] string username, - [FromForm] string displayName, - [FromForm] string attType, - [FromForm] string authType, - [FromForm] string residentKey, - [FromForm] string userVerification) - { - try + if (string.IsNullOrEmpty(username)) { - - if (string.IsNullOrEmpty(username)) - { - username = $"{displayName} (Usernameless user created at {DateTime.UtcNow})"; - } - - // 1. Get user from DB by username (in our example, auto create missing users) - var user = DemoStorage.GetOrAddUser(username, () => new Fido2User - { - DisplayName = displayName, - Name = username, - Id = Encoding.UTF8.GetBytes(username) // byte representation of userID is required - }); - - // 2. Get user existing keys by username - var existingKeys = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); - - // 3. Create options - var authenticatorSelection = new AuthenticatorSelection - { - ResidentKey = residentKey.ToEnum(), - UserVerification = userVerification.ToEnum() - }; - - if (!string.IsNullOrEmpty(authType)) - authenticatorSelection.AuthenticatorAttachment = authType.ToEnum(); - - var exts = new AuthenticationExtensionsClientInputs() - { - Extensions = true, - UserVerificationMethod = true, - }; - - var options = _fido2.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum(), exts); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); - - // 5. return options to client - return Json(options); + username = $"{displayName} (Usernameless user created at {DateTime.UtcNow})"; } - catch (Exception e) + + // 1. Get user from DB by username (in our example, auto create missing users) + var user = DemoStorage.GetOrAddUser(username, () => new Fido2User { - return Json(new CredentialCreateOptions { Status = "error", ErrorMessage = FormatException(e) }); - } + DisplayName = displayName, + Name = username, + Id = Encoding.UTF8.GetBytes(username) // byte representation of userID is required + }); + + // 2. Get user existing keys by username + var existingKeys = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); + + // 3. Create options + var authenticatorSelection = new AuthenticatorSelection + { + ResidentKey = residentKey.ToEnum(), + UserVerification = userVerification.ToEnum() + }; + + if (!string.IsNullOrEmpty(authType)) + authenticatorSelection.AuthenticatorAttachment = authType.ToEnum(); + + var exts = new AuthenticationExtensionsClientInputs() + { + Extensions = true, + UserVerificationMethod = true, + }; + + var options = _fido2.RequestNewCredential(user, existingKeys, authenticatorSelection, attType.ToEnum(), exts); + + // 4. Temporarily store options, session/in-memory cache/redis/db + HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); + + // 5. return options to client + return Json(options); + } + catch (Exception e) + { + return Json(new CredentialCreateOptions { Status = "error", ErrorMessage = FormatException(e) }); } + } - [HttpPost] - [Route("/makeCredential")] - public async Task MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) + [HttpPost] + [Route("/makeCredential")] + public async Task MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) + { + try { - try + // 1. get the options we sent the client + var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); + var options = CredentialCreateOptions.FromJson(jsonOptions); + + // 2. Create callback so that lib can verify credential id is unique to this user + IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) => { - // 1. get the options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); - var options = CredentialCreateOptions.FromJson(jsonOptions); - - // 2. Create callback so that lib can verify credential id is unique to this user - IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) => - { - var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); - if (users.Count > 0) - return false; - - return true; - }; - - // 2. Verify and make the credentials - var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken); - - // 3. Store the credentials in db - DemoStorage.AddCredentialToUser(options.User, new StoredCredential - { - Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), - PublicKey = success.Result.PublicKey, - UserHandle = success.Result.User.Id, - SignatureCounter = success.Result.Counter, - CredType = success.Result.CredType, - RegDate = DateTime.Now, - AaGuid = success.Result.Aaguid - }); - - // Remove Certificates from success because System.Text.Json cannot serialize them properly. See https://github.com/passwordless-lib/fido2-net-lib/issues/328 - success.Result.AttestationCertificate = null; - success.Result.AttestationCertificateChain = null; - - // 4. return "ok" to the client - return Json(success); - } - catch (Exception e) + var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); + if (users.Count > 0) + return false; + + return true; + }; + + // 2. Verify and make the credentials + var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken); + + // 3. Store the credentials in db + DemoStorage.AddCredentialToUser(options.User, new StoredCredential { - return Json(new CredentialMakeResult(status: "error", errorMessage: FormatException(e), result: null)); - } + Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), + PublicKey = success.Result.PublicKey, + UserHandle = success.Result.User.Id, + SignatureCounter = success.Result.Counter, + CredType = success.Result.CredType, + RegDate = DateTime.Now, + AaGuid = success.Result.Aaguid + }); + + // Remove Certificates from success because System.Text.Json cannot serialize them properly. See https://github.com/passwordless-lib/fido2-net-lib/issues/328 + success.Result.AttestationCertificate = null; + success.Result.AttestationCertificateChain = null; + + // 4. return "ok" to the client + return Json(success); + } + catch (Exception e) + { + return Json(new CredentialMakeResult(status: "error", errorMessage: FormatException(e), result: null)); } + } - [HttpPost] - [Route("/assertionOptions")] - public ActionResult AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification) + [HttpPost] + [Route("/assertionOptions")] + public ActionResult AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification) + { + try { - try - { - var existingCredentials = new List(); - - if (!string.IsNullOrEmpty(username)) - { - // 1. Get user from DB - var user = DemoStorage.GetUser(username) ?? throw new ArgumentException("Username was not registered"); - - // 2. Get registered credentials from database - existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); - } - - var exts = new AuthenticationExtensionsClientInputs() - { - UserVerificationMethod = true - }; - - // 3. Create options - var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum(); - var options = _fido2.GetAssertionOptions( - existingCredentials, - uv, - exts - ); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); - - // 5. Return options to client - return Json(options); - } + var existingCredentials = new List(); - catch (Exception e) + if (!string.IsNullOrEmpty(username)) { - return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) }); + // 1. Get user from DB + var user = DemoStorage.GetUser(username) ?? throw new ArgumentException("Username was not registered"); + + // 2. Get registered credentials from database + existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); } + + var exts = new AuthenticationExtensionsClientInputs() + { + UserVerificationMethod = true + }; + + // 3. Create options + var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum(); + var options = _fido2.GetAssertionOptions( + existingCredentials, + uv, + exts + ); + + // 4. Temporarily store options, session/in-memory cache/redis/db + HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); + + // 5. Return options to client + return Json(options); } - [HttpPost] - [Route("/makeAssertion")] - public async Task MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) + catch (Exception e) { - try - { - // 1. Get the assertion options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); - var options = AssertionOptions.FromJson(jsonOptions); + return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) }); + } + } + + [HttpPost] + [Route("/makeAssertion")] + public async Task MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) + { + try + { + // 1. Get the assertion options we sent the client + var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); + var options = AssertionOptions.FromJson(jsonOptions); - // 2. Get registered credential from database - var creds = DemoStorage.GetCredentialById(clientResponse.Id) ?? throw new Exception("Unknown credentials"); + // 2. Get registered credential from database + var creds = DemoStorage.GetCredentialById(clientResponse.Id) ?? throw new Exception("Unknown credentials"); - // 3. Get credential counter from database - var storedCounter = creds.SignatureCounter; + // 3. Get credential counter from database + var storedCounter = creds.SignatureCounter; - // 4. Create callback to check if userhandle owns the credentialId - IsUserHandleOwnerOfCredentialIdAsync callback = static async (args, cancellationToken) => - { - var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken); - return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); - }; + // 4. Create callback to check if userhandle owns the credentialId + IsUserHandleOwnerOfCredentialIdAsync callback = static async (args, cancellationToken) => + { + var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken); + return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); + }; - // 5. Make the assertion - var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken); + // 5. Make the assertion + var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken); - // 6. Store the updated counter - DemoStorage.UpdateCounter(res.CredentialId, res.Counter); + // 6. Store the updated counter + DemoStorage.UpdateCounter(res.CredentialId, res.Counter); - // 7. return OK to client - return Json(res); - } - catch (Exception e) - { - return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) }); - } + // 7. return OK to client + return Json(res); + } + catch (Exception e) + { + return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) }); } } } diff --git a/Demo/Program.cs b/Demo/Program.cs index 7cb930140..2942399dd 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -1,22 +1,21 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace Fido2Demo +namespace Fido2Demo; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + CreateHostBuilder(args).Build().Run(); + } - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } } diff --git a/Demo/RouteHelperExtensions.cs b/Demo/RouteHelperExtensions.cs index a4098ff53..aff51ddb8 100644 --- a/Demo/RouteHelperExtensions.cs +++ b/Demo/RouteHelperExtensions.cs @@ -5,34 +5,33 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; -namespace Fido2Demo +namespace Fido2Demo; + +public static class RouteHelperExtensions { - public static class RouteHelperExtensions + public static RewriteOptions AddRedirectToWWwIfPasswordlessDomain(this RewriteOptions options) { - public static RewriteOptions AddRedirectToWWwIfPasswordlessDomain(this RewriteOptions options) - { - options.Add(new RedirectToWwwIfPasswordlessDomainRule()); - return options; - } + options.Add(new RedirectToWwwIfPasswordlessDomainRule()); + return options; + } - public class RedirectToWwwIfPasswordlessDomainRule : IRule + public class RedirectToWwwIfPasswordlessDomainRule : IRule + { + public virtual void ApplyRule(RewriteContext context) { - public virtual void ApplyRule(RewriteContext context) + var req = context.HttpContext.Request; + if (req.Host.Host is "passwordless.dev" or "fido2.azurewebsites.net") { - var req = context.HttpContext.Request; - if (req.Host.Host is "passwordless.dev" or "fido2.azurewebsites.net") - { - var wwwHost = new HostString("www.passwordless.dev"); - var newUrl = UriHelper.BuildAbsolute("https", wwwHost, req.PathBase, req.Path, req.QueryString); - var response = context.HttpContext.Response; - response.StatusCode = 301; - response.Headers[HeaderNames.Location] = newUrl; - context.Result = RuleResult.EndResponse; - } - - context.Result = RuleResult.ContinueRules; - return; + var wwwHost = new HostString("www.passwordless.dev"); + var newUrl = UriHelper.BuildAbsolute("https", wwwHost, req.PathBase, req.Path, req.QueryString); + var response = context.HttpContext.Response; + response.StatusCode = 301; + response.Headers[HeaderNames.Location] = newUrl; + context.Result = RuleResult.EndResponse; } + + context.Result = RuleResult.ContinueRules; + return; } } } diff --git a/Demo/Startup.cs b/Demo/Startup.cs index 8b0d0d194..86e63f13d 100644 --- a/Demo/Startup.cs +++ b/Demo/Startup.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -9,80 +10,79 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Fido2Demo +namespace Fido2Demo; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(opts => { - services.AddRazorPages(opts => - { - // we don't care about antiforgery in the demo - opts.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()); - }); + // we don't care about antiforgery in the demo + opts.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()); + }); - // Use the in-memory implementation of IDistributedCache. - services.AddMemoryCache(); - services.AddDistributedMemoryCache(); + // Use the in-memory implementation of IDistributedCache. + services.AddMemoryCache(); + services.AddDistributedMemoryCache(); - services.AddSession(options => - { - // Set a short timeout for easy testing. - options.IdleTimeout = TimeSpan.FromMinutes(2); - options.Cookie.HttpOnly = true; - // Strict SameSite mode is required because the default mode used - // by ASP.NET Core 3 isn't understood by the Conformance Tool - // and breaks conformance testing - options.Cookie.SameSite = SameSiteMode.Unspecified; - }); + services.AddSession(options => + { + // Set a short timeout for easy testing. + options.IdleTimeout = TimeSpan.FromMinutes(2); + options.Cookie.HttpOnly = true; + // Strict SameSite mode is required because the default mode used + // by ASP.NET Core 3 isn't understood by the Conformance Tool + // and breaks conformance testing + options.Cookie.SameSite = SameSiteMode.Unspecified; + }); - services.AddFido2(options => - { - options.ServerDomain = Configuration["fido2:serverDomain"]; - options.ServerName = "FIDO2 Test"; - options.Origins = Configuration.GetSection("fido2:origins").Get>(); - options.TimestampDriftTolerance = Configuration.GetValue("fido2:timestampDriftTolerance"); - options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"]; - }) - .AddCachedMetadataService(config => + services.AddFido2(options => + { + options.ServerDomain = Configuration["fido2:serverDomain"]; + options.ServerName = "FIDO2 Test"; + options.Origins = Configuration.GetSection("fido2:origins").Get>(); + options.TimestampDriftTolerance = Configuration.GetValue("fido2:timestampDriftTolerance"); + options.MDSCacheDirPath = Configuration["fido2:MDSCacheDirPath"]; + }) + .AddCachedMetadataService(config => + { + config.AddFidoMetadataRepository(httpClientBuilder => { - config.AddFidoMetadataRepository(httpClientBuilder => - { - //TODO: any specific config you want for accessing the MDS - }); + //TODO: any specific config you want for accessing the MDS }); - } + }); + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - app.UseRewriter(new RewriteOptions().AddRedirectToWWwIfPasswordlessDomain()); - } - - app.UseSession(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapFallbackToPage("/", "/overview"); - endpoints.MapRazorPages(); - endpoints.MapControllers(); - }); + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseRewriter(new RewriteOptions().AddRedirectToWWwIfPasswordlessDomain()); } + + app.UseSession(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapFallbackToPage("/", "/overview"); + endpoints.MapRazorPages(); + endpoints.MapControllers(); + }); } } diff --git a/Demo/TestController.cs b/Demo/TestController.cs index 0932a5727..ce3c071aa 100644 --- a/Demo/TestController.cs +++ b/Demo/TestController.cs @@ -3,208 +3,209 @@ using System.Text; using System.Threading; using System.Threading.Tasks; + using Fido2NetLib; using Fido2NetLib.Development; using Fido2NetLib.Objects; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; -namespace Fido2Demo +namespace Fido2Demo; + +public class TestController : Controller { - public class TestController : Controller + /* CONFORMANCE TESTING ENDPOINTS */ + private static readonly DevelopmentInMemoryStore DemoStorage = new (); + + private readonly IFido2 _fido2; + private readonly string _origin; + + public TestController(IOptions fido2Configuration) + { + _origin = fido2Configuration.Value.FullyQualifiedOrigins.FirstOrDefault(); + + _fido2 = new Fido2(new Fido2Configuration + { + ServerDomain = fido2Configuration.Value.ServerDomain, + ServerName = fido2Configuration.Value.ServerName, + Origins = fido2Configuration.Value.FullyQualifiedOrigins, + }, + ConformanceTesting.MetadataServiceInstance( + System.IO.Path.Combine(fido2Configuration.Value.MDSCacheDirPath, @"Conformance"), _origin) + ); + } + + [HttpPost] + [Route("/attestation/options")] + public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams opts) { - /* CONFORMANCE TESTING ENDPOINTS */ - private static readonly DevelopmentInMemoryStore DemoStorage = new (); + var attType = opts.Attestation; - private readonly IFido2 _fido2; - private readonly string _origin; + var username = Array.Empty(); - public TestController(IOptions fido2Configuration) + try { - _origin = fido2Configuration.Value.FullyQualifiedOrigins.FirstOrDefault(); - - _fido2 = new Fido2(new Fido2Configuration - { - ServerDomain = fido2Configuration.Value.ServerDomain, - ServerName = fido2Configuration.Value.ServerName, - Origins = fido2Configuration.Value.FullyQualifiedOrigins, - }, - ConformanceTesting.MetadataServiceInstance( - System.IO.Path.Combine(fido2Configuration.Value.MDSCacheDirPath, @"Conformance"), _origin) - ); + username = Base64Url.Decode(opts.Username); } - - [HttpPost] - [Route("/attestation/options")] - public JsonResult MakeCredentialOptionsTest([FromBody] TEST_MakeCredentialParams opts) + catch (FormatException) { - var attType = opts.Attestation; - - var username = Array.Empty(); - - try - { - username = Base64Url.Decode(opts.Username); - } - catch (FormatException) - { - username = Encoding.UTF8.GetBytes(opts.Username); - } - - // 1. Get user from DB by username (in our example, auto create missing users) - var user = DemoStorage.GetOrAddUser(opts.Username, () => new Fido2User - { - DisplayName = opts.DisplayName, - Name = opts.Username, - Id = username // byte representation of userID is required - }); - - // 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); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); - - // 5. return options to client - return Json(options); + username = Encoding.UTF8.GetBytes(opts.Username); } - [HttpPost] - [Route("/attestation/result")] - public async Task MakeCredentialResultTest([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) + // 1. Get user from DB by username (in our example, auto create missing users) + var user = DemoStorage.GetOrAddUser(opts.Username, () => new Fido2User { + DisplayName = opts.DisplayName, + Name = opts.Username, + Id = username // byte representation of userID is required + }); - // 1. get the options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); - var options = CredentialCreateOptions.FromJson(jsonOptions); - - // 2. Create callback so that lib can verify credential id is unique to this user - IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) => - { - var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); - return users.Count <= 0; - }; - - // 2. Verify and make the credentials - var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken); - - // 3. Store the credentials in db - DemoStorage.AddCredentialToUser(options.User, new StoredCredential - { - Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), - PublicKey = success.Result.PublicKey, - UserHandle = success.Result.User.Id, - SignatureCounter = success.Result.Counter - }); - - // 4. return "ok" to the client - return Json(success); - } + // 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); + + // 4. Temporarily store options, session/in-memory cache/redis/db + HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson()); + + // 5. return options to client + return Json(options); + } + + [HttpPost] + [Route("/attestation/result")] + public async Task MakeCredentialResultTest([FromBody] AuthenticatorAttestationRawResponse attestationResponse, CancellationToken cancellationToken) + { - [HttpPost] - [Route("/assertion/options")] - public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams assertionClientParams) + // 1. get the options we sent the client + var jsonOptions = HttpContext.Session.GetString("fido2.attestationOptions"); + var options = CredentialCreateOptions.FromJson(jsonOptions); + + // 2. Create callback so that lib can verify credential id is unique to this user + IsCredentialIdUniqueToUserAsyncDelegate callback = static async (args, cancellationToken) => { - var username = assertionClientParams.Username; - // 1. Get user from DB - var user = DemoStorage.GetUser(username); - if (user == null) - return NotFound("username was not registered"); - - // 2. Get registered credentials from database - var existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); - - var 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 - ); - - // 4. Temporarily store options, session/in-memory cache/redis/db - HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); - - // 5. Return options to client - return Json(options); - } + var users = await DemoStorage.GetUsersByCredentialIdAsync(args.CredentialId, cancellationToken); + return users.Count <= 0; + }; + + // 2. Verify and make the credentials + var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback, cancellationToken: cancellationToken); - [HttpPost] - [Route("/assertion/result")] - public async Task MakeAssertionTest([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) + // 3. Store the credentials in db + DemoStorage.AddCredentialToUser(options.User, new StoredCredential { - // 1. Get the assertion options we sent the client - var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); - var options = AssertionOptions.FromJson(jsonOptions); + Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), + PublicKey = success.Result.PublicKey, + UserHandle = success.Result.User.Id, + SignatureCounter = success.Result.Counter + }); + + // 4. return "ok" to the client + return Json(success); + } - // 2. Get registered credential from database - var creds = DemoStorage.GetCredentialById(clientResponse.Id); + [HttpPost] + [Route("/assertion/options")] + public IActionResult AssertionOptionsTest([FromBody] TEST_AssertionClientParams assertionClientParams) + { + var username = assertionClientParams.Username; + // 1. Get user from DB + var user = DemoStorage.GetUser(username); + if (user == null) + return NotFound("username was not registered"); + + // 2. Get registered credentials from database + var existingCredentials = DemoStorage.GetCredentialsByUser(user).Select(c => c.Descriptor).ToList(); + + var 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 + ); + + // 4. Temporarily store options, session/in-memory cache/redis/db + HttpContext.Session.SetString("fido2.assertionOptions", options.ToJson()); + + // 5. Return options to client + return Json(options); + } - // 3. Get credential counter from database - var storedCounter = creds.SignatureCounter; + [HttpPost] + [Route("/assertion/result")] + public async Task MakeAssertionTest([FromBody] AuthenticatorAssertionRawResponse clientResponse, CancellationToken cancellationToken) + { + // 1. Get the assertion options we sent the client + var jsonOptions = HttpContext.Session.GetString("fido2.assertionOptions"); + var options = AssertionOptions.FromJson(jsonOptions); - // 4. Create callback to check if userhandle owns the credentialId - IsUserHandleOwnerOfCredentialIdAsync callback = static async (args, cancellationToken) => - { - var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken); - return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); - }; + // 2. Get registered credential from database + var creds = DemoStorage.GetCredentialById(clientResponse.Id); - // 5. Make the assertion - var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken); + // 3. Get credential counter from database + var storedCounter = creds.SignatureCounter; - // 6. Store the updated counter - DemoStorage.UpdateCounter(res.CredentialId, res.Counter); + // 4. Create callback to check if userhandle owns the credentialId + IsUserHandleOwnerOfCredentialIdAsync callback = static async (args, cancellationToken) => + { + var storedCreds = await DemoStorage.GetCredentialsByUserHandleAsync(args.UserHandle, cancellationToken); + return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId)); + }; - var testRes = new - { - status = "ok", - errorMessage = "" - }; + // 5. Make the assertion + var res = await _fido2.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, callback, cancellationToken: cancellationToken); - // 7. return OK to client - return Json(testRes); - } + // 6. Store the updated counter + DemoStorage.UpdateCounter(res.CredentialId, res.Counter); - /// - /// For testing - /// - public class TEST_AssertionClientParams + var testRes = new { - public string Username { get; set; } - public UserVerificationRequirement? UserVerification { get; set; } - public AuthenticatorSelection authenticatorSelection { get; set; } - public AuthenticationExtensionsClientOutputs Extensions { get; set; } - } + status = "ok", + errorMessage = "" + }; - 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 AuthenticationExtensionsClientOutputs Extensions { get; set; } - } + // 7. return OK to client + return Json(testRes); + } + + /// + /// For testing + /// + public class TEST_AssertionClientParams + { + public string Username { get; set; } + public UserVerificationRequirement? UserVerification { get; set; } + public AuthenticatorSelection authenticatorSelection { get; set; } + public AuthenticationExtensionsClientOutputs Extensions { get; set; } + } + + 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 AuthenticationExtensionsClientOutputs Extensions { get; set; } } } diff --git a/Demo/UrlHelperExtensions.cs b/Demo/UrlHelperExtensions.cs index 354f08ade..440f2e4fa 100644 --- a/Demo/UrlHelperExtensions.cs +++ b/Demo/UrlHelperExtensions.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Mvc; -namespace Fido2Demo +namespace Fido2Demo; + +public static class UrlHelperExtensions { - public static class UrlHelperExtensions + public static string ToGithub(this IUrlHelper url, string path) { - public static string ToGithub(this IUrlHelper url, string path) - { - return "https://github.com/abergs/fido2-net-lib/blob/master/" + path; - } + return "https://github.com/abergs/fido2-net-lib/blob/master/" + path; } } diff --git a/Src/Fido2.AspNet/DateTimeUtilities.cs b/Src/Fido2.AspNet/DateTimeUtilities.cs index f4eb8a886..e393808b0 100644 --- a/Src/Fido2.AspNet/DateTimeUtilities.cs +++ b/Src/Fido2.AspNet/DateTimeUtilities.cs @@ -1,35 +1,30 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal static class DateTimeUtilities { - internal static class DateTimeUtilities + /// + /// Finds the nearest future point in time that aligns with the increment provided + /// e.g. 12:58:23 -> 13:00 if the increment provided is 2 minutes + /// + /// The time from which to calculated the next increment + /// The increment used to calculate the new time + /// + public static DateTimeOffset GetNextIncrement(this DateTimeOffset startTime, TimeSpan increment) { - /// - /// Finds the nearest future point in time that aligns with the increment provided - /// e.g. 12:58:23 -> 13:00 if the increment provided is 2 minutes - /// - /// The time from which to calculated the next increment - /// The increment used to calculate the new time - /// - public static DateTimeOffset GetNextIncrement(this DateTimeOffset startTime, TimeSpan increment) - { - //Find next increment - var nextIncrementTicks = (long)(Math.Ceiling((decimal)startTime.Ticks / (decimal)increment.Ticks) * (decimal)increment.Ticks); - - //Find the difference between the start time and the target time - var timeSpanDiff = TimeSpan.FromTicks(nextIncrementTicks).Subtract(TimeSpan.FromTicks(startTime.Ticks)); + //Find next increment + var nextIncrementTicks = (long)(Math.Ceiling((decimal)startTime.Ticks / (decimal)increment.Ticks) * (decimal)increment.Ticks); - //If the calculated difference is 0 then make it the increment value - if (timeSpanDiff.Ticks == 0) - timeSpanDiff = TimeSpan.FromTicks(increment.Ticks); + //Find the difference between the start time and the target time + var timeSpanDiff = TimeSpan.FromTicks(nextIncrementTicks).Subtract(TimeSpan.FromTicks(startTime.Ticks)); - //Add the difference to the normalised time - return startTime.Add(timeSpanDiff); - } + //If the calculated difference is 0 then make it the increment value + if (timeSpanDiff.Ticks == 0) + timeSpanDiff = TimeSpan.FromTicks(increment.Ticks); + //Add the difference to the normalised time + return startTime.Add(timeSpanDiff); } + } diff --git a/Src/Fido2.AspNet/DistributedCacheMetadataService.cs b/Src/Fido2.AspNet/DistributedCacheMetadataService.cs index 6f00db18d..a045f7f76 100644 --- a/Src/Fido2.AspNet/DistributedCacheMetadataService.cs +++ b/Src/Fido2.AspNet/DistributedCacheMetadataService.cs @@ -4,208 +4,208 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; -namespace Fido2NetLib +namespace Fido2NetLib; + +public class DistributedCacheMetadataService : IMetadataService { - public class DistributedCacheMetadataService : IMetadataService - { - protected readonly IDistributedCache _distributedCache; - protected readonly IMemoryCache _memoryCache; - protected readonly ISystemClock _systemClock; + protected readonly IDistributedCache _distributedCache; + protected readonly IMemoryCache _memoryCache; + protected readonly ISystemClock _systemClock; - protected readonly List _repositories; - protected readonly ILogger _logger; + protected readonly List _repositories; + protected readonly ILogger _logger; - protected readonly TimeSpan _defaultMemoryCacheInterval = TimeSpan.FromHours(1); - protected readonly TimeSpan _nextUpdateBufferPeriod = TimeSpan.FromHours(25); - protected readonly TimeSpan _defaultDistributedCacheInterval = TimeSpan.FromDays(8); + protected readonly TimeSpan _defaultMemoryCacheInterval = TimeSpan.FromHours(1); + protected readonly TimeSpan _nextUpdateBufferPeriod = TimeSpan.FromHours(25); + protected readonly TimeSpan _defaultDistributedCacheInterval = TimeSpan.FromDays(8); - protected const string CACHE_PREFIX = nameof(DistributedCacheMetadataService) + ":V2"; + protected const string CACHE_PREFIX = nameof(DistributedCacheMetadataService) + ":V2"; - public DistributedCacheMetadataService( - IEnumerable repositories, - IDistributedCache distributedCache, - IMemoryCache memoryCache, - ILogger logger, - ISystemClock systemClock) - { + public DistributedCacheMetadataService( + IEnumerable repositories, + IDistributedCache distributedCache, + IMemoryCache memoryCache, + ILogger logger, + ISystemClock systemClock) + { - if (repositories == null) - throw new ArgumentNullException(nameof(repositories)); + if (repositories == null) + throw new ArgumentNullException(nameof(repositories)); - _repositories = repositories.ToList(); - _distributedCache = distributedCache; - _memoryCache = memoryCache; - _logger = logger; - _systemClock = systemClock; - } + _repositories = repositories.ToList(); + _distributedCache = distributedCache; + _memoryCache = memoryCache; + _logger = logger; + _systemClock = systemClock; + } - public virtual bool ConformanceTesting() - { - return _repositories.Any(o => o.GetType() == typeof(ConformanceMetadataRepository)); - } + public virtual bool ConformanceTesting() + { + return _repositories.Any(o => o.GetType() == typeof(ConformanceMetadataRepository)); + } - protected virtual string GetBlobCacheKey(IMetadataRepository repository) - { - return $"{CACHE_PREFIX}:{repository.GetType().Name}:TOC"; - } + protected virtual string GetBlobCacheKey(IMetadataRepository repository) + { + return $"{CACHE_PREFIX}:{repository.GetType().Name}:TOC"; + } - protected virtual DateTimeOffset? GetNextUpdateTimeFromPayload(MetadataBLOBPayload blob) + protected virtual DateTimeOffset? GetNextUpdateTimeFromPayload(MetadataBLOBPayload blob) + { + if (!string.IsNullOrWhiteSpace(blob?.NextUpdate) + && DateTimeOffset.TryParseExact( + blob.NextUpdate, + new[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "o" }, //Sould be ISO8601 date but allow for other ISO-like formats too + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, + out var parsedDate)) { - if (!string.IsNullOrWhiteSpace(blob?.NextUpdate) - && DateTimeOffset.TryParseExact( - blob.NextUpdate, - new[] { "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "o" }, //Sould be ISO8601 date but allow for other ISO-like formats too - System.Globalization.CultureInfo.InvariantCulture, - System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, - out var parsedDate)) - { - return parsedDate; - } - - return null; + return parsedDate; } - protected virtual DateTimeOffset GetMemoryCacheAbsoluteExpiryTime(DateTimeOffset? nextUpdateTime) - { - var expiryTime = _systemClock.UtcNow.GetNextIncrement(_defaultMemoryCacheInterval); + return null; + } - //Ensure that memory cache expiry time never exceeds the next update time from the service - if(nextUpdateTime.HasValue && expiryTime > nextUpdateTime.Value) expiryTime = nextUpdateTime.Value; + protected virtual DateTimeOffset GetMemoryCacheAbsoluteExpiryTime(DateTimeOffset? nextUpdateTime) + { + var expiryTime = _systemClock.UtcNow.GetNextIncrement(_defaultMemoryCacheInterval); - return expiryTime; - } + //Ensure that memory cache expiry time never exceeds the next update time from the service + if(nextUpdateTime.HasValue && expiryTime > nextUpdateTime.Value) expiryTime = nextUpdateTime.Value; - protected virtual DateTimeOffset GetDistributedCacheAbsoluteExpiryTime(DateTimeOffset? nextUpdatTime) - { - if (nextUpdatTime.HasValue) - { - if (nextUpdatTime > _systemClock.UtcNow) - return nextUpdatTime.Value.Add(_defaultDistributedCacheInterval); - } + return expiryTime; + } - return _systemClock.UtcNow.Add(_defaultDistributedCacheInterval); + protected virtual DateTimeOffset GetDistributedCacheAbsoluteExpiryTime(DateTimeOffset? nextUpdatTime) + { + if (nextUpdatTime.HasValue) + { + if (nextUpdatTime > _systemClock.UtcNow) + return nextUpdatTime.Value.Add(_defaultDistributedCacheInterval); } - protected virtual async Task GetRepositoryPayloadWithErrorHandling(IMetadataRepository repository, CancellationToken cancellationToken = default) + return _systemClock.UtcNow.Add(_defaultDistributedCacheInterval); + } + + protected virtual async Task GetRepositoryPayloadWithErrorHandling(IMetadataRepository repository, CancellationToken cancellationToken = default) + { + try { - try - { - return await repository.GetBLOBAsync(cancellationToken); - } - catch(Exception ex) - { - _logger.LogError(ex, "Could not fetch metadata from {0}", repository.GetType().Name); - return null; - } + return await repository.GetBLOBAsync(cancellationToken); } - - protected virtual async Task StoreDistributedCachedBlob(IMetadataRepository repository, MetadataBLOBPayload payload, CancellationToken cancellationToken = default) + catch(Exception ex) { - await _distributedCache.SetStringAsync( - GetBlobCacheKey(repository), - JsonSerializer.Serialize(payload), - new DistributedCacheEntryOptions() - { - AbsoluteExpiration = GetDistributedCacheAbsoluteExpiryTime(GetNextUpdateTimeFromPayload(payload)) - }, - cancellationToken); + _logger.LogError(ex, "Could not fetch metadata from {0}", repository.GetType().Name); + return null; } + } - protected virtual async Task GetDistributedCachedBlob(IMetadataRepository repository, CancellationToken cancellationToken = default) - { - var cacheKey = GetBlobCacheKey(repository); + protected virtual async Task StoreDistributedCachedBlob(IMetadataRepository repository, MetadataBLOBPayload payload, CancellationToken cancellationToken = default) + { + await _distributedCache.SetStringAsync( + GetBlobCacheKey(repository), + JsonSerializer.Serialize(payload), + new DistributedCacheEntryOptions() + { + AbsoluteExpiration = GetDistributedCacheAbsoluteExpiryTime(GetNextUpdateTimeFromPayload(payload)) + }, + cancellationToken); + } - var distributedCacheEntry = await _distributedCache.GetStringAsync(cacheKey, cancellationToken); - if (distributedCacheEntry != null) + protected virtual async Task GetDistributedCachedBlob(IMetadataRepository repository, CancellationToken cancellationToken = default) + { + var cacheKey = GetBlobCacheKey(repository); + + var distributedCacheEntry = await _distributedCache.GetStringAsync(cacheKey, cancellationToken); + if (distributedCacheEntry != null) + { + try { - try - { - var cachedBlob = JsonSerializer.Deserialize(distributedCacheEntry); - var nextUpdateTime = GetNextUpdateTimeFromPayload(cachedBlob); + var cachedBlob = JsonSerializer.Deserialize(distributedCacheEntry); + var nextUpdateTime = GetNextUpdateTimeFromPayload(cachedBlob); - //If the cache until time is in the past then update and return new data, otherwise return the cached value - if (nextUpdateTime == null || nextUpdateTime.Value.Add(_nextUpdateBufferPeriod) < _systemClock.UtcNow) + //If the cache until time is in the past then update and return new data, otherwise return the cached value + if (nextUpdateTime == null || nextUpdateTime.Value.Add(_nextUpdateBufferPeriod) < _systemClock.UtcNow) + { + var payload = await GetRepositoryPayloadWithErrorHandling(repository, cancellationToken); + if (payload != null) { - var payload = await GetRepositoryPayloadWithErrorHandling(repository, cancellationToken); - if (payload != null) - { - await StoreDistributedCachedBlob(repository, payload, cancellationToken); - return payload; - } + await StoreDistributedCachedBlob(repository, payload, cancellationToken); + return payload; } - - return cachedBlob; - } - catch (JsonException ex) - { - _logger.LogWarning(ex, "{0}: Invalid BLOB value in distributed cache", nameof(DistributedCacheMetadataService)); } - } - var repoBlob = await GetRepositoryPayloadWithErrorHandling(repository, cancellationToken); - if (repoBlob != null) + return cachedBlob; + } + catch (JsonException ex) { - await StoreDistributedCachedBlob(repository, repoBlob, cancellationToken); + _logger.LogWarning(ex, "{0}: Invalid BLOB value in distributed cache", nameof(DistributedCacheMetadataService)); } + } - return repoBlob; + var repoBlob = await GetRepositoryPayloadWithErrorHandling(repository, cancellationToken); + if (repoBlob != null) + { + await StoreDistributedCachedBlob(repository, repoBlob, cancellationToken); } - protected virtual async Task GetMemoryCachedPayload(IMetadataRepository repository, CancellationToken cancellationToken = default) + return repoBlob; + } + + protected virtual async Task GetMemoryCachedPayload(IMetadataRepository repository, CancellationToken cancellationToken = default) + { + var cacheKey = GetBlobCacheKey(repository); + + var memCacheEntry = await _memoryCache.GetOrCreateAsync(cacheKey, async memCacheEntry => { - var cacheKey = GetBlobCacheKey(repository); + var distributedCacheBlob = await GetDistributedCachedBlob(repository, cancellationToken); - var memCacheEntry = await _memoryCache.GetOrCreateAsync(cacheKey, async memCacheEntry => + if(distributedCacheBlob != null) { - var distributedCacheBlob = await GetDistributedCachedBlob(repository, cancellationToken); - - if(distributedCacheBlob != null) - { - var nextUpdateTime = GetNextUpdateTimeFromPayload(distributedCacheBlob); + var nextUpdateTime = GetNextUpdateTimeFromPayload(distributedCacheBlob); - memCacheEntry.AbsoluteExpiration = GetMemoryCacheAbsoluteExpiryTime(nextUpdateTime); + memCacheEntry.AbsoluteExpiration = GetMemoryCacheAbsoluteExpiryTime(nextUpdateTime); - return distributedCacheBlob; - } + return distributedCacheBlob; + } - return null; - }); + return null; + }); - return memCacheEntry; - } + return memCacheEntry; + } - public async Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default) - { - var aaguidComparisonString = aaguid.ToString("D"); + public async Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default) + { + var aaguidComparisonString = aaguid.ToString("D"); - var memCacheEntry = await _memoryCache.GetOrCreateAsync( - $"{CACHE_PREFIX}:{aaguidComparisonString}", - async entry => + var memCacheEntry = await _memoryCache.GetOrCreateAsync( + $"{CACHE_PREFIX}:{aaguidComparisonString}", + async entry => + { + foreach (var repo in _repositories) { - foreach (var repo in _repositories) + var cachedPayload = await GetMemoryCachedPayload(repo, cancellationToken); + if (cachedPayload != null) { - var cachedPayload = await GetMemoryCachedPayload(repo, cancellationToken); - if (cachedPayload != null) + var matchingEntry = cachedPayload.Entries?.FirstOrDefault(o => o.AaGuid == aaguidComparisonString); + if (matchingEntry != null) { - var matchingEntry = cachedPayload.Entries?.FirstOrDefault(o => o.AaGuid == aaguidComparisonString); - if (matchingEntry != null) - { - entry.AbsoluteExpiration = GetMemoryCacheAbsoluteExpiryTime(GetNextUpdateTimeFromPayload(cachedPayload)); - return matchingEntry; - } + entry.AbsoluteExpiration = GetMemoryCacheAbsoluteExpiryTime(GetNextUpdateTimeFromPayload(cachedPayload)); + return matchingEntry; } } + } - return null; + return null; - }); + }); - return memCacheEntry; - } + return memCacheEntry; } } diff --git a/Src/Fido2.AspNet/Fido2NetLibBuilderExtensions.cs b/Src/Fido2.AspNet/Fido2NetLibBuilderExtensions.cs index 06b26b80c..ebdd7cb51 100644 --- a/Src/Fido2.AspNet/Fido2NetLibBuilderExtensions.cs +++ b/Src/Fido2.AspNet/Fido2NetLibBuilderExtensions.cs @@ -1,121 +1,122 @@ using System; using System.Net.Http; + using Fido2NetLib; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class Fido2NetLibBuilderExtensions { - public static class Fido2NetLibBuilderExtensions + public static IFido2NetLibBuilder AddFido2(this IServiceCollection services, IConfiguration configuration) { - public static IFido2NetLibBuilder AddFido2(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); + services.Configure(configuration); - services.AddSingleton( - resolver => resolver.GetRequiredService>().Value); + services.AddSingleton( + resolver => resolver.GetRequiredService>().Value); - services.AddServices(); + services.AddServices(); - return new Fido2NetLibBuilder(services); - } + return new Fido2NetLibBuilder(services); + } - private static void AddServices(this IServiceCollection services) - { - services.AddTransient(); - services.AddSingleton(); //Default implementation if we choose not to enable MDS - services.TryAddSingleton(); - } + private static void AddServices(this IServiceCollection services) + { + services.AddTransient(); + services.AddSingleton(); //Default implementation if we choose not to enable MDS + services.TryAddSingleton(); + } - public static IFido2NetLibBuilder AddFido2(this IServiceCollection services, Action setupAction) - { - services.Configure(setupAction); + public static IFido2NetLibBuilder AddFido2(this IServiceCollection services, Action setupAction) + { + services.Configure(setupAction); - services.AddSingleton( - resolver => resolver.GetRequiredService>().Value); + services.AddSingleton( + resolver => resolver.GetRequiredService>().Value); - services.AddServices(); + services.AddServices(); - return new Fido2NetLibBuilder(services); - } + return new Fido2NetLibBuilder(services); + } - public static void AddCachedMetadataService(this IFido2NetLibBuilder builder, Action configAction) - { - builder.AddMetadataService(); + public static void AddCachedMetadataService(this IFido2NetLibBuilder builder, Action configAction) + { + builder.AddMetadataService(); - configAction(new Fido2NetLibBuilder(builder.Services)); - } + configAction(new Fido2NetLibBuilder(builder.Services)); + } - public static IFido2MetadataServiceBuilder AddFileSystemMetadataRepository(this IFido2MetadataServiceBuilder builder, string directoryPath) - { - builder.Services.AddTransient(r => - { - return new FileSystemMetadataRepository(directoryPath); - }); - - return builder; - } - - public static IFido2MetadataServiceBuilder AddConformanceMetadataRepository( - this IFido2MetadataServiceBuilder builder, - HttpClient client = null, - string origin = "") + public static IFido2MetadataServiceBuilder AddFileSystemMetadataRepository(this IFido2MetadataServiceBuilder builder, string directoryPath) + { + builder.Services.AddTransient(r => { - builder.Services.AddTransient(provider => - { - return new ConformanceMetadataRepository(client, origin); - }); + return new FileSystemMetadataRepository(directoryPath); + }); - return builder; - } + return builder; + } - public static IFido2MetadataServiceBuilder AddFidoMetadataRepository(this IFido2MetadataServiceBuilder builder, Action clientBuilder = null) + public static IFido2MetadataServiceBuilder AddConformanceMetadataRepository( + this IFido2MetadataServiceBuilder builder, + HttpClient client = null, + string origin = "") + { + builder.Services.AddTransient(provider => { - var httpClientBuilder = builder.Services.AddHttpClient(nameof(Fido2MetadataServiceRepository)); + return new ConformanceMetadataRepository(client, origin); + }); - if(clientBuilder != null) clientBuilder(httpClientBuilder); + return builder; + } - builder.Services.AddTransient(); + public static IFido2MetadataServiceBuilder AddFidoMetadataRepository(this IFido2MetadataServiceBuilder builder, Action clientBuilder = null) + { + var httpClientBuilder = builder.Services.AddHttpClient(nameof(Fido2MetadataServiceRepository)); - return builder; - } + if(clientBuilder != null) clientBuilder(httpClientBuilder); - private static void AddMetadataService(this IFido2NetLibBuilder builder) where TService : class, IMetadataService - { - builder.Services.AddScoped(); - } - } + builder.Services.AddTransient(); - public interface IFido2NetLibBuilder - { - IServiceCollection Services { get; } + return builder; } - public interface IFido2MetadataServiceBuilder + private static void AddMetadataService(this IFido2NetLibBuilder builder) where TService : class, IMetadataService { - IServiceCollection Services { get; } + builder.Services.AddScoped(); } +} + +public interface IFido2NetLibBuilder +{ + IServiceCollection Services { get; } +} + +public interface IFido2MetadataServiceBuilder +{ + IServiceCollection Services { get; } +} - public class Fido2NetLibBuilder : IFido2NetLibBuilder, IFido2MetadataServiceBuilder +public class Fido2NetLibBuilder : IFido2NetLibBuilder, IFido2MetadataServiceBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// The services. + /// services + public Fido2NetLibBuilder(IServiceCollection services) { - /// - /// Initializes a new instance of the class. - /// - /// The services. - /// services - public Fido2NetLibBuilder(IServiceCollection services) - { - Services = services ?? throw new ArgumentNullException(nameof(services)); - } - - /// - /// Gets the services. - /// - /// - /// The services. - /// - public IServiceCollection Services { get; } + Services = services ?? throw new ArgumentNullException(nameof(services)); } + + /// + /// Gets the services. + /// + /// + /// The services. + /// + public IServiceCollection Services { get; } } diff --git a/Src/Fido2.AspNet/NullMetadataService.cs b/Src/Fido2.AspNet/NullMetadataService.cs index ddef7bc7c..5e2f075aa 100644 --- a/Src/Fido2.AspNet/NullMetadataService.cs +++ b/Src/Fido2.AspNet/NullMetadataService.cs @@ -2,18 +2,17 @@ using System.Threading; using System.Threading.Tasks; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal sealed class NullMetadataService : IMetadataService { - internal class NullMetadataService : IMetadataService + public Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default) { - public Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default) - { - return Task.FromResult((MetadataBLOBPayloadEntry)null); - } + return Task.FromResult((MetadataBLOBPayloadEntry)null); + } - public bool ConformanceTesting() - { - return false; - } + public bool ConformanceTesting() + { + return false; } } diff --git a/Src/Fido2.Models/AssertionOptions.cs b/Src/Fido2.Models/AssertionOptions.cs index 1a802dfc5..cb6e5c5eb 100644 --- a/Src/Fido2.Models/AssertionOptions.cs +++ b/Src/Fido2.Models/AssertionOptions.cs @@ -6,74 +6,73 @@ using Fido2NetLib.Objects; using Fido2NetLib.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Sent to the browser when we want to Assert credentials and authenticate a user +/// +public class AssertionOptions : Fido2ResponseBase { /// - /// Sent to the browser when we want to Assert credentials and authenticate a user + /// 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. /// - 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; } + [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 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 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 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 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; } + /// + /// 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) + public static AssertionOptions Create(Fido2Configuration config, byte[] challenge, IEnumerable allowedCredentials, UserVerificationRequirement? userVerification, AuthenticationExtensionsClientInputs extensions) + { + return new AssertionOptions() { - return new AssertionOptions() - { - Status = "ok", - ErrorMessage = string.Empty, - Challenge = challenge, - Timeout = config.Timeout, - RpId = config.ServerDomain, - AllowCredentials = allowedCredentials ?? Array.Empty(), - UserVerification = userVerification, - Extensions = extensions - }; - } + 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, FidoModelSerializerContext.Default.AssertionOptions); - } + public string ToJson() + { + return JsonSerializer.Serialize(this, FidoModelSerializerContext.Default.AssertionOptions); + } - public static AssertionOptions FromJson(string json) - { - return JsonSerializer.Deserialize(json, FidoModelSerializerContext.Default.AssertionOptions); - } + public static AssertionOptions FromJson(string json) + { + return JsonSerializer.Deserialize(json, FidoModelSerializerContext.Default.AssertionOptions); } } diff --git a/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs b/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs index 1108d5eeb..6fcb0782a 100644 --- a/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs +++ b/Src/Fido2.Models/AuthenticatorAssertionRawResponse.cs @@ -4,48 +4,47 @@ using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Transport class for AssertionResponse +/// +public class AuthenticatorAssertionRawResponse { - /// - /// Transport class for AssertionResponse - /// - public class AuthenticatorAssertionRawResponse - { - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("id")] - public byte[] Id { get; set; } + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("id")] + public byte[] Id { get; set; } - // might be wrong to base64url encode this... - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("rawId")] - public byte[] RawId { get; set; } + // might be wrong to base64url encode this... + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("rawId")] + public byte[] RawId { get; set; } - [JsonPropertyName("response")] - public AssertionResponse Response { get; set; } + [JsonPropertyName("response")] + public AssertionResponse Response { get; set; } - [JsonPropertyName("type")] - public PublicKeyCredentialType? Type { get; set; } + [JsonPropertyName("type")] + public PublicKeyCredentialType? Type { get; set; } - [JsonPropertyName("extensions")] - public AuthenticationExtensionsClientOutputs Extensions { get; set; } + [JsonPropertyName("extensions")] + public AuthenticationExtensionsClientOutputs Extensions { get; set; } - public class AssertionResponse - { - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("authenticatorData")] - public byte[] AuthenticatorData { get; set; } + public class AssertionResponse + { + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("authenticatorData")] + public byte[] AuthenticatorData { get; set; } - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("signature")] - public byte[] Signature { get; set; } + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("signature")] + public byte[] Signature { get; set; } - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("clientDataJSON")] - public byte[] ClientDataJson { get; set; } + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("clientDataJSON")] + public byte[] ClientDataJson { get; set; } - [JsonPropertyName("userHandle")] - [JsonConverter(typeof(Base64UrlConverter))] - public byte[] UserHandle { get; set; } - } + [JsonPropertyName("userHandle")] + [JsonConverter(typeof(Base64UrlConverter))] + public byte[] UserHandle { get; set; } } } diff --git a/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs b/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs index 8a22690b3..25d5e4564 100644 --- a/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs +++ b/Src/Fido2.Models/AuthenticatorAttestationRawResponse.cs @@ -2,36 +2,35 @@ using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +public sealed class AuthenticatorAttestationRawResponse { - public sealed class AuthenticatorAttestationRawResponse - { - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("id")] - public byte[] Id { get; set; } + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("id")] + public byte[] Id { get; set; } - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("rawId")] - public byte[] RawId { get; set; } + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("rawId")] + public byte[] RawId { get; set; } - [JsonPropertyName("type")] - public PublicKeyCredentialType? Type { get; set; } + [JsonPropertyName("type")] + public PublicKeyCredentialType? Type { get; set; } - [JsonPropertyName("response")] - public ResponseData Response { get; set; } + [JsonPropertyName("response")] + public ResponseData Response { get; set; } - [JsonPropertyName("extensions")] - public AuthenticationExtensionsClientOutputs Extensions { get; set; } + [JsonPropertyName("extensions")] + public AuthenticationExtensionsClientOutputs Extensions { get; set; } - public sealed class ResponseData - { - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("attestationObject")] - public byte[] AttestationObject { get; set; } + public sealed class ResponseData + { + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("attestationObject")] + public byte[] AttestationObject { get; set; } - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("clientDataJSON")] - public byte[] ClientDataJson { get; set; } - } + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("clientDataJSON")] + public byte[] ClientDataJson { get; set; } } } diff --git a/Src/Fido2.Models/Base64Url.cs b/Src/Fido2.Models/Base64Url.cs index 1e0b45095..c9dd9222f 100644 --- a/Src/Fido2.Models/Base64Url.cs +++ b/Src/Fido2.Models/Base64Url.cs @@ -1,149 +1,147 @@ -using System; +using System; using System.Buffers; using System.Buffers.Text; -using System.Text.Unicode; - -namespace Fido2NetLib -{ - /// - /// Helper class to handle Base64Url. Based on Carbon.Jose source code. - /// - public static class Base64Url - { - /// - /// Converts arg data to a Base64Url encoded string. - /// - public static string Encode(ReadOnlySpan arg) - { - int base64Length = (int)(((long)arg.Length + 2) / 3 * 4); - char[] pooledBuffer = ArrayPool.Shared.Rent(base64Length); +namespace Fido2NetLib; - Convert.TryToBase64Chars(arg, pooledBuffer, out int encodedLength); +/// +/// Helper class to handle Base64Url. Based on Carbon.Jose source code. +/// +public static class Base64Url +{ + /// + /// Converts arg data to a Base64Url encoded string. + /// + public static string Encode(ReadOnlySpan arg) + { + int base64Length = (int)(((long)arg.Length + 2) / 3 * 4); - Span base64Url = pooledBuffer.AsSpan(0, encodedLength); + char[] pooledBuffer = ArrayPool.Shared.Rent(base64Length); - for (int i = 0; i < base64Url.Length; i++) - { - ref char c = ref base64Url[i]; + Convert.TryToBase64Chars(arg, pooledBuffer, out int encodedLength); - switch (c) - { - case '+': c = '-'; break; - case '/': c = '_'; break; - } - } + Span base64Url = pooledBuffer.AsSpan(0, encodedLength); - int equalIndex = base64Url.IndexOf('='); + for (int i = 0; i < base64Url.Length; i++) + { + ref char c = ref base64Url[i]; - if (equalIndex > -1) // remove trailing equal characters + switch (c) { - base64Url = base64Url.Slice(0, equalIndex); + case '+': c = '-'; break; + case '/': c = '_'; break; } + } + + int equalIndex = base64Url.IndexOf('='); - var result = new string(base64Url); - - ArrayPool.Shared.Return(pooledBuffer, clearArray: true); - - return result; - } - - /// - /// Decodes a Base64Url encoded string to its raw bytes. - /// - public static byte[] Decode(ReadOnlySpan text) + if (equalIndex > -1) // remove trailing equal characters { - int padCharCount = (text.Length % 4) switch - { - 2 => 2, - 3 => 1, - _ => 0 - }; + base64Url = base64Url.Slice(0, equalIndex); + } - int encodedLength = text.Length + padCharCount; + var result = new string(base64Url); - char[] buffer = ArrayPool.Shared.Rent(encodedLength); + ArrayPool.Shared.Return(pooledBuffer, clearArray: true); - text.CopyTo(buffer); + return result; + } - for (int i = 0; i < text.Length; i++) - { - ref char c = ref buffer[i]; + /// + /// Decodes a Base64Url encoded string to its raw bytes. + /// + public static byte[] Decode(ReadOnlySpan text) + { + int padCharCount = (text.Length % 4) switch + { + 2 => 2, + 3 => 1, + _ => 0 + }; - switch (c) - { - case '-': c = '+'; break; - case '_': c = '/'; break; - } - } + int encodedLength = text.Length + padCharCount; - if (padCharCount == 1) - { - buffer[encodedLength - 1] = '='; - } - else if (padCharCount == 2) + char[] buffer = ArrayPool.Shared.Rent(encodedLength); + + text.CopyTo(buffer); + + for (int i = 0; i < text.Length; i++) + { + ref char c = ref buffer[i]; + + switch (c) { - buffer[encodedLength - 1] = '='; - buffer[encodedLength - 2] = '='; + case '-': c = '+'; break; + case '_': c = '/'; break; } + } - var result = Convert.FromBase64CharArray(buffer, 0, encodedLength); + if (padCharCount == 1) + { + buffer[encodedLength - 1] = '='; + } + else if (padCharCount == 2) + { + buffer[encodedLength - 1] = '='; + buffer[encodedLength - 2] = '='; + } - ArrayPool.Shared.Return(buffer, true); + var result = Convert.FromBase64CharArray(buffer, 0, encodedLength); - return result; - } - - - /// - /// Decodes a Base64Url encoded string to its raw bytes. - /// - public static byte[] DecodeUtf8(ReadOnlySpan text) - { - int padCharCount = (text.Length % 4) switch - { - 2 => 2, - 3 => 1, - _ => 0 - }; + ArrayPool.Shared.Return(buffer, true); - int encodedLength = text.Length + padCharCount; + return result; + } - byte[] buffer = ArrayPool.Shared.Rent(encodedLength); - text.CopyTo(buffer); + /// + /// Decodes a Base64Url encoded string to its raw bytes. + /// + public static byte[] DecodeUtf8(ReadOnlySpan text) + { + int padCharCount = (text.Length % 4) switch + { + 2 => 2, + 3 => 1, + _ => 0 + }; - for (int i = 0; i < text.Length; i++) - { - ref byte c = ref buffer[i]; + int encodedLength = text.Length + padCharCount; - switch ((char)c) - { - case '-': c = (byte)'+'; break; - case '_': c = (byte)'/'; break; - } - } + byte[] buffer = ArrayPool.Shared.Rent(encodedLength); - if (padCharCount == 1) - { - buffer[encodedLength - 1] = (byte)'='; - } - else if (padCharCount == 2) - { - buffer[encodedLength - 1] = (byte)'='; - buffer[encodedLength - 2] = (byte)'='; - } + text.CopyTo(buffer); + + for (int i = 0; i < text.Length; i++) + { + ref byte c = ref buffer[i]; - if (OperationStatus.Done != Base64.DecodeFromUtf8InPlace(buffer.AsSpan(0, encodedLength), out int decodedLength)) + switch ((char)c) { - throw new FormatException("The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters."); + case '-': c = (byte)'+'; break; + case '_': c = (byte)'/'; break; } + } + + if (padCharCount == 1) + { + buffer[encodedLength - 1] = (byte)'='; + } + else if (padCharCount == 2) + { + buffer[encodedLength - 1] = (byte)'='; + buffer[encodedLength - 2] = (byte)'='; + } + + if (OperationStatus.Done != Base64.DecodeFromUtf8InPlace(buffer.AsSpan(0, encodedLength), out int decodedLength)) + { + throw new FormatException("The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters."); + } - var result = buffer.AsSpan(0, decodedLength).ToArray(); + var result = buffer.AsSpan(0, decodedLength).ToArray(); - ArrayPool.Shared.Return(buffer, true); + ArrayPool.Shared.Return(buffer, true); - return result; - } - } -} + return result; + } +} diff --git a/Src/Fido2.Models/COSETypes.cs b/Src/Fido2.Models/COSETypes.cs index 36c193ef4..a7d6e670a 100644 --- a/Src/Fido2.Models/COSETypes.cs +++ b/Src/Fido2.Models/COSETypes.cs @@ -1,193 +1,191 @@ - -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// CBOR Object Signing and Encryption RFC8152 https://tools.ietf.org/html/rfc8152 +/// +public static class COSE { /// - /// CBOR Object Signing and Encryption RFC8152 https://tools.ietf.org/html/rfc8152 + /// COSE Algorithms https://www.iana.org/assignments/cose/cose.xhtml#algorithms /// - public static class COSE + public enum Algorithm { + /// + /// RSASSA-PKCS1-v1_5 w/ SHA-1 + /// + RS1 = -65535, + /// + /// RSASSA-PKCS1-v1_5 w/ SHA-512 + /// + RS512 = -259, + /// + /// RSASSA-PKCS1-v1_5 w/ SHA-384 + /// + RS384 = -258, + /// + /// RSASSA-PKCS1-v1_5 w/ SHA-256 + /// + RS256 = -257, + /// + /// RSASSA-PSS w/ SHA-512 + /// + PS512 = -39, + /// + /// RSASSA-PSS w/ SHA-384 + /// + PS384 = -38, + /// + /// RSASSA-PSS w/ SHA-256 + /// + PS256 = -37, + /// + /// ECDSA w/ SHA-512 + /// + ES512 = -36, + /// + /// ECDSA w/ SHA-384 + /// + ES384 = -35, + /// + /// EdDSA + /// + EdDSA = -8, + /// + /// ECDSA w/ SHA-256 + /// + ES256 = -7, /// - /// COSE Algorithms https://www.iana.org/assignments/cose/cose.xhtml#algorithms - /// - public enum Algorithm - { - /// - /// RSASSA-PKCS1-v1_5 w/ SHA-1 - /// - RS1 = -65535, - /// - /// RSASSA-PKCS1-v1_5 w/ SHA-512 - /// - RS512 = -259, - /// - /// RSASSA-PKCS1-v1_5 w/ SHA-384 - /// - RS384 = -258, - /// - /// RSASSA-PKCS1-v1_5 w/ SHA-256 - /// - RS256 = -257, - /// - /// RSASSA-PSS w/ SHA-512 - /// - PS512 = -39, - /// - /// RSASSA-PSS w/ SHA-384 - /// - PS384 = -38, - /// - /// RSASSA-PSS w/ SHA-256 - /// - PS256 = -37, - /// - /// ECDSA w/ SHA-512 - /// - ES512 = -36, - /// - /// ECDSA w/ SHA-384 - /// - ES384 = -35, - /// - /// EdDSA - /// - EdDSA = -8, - /// - /// ECDSA w/ SHA-256 - /// - ES256 = -7, - /// - /// ECDSA using secp256k1 curve and SHA-256 - /// - ES256K = -47, - } - /// - /// COSE Key Common Parameters https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters - /// - public enum KeyCommonParameter - { - /// - /// This value is reserved - /// - Reserved = 0, - /// - /// Identification of the key type - /// - KeyType = 1, - /// - /// Key identification value - match to kid in message - /// - KeyId = 2, - /// - /// Key usage restriction to this algorithm - /// - Alg = 3, - /// - /// Restrict set of permissible operations - /// - KeyOps = 4, - /// - /// Base IV to be XORed with Partial IVs - /// - BaseIV = 5 - } - /// - /// COSE Key Type Parameters https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters - /// - public enum KeyTypeParameter - { - /// - /// EC identifier - /// - Crv = -1, - /// - /// Key Value - /// - K = -1, - /// - /// x-coordinate - /// - X = -2, - /// - /// y-coordinate - /// - Y = -3, - /// - /// the RSA modulus n - /// - N = -1, - /// - /// the RSA public exponent e - /// - E = -2 - } - /// - /// COSE Key Types https://www.iana.org/assignments/cose/cose.xhtml#key-type - /// - public enum KeyType - { - /// - /// This value is reserved - /// - Reserved = 0, - /// - /// Octet Key Pair - /// - OKP = 1, - /// - /// Elliptic Curve Keys w/ x- and y-coordinate pair - /// - EC2 = 2, - /// - /// RSA Key - /// - RSA = 3, - /// - /// Symmetric Keys - /// - Symmetric = 4 - } + /// ECDSA using secp256k1 curve and SHA-256 + /// + ES256K = -47, + } + /// + /// COSE Key Common Parameters https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters + /// + public enum KeyCommonParameter + { + /// + /// This value is reserved + /// + Reserved = 0, + /// + /// Identification of the key type + /// + KeyType = 1, + /// + /// Key identification value - match to kid in message + /// + KeyId = 2, + /// + /// Key usage restriction to this algorithm + /// + Alg = 3, + /// + /// Restrict set of permissible operations + /// + KeyOps = 4, + /// + /// Base IV to be XORed with Partial IVs + /// + BaseIV = 5 + } + /// + /// COSE Key Type Parameters https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + /// + public enum KeyTypeParameter + { + /// + /// EC identifier + /// + Crv = -1, + /// + /// Key Value + /// + K = -1, + /// + /// x-coordinate + /// + X = -2, + /// + /// y-coordinate + /// + Y = -3, + /// + /// the RSA modulus n + /// + N = -1, + /// + /// the RSA public exponent e + /// + E = -2 + } + /// + /// COSE Key Types https://www.iana.org/assignments/cose/cose.xhtml#key-type + /// + public enum KeyType + { + /// + /// This value is reserved + /// + Reserved = 0, + /// + /// Octet Key Pair + /// + OKP = 1, + /// + /// Elliptic Curve Keys w/ x- and y-coordinate pair + /// + EC2 = 2, + /// + /// RSA Key + /// + RSA = 3, + /// + /// Symmetric Keys + /// + Symmetric = 4 + } - /// - /// COSE Elliptic Curves https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves - /// - public enum EllipticCurve - { - /// - /// This value is reserved - /// - Reserved = 0, - /// - /// NIST P-256 also known as secp256r1 - /// - P256 = 1, - /// - /// NIST P-384 also known as secp384r1 - /// - P384 = 2, - /// - /// NIST P-521 also known as secp521r1 - /// - P521 = 3, - /// - /// X25519 for use w/ ECDH only - /// - X25519 = 4, - /// - /// X448 for use w/ ECDH only - /// - X448 = 5, - /// - /// Ed25519 for use w/ EdDSA only - /// - Ed25519 = 6, - /// - /// Ed448 for use w/ EdDSA only - /// - Ed448 = 7, - /// - /// secp256k1 (pending IANA - requested assignment 8) - /// - P256K = 8 - } + /// + /// COSE Elliptic Curves https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves + /// + public enum EllipticCurve + { + /// + /// This value is reserved + /// + Reserved = 0, + /// + /// NIST P-256 also known as secp256r1 + /// + P256 = 1, + /// + /// NIST P-384 also known as secp384r1 + /// + P384 = 2, + /// + /// NIST P-521 also known as secp521r1 + /// + P521 = 3, + /// + /// X25519 for use w/ ECDH only + /// + X25519 = 4, + /// + /// X448 for use w/ ECDH only + /// + X448 = 5, + /// + /// Ed25519 for use w/ EdDSA only + /// + Ed25519 = 6, + /// + /// Ed448 for use w/ EdDSA only + /// + Ed448 = 7, + /// + /// secp256k1 (pending IANA - requested assignment 8) + /// + P256K = 8 } } diff --git a/Src/Fido2.Models/Converters/Base64Converter.cs b/Src/Fido2.Models/Converters/Base64Converter.cs index fe7e63664..35a0d5daf 100644 --- a/Src/Fido2.Models/Converters/Base64Converter.cs +++ b/Src/Fido2.Models/Converters/Base64Converter.cs @@ -2,28 +2,27 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Custom Converter for encoding/encoding byte[] using Base64Url instead of default Base64. +/// +public sealed class Base64UrlConverter : JsonConverter { - /// - /// Custom Converter for encoding/encoding byte[] using Base64Url instead of default Base64. - /// - public sealed class Base64UrlConverter : JsonConverter + public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (!reader.HasValueSequence) { - if (!reader.HasValueSequence) - { - return Base64Url.DecodeUtf8(reader.ValueSpan); - } - else - { - return Base64Url.Decode(reader.GetString()); - } + return Base64Url.DecodeUtf8(reader.ValueSpan); } - - public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) + else { - writer.WriteStringValue(Base64Url.Encode(value)); + return Base64Url.Decode(reader.GetString()); } } + + public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) + { + writer.WriteStringValue(Base64Url.Encode(value)); + } } diff --git a/Src/Fido2.Models/Converters/EnumNameMapper.cs b/Src/Fido2.Models/Converters/EnumNameMapper.cs index e86d209ed..56646e6d1 100644 --- a/Src/Fido2.Models/Converters/EnumNameMapper.cs +++ b/Src/Fido2.Models/Converters/EnumNameMapper.cs @@ -4,78 +4,77 @@ using System.Reflection; using System.Runtime.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +public static class EnumNameMapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> + where TEnum: struct, Enum { - public static class EnumNameMapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum> - where TEnum: struct, Enum + private static readonly Dictionary valueToNames = GetIdToNameMap(); + private static readonly Dictionary namesToValues = Invert(valueToNames); + + private static Dictionary Invert(Dictionary map) { - private static readonly Dictionary valueToNames = GetIdToNameMap(); - private static readonly Dictionary namesToValues = Invert(valueToNames); + var result = new Dictionary(map.Count, StringComparer.OrdinalIgnoreCase); - private static Dictionary Invert(Dictionary map) + foreach (var item in map) { - var result = new Dictionary(map.Count, StringComparer.OrdinalIgnoreCase); - - foreach (var item in map) - { - result[item.Value] = item.Key; - } - - return result; + result[item.Value] = item.Key; } - public static bool TryGetValue(string name, bool ignoreCase, out TEnum value) + return result; + } + + public static bool TryGetValue(string name, bool ignoreCase, out TEnum value) + { + if (namesToValues.TryGetValue(name, out value)) { - if (namesToValues.TryGetValue(name, out value)) - { - if (!ignoreCase && !valueToNames[value].Equals(name, StringComparison.Ordinal)) - { - value = default; - - return false; - } - else - { - return true; - } - } - else + if (!ignoreCase && !valueToNames[value].Equals(name, StringComparison.Ordinal)) { value = default; return false; } + else + { + return true; + } } - - public static bool TryGetValue(string name, out TEnum value) + else { - return namesToValues.TryGetValue(name, out value); - } + value = default; - public static string GetName(TEnum value) - { - return valueToNames[value]; + return false; } + } - public static IEnumerable GetNames() - { - return namesToValues.Keys; - } + public static bool TryGetValue(string name, out TEnum value) + { + return namesToValues.TryGetValue(name, out value); + } - private static Dictionary GetIdToNameMap() - { - var dic = new Dictionary(); + public static string GetName(TEnum value) + { + return valueToNames[value]; + } - foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)) - { - var description = field.GetCustomAttribute(false); + public static IEnumerable GetNames() + { + return namesToValues.Keys; + } + + private static Dictionary GetIdToNameMap() + { + var dic = new Dictionary(); - var value = (TEnum)field.GetValue(null); + foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var description = field.GetCustomAttribute(false); - dic[value] = description is not null ? description.Value : value.ToString(); - } + var value = (TEnum)field.GetValue(null); - return dic; + dic[value] = description is not null ? description.Value : value.ToString(); } + + return dic; } } diff --git a/Src/Fido2.Models/Converters/FidoEnumConverter.cs b/Src/Fido2.Models/Converters/FidoEnumConverter.cs index 0476a0e20..572820387 100644 --- a/Src/Fido2.Models/Converters/FidoEnumConverter.cs +++ b/Src/Fido2.Models/Converters/FidoEnumConverter.cs @@ -3,29 +3,27 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Fido2NetLib -{ +namespace Fido2NetLib; - public sealed class FidoEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter - where T: struct, Enum +public sealed class FidoEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter + where T: struct, Enum +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - string text = reader.GetString(); + string text = reader.GetString(); - if (EnumNameMapper.TryGetValue(reader.GetString(), out T value)) - { - return value; - } - else - { - throw new JsonException($"Invalid enum value = {text}"); - } + if (EnumNameMapper.TryGetValue(reader.GetString(), out T value)) + { + return value; } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + else { - writer.WriteStringValue(EnumNameMapper.GetName(value)); + throw new JsonException($"Invalid enum value = {text}"); } } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(EnumNameMapper.GetName(value)); + } } diff --git a/Src/Fido2.Models/CredentialCreateOptions.cs b/Src/Fido2.Models/CredentialCreateOptions.cs index 695f53569..cfadde1c4 100644 --- a/Src/Fido2.Models/CredentialCreateOptions.cs +++ b/Src/Fido2.Models/CredentialCreateOptions.cs @@ -1,266 +1,265 @@ using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Fido2NetLib.Objects; using Fido2NetLib.Serialization; -namespace Fido2NetLib -{ - public sealed class CredentialCreateOptions : Fido2ResponseBase - { - /// - /// - /// This member contains data about the Relying Party responsible for the request. - /// 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")] - 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")] - public Fido2User User { get; set; } - - /// - /// Must be generated by the Server (Relying Party) - /// - [JsonPropertyName("challenge")] - [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; } - - /// - /// 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; } - - /// - /// This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance.The default is none. - /// - [JsonPropertyName("attestation")] - public AttestationConveyancePreference Attestation { get; set; } = AttestationConveyancePreference.None; - - /// - /// 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; } - - /// - /// 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("excludeCredentials")] - public List ExcludeCredentials { 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 CredentialCreateOptions Create(Fido2Configuration config, byte[] challenge, Fido2User user, AuthenticatorSelection authenticatorSelection, AttestationConveyancePreference attestationConveyancePreference, List excludeCredentials, AuthenticationExtensionsClientInputs extensions) - { - return new CredentialCreateOptions - { - Status = "ok", - ErrorMessage = string.Empty, - Challenge = challenge, - Rp = new PublicKeyCredentialRpEntity(config.ServerDomain, config.ServerName, config.ServerIcon), - Timeout = config.Timeout, - User = user, - PubKeyCredParams = new List(10) - { - // Add additional as appropriate - PubKeyCredParam.ES256, - PubKeyCredParam.RS256, - PubKeyCredParam.PS256, - PubKeyCredParam.ES384, - PubKeyCredParam.RS384, - PubKeyCredParam.PS384, - PubKeyCredParam.ES512, - PubKeyCredParam.RS512, - PubKeyCredParam.PS512, - PubKeyCredParam.Ed25519, - }, - AuthenticatorSelection = authenticatorSelection, - Attestation = attestationConveyancePreference, - ExcludeCredentials = excludeCredentials ?? new List(), - Extensions = extensions - }; - } - - public string ToJson() - { - return JsonSerializer.Serialize(this, FidoModelSerializerContext.Default.CredentialCreateOptions); - } - - public static CredentialCreateOptions FromJson(string json) - { - return JsonSerializer.Deserialize(json, FidoModelSerializerContext.Default.CredentialCreateOptions); - } - } - - public sealed class PubKeyCredParam - { - /// - /// Constructs a PubKeyCredParam instance - /// - [JsonConstructor] - public PubKeyCredParam(COSE.Algorithm alg, PublicKeyCredentialType type = PublicKeyCredentialType.PublicKey) +namespace Fido2NetLib; + +public sealed class CredentialCreateOptions : Fido2ResponseBase +{ + /// + /// + /// This member contains data about the Relying Party responsible for the request. + /// 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")] + 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")] + public Fido2User User { get; set; } + + /// + /// Must be generated by the Server (Relying Party) + /// + [JsonPropertyName("challenge")] + [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; } + + /// + /// 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; } + + /// + /// This member is intended for use by Relying Parties that wish to express their preference for attestation conveyance.The default is none. + /// + [JsonPropertyName("attestation")] + public AttestationConveyancePreference Attestation { get; set; } = AttestationConveyancePreference.None; + + /// + /// 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; } + + /// + /// 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("excludeCredentials")] + public List ExcludeCredentials { 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 CredentialCreateOptions Create(Fido2Configuration config, byte[] challenge, Fido2User user, AuthenticatorSelection authenticatorSelection, AttestationConveyancePreference attestationConveyancePreference, List excludeCredentials, AuthenticationExtensionsClientInputs extensions) + { + return new CredentialCreateOptions { - Type = type; - Alg = alg; - } - - /// - /// The type member specifies the type of credential to be created. - /// - [JsonPropertyName("type")] - public PublicKeyCredentialType Type { get; } - - /// - /// The alg member specifies the cryptographic signature algorithm with which the newly generated credential will be used, and thus also the type of asymmetric key pair to be generated, e.g., RSA or Elliptic Curve. - /// - [JsonPropertyName("alg")] - public COSE.Algorithm Alg { get; } + Status = "ok", + ErrorMessage = string.Empty, + Challenge = challenge, + Rp = new PublicKeyCredentialRpEntity(config.ServerDomain, config.ServerName, config.ServerIcon), + Timeout = config.Timeout, + User = user, + PubKeyCredParams = new List(10) + { + // Add additional as appropriate + PubKeyCredParam.ES256, + PubKeyCredParam.RS256, + PubKeyCredParam.PS256, + PubKeyCredParam.ES384, + PubKeyCredParam.RS384, + PubKeyCredParam.PS384, + PubKeyCredParam.ES512, + PubKeyCredParam.RS512, + PubKeyCredParam.PS512, + PubKeyCredParam.Ed25519, + }, + AuthenticatorSelection = authenticatorSelection, + Attestation = attestationConveyancePreference, + ExcludeCredentials = excludeCredentials ?? new List(), + Extensions = extensions + }; + } + + public string ToJson() + { + return JsonSerializer.Serialize(this, FidoModelSerializerContext.Default.CredentialCreateOptions); + } - public static readonly PubKeyCredParam ES256 = new(COSE.Algorithm.ES256); // External authenticators support the ES256 algorithm - public static readonly PubKeyCredParam ES384 = new(COSE.Algorithm.ES384); - public static readonly PubKeyCredParam ES512 = new(COSE.Algorithm.ES512); - public static readonly PubKeyCredParam RS256 = new(COSE.Algorithm.RS256); // Supported by windows hello - public static readonly PubKeyCredParam RS384 = new(COSE.Algorithm.RS384); - public static readonly PubKeyCredParam RS512 = new(COSE.Algorithm.RS512); - public static readonly PubKeyCredParam PS256 = new(COSE.Algorithm.PS256); - public static readonly PubKeyCredParam PS384 = new(COSE.Algorithm.PS384); - public static readonly PubKeyCredParam PS512 = new(COSE.Algorithm.PS512); - public static readonly PubKeyCredParam Ed25519 = new(COSE.Algorithm.EdDSA); + public static CredentialCreateOptions FromJson(string json) + { + return JsonSerializer.Deserialize(json, FidoModelSerializerContext.Default.CredentialCreateOptions); + } +} + +public sealed class PubKeyCredParam +{ + /// + /// Constructs a PubKeyCredParam instance + /// + [JsonConstructor] + public PubKeyCredParam(COSE.Algorithm alg, PublicKeyCredentialType type = PublicKeyCredentialType.PublicKey) + { + Type = type; + Alg = alg; } -#nullable enable /// - /// PublicKeyCredentialRpEntity - /// - public sealed class PublicKeyCredentialRpEntity - { - public PublicKeyCredentialRpEntity(string id, string name, string? icon = null) - { - Name = name; - Id = id; - Icon = icon; - } + /// The type member specifies the type of credential to be created. + /// + [JsonPropertyName("type")] + public PublicKeyCredentialType Type { get; } - /// - /// A unique identifier for the Relying Party entity, which sets the RP ID. - /// - [JsonPropertyName("id")] - 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; } + /// + /// The alg member specifies the cryptographic signature algorithm with which the newly generated credential will be used, and thus also the type of asymmetric key pair to be generated, e.g., RSA or Elliptic Curve. + /// + [JsonPropertyName("alg")] + public COSE.Algorithm Alg { get; } + + public static readonly PubKeyCredParam ES256 = new(COSE.Algorithm.ES256); // External authenticators support the ES256 algorithm + public static readonly PubKeyCredParam ES384 = new(COSE.Algorithm.ES384); + public static readonly PubKeyCredParam ES512 = new(COSE.Algorithm.ES512); + public static readonly PubKeyCredParam RS256 = new(COSE.Algorithm.RS256); // Supported by windows hello + public static readonly PubKeyCredParam RS384 = new(COSE.Algorithm.RS384); + public static readonly PubKeyCredParam RS512 = new(COSE.Algorithm.RS512); + public static readonly PubKeyCredParam PS256 = new(COSE.Algorithm.PS256); + public static readonly PubKeyCredParam PS384 = new(COSE.Algorithm.PS384); + public static readonly PubKeyCredParam PS512 = new(COSE.Algorithm.PS512); + public static readonly PubKeyCredParam Ed25519 = new(COSE.Algorithm.EdDSA); +} - [JsonPropertyName("icon")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Icon { get; set; } +#nullable enable +/// +/// PublicKeyCredentialRpEntity +/// +public sealed class PublicKeyCredentialRpEntity +{ + public PublicKeyCredentialRpEntity(string id, string name, string? icon = null) + { + Name = name; + Id = id; + Icon = icon; } -#nullable disable - /// - /// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements regarding authenticator attributes. - /// https://w3c.github.io/webauthn/#dictionary-authenticatorSelection - /// - public class AuthenticatorSelection - { - /// - /// 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)] - public AuthenticatorAttachment? AuthenticatorAttachment { get; set; } - - private ResidentKeyRequirement _residentKey; - /// - /// 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")] - public ResidentKeyRequirement ResidentKey + /// + /// A unique identifier for the Relying Party entity, which sets the RP ID. + /// + [JsonPropertyName("id")] + 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; } +} +#nullable disable + +/// +/// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements regarding authenticator attributes. +/// https://w3c.github.io/webauthn/#dictionary-authenticatorSelection +/// +public class AuthenticatorSelection +{ + /// + /// 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)] + public AuthenticatorAttachment? AuthenticatorAttachment { get; set; } + + private ResidentKeyRequirement _residentKey; + /// + /// 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")] + public ResidentKeyRequirement ResidentKey + { + get => _residentKey; + set { - get => _residentKey; - set + _residentKey = value; + _requireResidentKey = value switch { - _residentKey = value; - _requireResidentKey = value switch - { - ResidentKeyRequirement.Required => true, - ResidentKeyRequirement.Preferred or ResidentKeyRequirement.Discouraged => false, - _ => throw new NotImplementedException(), - }; - } - } - - private bool _requireResidentKey; - /// - /// 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. - /// - [Obsolete("Use property ResidentKey.")] - [JsonPropertyName("requireResidentKey")] - public bool RequireResidentKey - { - get => _requireResidentKey; - set - { - _requireResidentKey = value; - _residentKey = value ? ResidentKeyRequirement.Required : ResidentKeyRequirement.Discouraged; - } - } - - /// - /// 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")] - public UserVerificationRequirement UserVerification { get; set; } - - public static AuthenticatorSelection Default => new AuthenticatorSelection - { - AuthenticatorAttachment = null, - ResidentKey = ResidentKeyRequirement.Discouraged, - UserVerification = UserVerificationRequirement.Preferred - }; - } - - public class Fido2User - { - /// - /// 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 - /// - [JsonPropertyName("name")] - public string Name { get; set; } - - /// - /// 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")] - [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; } - } -} + ResidentKeyRequirement.Required => true, + ResidentKeyRequirement.Preferred or ResidentKeyRequirement.Discouraged => false, + _ => throw new NotImplementedException(), + }; + } + } + + private bool _requireResidentKey; + /// + /// 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. + /// + [Obsolete("Use property ResidentKey.")] + [JsonPropertyName("requireResidentKey")] + public bool RequireResidentKey + { + get => _requireResidentKey; + set + { + _requireResidentKey = value; + _residentKey = value ? ResidentKeyRequirement.Required : ResidentKeyRequirement.Discouraged; + } + } + + /// + /// 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")] + public UserVerificationRequirement UserVerification { get; set; } + + public static AuthenticatorSelection Default => new AuthenticatorSelection + { + AuthenticatorAttachment = null, + ResidentKey = ResidentKeyRequirement.Discouraged, + UserVerification = UserVerificationRequirement.Preferred + }; +} + +public class Fido2User +{ + /// + /// 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 + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// 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")] + [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; } +} diff --git a/Src/Fido2.Models/Fido2Configuration.cs b/Src/Fido2.Models/Fido2Configuration.cs index 86a4a80d4..c3cb8012d 100644 --- a/Src/Fido2.Models/Fido2Configuration.cs +++ b/Src/Fido2.Models/Fido2Configuration.cs @@ -2,116 +2,115 @@ using System.Collections.Generic; using System.Linq; -namespace Fido2NetLib +namespace Fido2NetLib; + +public class Fido2Configuration { - public class Fido2Configuration - { - private HashSet _origins; - private HashSet _fullyQualifiedOrigins; + private HashSet _origins; + private HashSet _fullyQualifiedOrigins; - /// - /// Create the configuration for Fido2. - /// - public Fido2Configuration() - { - } + /// + /// Create the configuration for Fido2. + /// + public Fido2Configuration() + { + } - /// - /// 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. - /// - public uint Timeout { get; set; } = 60000; - - /// - /// TimestampDriftTolerance specifies a time in milliseconds that will be allowed for clock drift on a timestamped attestation. - /// - public int TimestampDriftTolerance { get; set; } = 0; //Pretty sure 0 will never work - need a better default? - - /// - /// The size of the challenges sent to the client - /// - public int ChallengeSize { get; set; } = 16; - - /// - /// The effetive domain of the RP. Should be unique and will be used as the identity for the RP. - /// - public string ServerDomain { get; set; } - - /// - /// A human-friendly name of the RP. - /// - public string ServerName { get; set; } - - /// - /// A serialized URL which resolves to an image associated with the entity. For example, this could be a user’s avatar or a Relying Party's logo. This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of needing more storage. - /// - public string ServerIcon { get; set; } - - /// - /// Server origin, including protocol host and port. - /// - [Obsolete("This property is obsolete. Use Origins instead.")] - public string Origin { get; set; } - - /// - /// Server origins, including protocol host and port. - /// - public HashSet Origins + /// + /// 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. + /// + public uint Timeout { get; set; } = 60000; + + /// + /// TimestampDriftTolerance specifies a time in milliseconds that will be allowed for clock drift on a timestamped attestation. + /// + public int TimestampDriftTolerance { get; set; } = 0; //Pretty sure 0 will never work - need a better default? + + /// + /// The size of the challenges sent to the client + /// + public int ChallengeSize { get; set; } = 16; + + /// + /// The effetive domain of the RP. Should be unique and will be used as the identity for the RP. + /// + public string ServerDomain { get; set; } + + /// + /// A human-friendly name of the RP. + /// + public string ServerName { get; set; } + + /// + /// A serialized URL which resolves to an image associated with the entity. For example, this could be a user’s avatar or a Relying Party's logo. This URL MUST be an a priori authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for an icon member’s value. Authenticators MAY ignore an icon member’s value if its length is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL, at the cost of needing more storage. + /// + public string ServerIcon { get; set; } + + /// + /// Server origin, including protocol host and port. + /// + [Obsolete("This property is obsolete. Use Origins instead.")] + public string Origin { get; set; } + + /// + /// Server origins, including protocol host and port. + /// + public HashSet Origins + { + get { - get + if (_origins == null) { - if (_origins == null) - { - _origins = new HashSet(); + _origins = new HashSet(); - // Since we're depricating Origin we ease the transition to move the value automatically, unless its null + // Since we're depricating Origin we ease the transition to move the value automatically, unless its null #pragma warning disable CS0618 // Type or member is obsolete - if (Origin != null) - { - _origins.Add(Origin); - } -#pragma warning restore CS0618 // Type or member is obsolete + if (Origin != null) + { + _origins.Add(Origin); } - - return _origins; +#pragma warning restore CS0618 // Type or member is obsolete } - set - { - _origins = value; - _fullyQualifiedOrigins = new HashSet(value.Select(o => o.ToFullyQualifiedOrigin()), StringComparer.OrdinalIgnoreCase); - } + return _origins; } - /// - /// Fully Qualified Server origins, generated automatically from Origins. - /// - public HashSet FullyQualifiedOrigins + set { - get => _fullyQualifiedOrigins ?? new HashSet - { -#pragma warning disable CS0618 - Origin?.ToFullyQualifiedOrigin() -#pragma warning restore CS0618 - }; - private set => _fullyQualifiedOrigins = value; + _origins = value; + _fullyQualifiedOrigins = new HashSet(value.Select(o => o.ToFullyQualifiedOrigin()), StringComparer.OrdinalIgnoreCase); } + } - /// - /// Metadata service cache directory path. - /// - public string MDSCacheDirPath { get; set; } - - /// - /// List of metadata statuses for an authenticator that should cause attestations to be rejected. - /// - public AuthenticatorStatus[] UndesiredAuthenticatorMetadataStatuses { get; set; } = new AuthenticatorStatus[] + /// + /// Fully Qualified Server origins, generated automatically from Origins. + /// + public HashSet FullyQualifiedOrigins + { + get => _fullyQualifiedOrigins ?? new HashSet { - AuthenticatorStatus.ATTESTATION_KEY_COMPROMISE, - AuthenticatorStatus.USER_VERIFICATION_BYPASS, - AuthenticatorStatus.USER_KEY_REMOTE_COMPROMISE, - AuthenticatorStatus.USER_KEY_PHYSICAL_COMPROMISE, - AuthenticatorStatus.REVOKED +#pragma warning disable CS0618 + Origin?.ToFullyQualifiedOrigin() +#pragma warning restore CS0618 }; + private set => _fullyQualifiedOrigins = value; } + + /// + /// Metadata service cache directory path. + /// + public string MDSCacheDirPath { get; set; } + + /// + /// List of metadata statuses for an authenticator that should cause attestations to be rejected. + /// + public AuthenticatorStatus[] UndesiredAuthenticatorMetadataStatuses { get; set; } = new AuthenticatorStatus[] + { + AuthenticatorStatus.ATTESTATION_KEY_COMPROMISE, + AuthenticatorStatus.USER_VERIFICATION_BYPASS, + AuthenticatorStatus.USER_KEY_REMOTE_COMPROMISE, + AuthenticatorStatus.USER_KEY_PHYSICAL_COMPROMISE, + AuthenticatorStatus.REVOKED + }; } diff --git a/Src/Fido2.Models/Fido2ResponseBase.cs b/Src/Fido2.Models/Fido2ResponseBase.cs index b69a1adc9..fe5e918f6 100644 --- a/Src/Fido2.Models/Fido2ResponseBase.cs +++ b/Src/Fido2.Models/Fido2ResponseBase.cs @@ -1,13 +1,12 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +public abstract class Fido2ResponseBase { - public abstract class Fido2ResponseBase - { - [JsonPropertyName("status")] - public string Status { get; set; } + [JsonPropertyName("status")] + public string Status { get; set; } - [JsonPropertyName("errorMessage")] - public string ErrorMessage { get; set; } - } + [JsonPropertyName("errorMessage")] + public string ErrorMessage { get; set; } } diff --git a/Src/Fido2.Models/Metadata/AlternativeDescriptions.cs b/Src/Fido2.Models/Metadata/AlternativeDescriptions.cs index d4aabe265..b0b838dc5 100644 --- a/Src/Fido2.Models/Metadata/AlternativeDescriptions.cs +++ b/Src/Fido2.Models/Metadata/AlternativeDescriptions.cs @@ -1,29 +1,28 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// This descriptor contains description in alternative languages. +/// +/// +/// +/// +public class AlternativeDescriptions { /// - /// This descriptor contains description in alternative languages. + /// Gets or sets alternative descriptions of the authenticator. + /// + /// Contains IETF language codes as key (e.g. "ru-RU", "de", "fr-FR") and a localized description as value. + /// /// /// - /// + /// Contains IETF language codes, defined by a primary language subtag, + /// followed by a region subtag based on a two-letter country code from [ISO3166] alpha-2 (usually written in upper case). + /// Each description SHALL NOT exceed a maximum length of 200 characters. + /// Description values can contain any UTF-8 characters. /// - public class AlternativeDescriptions - { - /// - /// Gets or sets alternative descriptions of the authenticator. - /// - /// Contains IETF language codes as key (e.g. "ru-RU", "de", "fr-FR") and a localized description as value. - /// - /// - /// - /// Contains IETF language codes, defined by a primary language subtag, - /// followed by a region subtag based on a two-letter country code from [ISO3166] alpha-2 (usually written in upper case). - /// Each description SHALL NOT exceed a maximum length of 200 characters. - /// Description values can contain any UTF-8 characters. - /// - [JsonPropertyName("alternativeDescriptions")] - public Dictionary IETFLanguageCodesMembers { get; set; } - } + [JsonPropertyName("alternativeDescriptions")] + public Dictionary IETFLanguageCodesMembers { get; set; } } diff --git a/Src/Fido2.Models/Metadata/AuthenticatorStatus.cs b/Src/Fido2.Models/Metadata/AuthenticatorStatus.cs index dbdad9b12..b5f44ee69 100644 --- a/Src/Fido2.Models/Metadata/AuthenticatorStatus.cs +++ b/Src/Fido2.Models/Metadata/AuthenticatorStatus.cs @@ -1,89 +1,88 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Describes the status of an authenticator model as identified by its AAID and potentially some additional information (such as a specific attestation key). +/// +/// +/// +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AuthenticatorStatus { /// - /// Describes the status of an authenticator model as identified by its AAID and potentially some additional information (such as a specific attestation key). + /// This authenticator is not FIDO certified. + /// + NOT_FIDO_CERTIFIED, + /// + /// This authenticator has passed FIDO functional certification. + /// + /// This certification scheme is phased out and will be replaced by FIDO_CERTIFIED_L1. + /// + /// + FIDO_CERTIFIED, + /// + /// Indicates that malware is able to bypass the user verification. + /// This means that the authenticator could be used without the user's consent and potentially even without the user's knowledge. + /// + USER_VERIFICATION_BYPASS, + /// + /// Indicates that an attestation key for this authenticator is known to be compromised. + /// + /// + /// Additional data should be supplied, including the key identifier and the date of compromise, if known. + /// + ATTESTATION_KEY_COMPROMISE, + /// + /// This authenticator has identified weaknesses that allow registered keys to be compromised and should not be trusted. + /// This would include both, e.g. weak entropy that causes predictable keys to be generated or side channels that allow keys or signatures to be forged, guessed or extracted. + /// + USER_KEY_REMOTE_COMPROMISE, + /// + /// This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys to be extracted by an adversary in physical possession of the device. + /// + USER_KEY_PHYSICAL_COMPROMISE, + /// + /// A software or firmware update is available for the device. + /// + UPDATE_AVAILABLE, + /// + /// The FIDO Alliance has determined that this authenticator should not be trusted. /// /// - /// + /// For example: if it is known to be a fraudulent product or contain a deliberate backdoor. /// - [JsonConverter(typeof(JsonStringEnumConverter))] - public enum AuthenticatorStatus - { - /// - /// This authenticator is not FIDO certified. - /// - NOT_FIDO_CERTIFIED, - /// - /// This authenticator has passed FIDO functional certification. - /// - /// This certification scheme is phased out and will be replaced by FIDO_CERTIFIED_L1. - /// - /// - FIDO_CERTIFIED, - /// - /// Indicates that malware is able to bypass the user verification. - /// This means that the authenticator could be used without the user's consent and potentially even without the user's knowledge. - /// - USER_VERIFICATION_BYPASS, - /// - /// Indicates that an attestation key for this authenticator is known to be compromised. - /// - /// - /// Additional data should be supplied, including the key identifier and the date of compromise, if known. - /// - ATTESTATION_KEY_COMPROMISE, - /// - /// This authenticator has identified weaknesses that allow registered keys to be compromised and should not be trusted. - /// This would include both, e.g. weak entropy that causes predictable keys to be generated or side channels that allow keys or signatures to be forged, guessed or extracted. - /// - USER_KEY_REMOTE_COMPROMISE, - /// - /// This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys to be extracted by an adversary in physical possession of the device. - /// - USER_KEY_PHYSICAL_COMPROMISE, - /// - /// A software or firmware update is available for the device. - /// - UPDATE_AVAILABLE, - /// - /// The FIDO Alliance has determined that this authenticator should not be trusted. - /// - /// - /// For example: if it is known to be a fraudulent product or contain a deliberate backdoor. - /// - REVOKED, - /// - /// The authenticator vendor has completed and submitted the self-certification checklist to the FIDO Alliance. - /// - /// - /// If this completed checklist is publicly available, the URL will be specified in . - /// - SELF_ASSERTION_SUBMITTED, - /// - /// The authenticator has passed FIDO Authenticator certification at level 1. This level is the more strict successor of FIDO_CERTIFIED. - /// - FIDO_CERTIFIED_L1, - /// - /// The authenticator has passed FIDO Authenticator certification at level 1+. This level is the more than level . - /// - FIDO_CERTIFIED_L1plus, - /// - /// The authenticator has passed FIDO Authenticator certification at level 2. This level is more strict than level . - /// - FIDO_CERTIFIED_L2, - /// - /// The authenticator has passed FIDO Authenticator certification at level 2+. This level is more strict than level . - /// - FIDO_CERTIFIED_L2plus, - /// - /// The authenticator has passed FIDO Authenticator certification at level 3. This level is more strict than level . - /// - FIDO_CERTIFIED_L3, - /// - /// The authenticator has passed FIDO Authenticator certification at level 3+. This level is more strict than level . - /// - FIDO_CERTIFIED_L3plus - }; -} + REVOKED, + /// + /// The authenticator vendor has completed and submitted the self-certification checklist to the FIDO Alliance. + /// + /// + /// If this completed checklist is publicly available, the URL will be specified in . + /// + SELF_ASSERTION_SUBMITTED, + /// + /// The authenticator has passed FIDO Authenticator certification at level 1. This level is the more strict successor of FIDO_CERTIFIED. + /// + FIDO_CERTIFIED_L1, + /// + /// The authenticator has passed FIDO Authenticator certification at level 1+. This level is the more than level . + /// + FIDO_CERTIFIED_L1plus, + /// + /// The authenticator has passed FIDO Authenticator certification at level 2. This level is more strict than level . + /// + FIDO_CERTIFIED_L2, + /// + /// The authenticator has passed FIDO Authenticator certification at level 2+. This level is more strict than level . + /// + FIDO_CERTIFIED_L2plus, + /// + /// The authenticator has passed FIDO Authenticator certification at level 3. This level is more strict than level . + /// + FIDO_CERTIFIED_L3, + /// + /// The authenticator has passed FIDO Authenticator certification at level 3+. This level is more strict than level . + /// + FIDO_CERTIFIED_L3plus +}; diff --git a/Src/Fido2.Models/Metadata/BiometricAccuracyDescriptor.cs b/Src/Fido2.Models/Metadata/BiometricAccuracyDescriptor.cs index ffec9b41b..e55f28503 100644 --- a/Src/Fido2.Models/Metadata/BiometricAccuracyDescriptor.cs +++ b/Src/Fido2.Models/Metadata/BiometricAccuracyDescriptor.cs @@ -1,57 +1,56 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// The BiometricAccuracyDescriptor describes relevant accuracy/complexity aspects in the case of a biometric user verification method. +/// +/// +/// +/// +public sealed class BiometricAccuracyDescriptor { /// - /// The BiometricAccuracyDescriptor describes relevant accuracy/complexity aspects in the case of a biometric user verification method. + /// Gets or sets the false rejection rate. + /// For example a FRR of 10% would be encoded as 0.1. /// /// - /// + /// [ISO19795-1] for a single template, i.e. the percentage of verification transactions with truthful claims of identity that are incorrectly denied. /// - public sealed class BiometricAccuracyDescriptor - { - /// - /// Gets or sets the false rejection rate. - /// For example a FRR of 10% would be encoded as 0.1. - /// - /// - /// [ISO19795-1] for a single template, i.e. the percentage of verification transactions with truthful claims of identity that are incorrectly denied. - /// - [JsonPropertyName("selfAttestedFRR")] - public double SelfAttestedFRR { get; set; } + [JsonPropertyName("selfAttestedFRR")] + public double SelfAttestedFRR { get; set; } - /// - /// Gets or sets the false acceptance rate. - /// For example a FAR of 0.002% would be encoded as 0.00002. - /// - [JsonPropertyName("selfAttestedFAR")] - public double SelfAttestedFAR { get; set; } + /// + /// Gets or sets the false acceptance rate. + /// For example a FAR of 0.002% would be encoded as 0.00002. + /// + [JsonPropertyName("selfAttestedFAR")] + public double SelfAttestedFAR { get; set; } - /// - /// Gets or sets the maximum number of alternative templates from different fingers allowed. - /// - /// - /// For other modalities, multiple parts of the body that can be used interchangeably. - /// For example: 3 if the user is allowed to enroll up to 3 different fingers to a fingerprint based authenticator. - /// - [JsonPropertyName("maxTemplates")] - public ushort MaxTemplates { get; set; } + /// + /// Gets or sets the maximum number of alternative templates from different fingers allowed. + /// + /// + /// For other modalities, multiple parts of the body that can be used interchangeably. + /// For example: 3 if the user is allowed to enroll up to 3 different fingers to a fingerprint based authenticator. + /// + [JsonPropertyName("maxTemplates")] + public ushort MaxTemplates { get; set; } - /// - /// Gets or sets the maximum number of false attempts before the authenticator will block this method (at least for some time). - /// Zero (0) means it will never block. - /// - [JsonPropertyName("maxRetries")] - public ushort MaxRetries { get; set; } + /// + /// Gets or sets the maximum number of false attempts before the authenticator will block this method (at least for some time). + /// Zero (0) means it will never block. + /// + [JsonPropertyName("maxRetries")] + public ushort MaxRetries { get; set; } - /// - /// Gets or sets the enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar). - /// Zero (0) means that this user verification method will be blocked either permanently or until an alternative user verification method succeeded. - /// - /// - /// All alternative user verification methods MUST be specified appropriately in the metadata in . - /// - [JsonPropertyName("blockSlowdown")] - public ushort BlockSlowdown { get; set; } - } + /// + /// Gets or sets the enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar). + /// Zero (0) means that this user verification method will be blocked either permanently or until an alternative user verification method succeeded. + /// + /// + /// All alternative user verification methods MUST be specified appropriately in the metadata in . + /// + [JsonPropertyName("blockSlowdown")] + public ushort BlockSlowdown { get; set; } } diff --git a/Src/Fido2.Models/Metadata/BiometricStatusReport.cs b/Src/Fido2.Models/Metadata/BiometricStatusReport.cs index 8b8ce3309..f74aef2bb 100644 --- a/Src/Fido2.Models/Metadata/BiometricStatusReport.cs +++ b/Src/Fido2.Models/Metadata/BiometricStatusReport.cs @@ -1,66 +1,65 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Contains the current BiometricStatusReport of one of the authenticator's biometric component. +/// +/// +/// +/// +public class BiometricStatusReport { /// - /// Contains the current BiometricStatusReport of one of the authenticator's biometric component. + /// Gets or sets the level of the biometric certification of this biometric component of the authenticator. + /// + [JsonPropertyName("certLevel"), Required] + public ushort CertLevel { get; set; } + /// + /// Gets or sets a single USER_VERIFY constant indicating the modality of the biometric component. /// /// - /// + /// This is not a bit flag combination. + /// This value MUST be non-zero and this value MUST correspond to one or more entries in field userVerificationDetails in the related Metadata Statement. /// - public class BiometricStatusReport - { - /// - /// Gets or sets the level of the biometric certification of this biometric component of the authenticator. - /// - [JsonPropertyName("certLevel"), Required] - public ushort CertLevel { get; set; } - /// - /// Gets or sets a single USER_VERIFY constant indicating the modality of the biometric component. - /// - /// - /// This is not a bit flag combination. - /// This value MUST be non-zero and this value MUST correspond to one or more entries in field userVerificationDetails in the related Metadata Statement. - /// - [JsonPropertyName("modality"), Required] - public ulong Modality { get; set; } + [JsonPropertyName("modality"), Required] + public ulong Modality { get; set; } - /// - /// Gets or sets a ISO-8601 formatted date since when the certLevel achieved, if applicable. - /// If no date is given, the status is assumed to be effective while present. - /// - [JsonPropertyName("effectiveDate")] - public string EffectiveDate { get; set; } + /// + /// Gets or sets a ISO-8601 formatted date since when the certLevel achieved, if applicable. + /// If no date is given, the status is assumed to be effective while present. + /// + [JsonPropertyName("effectiveDate")] + public string EffectiveDate { get; set; } - /// - /// Gets or sets the externally visible aspects of the Biometric Certification evaluation. - /// - [JsonPropertyName("certificationDescriptor")] - public string CertificationDescriptor { get; set; } + /// + /// Gets or sets the externally visible aspects of the Biometric Certification evaluation. + /// + [JsonPropertyName("certificationDescriptor")] + public string CertificationDescriptor { get; set; } - /// - /// Gets or sets the unique identifier for the issued Biometric Certification. - /// - [JsonPropertyName("certificateNumber")] - public string CertificateNumber { get; set; } + /// + /// Gets or sets the unique identifier for the issued Biometric Certification. + /// + [JsonPropertyName("certificateNumber")] + public string CertificateNumber { get; set; } - /// - /// Gets or sets the version of the Biometric Certification Policy the implementation is Certified to. - /// - /// - /// For example: "1.0.0". - /// - [JsonPropertyName("certificationPolicyVersion")] - public string CertificationPolicyVersion { get; set; } + /// + /// Gets or sets the version of the Biometric Certification Policy the implementation is Certified to. + /// + /// + /// For example: "1.0.0". + /// + [JsonPropertyName("certificationPolicyVersion")] + public string CertificationPolicyVersion { get; set; } - /// - /// Gets or sets the version of the Biometric Requirements the implementation is certified to. - /// - /// - /// For example: "1.0.0". - /// - [JsonPropertyName("certificationRequirementsVersion")] - public string CertificationRequirementsVersion { get; set; } - } + /// + /// Gets or sets the version of the Biometric Requirements the implementation is certified to. + /// + /// + /// For example: "1.0.0". + /// + [JsonPropertyName("certificationRequirementsVersion")] + public string CertificationRequirementsVersion { get; set; } } diff --git a/Src/Fido2.Models/Metadata/CodeAccuracyDescriptor.cs b/Src/Fido2.Models/Metadata/CodeAccuracyDescriptor.cs index 3dc4f287e..e45354aaa 100644 --- a/Src/Fido2.Models/Metadata/CodeAccuracyDescriptor.cs +++ b/Src/Fido2.Models/Metadata/CodeAccuracyDescriptor.cs @@ -1,43 +1,42 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// The CodeAccuracyDescriptor describes the relevant accuracy/complexity aspects of passcode user verification methods. +/// +/// +/// +/// +public sealed class CodeAccuracyDescriptor { /// - /// The CodeAccuracyDescriptor describes the relevant accuracy/complexity aspects of passcode user verification methods. + /// Gets or sets the numeric system base (radix) of the code, e.g. 10 in the case of decimal digits. + /// + [JsonPropertyName("base"), Required] + public ushort Base { get; set; } + + /// + /// Gets or sets the minimum number of digits of the given base required for that code, e.g. 4 in the case of 4 digits. + /// + [JsonPropertyName("minLength"), Required] + public ushort MinLength { get; set; } + + /// + /// Gets or sets the maximum number of false attempts before the authenticator will block this method (at least for some time). + /// Zero (0) means it will never block. + /// + [JsonPropertyName("maxRetries")] + public ushort MaxRetries { get; set; } + + /// + /// Gets or sets the enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar). + /// Zero (0) means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded. /// /// - /// + /// All alternative user verification methods MUST be specified appropriately in the Metadata in . /// - public sealed class CodeAccuracyDescriptor - { - /// - /// Gets or sets the numeric system base (radix) of the code, e.g. 10 in the case of decimal digits. - /// - [JsonPropertyName("base"), Required] - public ushort Base { get; set; } - - /// - /// Gets or sets the minimum number of digits of the given base required for that code, e.g. 4 in the case of 4 digits. - /// - [JsonPropertyName("minLength"), Required] - public ushort MinLength { get; set; } - - /// - /// Gets or sets the maximum number of false attempts before the authenticator will block this method (at least for some time). - /// Zero (0) means it will never block. - /// - [JsonPropertyName("maxRetries")] - public ushort MaxRetries { get; set; } - - /// - /// Gets or sets the enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar). - /// Zero (0) means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded. - /// - /// - /// All alternative user verification methods MUST be specified appropriately in the Metadata in . - /// - [JsonPropertyName("blockSlowdown")] - public ushort BlockSlowdown { get; set; } - } + [JsonPropertyName("blockSlowdown")] + public ushort BlockSlowdown { get; set; } } diff --git a/Src/Fido2.Models/Metadata/DisplayPNGCharacteristicsDescriptor.cs b/Src/Fido2.Models/Metadata/DisplayPNGCharacteristicsDescriptor.cs index c96a75454..4a3a772fb 100644 --- a/Src/Fido2.Models/Metadata/DisplayPNGCharacteristicsDescriptor.cs +++ b/Src/Fido2.Models/Metadata/DisplayPNGCharacteristicsDescriptor.cs @@ -1,62 +1,61 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// The DisplayPNGCharacteristicsDescriptor describes a PNG image characteristics as defined in the PNG [PNG] spec for IHDR (image header) and PLTE (palette table) +/// +/// +/// +/// +public sealed class DisplayPNGCharacteristicsDescriptor { /// - /// The DisplayPNGCharacteristicsDescriptor describes a PNG image characteristics as defined in the PNG [PNG] spec for IHDR (image header) and PLTE (palette table) - /// - /// - /// - /// - public sealed class DisplayPNGCharacteristicsDescriptor - { - /// - /// Gets or sets the image width. - /// - [JsonPropertyName("width"), Required] - public ulong Width { get; set; } - - /// - /// Gets or sets the image height. - /// - [JsonPropertyName("height"), Required] - public ulong Height { get; set; } - - /// - /// Gets or sets the bit depth - bits per sample or per palette index. - /// - [JsonPropertyName("bitDepth"), Required] - public byte BitDepth { get; set; } - - /// - /// Gets or sets the color type defines the PNG image type. - /// - [JsonPropertyName("colorType"), Required] - public byte ColorType { get; set; } - - /// - /// Gets or sets the compression method used to compress the image data. - /// - [JsonPropertyName("compression"), Required] - public byte Compression { get; set; } - - /// - /// Gets or sets the filter method is the preprocessing method applied to the image data before compression. - /// - [JsonPropertyName("filter"), Required] - public byte Filter { get; set; } - - /// - /// Gets or sets the interlace method is the transmission order of the image data. - /// - [JsonPropertyName("interlace"), Required] - public byte Interlace { get; set; } - - /// - /// Gets or sets the palette (1 to 256 palette entries). - /// - [JsonPropertyName("plte")] - public RgbPaletteEntry[] Plte { get; set; } - } + /// Gets or sets the image width. + /// + [JsonPropertyName("width"), Required] + public ulong Width { get; set; } + + /// + /// Gets or sets the image height. + /// + [JsonPropertyName("height"), Required] + public ulong Height { get; set; } + + /// + /// Gets or sets the bit depth - bits per sample or per palette index. + /// + [JsonPropertyName("bitDepth"), Required] + public byte BitDepth { get; set; } + + /// + /// Gets or sets the color type defines the PNG image type. + /// + [JsonPropertyName("colorType"), Required] + public byte ColorType { get; set; } + + /// + /// Gets or sets the compression method used to compress the image data. + /// + [JsonPropertyName("compression"), Required] + public byte Compression { get; set; } + + /// + /// Gets or sets the filter method is the preprocessing method applied to the image data before compression. + /// + [JsonPropertyName("filter"), Required] + public byte Filter { get; set; } + + /// + /// Gets or sets the interlace method is the transmission order of the image data. + /// + [JsonPropertyName("interlace"), Required] + public byte Interlace { get; set; } + + /// + /// Gets or sets the palette (1 to 256 palette entries). + /// + [JsonPropertyName("plte")] + public RgbPaletteEntry[] Plte { get; set; } } diff --git a/Src/Fido2.Models/Metadata/EcdaaTrustAnchor.cs b/Src/Fido2.Models/Metadata/EcdaaTrustAnchor.cs index 780e3f609..a2d2540eb 100644 --- a/Src/Fido2.Models/Metadata/EcdaaTrustAnchor.cs +++ b/Src/Fido2.Models/Metadata/EcdaaTrustAnchor.cs @@ -1,52 +1,51 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Represents the the ECDAA attestation data. +/// +/// +/// +/// In the case of ECDAA attestation, the ECDAA-Issuer's trust anchor MUST be specified in this field. +/// +public sealed class EcdaaTrustAnchor { /// - /// Represents the the ECDAA attestation data. + /// Gets or sets a base64url encoding of the result of ECPoint2ToB of the ECPoint2 X=P2​x​​. /// - /// - /// - /// In the case of ECDAA attestation, the ECDAA-Issuer's trust anchor MUST be specified in this field. - /// - public sealed class EcdaaTrustAnchor - { - /// - /// Gets or sets a base64url encoding of the result of ECPoint2ToB of the ECPoint2 X=P2​x​​. - /// - [JsonPropertyName("x"), Required] - public string X { get; set; } + [JsonPropertyName("x"), Required] + public string X { get; set; } - /// - /// Gets or sets a base64url encoding of the result of ECPoint2ToB of the ECPoint2. - /// - [JsonPropertyName("y"), Required] - public string Y { get; set; } + /// + /// Gets or sets a base64url encoding of the result of ECPoint2ToB of the ECPoint2. + /// + [JsonPropertyName("y"), Required] + public string Y { get; set; } - /// - /// Gets or sets a base64url encoding of the result of BigNumberToB(c). - /// - [JsonPropertyName("c"), Required] - public string C { get; set; } + /// + /// Gets or sets a base64url encoding of the result of BigNumberToB(c). + /// + [JsonPropertyName("c"), Required] + public string C { get; set; } - /// - /// Gets or sets the base64url encoding of the result of BigNumberToB(sx). - /// - [JsonPropertyName("sx"), Required] - public string SX { get; set; } + /// + /// Gets or sets the base64url encoding of the result of BigNumberToB(sx). + /// + [JsonPropertyName("sx"), Required] + public string SX { get; set; } - /// - /// Gets or sets the base64url encoding of the result of BigNumberToB(sy). - /// - [JsonPropertyName("sy"), Required] - public string SY { get; set; } + /// + /// Gets or sets the base64url encoding of the result of BigNumberToB(sy). + /// + [JsonPropertyName("sy"), Required] + public string SY { get; set; } - /// - /// Gets or sets a name of the Barreto-Naehrig elliptic curve for G1. - /// "BN_P256", "BN_P638", "BN_ISOP256", and "BN_ISOP512" are supported. - /// - [JsonPropertyName("G1Curve"), Required] - public string G1Curve { get; set; } - } + /// + /// Gets or sets a name of the Barreto-Naehrig elliptic curve for G1. + /// "BN_P256", "BN_P638", "BN_ISOP256", and "BN_ISOP512" are supported. + /// + [JsonPropertyName("G1Curve"), Required] + public string G1Curve { get; set; } } diff --git a/Src/Fido2.Models/Metadata/ExtensionDescriptor.cs b/Src/Fido2.Models/Metadata/ExtensionDescriptor.cs index 725a257ca..9adca29df 100644 --- a/Src/Fido2.Models/Metadata/ExtensionDescriptor.cs +++ b/Src/Fido2.Models/Metadata/ExtensionDescriptor.cs @@ -1,52 +1,51 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// This descriptor contains an extension supported by the authenticator. +/// +/// +/// +/// +public class ExtensionDescriptor { /// - /// This descriptor contains an extension supported by the authenticator. + /// Gets or sets the identifier that identifies the extension. + /// + [JsonPropertyName("id"), Required] + public string Id { get; set; } + + /// + /// Gets or sets the tag. + /// This field may be empty. /// /// - /// + /// The TAG of the extension if this was assigned. TAGs are assigned to extensions if they could appear in an assertion. /// - public class ExtensionDescriptor - { - /// - /// Gets or sets the identifier that identifies the extension. - /// - [JsonPropertyName("id"), Required] - public string Id { get; set; } + [JsonPropertyName("tag")] + public ushort Tag { get; set; } - /// - /// Gets or sets the tag. - /// This field may be empty. - /// - /// - /// The TAG of the extension if this was assigned. TAGs are assigned to extensions if they could appear in an assertion. - /// - [JsonPropertyName("tag")] - public ushort Tag { get; set; } - - /// - /// Gets or sets arbitrary data further describing the extension and/or data needed to correctly process the extension. - /// This field may be empty. - /// - /// - /// This field MAY be missing or it MAY be empty. - /// - [JsonPropertyName("data")] - public string Data { get; set; } + /// + /// Gets or sets arbitrary data further describing the extension and/or data needed to correctly process the extension. + /// This field may be empty. + /// + /// + /// This field MAY be missing or it MAY be empty. + /// + [JsonPropertyName("data")] + public string Data { get; set; } - /// - /// Gets or sets a value indication whether an unknown extensions must be ignored (false) or must lead to an error (true) when the extension is to be processed by the FIDO Server, FIDO Client, ASM, or FIDO Authenticator. - /// - /// - /// - /// A value of false indicates that unknown extensions MUST be ignored. - /// A value of true indicates that unknown extensions MUST result in an error. - /// - /// - [JsonPropertyName("fail_if_unknown"), Required] - public bool Fail_If_Unknown { get; set; } - } + /// + /// Gets or sets a value indication whether an unknown extensions must be ignored (false) or must lead to an error (true) when the extension is to be processed by the FIDO Server, FIDO Client, ASM, or FIDO Authenticator. + /// + /// + /// + /// A value of false indicates that unknown extensions MUST be ignored. + /// A value of true indicates that unknown extensions MUST result in an error. + /// + /// + [JsonPropertyName("fail_if_unknown"), Required] + public bool Fail_If_Unknown { get; set; } } diff --git a/Src/Fido2.Models/Metadata/MetadataBLOBPayload.cs b/Src/Fido2.Models/Metadata/MetadataBLOBPayload.cs index cd137d58d..275e79af6 100644 --- a/Src/Fido2.Models/Metadata/MetadataBLOBPayload.cs +++ b/Src/Fido2.Models/Metadata/MetadataBLOBPayload.cs @@ -1,50 +1,49 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Represents the MetadataBLOBPayload +/// +/// +/// +/// +public sealed class MetadataBLOBPayload { /// - /// Represents the MetadataBLOBPayload + /// Gets or sets the legalHeader, if present, contains a legal guide for accessing and using metadata. /// /// - /// + /// This value MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. /// - public sealed class MetadataBLOBPayload - { - /// - /// Gets or sets the legalHeader, if present, contains a legal guide for accessing and using metadata. - /// - /// - /// This value MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. - /// - [JsonPropertyName("legalHeader")] - public string LegalHeader { get; set; } + [JsonPropertyName("legalHeader")] + public string LegalHeader { get; set; } - /// - /// Gets or sets the serial number of this UAF Metadata BLOB Payload. - /// - /// - /// Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor BLOB will have a no value exactly incremented by one. - /// - [JsonPropertyName("no"), Required] - public int Number { get; set; } + /// + /// Gets or sets the serial number of this UAF Metadata BLOB Payload. + /// + /// + /// Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor BLOB will have a no value exactly incremented by one. + /// + [JsonPropertyName("no"), Required] + public int Number { get; set; } - /// - /// Gets or sets a formatted date (ISO-8601) when the next update will be provided at latest. - /// - [JsonPropertyName("nextUpdate"), Required] - public string NextUpdate { get; set; } - - /// - /// Gets or sets a list of zero or more entries of . - /// - [JsonPropertyName("entries"), Required] - public MetadataBLOBPayloadEntry[] Entries { get; set; } + /// + /// Gets or sets a formatted date (ISO-8601) when the next update will be provided at latest. + /// + [JsonPropertyName("nextUpdate"), Required] + public string NextUpdate { get; set; } + + /// + /// Gets or sets a list of zero or more entries of . + /// + [JsonPropertyName("entries"), Required] + public MetadataBLOBPayloadEntry[] Entries { get; set; } - /// - /// The "alg" property from the original JWT header. Used to validate MetadataStatements. - /// - [JsonPropertyName("jwtAlg")] - public string JwtAlg { get; set; } - } + /// + /// The "alg" property from the original JWT header. Used to validate MetadataStatements. + /// + [JsonPropertyName("jwtAlg")] + public string JwtAlg { get; set; } } diff --git a/Src/Fido2.Models/Metadata/MetadataBLOBPayloadEntry.cs b/Src/Fido2.Models/Metadata/MetadataBLOBPayloadEntry.cs index 88c027229..e4c191511 100644 --- a/Src/Fido2.Models/Metadata/MetadataBLOBPayloadEntry.cs +++ b/Src/Fido2.Models/Metadata/MetadataBLOBPayloadEntry.cs @@ -2,91 +2,90 @@ using System.Linq; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Represents the metadata BLOB payload data strucutre. +/// +/// +/// +/// +public sealed class MetadataBLOBPayloadEntry { /// - /// Represents the metadata BLOB payload data strucutre. + /// Gets or sets the AAID. + /// The AAID of the authenticator this metadata BLOB payload entry relates to. /// - /// - /// - /// - public sealed class MetadataBLOBPayloadEntry - { - /// - /// Gets or sets the AAID. - /// The AAID of the authenticator this metadata BLOB payload entry relates to. - /// - [JsonPropertyName("aaid")] - public string Aaid { get; set; } + [JsonPropertyName("aaid")] + public string Aaid { get; set; } - /// - /// Gets or sets the AAGUID. - /// The Authenticator Attestation GUID. - /// - [JsonPropertyName("aaguid")] - public string AaGuid { get; set; } + /// + /// Gets or sets the AAGUID. + /// The Authenticator Attestation GUID. + /// + [JsonPropertyName("aaguid")] + public string AaGuid { get; set; } - /// - /// Gets or sets a list of the attestation certificate public key identifiers encoded as hex string. - /// - /// - /// - /// The hex string must not contain any non-hex characters (e.g. spaces). - /// All hex letters must be lower case. - /// This field must be set if neither aaid nor aaguid are set. - /// Setting this field implies that the attestation certificate(s) are dedicated to a single authenticator model. - /// - /// FIDO U2F authenticators do not support AAID nor AAGUID, but they use attestation certificates dedicated to a single authenticator model. - /// - [JsonPropertyName("attestationCertificateKeyIdentifiers")] - public string[] AttestationCertificateKeyIdentifiers { get; set; } + /// + /// Gets or sets a list of the attestation certificate public key identifiers encoded as hex string. + /// + /// + /// + /// The hex string must not contain any non-hex characters (e.g. spaces). + /// All hex letters must be lower case. + /// This field must be set if neither aaid nor aaguid are set. + /// Setting this field implies that the attestation certificate(s) are dedicated to a single authenticator model. + /// + /// FIDO U2F authenticators do not support AAID nor AAGUID, but they use attestation certificates dedicated to a single authenticator model. + /// + [JsonPropertyName("attestationCertificateKeyIdentifiers")] + public string[] AttestationCertificateKeyIdentifiers { get; set; } - /// - /// Gets or sets the metadata statement. - /// - [JsonPropertyName("metadataStatement")] - public MetadataStatement MetadataStatement { get; set; } + /// + /// Gets or sets the metadata statement. + /// + [JsonPropertyName("metadataStatement")] + public MetadataStatement MetadataStatement { get; set; } - /// - /// Gets or sets the status of the FIDO Biometric Certification of one or more biometric components of the Authenticator. - /// - [JsonPropertyName("biometricStatusReports")] - public BiometricStatusReport[] BiometricStatusReports { get; set; } + /// + /// Gets or sets the status of the FIDO Biometric Certification of one or more biometric components of the Authenticator. + /// + [JsonPropertyName("biometricStatusReports")] + public BiometricStatusReport[] BiometricStatusReports { get; set; } - /// - /// Gets or sets an array of status reports applicable to this authenticator. - /// - [JsonPropertyName("statusReports"), Required] - public StatusReport[] StatusReports { get; set; } + /// + /// Gets or sets an array of status reports applicable to this authenticator. + /// + [JsonPropertyName("statusReports"), Required] + public StatusReport[] StatusReports { get; set; } - /// - /// Gets or sets ISO-8601 formatted date since when the status report array was set to the current value. - /// - [JsonPropertyName("timeOfLastStatusChange")] - public string TimeOfLastStatusChange { get; set; } + /// + /// Gets or sets ISO-8601 formatted date since when the status report array was set to the current value. + /// + [JsonPropertyName("timeOfLastStatusChange")] + public string TimeOfLastStatusChange { get; set; } - /// - /// Gets or sets an URL of a list of rogue (i.e. untrusted) individual authenticators. - /// - [JsonPropertyName("rogueListURL")] - public string RogueListURL { get; set; } + /// + /// Gets or sets an URL of a list of rogue (i.e. untrusted) individual authenticators. + /// + [JsonPropertyName("rogueListURL")] + public string RogueListURL { get; set; } - /// - /// Gets or sets the hash value computed of . - /// - /// - /// This hash value must be present and non-empty whenever rogueListURL is present. - /// - [JsonPropertyName("rogueListHash")] - public string RogueListHash { get; set; } + /// + /// Gets or sets the hash value computed of . + /// + /// + /// This hash value must be present and non-empty whenever rogueListURL is present. + /// + [JsonPropertyName("rogueListHash")] + public string RogueListHash { get; set; } - /// - /// Gets the latest, most current status report for the authenticator. - /// - /// Latest status report, or null if there are no reports. - public StatusReport GetLatestStatusReport() - { - return StatusReports.LastOrDefault(); - } + /// + /// Gets the latest, most current status report for the authenticator. + /// + /// Latest status report, or null if there are no reports. + public StatusReport GetLatestStatusReport() + { + return StatusReports.LastOrDefault(); } } diff --git a/Src/Fido2.Models/Metadata/MetadataStatement.cs b/Src/Fido2.Models/Metadata/MetadataStatement.cs index 1130e0f40..95afeeebf 100644 --- a/Src/Fido2.Models/Metadata/MetadataStatement.cs +++ b/Src/Fido2.Models/Metadata/MetadataStatement.cs @@ -1,201 +1,200 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Represents the metadata statement. +/// +/// +/// +/// +public class MetadataStatement { /// - /// Represents the metadata statement. + /// Gets or sets the legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. + /// + [JsonPropertyName("legalHeader")] + public string LegalHeader { get; set; } + + /// + /// Gets or set the Authenticator Attestation ID. + /// + /// + /// Note: FIDO UAF Authenticators support AAID, but they don't support AAGUID. + /// + [JsonPropertyName("aaid")] + public string Aaid { get; set; } + + /// + /// Gets or sets the Authenticator Attestation GUID. + /// + /// + /// This field MUST be set if the authenticator implements FIDO 2. + /// Note: FIDO 2 Authenticators support AAGUID, but they don't support AAID. + /// + [JsonPropertyName("aaguid")] + public string AaGuid { get; set; } + + /// + /// Gets or sets a list of the attestation certificate public key identifiers encoded as hex string. + /// + [JsonPropertyName("attestationCertificateKeyIdentifiers")] + public string[] AttestationCertificateKeyIdentifiers { get; set; } + + /// + /// Gets or sets a human-readable, short description of the authenticator, in English. + /// + [JsonPropertyName("description"), Required] + public string Description { get; set; } + + /// + /// Gets or set a list of human-readable short descriptions of the authenticator in different languages. + /// + [JsonPropertyName("alternativeDescriptions")] + public AlternativeDescriptions IETFLanguageCodesMembers { get; set; } + + /// + /// Gets or set earliest (i.e. lowest) trustworthy authenticatorVersion meeting the requirements specified in this metadata statement. + /// + [JsonPropertyName("authenticatorVersion"), Required] + public ulong AuthenticatorVersion { get; set; } + + /// + /// Gets or set the FIDO protocol family. + /// The values "uaf", "u2f", and "fido2" are supported. + /// + [JsonPropertyName("protocolFamily"), Required] + public string ProtocolFamily { get; set; } + + /// + /// The Metadata Schema version + /// Metadata schema version defines what schema of the metadata statement is currently present.The schema version of this version of the specification is 3. + /// + [JsonPropertyName("schema"), Required] + public ushort Schema { get; set; } + + /// + /// Gets or sets the FIDO unified protocol version(s) (related to the specific protocol family) supported by this authenticator. + /// + [JsonPropertyName("upv"), Required] + public UafVersion[] Upv { get; set; } + + /// + /// Gets or sets the list of authentication algorithms supported by the authenticator. + /// + [JsonPropertyName("authenticationAlgorithms"), Required] + public string[] AuthenticationAlgorithms { get; set; } + + /// + /// Gets or sets the list of public key formats supported by the authenticator during registration operations. + /// + [JsonPropertyName("publicKeyAlgAndEncodings"), Required] + public string[] PublicKeyAlgAndEncodings { get; set; } + /// + /// Gets or sets the supported attestation type(s). + /// + /// + /// For example: TAG_ATTESTATION_BASIC_FULL(0x3E07), TAG_ATTESTATION_BASIC_SURROGATE(0x3E08). + /// + [JsonPropertyName("attestationTypes"), Required] + public string[] AttestationTypes { get; set; } + + /// + /// Gets or sets a list of alternative VerificationMethodANDCombinations. + /// + [JsonPropertyName("userVerificationDetails"), Required] + public VerificationMethodDescriptor[][] UserVerificationDetails { get; set; } + + /// + /// Gets or sets a 16-bit number representing the bit fields defined by the KEY_PROTECTION constants. + /// + [JsonPropertyName("keyProtection"), Required] + public string[] KeyProtection { get; set; } + + /// + /// Gets or sets a value indicating whether the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions. /// /// - /// + /// + /// This entry is set to true, if the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions. + /// This entry is set to false, if the authenticator doesn't restrict the Uauth key to only sign valid FIDO signature assertions. In this case, the calling application could potentially get any hash value signed by the authenticator. + /// If this field is missing, the assumed value is isKeyRestricted=true. + /// /// - public class MetadataStatement - { - /// - /// Gets or sets the legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. - /// - [JsonPropertyName("legalHeader")] - public string LegalHeader { get; set; } - - /// - /// Gets or set the Authenticator Attestation ID. - /// - /// - /// Note: FIDO UAF Authenticators support AAID, but they don't support AAGUID. - /// - [JsonPropertyName("aaid")] - public string Aaid { get; set; } - - /// - /// Gets or sets the Authenticator Attestation GUID. - /// - /// - /// This field MUST be set if the authenticator implements FIDO 2. - /// Note: FIDO 2 Authenticators support AAGUID, but they don't support AAID. - /// - [JsonPropertyName("aaguid")] - public string AaGuid { get; set; } - - /// - /// Gets or sets a list of the attestation certificate public key identifiers encoded as hex string. - /// - [JsonPropertyName("attestationCertificateKeyIdentifiers")] - public string[] AttestationCertificateKeyIdentifiers { get; set; } - - /// - /// Gets or sets a human-readable, short description of the authenticator, in English. - /// - [JsonPropertyName("description"), Required] - public string Description { get; set; } - - /// - /// Gets or set a list of human-readable short descriptions of the authenticator in different languages. - /// - [JsonPropertyName("alternativeDescriptions")] - public AlternativeDescriptions IETFLanguageCodesMembers { get; set; } - - /// - /// Gets or set earliest (i.e. lowest) trustworthy authenticatorVersion meeting the requirements specified in this metadata statement. - /// - [JsonPropertyName("authenticatorVersion"), Required] - public ulong AuthenticatorVersion { get; set; } - - /// - /// Gets or set the FIDO protocol family. - /// The values "uaf", "u2f", and "fido2" are supported. - /// - [JsonPropertyName("protocolFamily"), Required] - public string ProtocolFamily { get; set; } - - /// - /// The Metadata Schema version - /// Metadata schema version defines what schema of the metadata statement is currently present.The schema version of this version of the specification is 3. - /// - [JsonPropertyName("schema"), Required] - public ushort Schema { get; set; } - - /// - /// Gets or sets the FIDO unified protocol version(s) (related to the specific protocol family) supported by this authenticator. - /// - [JsonPropertyName("upv"), Required] - public UafVersion[] Upv { get; set; } - - /// - /// Gets or sets the list of authentication algorithms supported by the authenticator. - /// - [JsonPropertyName("authenticationAlgorithms"), Required] - public string[] AuthenticationAlgorithms { get; set; } - - /// - /// Gets or sets the list of public key formats supported by the authenticator during registration operations. - /// - [JsonPropertyName("publicKeyAlgAndEncodings"), Required] - public string[] PublicKeyAlgAndEncodings { get; set; } - /// - /// Gets or sets the supported attestation type(s). - /// - /// - /// For example: TAG_ATTESTATION_BASIC_FULL(0x3E07), TAG_ATTESTATION_BASIC_SURROGATE(0x3E08). - /// - [JsonPropertyName("attestationTypes"), Required] - public string[] AttestationTypes { get; set; } - - /// - /// Gets or sets a list of alternative VerificationMethodANDCombinations. - /// - [JsonPropertyName("userVerificationDetails"), Required] - public VerificationMethodDescriptor[][] UserVerificationDetails { get; set; } - - /// - /// Gets or sets a 16-bit number representing the bit fields defined by the KEY_PROTECTION constants. - /// - [JsonPropertyName("keyProtection"), Required] - public string[] KeyProtection { get; set; } - - /// - /// Gets or sets a value indicating whether the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions. - /// - /// - /// - /// This entry is set to true, if the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions. - /// This entry is set to false, if the authenticator doesn't restrict the Uauth key to only sign valid FIDO signature assertions. In this case, the calling application could potentially get any hash value signed by the authenticator. - /// If this field is missing, the assumed value is isKeyRestricted=true. - /// - /// - [JsonPropertyName("isKeyRestricted")] - public bool IsKeyRestricted { get; set; } - - /// - /// Gets or sets a value indicating whether the Uauth key usage always requires a fresh user verification. - /// - [JsonPropertyName("isFreshUserVerificationRequired")] - public bool IsFreshUserVerificationRequired { get; set; } - - /// - /// Gets or sets a 16-bit number representing the bit fields defined by the MATCHER_PROTECTION constants. - /// - [JsonPropertyName("matcherProtection"), Required] - public string[] MatcherProtection { get; set; } - - /// - /// Gets or sets the authenticator's overall claimed cryptographic strength in bits (sometimes also called security strength or security level). - /// - /// If this value is absent, the cryptographic strength is unknown. - [JsonPropertyName("cryptoStrength")] - public ushort CryptoStrength { get; set; } - - /// - /// Gets or sets a 32-bit number representing the bit fields defined by the ATTACHMENT_HINT constants. - /// - [JsonPropertyName("attachmentHint")] - public string[] AttachmentHint { get; set; } - - /// - /// Gets or sets a 16-bit number representing a combination of the bit flags defined by the TRANSACTION_CONFIRMATION_DISPLAY constants. - /// - [JsonPropertyName("tcDisplay"), Required] - public string[] TcDisplay { get; set; } - - /// - /// Gets or sets the supported MIME content type [RFC2049] for the transaction confirmation display, such as text/plain or image/png. - /// - [JsonPropertyName("tcDisplayContentType")] - public string TcDisplayContentType { get; set; } - - /// - /// Gets or sets a list of alternative DisplayPNGCharacteristicsDescriptor. - /// - [JsonPropertyName("tcDisplayPNGCharacteristics")] - public DisplayPNGCharacteristicsDescriptor[] TcDisplayPNGCharacteristics { get; set; } - - /// - /// Gets or sets a list of a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model. - /// - [JsonPropertyName("attestationRootCertificates"), Required] - public string[] AttestationRootCertificates { get; set; } - - /// - /// Gets or set a list of trust anchors used for ECDAA attestation. - /// - [JsonPropertyName("ecdaaTrustAnchors")] - public EcdaaTrustAnchor[] EcdaaTrustAnchors { get; set; } - - /// - /// Gets or set a data: url [RFC2397] encoded PNG [PNG] icon for the Authenticator. - /// - [JsonPropertyName("icon")] - public string Icon { get; set; } - - /// - /// Gets or sets a list of extensions supported by the authenticator. - /// - [JsonPropertyName("supportedExtensions")] - public ExtensionDescriptor[] SupportedExtensions { get; set; } - - /// - /// Gets or sets a computed hash value of this . - /// NOTE: This supports the internal infrastructure of Fido2Net and isn't intented to be used by user code. - /// - public string Hash { get; set; } - } + [JsonPropertyName("isKeyRestricted")] + public bool IsKeyRestricted { get; set; } + + /// + /// Gets or sets a value indicating whether the Uauth key usage always requires a fresh user verification. + /// + [JsonPropertyName("isFreshUserVerificationRequired")] + public bool IsFreshUserVerificationRequired { get; set; } + + /// + /// Gets or sets a 16-bit number representing the bit fields defined by the MATCHER_PROTECTION constants. + /// + [JsonPropertyName("matcherProtection"), Required] + public string[] MatcherProtection { get; set; } + + /// + /// Gets or sets the authenticator's overall claimed cryptographic strength in bits (sometimes also called security strength or security level). + /// + /// If this value is absent, the cryptographic strength is unknown. + [JsonPropertyName("cryptoStrength")] + public ushort CryptoStrength { get; set; } + + /// + /// Gets or sets a 32-bit number representing the bit fields defined by the ATTACHMENT_HINT constants. + /// + [JsonPropertyName("attachmentHint")] + public string[] AttachmentHint { get; set; } + + /// + /// Gets or sets a 16-bit number representing a combination of the bit flags defined by the TRANSACTION_CONFIRMATION_DISPLAY constants. + /// + [JsonPropertyName("tcDisplay"), Required] + public string[] TcDisplay { get; set; } + + /// + /// Gets or sets the supported MIME content type [RFC2049] for the transaction confirmation display, such as text/plain or image/png. + /// + [JsonPropertyName("tcDisplayContentType")] + public string TcDisplayContentType { get; set; } + + /// + /// Gets or sets a list of alternative DisplayPNGCharacteristicsDescriptor. + /// + [JsonPropertyName("tcDisplayPNGCharacteristics")] + public DisplayPNGCharacteristicsDescriptor[] TcDisplayPNGCharacteristics { get; set; } + + /// + /// Gets or sets a list of a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model. + /// + [JsonPropertyName("attestationRootCertificates"), Required] + public string[] AttestationRootCertificates { get; set; } + + /// + /// Gets or set a list of trust anchors used for ECDAA attestation. + /// + [JsonPropertyName("ecdaaTrustAnchors")] + public EcdaaTrustAnchor[] EcdaaTrustAnchors { get; set; } + + /// + /// Gets or set a data: url [RFC2397] encoded PNG [PNG] icon for the Authenticator. + /// + [JsonPropertyName("icon")] + public string Icon { get; set; } + + /// + /// Gets or sets a list of extensions supported by the authenticator. + /// + [JsonPropertyName("supportedExtensions")] + public ExtensionDescriptor[] SupportedExtensions { get; set; } + + /// + /// Gets or sets a computed hash value of this . + /// NOTE: This supports the internal infrastructure of Fido2Net and isn't intented to be used by user code. + /// + public string Hash { get; set; } } diff --git a/Src/Fido2.Models/Metadata/PatternAccuracyDescriptor.cs b/Src/Fido2.Models/Metadata/PatternAccuracyDescriptor.cs index ebbca2b80..53227e8c7 100644 --- a/Src/Fido2.Models/Metadata/PatternAccuracyDescriptor.cs +++ b/Src/Fido2.Models/Metadata/PatternAccuracyDescriptor.cs @@ -1,37 +1,36 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// The PatternAccuracyDescriptor describes relevant accuracy/complexity aspects in the case that a pattern is used as the user verification method. +/// +/// +/// +/// +public sealed class PatternAccuracyDescriptor { /// - /// The PatternAccuracyDescriptor describes relevant accuracy/complexity aspects in the case that a pattern is used as the user verification method. + /// Gets or sets the number of possible patterns (having the minimum length) out of which exactly one would be the right one, i.e. 1/probability in the case of equal distribution. /// - /// - /// - /// - public sealed class PatternAccuracyDescriptor - { - /// - /// Gets or sets the number of possible patterns (having the minimum length) out of which exactly one would be the right one, i.e. 1/probability in the case of equal distribution. - /// - [JsonPropertyName("minComplexity"), Required] - public ulong MinComplexity { get; set; } + [JsonPropertyName("minComplexity"), Required] + public ulong MinComplexity { get; set; } - /// - /// Gets or sets maximum number of false attempts before the authenticator will block authentication using this method (at least temporarily). - /// Zero (0) means it will never block. - /// - [JsonPropertyName("maxRetries")] - public ushort MaxRetries { get; set; } + /// + /// Gets or sets maximum number of false attempts before the authenticator will block authentication using this method (at least temporarily). + /// Zero (0) means it will never block. + /// + [JsonPropertyName("maxRetries")] + public ushort MaxRetries { get; set; } - /// - /// Gets or sets the enforced minimum number of seconds wait time after blocking (due to forced reboot or similar mechanism). - /// Zero (0) means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded. - /// - /// - /// All alternative user verification methods MUST be specified appropriately in the metadata under userVerificationDetails. - /// - [JsonPropertyName("blockSlowdown")] - public ushort BlockSlowdown { get; set; } - } + /// + /// Gets or sets the enforced minimum number of seconds wait time after blocking (due to forced reboot or similar mechanism). + /// Zero (0) means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded. + /// + /// + /// All alternative user verification methods MUST be specified appropriately in the metadata under userVerificationDetails. + /// + [JsonPropertyName("blockSlowdown")] + public ushort BlockSlowdown { get; set; } } diff --git a/Src/Fido2.Models/Metadata/RgbPaletteEntry.cs b/Src/Fido2.Models/Metadata/RgbPaletteEntry.cs index c492d6be6..7e237a09b 100644 --- a/Src/Fido2.Models/Metadata/RgbPaletteEntry.cs +++ b/Src/Fido2.Models/Metadata/RgbPaletteEntry.cs @@ -1,32 +1,31 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// The rgbPaletteEntry is an RGB three-sample tuple palette entry. +/// +/// +/// +/// +public class RgbPaletteEntry { /// - /// The rgbPaletteEntry is an RGB three-sample tuple palette entry. + /// Gets or sets the red channel sample value. /// - /// - /// - /// - public class RgbPaletteEntry - { - /// - /// Gets or sets the red channel sample value. - /// - [JsonPropertyName("r"), Required] - public ushort R { get; set; } + [JsonPropertyName("r"), Required] + public ushort R { get; set; } - /// - /// Gets or sets the green channel sample value. - /// - [JsonPropertyName("g"), Required] - public ushort G { get; set; } + /// + /// Gets or sets the green channel sample value. + /// + [JsonPropertyName("g"), Required] + public ushort G { get; set; } - /// - /// Gets or sets the blue channel sample value. - /// - [JsonPropertyName("b"), Required] - public ushort B { get; set; } - } + /// + /// Gets or sets the blue channel sample value. + /// + [JsonPropertyName("b"), Required] + public ushort B { get; set; } } diff --git a/Src/Fido2.Models/Metadata/StatusReport.cs b/Src/Fido2.Models/Metadata/StatusReport.cs index 5b959f0e5..7ddcad5d0 100644 --- a/Src/Fido2.Models/Metadata/StatusReport.cs +++ b/Src/Fido2.Models/Metadata/StatusReport.cs @@ -1,70 +1,69 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Contains an AuthenticatorStatus and additional data associated with it, if any. +/// +/// +/// +/// +public sealed class StatusReport { /// - /// Contains an AuthenticatorStatus and additional data associated with it, if any. + /// Gets or sets the status of the authenticator. + /// Additional fields may be set depending on this value. /// - /// - /// - /// - public sealed class StatusReport - { - /// - /// Gets or sets the status of the authenticator. - /// Additional fields may be set depending on this value. - /// - [JsonPropertyName("status"), Required] - public AuthenticatorStatus Status { get; set; } + [JsonPropertyName("status"), Required] + public AuthenticatorStatus Status { get; set; } - /// - /// Gets or set the ISO-8601 formatted date since when the status code was set, if applicable. - /// If no date is given, the status is assumed to be effective while present. - /// - [JsonPropertyName("effectiveDate")] - public string EffectiveDate { get; set; } + /// + /// Gets or set the ISO-8601 formatted date since when the status code was set, if applicable. + /// If no date is given, the status is assumed to be effective while present. + /// + [JsonPropertyName("effectiveDate")] + public string EffectiveDate { get; set; } - /// - /// Gets or sets Base64-encoded PKIX certificate value related to the current status, if applicable. - /// - /// - /// Base64-encoded [RFC4648] (not base64url!) / DER [ITU-X690-2008] PKIX certificate. - /// - [JsonPropertyName("certificate")] - public string Certificate { get; set; } + /// + /// Gets or sets Base64-encoded PKIX certificate value related to the current status, if applicable. + /// + /// + /// Base64-encoded [RFC4648] (not base64url!) / DER [ITU-X690-2008] PKIX certificate. + /// + [JsonPropertyName("certificate")] + public string Certificate { get; set; } - /// - /// Gets or sets the HTTPS URL where additional information may be found related to the current status, if applicable. - /// - /// - /// For example a link to a web page describing an available firmware update in the case of status , or a link to a description of an identified issue in the case of status . - /// - [JsonPropertyName("url")] - public string Url { get; set; } + /// + /// Gets or sets the HTTPS URL where additional information may be found related to the current status, if applicable. + /// + /// + /// For example a link to a web page describing an available firmware update in the case of status , or a link to a description of an identified issue in the case of status . + /// + [JsonPropertyName("url")] + public string Url { get; set; } - /// - /// Gets or sets a description of the externally visible aspects of the Authenticator Certification evaluation. - /// - [JsonPropertyName("certificationDescriptor")] - public string CertificationDescriptor { get; set; } + /// + /// Gets or sets a description of the externally visible aspects of the Authenticator Certification evaluation. + /// + [JsonPropertyName("certificationDescriptor")] + public string CertificationDescriptor { get; set; } - /// - /// Gets or sets the unique identifier for the issued Certification. - /// - [JsonPropertyName("certificateNumber")] - public string CertificateNumber { get; set; } + /// + /// Gets or sets the unique identifier for the issued Certification. + /// + [JsonPropertyName("certificateNumber")] + public string CertificateNumber { get; set; } - /// - /// Gets or set the version of the Authenticator Certification Policy the implementation is Certified to. - /// - [JsonPropertyName("certificationPolicyVersion")] - public string CertificationPolicyVersion { get; set; } + /// + /// Gets or set the version of the Authenticator Certification Policy the implementation is Certified to. + /// + [JsonPropertyName("certificationPolicyVersion")] + public string CertificationPolicyVersion { get; set; } - /// - /// Gets or set the version of the Authenticator Security Requirements the implementation is Certified to. - /// - [JsonPropertyName("certificationRequirementsVersion")] - public string CertificationRequirementsVersion { get; set; } - } + /// + /// Gets or set the version of the Authenticator Security Requirements the implementation is Certified to. + /// + [JsonPropertyName("certificationRequirementsVersion")] + public string CertificationRequirementsVersion { get; set; } } diff --git a/Src/Fido2.Models/Metadata/UafVersion.cs b/Src/Fido2.Models/Metadata/UafVersion.cs index ceb790672..3b798b5ac 100644 --- a/Src/Fido2.Models/Metadata/UafVersion.cs +++ b/Src/Fido2.Models/Metadata/UafVersion.cs @@ -1,24 +1,23 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Represents a generic version with major and minor fields. +/// +/// +/// https://fidoalliance.org/specs/fido-uaf-v1.2-rd-20171128/fido-uaf-protocol-v1.2-rd-20171128.html#version-interface +/// +public class UafVersion { /// - /// Represents a generic version with major and minor fields. + /// Major version + /// + [JsonPropertyName("major")] + public ushort Major { get; set; } + /// + /// Minor version /// - /// - /// https://fidoalliance.org/specs/fido-uaf-v1.2-rd-20171128/fido-uaf-protocol-v1.2-rd-20171128.html#version-interface - /// - public class UafVersion - { - /// - /// Major version - /// - [JsonPropertyName("major")] - public ushort Major { get; set; } - /// - /// Minor version - /// - [JsonPropertyName("minor")] - public ushort Minor { get; set; } - } + [JsonPropertyName("minor")] + public ushort Minor { get; set; } } diff --git a/Src/Fido2.Models/Metadata/UserVerificationMethods.cs b/Src/Fido2.Models/Metadata/UserVerificationMethods.cs index b3401a644..277def09b 100644 --- a/Src/Fido2.Models/Metadata/UserVerificationMethods.cs +++ b/Src/Fido2.Models/Metadata/UserVerificationMethods.cs @@ -1,82 +1,81 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/** + * User Verification Methods Short Form + * + * The USER_VERIFY constants are flags in a bitfield represented as a 32 bit long integer. They describe the methods and capabilities of an UAF authenticator for locally verifying a user. The operational details of these methods are opaque to the server. These constants are used in the authoritative metadata for an authenticator, reported and queried through the UAF Discovery APIs, and used to form authenticator policies in UAF protocol messages. + * + * https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-reg-v1.0-ps-20141208.html#user-verification-methods + */ +[JsonConverter(typeof(FidoEnumConverter))] +public enum UserVerificationMethods { - /** - * User Verification Methods Short Form - * - * The USER_VERIFY constants are flags in a bitfield represented as a 32 bit long integer. They describe the methods and capabilities of an UAF authenticator for locally verifying a user. The operational details of these methods are opaque to the server. These constants are used in the authoritative metadata for an authenticator, reported and queried through the UAF Discovery APIs, and used to form authenticator policies in UAF protocol messages. - * - * https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-reg-v1.0-ps-20141208.html#user-verification-methods - */ - [JsonConverter(typeof(FidoEnumConverter))] - public enum UserVerificationMethods - { - /// - /// This flag must be set if the authenticator is able to confirm user presence in any fashion. If this flag and no other is set for user verification, the guarantee is only that the authenticator cannot be operated without some human intervention, not necessarily that the presence verification provides any level of authentication of the human's identity. (e.g. a device that requires a touch to activate) - /// - [EnumMember(Value = "presence_internal")] - PRESENCE_INTERNAL = 1, - /// - /// This flag must be set if the authenticator uses any type of measurement of a fingerprint for user verification. - /// - [EnumMember(Value = "fingerprint_internal")] - FINGERPRINT_INTERNAL = 2, - /// - /// This flag must be set if the authenticator uses a local-only passcode (i.e. a passcode not known by the server) for user verification. - /// - [EnumMember(Value = "passcode_internal")] - PASSCODE_INTERNAL = 4, - /// - /// This flag must be set if the authenticator uses a voiceprint (also known as speaker recognition) for user verification. - /// - [EnumMember(Value = "voiceprint_internal")] - VOICEPRINT_INTERNAL = 8, - /// - /// This flag must be set if the authenticator uses any manner of face recognition to verify the user. - /// - [EnumMember(Value = "faceprint_internal")] - FACEPRINT_INTERNAL = 0x10, - /// - /// This flag must be set if the authenticator uses any form of location sensor or measurement for user verification. - /// - [EnumMember(Value = "location_internal")] - LOCATION_INTERNAL = 0x20, - /// - /// This flag must be set if the authenticator uses any form of eye biometrics for user verification. - /// - [EnumMember(Value = "eyeprint_internal")] - EYEPRINT_INTERNAL = 0x40, - /// - /// This flag must be set if the authenticator uses a drawn pattern for user verification. - /// - [EnumMember(Value = "pattern_internal")] - PATTERN_INTERNAL = 0x80, - /// - /// This flag must be set if the authenticator uses any measurement of a full hand (including palm-print, hand geometry or vein geometry) for user verification. - /// - [EnumMember(Value = "handprint_internal")] - HANDPRINT_INTERNAL = 0x100, - /// - /// This flag must be set if the authenticator uses a local-only passcode (i.e. a passcode not known by the server) for user verification that might be gathered outside the authenticator boundary. - /// - [EnumMember(Value = "passcode_external")] - PASSCODE_EXTERNAL = 0x800, - /// - /// This flag must be set if the authenticator uses a drawn pattern for user verification that might be gathered outside the authenticator boundary. - /// - [EnumMember(Value = "pattern_external")] - PATTERN_EXTERNAL = 0x1000, - /// - /// This flag must be set if the authenticator will respond without any user interaction (e.g. Silent Authenticator). - /// - [EnumMember(Value = "none")] - NONE = 0x200, - /// - /// If an authenticator sets multiple flags for user verification types, it may also set this flag to indicate that all verification methods will be enforced (e.g. faceprint AND voiceprint). If flags for multiple user verification methods are set and this flag is not set, verification with only one is necessary (e.g. fingerprint OR passcode). - /// - [EnumMember(Value = "all")] - ALL = 0x400, - } + /// + /// This flag must be set if the authenticator is able to confirm user presence in any fashion. If this flag and no other is set for user verification, the guarantee is only that the authenticator cannot be operated without some human intervention, not necessarily that the presence verification provides any level of authentication of the human's identity. (e.g. a device that requires a touch to activate) + /// + [EnumMember(Value = "presence_internal")] + PRESENCE_INTERNAL = 1, + /// + /// This flag must be set if the authenticator uses any type of measurement of a fingerprint for user verification. + /// + [EnumMember(Value = "fingerprint_internal")] + FINGERPRINT_INTERNAL = 2, + /// + /// This flag must be set if the authenticator uses a local-only passcode (i.e. a passcode not known by the server) for user verification. + /// + [EnumMember(Value = "passcode_internal")] + PASSCODE_INTERNAL = 4, + /// + /// This flag must be set if the authenticator uses a voiceprint (also known as speaker recognition) for user verification. + /// + [EnumMember(Value = "voiceprint_internal")] + VOICEPRINT_INTERNAL = 8, + /// + /// This flag must be set if the authenticator uses any manner of face recognition to verify the user. + /// + [EnumMember(Value = "faceprint_internal")] + FACEPRINT_INTERNAL = 0x10, + /// + /// This flag must be set if the authenticator uses any form of location sensor or measurement for user verification. + /// + [EnumMember(Value = "location_internal")] + LOCATION_INTERNAL = 0x20, + /// + /// This flag must be set if the authenticator uses any form of eye biometrics for user verification. + /// + [EnumMember(Value = "eyeprint_internal")] + EYEPRINT_INTERNAL = 0x40, + /// + /// This flag must be set if the authenticator uses a drawn pattern for user verification. + /// + [EnumMember(Value = "pattern_internal")] + PATTERN_INTERNAL = 0x80, + /// + /// This flag must be set if the authenticator uses any measurement of a full hand (including palm-print, hand geometry or vein geometry) for user verification. + /// + [EnumMember(Value = "handprint_internal")] + HANDPRINT_INTERNAL = 0x100, + /// + /// This flag must be set if the authenticator uses a local-only passcode (i.e. a passcode not known by the server) for user verification that might be gathered outside the authenticator boundary. + /// + [EnumMember(Value = "passcode_external")] + PASSCODE_EXTERNAL = 0x800, + /// + /// This flag must be set if the authenticator uses a drawn pattern for user verification that might be gathered outside the authenticator boundary. + /// + [EnumMember(Value = "pattern_external")] + PATTERN_EXTERNAL = 0x1000, + /// + /// This flag must be set if the authenticator will respond without any user interaction (e.g. Silent Authenticator). + /// + [EnumMember(Value = "none")] + NONE = 0x200, + /// + /// If an authenticator sets multiple flags for user verification types, it may also set this flag to indicate that all verification methods will be enforced (e.g. faceprint AND voiceprint). If flags for multiple user verification methods are set and this flag is not set, verification with only one is necessary (e.g. fingerprint OR passcode). + /// + [EnumMember(Value = "all")] + ALL = 0x400, } diff --git a/Src/Fido2.Models/Metadata/VerificationMethodDescriptor.cs b/Src/Fido2.Models/Metadata/VerificationMethodDescriptor.cs index 2f67f9642..cdc3670c3 100644 --- a/Src/Fido2.Models/Metadata/VerificationMethodDescriptor.cs +++ b/Src/Fido2.Models/Metadata/VerificationMethodDescriptor.cs @@ -1,40 +1,39 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// A descriptor for a specific base user verification method as implemented by the authenticator. +/// +/// +/// +/// +public class VerificationMethodDescriptor { /// - /// A descriptor for a specific base user verification method as implemented by the authenticator. + /// Gets or sets a single USER_VERIFY constant, not a bit flag combination. /// /// - /// + /// This value MUST be non-zero. /// - public class VerificationMethodDescriptor - { - /// - /// Gets or sets a single USER_VERIFY constant, not a bit flag combination. - /// - /// - /// This value MUST be non-zero. - /// - [JsonPropertyName("userVerificationMethod")] - public string UserVerificationMethod { get; set; } + [JsonPropertyName("userVerificationMethod")] + public string UserVerificationMethod { get; set; } - /// - /// Gets or sets a may optionally be used in the case of method USER_VERIFY_PASSCODE. - /// - [JsonPropertyName("caDesc")] - public CodeAccuracyDescriptor CaDesc { get; set; } + /// + /// Gets or sets a may optionally be used in the case of method USER_VERIFY_PASSCODE. + /// + [JsonPropertyName("caDesc")] + public CodeAccuracyDescriptor CaDesc { get; set; } - /// - /// Gets or sets a may optionally be used in the case of method USER_VERIFY_FINGERPRINT, USER_VERIFY_VOICEPRINT, USER_VERIFY_FACEPRINT, USER_VERIFY_EYEPRINT, or USER_VERIFY_HANDPRINT. - /// - [JsonPropertyName("baDesc")] - public BiometricAccuracyDescriptor BaDesc { get; set; } + /// + /// Gets or sets a may optionally be used in the case of method USER_VERIFY_FINGERPRINT, USER_VERIFY_VOICEPRINT, USER_VERIFY_FACEPRINT, USER_VERIFY_EYEPRINT, or USER_VERIFY_HANDPRINT. + /// + [JsonPropertyName("baDesc")] + public BiometricAccuracyDescriptor BaDesc { get; set; } - /// - /// Gets or sets a may optionally be used in case of method USER_VERIFY_PATTERN. - /// - [JsonPropertyName("paDesc")] - public PatternAccuracyDescriptor PaDesc { get; set; } - } + /// + /// Gets or sets a may optionally be used in case of method USER_VERIFY_PATTERN. + /// + [JsonPropertyName("paDesc")] + public PatternAccuracyDescriptor PaDesc { get; set; } } diff --git a/Src/Fido2.Models/Objects/AssertionVerificationResult.cs b/Src/Fido2.Models/Objects/AssertionVerificationResult.cs index c5fba83fc..7df970e10 100644 --- a/Src/Fido2.Models/Objects/AssertionVerificationResult.cs +++ b/Src/Fido2.Models/Objects/AssertionVerificationResult.cs @@ -1,11 +1,11 @@ -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// Result of the MakeAssertion verification +/// +public class AssertionVerificationResult : Fido2ResponseBase { - /// - /// Result of the MakeAssertion verification - /// - public class AssertionVerificationResult : Fido2ResponseBase - { - public byte[] CredentialId { get; set; } - public uint Counter { get; set; } - } + public byte[] CredentialId { get; set; } + + public uint Counter { get; set; } } diff --git a/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs b/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs index 0d846b1d0..b241bc191 100644 --- a/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs +++ b/Src/Fido2.Models/Objects/AttestationConveyancePreference.cs @@ -1,32 +1,31 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// AttestationConveyancePreference. +/// https://w3c.github.io/webauthn/#attestation-convey +/// +[JsonConverter(typeof(FidoEnumConverter))] +public enum AttestationConveyancePreference { /// - /// AttestationConveyancePreference. - /// https://w3c.github.io/webauthn/#attestation-convey + /// This value indicates that the Relying Party is not interested in authenticator attestation. For example, in order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, or to save a roundtrip to an Attestation CA. + /// This is the default value. /// - [JsonConverter(typeof(FidoEnumConverter))] - public enum AttestationConveyancePreference - { - /// - /// This value indicates that the Relying Party is not interested in authenticator attestation. For example, in order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, or to save a roundtrip to an Attestation CA. - /// This is the default value. - /// - [EnumMember(Value = "none")] - None, + [EnumMember(Value = "none")] + None, - /// - /// This value indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace the authenticator-generated attestation statements with attestation statements generated by an Anonymization CA, in order to protect the user’s privacy, or to assist Relying Parties with attestation verification in a heterogeneous ecosystem. - /// - [EnumMember(Value = "indirect")] - Indirect, + /// + /// This value indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace the authenticator-generated attestation statements with attestation statements generated by an Anonymization CA, in order to protect the user’s privacy, or to assist Relying Parties with attestation verification in a heterogeneous ecosystem. + /// + [EnumMember(Value = "indirect")] + Indirect, - /// - /// This value indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator. - /// - [EnumMember(Value = "direct")] - Direct - } + /// + /// This value indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator. + /// + [EnumMember(Value = "direct")] + Direct } diff --git a/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs b/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs index 1301de807..2defbd6a3 100644 --- a/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs +++ b/Src/Fido2.Models/Objects/AttestationVerificationSuccess.cs @@ -1,22 +1,21 @@ using System.Security.Cryptography.X509Certificates; using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// Holds parsed credential data +/// +public class AttestationVerificationSuccess : AssertionVerificationResult { - /// - /// Holds parsed credential data - /// - public class AttestationVerificationSuccess : AssertionVerificationResult - { - [JsonConverter(typeof(Base64UrlConverter))] - public byte[] PublicKey { get; set; } + [JsonConverter(typeof(Base64UrlConverter))] + public byte[] PublicKey { get; set; } - public Fido2User User { get; set; } - public string CredType { get; set; } - public System.Guid Aaguid { get; set; } + public Fido2User User { get; set; } + public string CredType { get; set; } + public System.Guid Aaguid { get; set; } #nullable enable - public X509Certificate2? AttestationCertificate { get; set; } + public X509Certificate2? AttestationCertificate { get; set; } #nullable disable - public X509Certificate2[] AttestationCertificateChain { get; set; } - } + public X509Certificate2[] AttestationCertificateChain { get; set; } } diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs index d99efca06..1aad9ef38 100644 --- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientInputs.cs @@ -1,50 +1,49 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// This is a dictionary containing the client extension output values for zero or more WebAuthn Extensions +/// +public sealed class AuthenticationExtensionsClientInputs { /// - /// This is a dictionary containing the client extension output values for zero or more WebAuthn Extensions + /// This extension allows for passing of conformance tests /// - public sealed class AuthenticationExtensionsClientInputs - { - /// - /// This extension allows for passing of conformance tests - /// - [JsonPropertyName("example.extension")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public object Example { get; set; } + [JsonPropertyName("example.extension")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + 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. - /// https://www.w3.org/TR/webauthn/#sctn-appid-extension - /// - [JsonPropertyName("appid")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string AppID { get; set; } + /// + /// This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO JavaScript APIs to request an assertion. + /// https://www.w3.org/TR/webauthn/#sctn-appid-extension + /// + [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 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; } + /// + /// 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; } - /// - /// This extension enables use of a user verification method. - /// https://www.w3.org/TR/webauthn/#sctn-uvm-extension - /// - [JsonPropertyName("uvm")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? UserVerificationMethod { get; set; } - } + /// + /// This extension enables use of a user verification method. + /// https://www.w3.org/TR/webauthn/#sctn-uvm-extension + /// + [JsonPropertyName("uvm")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? UserVerificationMethod { get; set; } } diff --git a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs index 1883465e5..e2e7271f1 100644 --- a/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs +++ b/Src/Fido2.Models/Objects/AuthenticationExtensionsClientOutputs.cs @@ -1,46 +1,45 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +public class AuthenticationExtensionsClientOutputs { - public class AuthenticationExtensionsClientOutputs - { - /// - /// This extension allows for passing of conformance tests - /// - [JsonPropertyName("example.extension")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public object Example { get; set; } + /// + /// This extension allows for passing of conformance tests + /// + [JsonPropertyName("example.extension")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object Example { get; set; } #nullable enable - /// - /// This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO JavaScript APIs to request an assertion. - /// https://www.w3.org/TR/webauthn/#sctn-appid-extension - /// - [JsonPropertyName("appid")] - public bool 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")] - public bool AuthenticatorSelection { get; set; } + /// + /// This extension allows WebAuthn Relying Parties that have previously registered a credential using the legacy FIDO JavaScript APIs to request an assertion. + /// https://www.w3.org/TR/webauthn/#sctn-appid-extension + /// + [JsonPropertyName("appid")] + public bool 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")] + public bool 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 string[]? Extensions { 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 string[]? Extensions { get; set; } - /// - /// This extension enables use of a user verification method. - /// https://www.w3.org/TR/webauthn/#sctn-uvm-extension - /// - [JsonPropertyName("uvm")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public ulong[][]? UserVerificationMethod { get; set; } - } + /// + /// This extension enables use of a user verification method. + /// https://www.w3.org/TR/webauthn/#sctn-uvm-extension + /// + [JsonPropertyName("uvm")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ulong[][]? UserVerificationMethod { get; set; } } diff --git a/Src/Fido2.Models/Objects/AuthenticatorAttachment.cs b/Src/Fido2.Models/Objects/AuthenticatorAttachment.cs index ecb20848c..fe3ffdb4d 100644 --- a/Src/Fido2.Models/Objects/AuthenticatorAttachment.cs +++ b/Src/Fido2.Models/Objects/AuthenticatorAttachment.cs @@ -1,30 +1,29 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// This enumeration’s values describe authenticators' attachment modalities. Relying Parties use this for two purposes: +/// to express a preferred authenticator attachment modality when calling navigator.credentials.create() to create a credential, and +/// to inform the client of the Relying Party's best belief about how to locate the managing authenticators of the credentials listed in allowCredentials when calling navigator.credentials.get(). +/// +/// +/// Note: An authenticator attachment modality selection option is available only in the [[Create]](origin, options, sameOriginWithAncestors) operation. The Relying Party may use it to, for example, ensure the user has a roaming credential for authenticating on another client device; or to specifically register a platform credential for easier reauthentication using a particular client device. The [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) operation has no authenticator attachment modality selection option, so the Relying Party SHOULD accept any of the user’s registered credentials. The client and user will then use whichever is available and convenient at the time. +/// https://w3c.github.io/webauthn/#attachment +/// +[JsonConverter(typeof(FidoEnumConverter))] +public enum AuthenticatorAttachment { /// - /// This enumeration’s values describe authenticators' attachment modalities. Relying Parties use this for two purposes: - /// to express a preferred authenticator attachment modality when calling navigator.credentials.create() to create a credential, and - /// to inform the client of the Relying Party's best belief about how to locate the managing authenticators of the credentials listed in allowCredentials when calling navigator.credentials.get(). + /// This value indicates platform attachment /// - /// - /// Note: An authenticator attachment modality selection option is available only in the [[Create]](origin, options, sameOriginWithAncestors) operation. The Relying Party may use it to, for example, ensure the user has a roaming credential for authenticating on another client device; or to specifically register a platform credential for easier reauthentication using a particular client device. The [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) operation has no authenticator attachment modality selection option, so the Relying Party SHOULD accept any of the user’s registered credentials. The client and user will then use whichever is available and convenient at the time. - /// https://w3c.github.io/webauthn/#attachment - /// - [JsonConverter(typeof(FidoEnumConverter))] - public enum AuthenticatorAttachment - { - /// - /// This value indicates platform attachment - /// - [EnumMember(Value = "platform")] - Platform, + [EnumMember(Value = "platform")] + Platform, - /// - /// This value indicates cross-platform attachment. - /// - [EnumMember(Value = "cross-platform")] - CrossPlatform - } + /// + /// This value indicates cross-platform attachment. + /// + [EnumMember(Value = "cross-platform")] + CrossPlatform } diff --git a/Src/Fido2.Models/Objects/AuthenticatorTransport.cs b/Src/Fido2.Models/Objects/AuthenticatorTransport.cs index 37ee495de..1d7a75dc8 100644 --- a/Src/Fido2.Models/Objects/AuthenticatorTransport.cs +++ b/Src/Fido2.Models/Objects/AuthenticatorTransport.cs @@ -1,37 +1,36 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// Authenticators may implement various transports for communicating with clients. This enumeration defines hints as to how clients might communicate with a particular authenticator in order to obtain an assertion for a specific credential. Note that these hints represent the WebAuthn Relying Party's best belief as to how an authenticator may be reached. A Relying Party may obtain a list of transports hints from some attestation statement formats or via some out-of-band mechanism; it is outside the scope of this specification to define that mechanism. +/// https://w3c.github.io/webauthn/#transport +/// +[JsonConverter(typeof(FidoEnumConverter))] +public enum AuthenticatorTransport { /// - /// Authenticators may implement various transports for communicating with clients. This enumeration defines hints as to how clients might communicate with a particular authenticator in order to obtain an assertion for a specific credential. Note that these hints represent the WebAuthn Relying Party's best belief as to how an authenticator may be reached. A Relying Party may obtain a list of transports hints from some attestation statement formats or via some out-of-band mechanism; it is outside the scope of this specification to define that mechanism. - /// https://w3c.github.io/webauthn/#transport + /// Indicates the respective authenticator can be contacted over removable USB. /// - [JsonConverter(typeof(FidoEnumConverter))] - public enum AuthenticatorTransport - { - /// - /// Indicates the respective authenticator can be contacted over removable USB. - /// - [EnumMember(Value = "usb")] - Usb, + [EnumMember(Value = "usb")] + Usb, - /// - /// Indicates the respective authenticator can be contacted over Near Field Communication (NFC). - /// - [EnumMember(Value = "nfc")] - Nfc, + /// + /// Indicates the respective authenticator can be contacted over Near Field Communication (NFC). + /// + [EnumMember(Value = "nfc")] + Nfc, - /// - /// Indicates the respective authenticator can be contacted over Bluetooth Smart(Bluetooth Low Energy / BLE) - /// - [EnumMember(Value = "ble")] - Ble, + /// + /// Indicates the respective authenticator can be contacted over Bluetooth Smart(Bluetooth Low Energy / BLE) + /// + [EnumMember(Value = "ble")] + Ble, - /// - /// Indicates the respective authenticator is contacted using a client device-specific transport.These authenticators are not removable from the client device. - /// - [EnumMember(Value = "internal")] - Internal, - } + /// + /// Indicates the respective authenticator is contacted using a client device-specific transport.These authenticators are not removable from the client device. + /// + [EnumMember(Value = "internal")] + Internal, } diff --git a/Src/Fido2.Models/Objects/KeyProtection.cs b/Src/Fido2.Models/Objects/KeyProtection.cs index 06f86d91c..71cb5ad3a 100644 --- a/Src/Fido2.Models/Objects/KeyProtection.cs +++ b/Src/Fido2.Models/Objects/KeyProtection.cs @@ -1,43 +1,42 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/** + * Key Protection Types Short Form + * + * The KEY_PROTECTION constants are flags in a bit field represented as a 16 bit long integer. They describe the method an authenticator uses to protect the private key material for FIDO registrations. Refer to [UAFAuthnrCommands] for more details on the relevance of keys and key protection. These constants are used in the authoritative metadata for an authenticator, reported and queried through the UAF Discovery APIs, and used to form authenticator policies in UAF protocol messages. + * + * https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-reg-v1.0-ps-20141208.html#key-protection-types + * type {Object} + */ +[JsonConverter(typeof(FidoEnumConverter))] +public enum KeyProtection { - /** - * Key Protection Types Short Form - * - * The KEY_PROTECTION constants are flags in a bit field represented as a 16 bit long integer. They describe the method an authenticator uses to protect the private key material for FIDO registrations. Refer to [UAFAuthnrCommands] for more details on the relevance of keys and key protection. These constants are used in the authoritative metadata for an authenticator, reported and queried through the UAF Discovery APIs, and used to form authenticator policies in UAF protocol messages. - * - * https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-reg-v1.0-ps-20141208.html#key-protection-types - * type {Object} - */ - [JsonConverter(typeof(FidoEnumConverter))] - public enum KeyProtection - { - /// - /// This flag must be set if the authenticator uses software-based key management. Exclusive in authenticator metadata with KEY_PROTECTION_HARDWARE, KEY_PROTECTION_TEE, KEY_PROTECTION_SECURE_ELEMENT - /// - [EnumMember(Value = "software")] - SOFTWARE = 1, - /// - /// This flag should be set if the authenticator uses hardware-based key management. Exclusive in authenticator metadata with KEY_PROTECTION_SOFTWARE - /// - [EnumMember(Value = "hardware")] - HARDWARE = 2, - /// - /// This flag should be set if the authenticator uses the Trusted Execution Environment [TEE] for key management. In authenticator metadata, this flag should be set in conjunction with KEY_PROTECTION_HARDWARE. Exclusive in authenticator metadata with KEY_PROTECTION_SOFTWARE, KEY_PROTECTION_SECURE_ELEMENT - /// - [EnumMember(Value = "tee")] - TEE = 4, - /// - /// This flag should be set if the authenticator uses a Secure Element [SecureElement] for key management. In authenticator metadata, this flag should be set in conjunction with KEY_PROTECTION_HARDWARE. Exclusive in authenticator metadata with KEY_PROTECTION_TEE, KEY_PROTECTION_SOFTWARE - /// - [EnumMember(Value = "secure_element")] - SECURE_ELEMENT = 0x8, - /// - /// This flag must be set if the authenticator does not store (wrapped) UAuth keys at the client, but relies on a server-provided key handle. This flag must be set in conjunction with one of the other KEY_PROTECTION flags to indicate how the local key handle wrapping key and operations are protected. Servers may unset this flag in authenticator policy if they are not prepared to store and return key handles, for example, if they have a requirement to respond indistinguishably to authentication attempts against userIDs that do and do not exist. Refer to [UAFProtocol] for more details. - /// - [EnumMember(Value = "remote_handle")] - REMOTE_HANDLE = 0x10, - } + /// + /// This flag must be set if the authenticator uses software-based key management. Exclusive in authenticator metadata with KEY_PROTECTION_HARDWARE, KEY_PROTECTION_TEE, KEY_PROTECTION_SECURE_ELEMENT + /// + [EnumMember(Value = "software")] + SOFTWARE = 1, + /// + /// This flag should be set if the authenticator uses hardware-based key management. Exclusive in authenticator metadata with KEY_PROTECTION_SOFTWARE + /// + [EnumMember(Value = "hardware")] + HARDWARE = 2, + /// + /// This flag should be set if the authenticator uses the Trusted Execution Environment [TEE] for key management. In authenticator metadata, this flag should be set in conjunction with KEY_PROTECTION_HARDWARE. Exclusive in authenticator metadata with KEY_PROTECTION_SOFTWARE, KEY_PROTECTION_SECURE_ELEMENT + /// + [EnumMember(Value = "tee")] + TEE = 4, + /// + /// This flag should be set if the authenticator uses a Secure Element [SecureElement] for key management. In authenticator metadata, this flag should be set in conjunction with KEY_PROTECTION_HARDWARE. Exclusive in authenticator metadata with KEY_PROTECTION_TEE, KEY_PROTECTION_SOFTWARE + /// + [EnumMember(Value = "secure_element")] + SECURE_ELEMENT = 0x8, + /// + /// This flag must be set if the authenticator does not store (wrapped) UAuth keys at the client, but relies on a server-provided key handle. This flag must be set in conjunction with one of the other KEY_PROTECTION flags to indicate how the local key handle wrapping key and operations are protected. Servers may unset this flag in authenticator policy if they are not prepared to store and return key handles, for example, if they have a requirement to respond indistinguishably to authentication attempts against userIDs that do and do not exist. Refer to [UAFProtocol] for more details. + /// + [EnumMember(Value = "remote_handle")] + REMOTE_HANDLE = 0x10, } diff --git a/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs b/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs index 9853d2aa0..9bfafa4cc 100644 --- a/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs +++ b/Src/Fido2.Models/Objects/PublicKeyCredentialDescriptor.cs @@ -1,44 +1,43 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// This object contains the attributes that are specified by a caller when referring to a public key credential as an input parameter to the create() or get() methods. It mirrors the fields of the PublicKeyCredential object returned by the latter methods. +/// Lazy implementation of https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor +/// todo: Should add validation of values as specified in spec +/// +public class PublicKeyCredentialDescriptor { - /// - /// This object contains the attributes that are specified by a caller when referring to a public key credential as an input parameter to the create() or get() methods. It mirrors the fields of the PublicKeyCredential object returned by the latter methods. - /// Lazy implementation of https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor - /// todo: Should add validation of values as specified in spec - /// - public class PublicKeyCredentialDescriptor + public PublicKeyCredentialDescriptor(byte[] credentialId) { - public PublicKeyCredentialDescriptor(byte[] credentialId) - { - Id = credentialId; - } + Id = credentialId; + } - public PublicKeyCredentialDescriptor() - { + public PublicKeyCredentialDescriptor() + { - } + } - /// - /// This member contains the type of the public key credential the caller is referring to. - /// - [JsonPropertyName("type")] - public PublicKeyCredentialType? Type { get; set; } = PublicKeyCredentialType.PublicKey; + /// + /// This member contains the type of the public key credential the caller is referring to. + /// + [JsonPropertyName("type")] + 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")] - public byte[] Id { get; set; } + /// + /// This member contains the credential ID of the public key credential the caller is referring to. + /// + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("id")] + public byte[] Id { get; set; } #nullable enable - /// - /// This OPTIONAL member contains a hint as to how the client might communicate with the managing authenticator of the public key credential the caller is referring to. - /// - [JsonPropertyName("transports")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public AuthenticatorTransport[]? Transports { get; set; } - }; -} + /// + /// This OPTIONAL member contains a hint as to how the client might communicate with the managing authenticator of the public key credential the caller is referring to. + /// + [JsonPropertyName("transports")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AuthenticatorTransport[]? Transports { get; set; } +}; diff --git a/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs b/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs index e4842b62d..bd298d3a6 100644 --- a/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs +++ b/Src/Fido2.Models/Objects/PublicKeyCredentialType.cs @@ -1,16 +1,15 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// PublicKeyCredentialType. +/// https://w3c.github.io/webauthn/#enumdef-publickeycredentialtype +/// +[JsonConverter(typeof(FidoEnumConverter))] +public enum PublicKeyCredentialType { - /// - /// PublicKeyCredentialType. - /// https://w3c.github.io/webauthn/#enumdef-publickeycredentialtype - /// - [JsonConverter(typeof(FidoEnumConverter))] - public enum PublicKeyCredentialType - { - [EnumMember(Value = "public-key")] - PublicKey - } + [EnumMember(Value = "public-key")] + PublicKey } diff --git a/Src/Fido2.Models/Objects/PublicKeyCredentialUserEntity.cs b/Src/Fido2.Models/Objects/PublicKeyCredentialUserEntity.cs index 44f382f96..47d067d0b 100644 --- a/Src/Fido2.Models/Objects/PublicKeyCredentialUserEntity.cs +++ b/Src/Fido2.Models/Objects/PublicKeyCredentialUserEntity.cs @@ -1,18 +1,17 @@ using System.ComponentModel.DataAnnotations; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +public sealed class PublicKeyCredentialUserEntity { - public sealed class PublicKeyCredentialUserEntity - { - [MaxLength(64)] #pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - public byte[] Id { get; set; } + [MaxLength(64)] + public byte[] Id { get; set; } #pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - public string Name { get; set; } + public string Name { get; set; } - public string DisplayName { get; set; } + public string DisplayName { get; set; } - public string Icon { get; set; } - } + public string Icon { get; set; } } diff --git a/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs b/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs index 9ddb8d394..263b3cea1 100644 --- a/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs +++ b/Src/Fido2.Models/Objects/ResidentKeyRequirement.cs @@ -1,31 +1,30 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +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). +/// https://w3c.github.io/webauthn/#enum-residentKeyRequirement +/// +[JsonConverter(typeof(FidoEnumConverter))] +public enum ResidentKeyRequirement { /// - /// This enumeration’s values describe the Relying Party's requirements for client-side discoverable credentials (formerly known as resident credentials or resident keys). - /// https://w3c.github.io/webauthn/#enum-residentKeyRequirement + /// The Relying Party requires a client-side discoverable credential. The client MUST return an error if a client-side discoverable credential cannot be created. /// - [JsonConverter(typeof(FidoEnumConverter))] - public enum ResidentKeyRequirement - { - /// - /// 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, + [EnumMember(Value = "required")] + Required, - /// - /// 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 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 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 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 } diff --git a/Src/Fido2.Models/Objects/UserVerificationRequirement.cs b/Src/Fido2.Models/Objects/UserVerificationRequirement.cs index 39e0ac7ca..dcec64276 100644 --- a/Src/Fido2.Models/Objects/UserVerificationRequirement.cs +++ b/Src/Fido2.Models/Objects/UserVerificationRequirement.cs @@ -1,31 +1,30 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// A WebAuthn Relying Party may require user verification for some of its operations but not for others, and may use this type to express its needs. +/// https://w3c.github.io/webauthn/#enumdef-userverificationrequirement +/// +[JsonConverter(typeof(FidoEnumConverter))] +public enum UserVerificationRequirement { /// - /// A WebAuthn Relying Party may require user verification for some of its operations but not for others, and may use this type to express its needs. - /// https://w3c.github.io/webauthn/#enumdef-userverificationrequirement + /// This value indicates that the Relying Party requires user verification for the operation and will fail the operation if the response does not have the UV flag set. /// - [JsonConverter(typeof(FidoEnumConverter))] - public enum UserVerificationRequirement - { - /// - /// This value indicates that the Relying Party requires user verification for the operation and will fail the operation if the response does not have the UV flag set. - /// - [EnumMember(Value = "required")] - Required, + [EnumMember(Value = "required")] + Required, - /// - /// This value indicates that the Relying Party prefers user verification for the operation if possible, but will not fail the operation if the response does not have the UV flag set. - /// - [EnumMember(Value = "preferred")] - Preferred, + /// + /// This value indicates that the Relying Party prefers user verification for the operation if possible, but will not fail the operation if the response does not have the UV flag set. + /// + [EnumMember(Value = "preferred")] + Preferred, - /// - /// This value indicates that the Relying Party does not want user verification employed during the operation(e.g., in the interest of minimizing disruption to the user interaction flow). - /// - [EnumMember(Value = "discouraged")] - Discouraged - } + /// + /// This value indicates that the Relying Party does not want user verification employed during the operation(e.g., in the interest of minimizing disruption to the user interaction flow). + /// + [EnumMember(Value = "discouraged")] + Discouraged } diff --git a/Src/Fido2.Models/Objects/Version.cs b/Src/Fido2.Models/Objects/Version.cs index c925397f5..d13503d87 100644 --- a/Src/Fido2.Models/Objects/Version.cs +++ b/Src/Fido2.Models/Objects/Version.cs @@ -1,26 +1,25 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Version represents a generic version with major and minor fields. +/// +/// +/// +/// + +public class Version { /// - /// Version represents a generic version with major and minor fields. + /// Major version. /// - /// - /// - /// - - public class Version - { - /// - /// Major version. - /// - [JsonPropertyName("major")] - public ushort Major { get; set; } + [JsonPropertyName("major")] + public ushort Major { get; set; } - /// - /// Minor version. - /// - [JsonPropertyName("minor")] - public ushort Minor { get; set; } - } + /// + /// Minor version. + /// + [JsonPropertyName("minor")] + public ushort Minor { get; set; } } diff --git a/Src/Fido2.Models/StringExtensions.cs b/Src/Fido2.Models/StringExtensions.cs index d073a1d10..f07f11a11 100644 --- a/Src/Fido2.Models/StringExtensions.cs +++ b/Src/Fido2.Models/StringExtensions.cs @@ -1,17 +1,16 @@ using System; -namespace Fido2NetLib +namespace Fido2NetLib; + +public static class StringExtensions { - public static class StringExtensions + public static string ToFullyQualifiedOrigin(this string origin) { - public static string ToFullyQualifiedOrigin(this string origin) - { - var uri = new Uri(origin); + var uri = new Uri(origin); - if (UriHostNameType.Unknown != uri.HostNameType) - return uri.IsDefaultPort ? $"{uri.Scheme}://{uri.Host}" : $"{uri.Scheme}://{uri.Host}:{uri.Port}"; + if (UriHostNameType.Unknown != uri.HostNameType) + return uri.IsDefaultPort ? $"{uri.Scheme}://{uri.Host}" : $"{uri.Scheme}://{uri.Host}:{uri.Port}"; - return origin; - } + return origin; } } diff --git a/Src/Fido2.Models/UndesiredMetdatataStatusFido2VerificationException.cs b/Src/Fido2.Models/UndesiredMetdatataStatusFido2VerificationException.cs index 821be91f6..bf1da3d8a 100644 --- a/Src/Fido2.Models/UndesiredMetdatataStatusFido2VerificationException.cs +++ b/Src/Fido2.Models/UndesiredMetdatataStatusFido2VerificationException.cs @@ -1,24 +1,23 @@ using System; using System.Runtime.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Exception thrown when a new attestation comes from an authenticator with a current reported security issue. +/// +[Serializable] +public class UndesiredMetdatataStatusFido2VerificationException : Fido2VerificationException { - /// - /// Exception thrown when a new attestation comes from an authenticator with a current reported security issue. - /// - [Serializable] - public class UndesiredMetdatataStatusFido2VerificationException : Fido2VerificationException + public UndesiredMetdatataStatusFido2VerificationException(StatusReport statusReport) : base($"Authenticator found with undesirable status. Was {statusReport.Status}") { - public UndesiredMetdatataStatusFido2VerificationException(StatusReport statusReport) : base($"Authenticator found with undesirable status. Was {statusReport.Status}") - { - StatusReport = statusReport; - } + StatusReport = statusReport; + } - protected UndesiredMetdatataStatusFido2VerificationException(SerializationInfo info, StreamingContext context) : base(info, context) { } + protected UndesiredMetdatataStatusFido2VerificationException(SerializationInfo info, StreamingContext context) : base(info, context) { } - /// - /// Status report from the authenticator that caused the attestation to be rejected. - /// - public StatusReport StatusReport { get; } - } + /// + /// Status report from the authenticator that caused the attestation to be rejected. + /// + public StatusReport StatusReport { get; } } diff --git a/Src/Fido2/Asn1Element.cs b/Src/Fido2/Asn1Element.cs index 2b49374b0..86fa2c591 100644 --- a/Src/Fido2/Asn1Element.cs +++ b/Src/Fido2/Asn1Element.cs @@ -3,194 +3,193 @@ using System.Formats.Asn1; using System.Numerics; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal readonly struct Asn1Element { - internal readonly struct Asn1Element + private readonly Asn1Tag _tag; + private readonly ReadOnlyMemory _encodedValue; + private readonly List? _elements; // set | sequence + + public Asn1Element( + Asn1Tag tag, + ReadOnlyMemory encodedValue, + List? elements = null) { - private readonly Asn1Tag _tag; - private readonly ReadOnlyMemory _encodedValue; - private readonly List? _elements; // set | sequence + _tag = tag; + _encodedValue = encodedValue; + _elements = elements; + } - public Asn1Element( - Asn1Tag tag, - ReadOnlyMemory encodedValue, - List? elements = null) - { - _tag = tag; - _encodedValue = encodedValue; - _elements = elements; - } + public IReadOnlyList Sequence + { + get => _elements ?? (IReadOnlyList)Array.Empty(); + } - public IReadOnlyList Sequence - { - get => _elements ?? (IReadOnlyList)Array.Empty(); - } + public Asn1Element this[int index] => Sequence[index]; - public Asn1Element this[int index] => Sequence[index]; + public Asn1Tag Tag => _tag; - public Asn1Tag Tag => _tag; + public int TagValue => _tag.TagValue; - public int TagValue => _tag.TagValue; + public TagClass TagClass => _tag.TagClass; - public TagClass TagClass => _tag.TagClass; + public bool IsSequence => _tag == Asn1Tag.Sequence; - public bool IsSequence => _tag == Asn1Tag.Sequence; + public bool IsInteger => _tag == Asn1Tag.Integer; - public bool IsInteger => _tag == Asn1Tag.Integer; + public bool IsOctetString => _tag == Asn1Tag.PrimitiveOctetString; - public bool IsOctetString => _tag == Asn1Tag.PrimitiveOctetString; + public bool IsConstructed => _tag.IsConstructed; - public bool IsConstructed => _tag.IsConstructed; + internal static Asn1Element CreateSequence(List elements) + { + return new Asn1Element(Asn1Tag.Sequence, Array.Empty(), elements); + } - internal static Asn1Element CreateSequence(List elements) - { - return new Asn1Element(Asn1Tag.Sequence, Array.Empty(), elements); - } + internal static Asn1Element CreateSetOf(List elements) + { + return new Asn1Element(Asn1Tag.SetOf, Array.Empty(), elements); + } - internal static Asn1Element CreateSetOf(List elements) + internal void CheckExactSequenceLength(int length) + { + if (Sequence.Count != length) { - return new Asn1Element(Asn1Tag.SetOf, Array.Empty(), elements); + string s = length != 1 ? "s" : ""; + throw new AsnContentException($"Must have exactly {length} element{s}. Found {Sequence.Count} elements."); } + } - internal void CheckExactSequenceLength(int length) + internal void CheckMinimumSequenceLength(int minimumLength) + { + if (Sequence.Count < minimumLength) { - if (Sequence.Count != length) - { - string s = length != 1 ? "s" : ""; - throw new AsnContentException($"Must have exactly {length} element{s}. Found {Sequence.Count} elements."); - } + string s = minimumLength != 1 ? "s" : ""; + + throw new AsnContentException($"Must have at least {minimumLength} element{s}. Found {Sequence.Count} elements."); } + } - internal void CheckMinimumSequenceLength(int minimumLength) - { - if (Sequence.Count < minimumLength) - { - string s = minimumLength != 1 ? "s" : ""; + public void CheckTag(Asn1Tag tag) + { + if (Tag != tag) + throw new AsnContentException($"Tag must be {tag}. Was {Tag}"); + } - throw new AsnContentException($"Must have at least {minimumLength} element{s}. Found {Sequence.Count} elements."); - } - } + internal void CheckConstructed() + { + if (!IsConstructed) + throw new AsnContentException("Must be constructed"); + } - public void CheckTag(Asn1Tag tag) - { - if (Tag != tag) - throw new AsnContentException($"Tag must be {tag}. Was {Tag}"); - } + internal void CheckPrimitive() + { + if (IsConstructed) + throw new AsnContentException("Must be a primitive"); + } - internal void CheckConstructed() - { - if (!IsConstructed) - throw new AsnContentException("Must be constructed"); - } + internal string GetOID() + { + return AsnDecoder.ReadObjectIdentifier(_encodedValue.Span, AsnEncodingRules.DER, out int _); + } - internal void CheckPrimitive() + internal string GetString() + { + if (TagValue == (int)UniversalTagNumber.UTF8String) { - if (IsConstructed) - throw new AsnContentException("Must be a primitive"); + return AsnDecoder.ReadCharacterString(_encodedValue.Span, AsnEncodingRules.BER, UniversalTagNumber.UTF8String, out _); } - - internal string GetOID() + else { - return AsnDecoder.ReadObjectIdentifier(_encodedValue.Span, AsnEncodingRules.DER, out int _); + throw new Exception("Unknown tag: " + Tag); } + } - internal string GetString() - { - if (TagValue == (int)UniversalTagNumber.UTF8String) - { - return AsnDecoder.ReadCharacterString(_encodedValue.Span, AsnEncodingRules.BER, UniversalTagNumber.UTF8String, out _); - } - else - { - throw new Exception("Unknown tag: " + Tag); - } - } + public BigInteger GetBigInteger() + { + return AsnDecoder.ReadInteger(_encodedValue.Span, AsnEncodingRules.DER, out _); + } - public BigInteger GetBigInteger() - { - return AsnDecoder.ReadInteger(_encodedValue.Span, AsnEncodingRules.DER, out _); - } + public ReadOnlySpan GetIntegerBytes() + { + return AsnDecoder.ReadIntegerBytes(_encodedValue.Span, AsnEncodingRules.DER, out _); + } - public ReadOnlySpan GetIntegerBytes() - { - return AsnDecoder.ReadIntegerBytes(_encodedValue.Span, AsnEncodingRules.DER, out _); - } + public byte[] GetOctetString() + { + return AsnDecoder.ReadOctetString(_encodedValue.Span, AsnEncodingRules.DER, out _); + } + + public byte[] GetOctetString(Asn1Tag expectedTag) + { + return AsnDecoder.ReadOctetString(_encodedValue.Span, AsnEncodingRules.DER, out _, expectedTag); + } + + public int GetInt32() + { + return AsnDecoder.TryReadInt32(_encodedValue.Span, AsnEncodingRules.BER, out int value, out int _) ? value : throw new Exception("Not an integer"); + } - public byte[] GetOctetString() + public byte[] GetBitString() + { + return AsnDecoder.ReadBitString(_encodedValue.Span, AsnEncodingRules.BER, out int _, out int _); + } + + public static Asn1Element Decode(ReadOnlyMemory data) + { + var reader = new AsnReader(data, AsnEncodingRules.BER); + + Asn1Tag tag = reader.PeekTag(); + + if (tag == Asn1Tag.Sequence) { - return AsnDecoder.ReadOctetString(_encodedValue.Span, AsnEncodingRules.DER, out _); + return new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSequence())); } - - public byte[] GetOctetString(Asn1Tag expectedTag) + else if (tag == Asn1Tag.SetOf) { - return AsnDecoder.ReadOctetString(_encodedValue.Span, AsnEncodingRules.DER, out _, expectedTag); + return new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf())); } - - public int GetInt32() + else if (tag.IsConstructed && tag.TagClass is TagClass.ContextSpecific) { - return AsnDecoder.TryReadInt32(_encodedValue.Span, AsnEncodingRules.BER, out int value, out int _) ? value : throw new Exception("Not an integer"); + return new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf(tag))); } - - public byte[] GetBitString() + else { - return AsnDecoder.ReadBitString(_encodedValue.Span, AsnEncodingRules.BER, out int _, out int _); + return new Asn1Element(tag, reader.ReadEncodedValue()); } + } - public static Asn1Element Decode(ReadOnlyMemory data) - { - var reader = new AsnReader(data, AsnEncodingRules.BER); + private static List ReadElements(AsnReader reader) + { + var elements = new List(); + while (reader.HasData) + { Asn1Tag tag = reader.PeekTag(); + Asn1Element el; + if (tag == Asn1Tag.Sequence) { - return new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSequence())); + el = new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSequence())); } else if (tag == Asn1Tag.SetOf) { - return new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf())); + el = new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf())); } else if (tag.IsConstructed && tag.TagClass is TagClass.ContextSpecific) { - return new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf(tag))); + el = new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf(tag))); } else { - return new Asn1Element(tag, reader.ReadEncodedValue()); - } - } - - private static List ReadElements(AsnReader reader) - { - var elements = new List(); - - while (reader.HasData) - { - Asn1Tag tag = reader.PeekTag(); - - Asn1Element el; - - if (tag == Asn1Tag.Sequence) - { - el = new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSequence())); - } - else if (tag == Asn1Tag.SetOf) - { - el = new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf())); - } - else if (tag.IsConstructed && tag.TagClass is TagClass.ContextSpecific) - { - el = new Asn1Element(tag, Array.Empty(), ReadElements(reader.ReadSetOf(tag))); - } - else - { - el = new Asn1Element(tag, reader.ReadEncodedValue()); - } - - elements.Add(el); + el = new Asn1Element(tag, reader.ReadEncodedValue()); } - return elements; + elements.Add(el); } + + return elements; } } diff --git a/Src/Fido2/AttestationFormat/AndroidKey.cs b/Src/Fido2/AttestationFormat/AndroidKey.cs index 799ea6487..c67ed432e 100644 --- a/Src/Fido2/AttestationFormat/AndroidKey.cs +++ b/Src/Fido2/AttestationFormat/AndroidKey.cs @@ -7,220 +7,219 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal sealed class AndroidKey : AttestationVerifier { - internal sealed class AndroidKey : AttestationVerifier + public static byte[]? AttestationExtensionBytes(X509ExtensionCollection exts) { - public static byte[]? AttestationExtensionBytes(X509ExtensionCollection exts) + foreach (var ext in exts) { - foreach (var ext in exts) + if (ext.Oid!.Value is "1.3.6.1.4.1.11129.2.1.17") // AttestationRecordOid { - if (ext.Oid!.Value is "1.3.6.1.4.1.11129.2.1.17") // AttestationRecordOid - { - return ext.RawData; - } + return ext.RawData; } - return null; } + return null; + } - public static byte[] GetAttestationChallenge(byte[] attExtBytes) - { - // https://developer.android.com/training/articles/security-key-attestation#certificate_schema - // attestationChallenge at index 4 - - var keyDescription = Asn1Element.Decode(attExtBytes); - return keyDescription[4].GetOctetString(); - } + public static byte[] GetAttestationChallenge(byte[] attExtBytes) + { + // https://developer.android.com/training/articles/security-key-attestation#certificate_schema + // attestationChallenge at index 4 - public static bool FindAllApplicationsField(byte[] attExtBytes) - { - // https://developer.android.com/training/articles/security-key-attestation#certificate_schema - // check both software and tee enforced AuthorizationList objects for presense of "allApplications" tag, number 600 + var keyDescription = Asn1Element.Decode(attExtBytes); + return keyDescription[4].GetOctetString(); + } - var keyDescription = Asn1Element.Decode(attExtBytes); + public static bool FindAllApplicationsField(byte[] attExtBytes) + { + // https://developer.android.com/training/articles/security-key-attestation#certificate_schema + // check both software and tee enforced AuthorizationList objects for presense of "allApplications" tag, number 600 - var softwareEnforced = keyDescription[6].Sequence; - foreach (Asn1Element s in softwareEnforced) - { - if (s.TagValue is 600) - return true; - } + var keyDescription = Asn1Element.Decode(attExtBytes); - var teeEnforced = keyDescription[7].Sequence; - foreach (Asn1Element s in teeEnforced) - { - if (s.TagValue is 600) - return true; - } + var softwareEnforced = keyDescription[6].Sequence; + foreach (Asn1Element s in softwareEnforced) + { + if (s.TagValue is 600) + return true; + } - return false; + var teeEnforced = keyDescription[7].Sequence; + foreach (Asn1Element s in teeEnforced) + { + if (s.TagValue is 600) + return true; } - public static bool IsOriginGenerated(byte[] attExtBytes) + return false; + } + + public static bool IsOriginGenerated(byte[] attExtBytes) + { + int softwareEnforcedOriginValue = 0; + int teeEnforcedOriginValue = 0; + // https://developer.android.com/training/articles/security-key-attestation#certificate_schema + // origin tag is 702 + var keyDescription = Asn1Element.Decode(attExtBytes); + + var softwareEnforced = keyDescription[6].Sequence; + foreach (Asn1Element s in softwareEnforced) { - int softwareEnforcedOriginValue = 0; - int teeEnforcedOriginValue = 0; - // https://developer.android.com/training/articles/security-key-attestation#certificate_schema - // origin tag is 702 - var keyDescription = Asn1Element.Decode(attExtBytes); - - var softwareEnforced = keyDescription[6].Sequence; - foreach (Asn1Element s in softwareEnforced) + switch (s.TagValue) { - switch (s.TagValue) - { - case 702: - softwareEnforcedOriginValue = s[0].GetInt32(); - break; - default: - break; - } + case 702: + softwareEnforcedOriginValue = s[0].GetInt32(); + break; + default: + break; } - - var teeEnforced = keyDescription[7].Sequence; - foreach (Asn1Element s in teeEnforced) + } + + var teeEnforced = keyDescription[7].Sequence; + foreach (Asn1Element s in teeEnforced) + { + switch (s.TagValue) { - switch (s.TagValue) - { - case 702: - teeEnforcedOriginValue = s[0].GetInt32(); - break; - default: - break; - } + case 702: + teeEnforcedOriginValue = s[0].GetInt32(); + break; + default: + break; } - - return (softwareEnforcedOriginValue is 0 && teeEnforcedOriginValue is 0); } + + return (softwareEnforcedOriginValue is 0 && teeEnforcedOriginValue is 0); + } - public static bool IsPurposeSign(byte[] attExtBytes) + public static bool IsPurposeSign(byte[] attExtBytes) + { + int softwareEnforcedPurposeValue = 2; + int teeEnforcedPurposeValue = 2; + // https://developer.android.com/training/articles/security-key-attestation#certificate_schema + // purpose tag is 1 + var keyDescription = Asn1Element.Decode(attExtBytes); + var softwareEnforced = keyDescription[6].Sequence; + + foreach (Asn1Element s in softwareEnforced) { - int softwareEnforcedPurposeValue = 2; - int teeEnforcedPurposeValue = 2; - // https://developer.android.com/training/articles/security-key-attestation#certificate_schema - // purpose tag is 1 - var keyDescription = Asn1Element.Decode(attExtBytes); - var softwareEnforced = keyDescription[6].Sequence; - - foreach (Asn1Element s in softwareEnforced) + switch (s.TagValue) { - switch (s.TagValue) - { - case 1: - softwareEnforcedPurposeValue = s[0][0].GetInt32(); - break; - default: - break; - } + case 1: + softwareEnforcedPurposeValue = s[0][0].GetInt32(); + break; + default: + break; } + } - var teeEnforced = keyDescription[7].Sequence; - foreach (Asn1Element s in teeEnforced) + var teeEnforced = keyDescription[7].Sequence; + foreach (Asn1Element s in teeEnforced) + { + switch (s.TagValue) { - switch (s.TagValue) - { - case 1: - teeEnforcedPurposeValue = s[0][0].GetInt32(); - break; - default: - break; - } + case 1: + teeEnforcedPurposeValue = s[0][0].GetInt32(); + break; + default: + break; } - - return (softwareEnforcedPurposeValue is 2 && teeEnforcedPurposeValue is 2); } - public override (AttestationType, X509Certificate2[]) Verify() - { - // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields - // (handled in base class) - if (attStmt.Count is 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation format android-key must have attestation statement"); + return (softwareEnforcedPurposeValue is 2 && teeEnforcedPurposeValue is 2); + } - if (!(Sig is CborByteString { Length: > 0 })) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android-key attestation signature"); + public override (AttestationType, X509Certificate2[]) Verify() + { + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields + // (handled in base class) + if (attStmt.Count is 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation format android-key must have attestation statement"); - // 2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash - // using the attestation public key in attestnCert with the algorithm specified in alg - if (!(X5c is CborArray { Length: > 0 } x5cArray)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AndroidKeyAttestation); + if (!(Sig is CborByteString { Length: > 0 })) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android-key attestation signature"); + + // 2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // using the attestation public key in attestnCert with the algorithm specified in alg + if (!(X5c is CborArray { Length: > 0 } x5cArray)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AndroidKeyAttestation); - if (Alg is not CborInteger) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android-key attestation algorithm"); + if (Alg is not CborInteger) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android-key attestation algorithm"); - var alg = (COSE.Algorithm)(int)Alg; - var trustPath = new X509Certificate2[x5cArray.Length]; + var alg = (COSE.Algorithm)(int)Alg; + var trustPath = new X509Certificate2[x5cArray.Length]; - for (int i = 0; i < x5cArray.Length; i++) + for (int i = 0; i < x5cArray.Length; i++) + { + if (x5cArray[i] is CborByteString { Length: > 0 } x5cObject) { - if (x5cArray[i] is CborByteString { Length: > 0 } x5cObject) + try { - try - { - trustPath[i] = new X509Certificate2(x5cObject.Value); - } - catch (Exception ex) when (i is 0) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"Failed to extract public key from android key: {ex.Message}", ex); - } + trustPath[i] = new X509Certificate2(x5cObject.Value); } - else + catch (Exception ex) when (i is 0) { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AndroidKeyAttestation); + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"Failed to extract public key from android key: {ex.Message}", ex); } } - - X509Certificate2 androidKeyCert = trustPath[0]; - ECDsa androidKeyPubKey = androidKeyCert.GetECDsaPublicKey()!; // attestation public key - - byte[] ecsig; - try + else { - ecsig = CryptoUtils.SigFromEcDsaSig((byte[])Sig, androidKeyPubKey.KeySize); - } - catch (Exception ex) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Failed to decode android key attestation signature from ASN.1 encoded form", ex); + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AndroidKeyAttestation); } + } - if (!androidKeyPubKey.VerifyData(Data, ecsig, CryptoUtils.HashAlgFromCOSEAlg(alg))) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android key attestation signature"); + X509Certificate2 androidKeyCert = trustPath[0]; + ECDsa androidKeyPubKey = androidKeyCert.GetECDsaPublicKey()!; // attestation public key + + byte[] ecsig; + try + { + ecsig = CryptoUtils.SigFromEcDsaSig((byte[])Sig, androidKeyPubKey.KeySize); + } + catch (Exception ex) + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Failed to decode android key attestation signature from ASN.1 encoded form", ex); + } - // 3. Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData. - if (!AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, (byte[])Sig)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Incorrect credentialPublicKey in android key attestation"); + if (!androidKeyPubKey.VerifyData(Data, ecsig, CryptoUtils.HashAlgFromCOSEAlg(alg))) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid android key attestation signature"); - // 4. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash - var attExtBytes = AttestationExtensionBytes(androidKeyCert.Extensions); - if (attExtBytes is null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Android key attestation certificate contains no AttestationRecord extension"); + // 3. Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData. + if (!AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, (byte[])Sig)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Incorrect credentialPublicKey in android key attestation"); - try - { - var attestationChallenge = GetAttestationChallenge(attExtBytes); - if (!clientDataHash.AsSpan().SequenceEqual(attestationChallenge)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Mismatch between attestationChallenge and hashedClientDataJson verifying android key attestation certificate extension"); - } - catch (Exception) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Malformed android key AttestationRecord extension verifying android key attestation certificate extension"); - } + // 4. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash + var attExtBytes = AttestationExtensionBytes(androidKeyCert.Extensions); + if (attExtBytes is null) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Android key attestation certificate contains no AttestationRecord extension"); - // 5. Verify the following using the appropriate authorization list from the attestation certificate extension data + try + { + var attestationChallenge = GetAttestationChallenge(attExtBytes); + if (!clientDataHash.AsSpan().SequenceEqual(attestationChallenge)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Mismatch between attestationChallenge and hashedClientDataJson verifying android key attestation certificate extension"); + } + catch (Exception) + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Malformed android key AttestationRecord extension verifying android key attestation certificate extension"); + } - // 5a. The AuthorizationList.allApplications field is not present, since PublicKeyCredential MUST be bound to the RP ID - if (FindAllApplicationsField(attExtBytes)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Found all applications field in android key attestation certificate extension"); + // 5. Verify the following using the appropriate authorization list from the attestation certificate extension data - // 5bi. The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED ( which == 0). - if (!IsOriginGenerated(attExtBytes)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Found origin field not set to KM_ORIGIN_GENERATED in android key attestation certificate extension"); + // 5a. The AuthorizationList.allApplications field is not present, since PublicKeyCredential MUST be bound to the RP ID + if (FindAllApplicationsField(attExtBytes)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Found all applications field in android key attestation certificate extension"); - // 5bii. The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN (which == 2). - if (!IsPurposeSign(attExtBytes)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension"); + // 5bi. The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED ( which == 0). + if (!IsOriginGenerated(attExtBytes)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Found origin field not set to KM_ORIGIN_GENERATED in android key attestation certificate extension"); - return (AttestationType.Basic, trustPath); - } + // 5bii. The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN (which == 2). + if (!IsPurposeSign(attExtBytes)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Found purpose field not set to KM_PURPOSE_SIGN in android key attestation certificate extension"); + + return (AttestationType.Basic, trustPath); } } diff --git a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs index 54442f877..e64895cff 100644 --- a/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs +++ b/Src/Fido2/AttestationFormat/AndroidSafetyNet.cs @@ -14,174 +14,173 @@ using Microsoft.IdentityModel.Tokens; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal sealed class AndroidSafetyNet : AttestationVerifier { - internal sealed class AndroidSafetyNet : AttestationVerifier - { - private const int _driftTolerance = 0; + private const int _driftTolerance = 0; - private static X509Certificate2 GetX509Certificate(string certString) + private static X509Certificate2 GetX509Certificate(string certString) + { + try { - try - { - var certBytes = Convert.FromBase64String(certString); - return new X509Certificate2(certBytes); - } - catch (Exception ex) - { - throw new ArgumentException("Could not parse X509 certificate.", ex); - } + var certBytes = Convert.FromBase64String(certString); + return new X509Certificate2(certBytes); + } + catch (Exception ex) + { + throw new ArgumentException("Could not parse X509 certificate.", ex); } + } - public override (AttestationType, X509Certificate2[]) Verify() + public override (AttestationType, X509Certificate2[]) Verify() + { + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform + // CBOR decoding on it to extract the contained fields + // (handled in base class) + if (!(attStmt["ver"] is CborTextString { Length: > 0 })) { - // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform - // CBOR decoding on it to extract the contained fields - // (handled in base class) - if (!(attStmt["ver"] is CborTextString { Length: > 0 })) - { - throw new Fido2VerificationException("Invalid version in SafetyNet data"); - } + throw new Fido2VerificationException("Invalid version in SafetyNet data"); + } - // 2. Verify that response is a valid SafetyNet response of version ver - var ver = (string)attStmt["ver"]!; + // 2. Verify that response is a valid SafetyNet response of version ver + var ver = (string)attStmt["ver"]!; - if (!(attStmt["response"] is CborByteString { Length: > 0})) - throw new Fido2VerificationException("Invalid response in SafetyNet data"); + if (!(attStmt["response"] is CborByteString { Length: > 0})) + throw new Fido2VerificationException("Invalid response in SafetyNet data"); - var response = (byte[])attStmt["response"]!; - var responseJWT = Encoding.UTF8.GetString(response); + var response = (byte[])attStmt["response"]!; + var responseJWT = Encoding.UTF8.GetString(response); - if (string.IsNullOrWhiteSpace(responseJWT)) - throw new Fido2VerificationException("SafetyNet response null or whitespace"); + if (string.IsNullOrWhiteSpace(responseJWT)) + throw new Fido2VerificationException("SafetyNet response null or whitespace"); - var jwtParts = responseJWT.Split('.'); + var jwtParts = responseJWT.Split('.'); - if (jwtParts.Length != 3) - throw new Fido2VerificationException("SafetyNet response JWT does not have the 3 expected components"); + if (jwtParts.Length != 3) + throw new Fido2VerificationException("SafetyNet response JWT does not have the 3 expected components"); - string jwtHeaderString = jwtParts[0]; + string jwtHeaderString = jwtParts[0]; - using var jwtHeaderJsonDoc = JsonDocument.Parse(Base64Url.Decode(jwtHeaderString)); - var jwtHeaderJson = jwtHeaderJsonDoc.RootElement; + using var jwtHeaderJsonDoc = JsonDocument.Parse(Base64Url.Decode(jwtHeaderString)); + var jwtHeaderJson = jwtHeaderJsonDoc.RootElement; - string[] x5cStrings = jwtHeaderJson.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array - ? x5cEl.ToStringArray() - : throw new Fido2VerificationException("SafetyNet response JWT header missing x5c"); + string[] x5cStrings = jwtHeaderJson.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array + ? x5cEl.ToStringArray() + : throw new Fido2VerificationException("SafetyNet response JWT header missing x5c"); - if (x5cStrings.Length is 0) - throw new Fido2VerificationException("No keys were present in the TOC header in SafetyNet response JWT"); + if (x5cStrings.Length is 0) + throw new Fido2VerificationException("No keys were present in the TOC header in SafetyNet response JWT"); - var certs = new X509Certificate2[x5cStrings.Length]; - var keys = new List(certs.Length); + var certs = new X509Certificate2[x5cStrings.Length]; + var keys = new List(certs.Length); - for (int i = 0; i < certs.Length; i++) - { - var cert = GetX509Certificate(x5cStrings[i]); - certs[i] = cert; - - if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) - { - keys.Add(new ECDsaSecurityKey(ecdsaPublicKey)); - } - else if (cert.GetRSAPublicKey() is RSA rsaPublicKey) - { - keys.Add(new RsaSecurityKey(rsaPublicKey)); - } - } + for (int i = 0; i < certs.Length; i++) + { + var cert = GetX509Certificate(x5cStrings[i]); + certs[i] = cert; - var validationParameters = new TokenValidationParameters + if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - ValidateIssuerSigningKey = true, - IssuerSigningKeys = keys - }; - - var tokenHandler = new JwtSecurityTokenHandler(); - SecurityToken validatedToken; - try - { - tokenHandler.ValidateToken(responseJWT, validationParameters, out validatedToken); + keys.Add(new ECDsaSecurityKey(ecdsaPublicKey)); } - catch (SecurityTokenException ex) + else if (cert.GetRSAPublicKey() is RSA rsaPublicKey) { - throw new Fido2VerificationException("SafetyNet response security token validation failed", ex); + keys.Add(new RsaSecurityKey(rsaPublicKey)); } + } - string? nonce = null; - bool? ctsProfileMatch = null; - DateTimeOffset? timestamp = null; - - var jwtToken = (JwtSecurityToken)validatedToken; + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = keys + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + SecurityToken validatedToken; + try + { + tokenHandler.ValidateToken(responseJWT, validationParameters, out validatedToken); + } + catch (SecurityTokenException ex) + { + throw new Fido2VerificationException("SafetyNet response security token validation failed", ex); + } - foreach (var claim in jwtToken.Claims) - { - if (claim is { Type: "nonce", ValueType: "http://www.w3.org/2001/XMLSchema#string" } && claim.Value.Length != 0) - { - nonce = claim.Value; - } - if (claim is { Type: "ctsProfileMatch", ValueType: "http://www.w3.org/2001/XMLSchema#boolean" }) - { - ctsProfileMatch = bool.Parse(claim.Value); - } - if (claim is { Type: "timestampMs", ValueType: "http://www.w3.org/2001/XMLSchema#integer64" }) - { - timestamp = DateTimeOffset.UnixEpoch.AddMilliseconds(double.Parse(claim.Value, CultureInfo.InvariantCulture)); - } - } + string? nonce = null; + bool? ctsProfileMatch = null; + DateTimeOffset? timestamp = null; - if (!timestamp.HasValue) - { - throw new Fido2VerificationException($"SafetyNet timestampMs not found SafetyNet attestation"); - } + var jwtToken = (JwtSecurityToken)validatedToken; - var notAfter = DateTimeOffset.UtcNow.AddMilliseconds(_driftTolerance); - var notBefore = DateTimeOffset.UtcNow.AddMinutes(-1).AddMilliseconds(-(_driftTolerance)); - if ((notAfter < timestamp) || ((notBefore) > timestamp.Value)) + foreach (var claim in jwtToken.Claims) + { + if (claim is { Type: "nonce", ValueType: "http://www.w3.org/2001/XMLSchema#string" } && claim.Value.Length != 0) { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"SafetyNet timestampMs must be between one minute ago and now, got: {timestamp:o}"); + nonce = claim.Value; } - - // 3. Verify that the nonce in the response is identical to the SHA-256 hash of the concatenation of authenticatorData and clientDataHash - if (string.IsNullOrEmpty(nonce)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Nonce value not found in SafetyNet attestation"); - - byte[] nonceHash; - try + if (claim is { Type: "ctsProfileMatch", ValueType: "http://www.w3.org/2001/XMLSchema#boolean" }) { - nonceHash = Convert.FromBase64String(nonce); + ctsProfileMatch = bool.Parse(claim.Value); } - catch (Exception ex) + if (claim is { Type: "timestampMs", ValueType: "http://www.w3.org/2001/XMLSchema#integer64" }) { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Nonce value not base64string in SafetyNet attestation", ex); + timestamp = DateTimeOffset.UnixEpoch.AddMilliseconds(double.Parse(claim.Value, CultureInfo.InvariantCulture)); } + } - Span dataHash = stackalloc byte[32]; - SHA256.HashData(Data, dataHash); + if (!timestamp.HasValue) + { + throw new Fido2VerificationException($"SafetyNet timestampMs not found SafetyNet attestation"); + } - if (!dataHash.SequenceEqual(nonceHash)) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"SafetyNet response nonce / hash value mismatch, nonce {Convert.ToHexString(nonceHash)}, hash {Convert.ToHexString(dataHash)}"); - } + var notAfter = DateTimeOffset.UtcNow.AddMilliseconds(_driftTolerance); + var notBefore = DateTimeOffset.UtcNow.AddMinutes(-1).AddMilliseconds(-(_driftTolerance)); + if ((notAfter < timestamp) || ((notBefore) > timestamp.Value)) + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"SafetyNet timestampMs must be between one minute ago and now, got: {timestamp:o}"); + } - // 4. Let attestationCert be the attestation certificate - var attestationCert = certs[0]; - var subject = attestationCert.GetNameInfo(X509NameType.DnsName, false); + // 3. Verify that the nonce in the response is identical to the SHA-256 hash of the concatenation of authenticatorData and clientDataHash + if (string.IsNullOrEmpty(nonce)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Nonce value not found in SafetyNet attestation"); - // 5. Verify that the attestation certificate is issued to the hostname "attest.android.com" - if (subject is not "attest.android.com") - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"Invalid SafetyNet attestation cert DnsName. Expected 'attest.android.com'. Was '{subject}'"); + byte[] nonceHash; + try + { + nonceHash = Convert.FromBase64String(nonce); + } + catch (Exception ex) + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Nonce value not base64string in SafetyNet attestation", ex); + } - // 6. Verify that the ctsProfileMatch attribute in the payload of response is true - if (ctsProfileMatch is null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SafetyNet response ctsProfileMatch missing"); - - if (true != ctsProfileMatch) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SafetyNet response ctsProfileMatch false"); + Span dataHash = stackalloc byte[32]; + SHA256.HashData(Data, dataHash); - return (AttestationType.Basic, new X509Certificate2[] { attestationCert }); + if (!dataHash.SequenceEqual(nonceHash)) + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"SafetyNet response nonce / hash value mismatch, nonce {Convert.ToHexString(nonceHash)}, hash {Convert.ToHexString(dataHash)}"); } + + // 4. Let attestationCert be the attestation certificate + var attestationCert = certs[0]; + var subject = attestationCert.GetNameInfo(X509NameType.DnsName, false); + + // 5. Verify that the attestation certificate is issued to the hostname "attest.android.com" + if (subject is not "attest.android.com") + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, $"Invalid SafetyNet attestation cert DnsName. Expected 'attest.android.com'. Was '{subject}'"); + + // 6. Verify that the ctsProfileMatch attribute in the payload of response is true + if (ctsProfileMatch is null) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SafetyNet response ctsProfileMatch missing"); + + if (true != ctsProfileMatch) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SafetyNet response ctsProfileMatch false"); + + return (AttestationType.Basic, new X509Certificate2[] { attestationCert }); } } diff --git a/Src/Fido2/AttestationFormat/Apple.cs b/Src/Fido2/AttestationFormat/Apple.cs index 7c8f1c50e..d1c77b329 100644 --- a/Src/Fido2/AttestationFormat/Apple.cs +++ b/Src/Fido2/AttestationFormat/Apple.cs @@ -7,85 +7,84 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal sealed class Apple : AttestationVerifier { - internal sealed class Apple : AttestationVerifier + public static byte[] GetAppleAttestationExtensionValue(X509ExtensionCollection exts) { - public static byte[] GetAppleAttestationExtensionValue(X509ExtensionCollection exts) + var appleExtension = exts.Cast().FirstOrDefault(static e => e.Oid!.Value is "1.2.840.113635.100.8.2"); + + if (appleExtension is null || appleExtension.RawData is null || appleExtension.RawData.Length < 0x26) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Extension with OID 1.2.840.113635.100.8.2 not found on Apple attestation credCert"); + + try + { + var appleAttestationASN = Asn1Element.Decode(appleExtension.RawData); + appleAttestationASN.CheckTag(new Asn1Tag(UniversalTagNumber.Sequence, isConstructed: true)); + appleAttestationASN.CheckExactSequenceLength(1); + + var appleAttestationASNSequence = appleAttestationASN[0]; + appleAttestationASNSequence.CheckConstructed(); + appleAttestationASNSequence.CheckExactSequenceLength(1); + + appleAttestationASNSequence[0].CheckTag(Asn1Tag.PrimitiveOctetString); + + return appleAttestationASNSequence[0].GetOctetString(); + } + + catch (Exception ex) { - var appleExtension = exts.Cast().FirstOrDefault(static e => e.Oid!.Value is "1.2.840.113635.100.8.2"); - - if (appleExtension is null || appleExtension.RawData is null || appleExtension.RawData.Length < 0x26) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Extension with OID 1.2.840.113635.100.8.2 not found on Apple attestation credCert"); - - try - { - var appleAttestationASN = Asn1Element.Decode(appleExtension.RawData); - appleAttestationASN.CheckTag(new Asn1Tag(UniversalTagNumber.Sequence, isConstructed: true)); - appleAttestationASN.CheckExactSequenceLength(1); - - var appleAttestationASNSequence = appleAttestationASN[0]; - appleAttestationASNSequence.CheckConstructed(); - appleAttestationASNSequence.CheckExactSequenceLength(1); - - appleAttestationASNSequence[0].CheckTag(Asn1Tag.PrimitiveOctetString); - - return appleAttestationASNSequence[0].GetOctetString(); - } - - catch (Exception ex) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Apple attestation extension has invalid data", ex); - } + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Apple attestation extension has invalid data", ex); } + } - public override (AttestationType, X509Certificate2[]) Verify() + public override (AttestationType, X509Certificate2[]) Verify() + { + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields. + if (!(X5c is CborArray { Length: >= 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 })) { - // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields. - if (!(X5c is CborArray { Length: >= 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 })) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AppleAttestation); - } + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_AppleAttestation); + } - // 2. Verify x5c is a valid certificate chain starting from the credCert to the Apple WebAuthn root certificate. - // This happens in AuthenticatorAttestationResponse.VerifyAsync using metadata from MDS3 + // 2. Verify x5c is a valid certificate chain starting from the credCert to the Apple WebAuthn root certificate. + // This happens in AuthenticatorAttestationResponse.VerifyAsync using metadata from MDS3 - var trustPath = new X509Certificate2[x5cArray.Length]; + var trustPath = new X509Certificate2[x5cArray.Length]; - for (int i = 0; i < trustPath.Length; i++) - { - trustPath[i] = new X509Certificate2((byte[])x5cArray[i]); - } + for (int i = 0; i < trustPath.Length; i++) + { + trustPath[i] = new X509Certificate2((byte[])x5cArray[i]); + } - // credCert is the first certificate in the trust path - var credCert = trustPath[0]; + // credCert is the first certificate in the trust path + var credCert = trustPath[0]; - // 3. Concatenate authenticatorData and clientDataHash to form nonceToHash. - ReadOnlySpan nonceToHash = Data; + // 3. Concatenate authenticatorData and clientDataHash to form nonceToHash. + ReadOnlySpan nonceToHash = Data; - // 4. Perform SHA-256 hash of nonceToHash to produce nonce. - Span nonce = stackalloc byte[32]; - SHA256.HashData(nonceToHash, nonce); + // 4. Perform SHA-256 hash of nonceToHash to produce nonce. + Span nonce = stackalloc byte[32]; + SHA256.HashData(nonceToHash, nonce); - // 5. Verify nonce matches the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert. - var appleExtensionBytes = GetAppleAttestationExtensionValue(credCert.Extensions); + // 5. Verify nonce matches the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert. + var appleExtensionBytes = GetAppleAttestationExtensionValue(credCert.Extensions); - if (!nonce.SequenceEqual(appleExtensionBytes)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Mismatch between nonce and credCert attestation extension in Apple attestation"); + if (!nonce.SequenceEqual(appleExtensionBytes)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Mismatch between nonce and credCert attestation extension in Apple attestation"); - // 6. Verify credential public key matches the Subject Public Key of credCert. - // First, obtain COSE algorithm being used from credential public key - var coseAlg = (COSE.Algorithm)(int)CredentialPublicKey[COSE.KeyCommonParameter.Alg]; + // 6. Verify credential public key matches the Subject Public Key of credCert. + // First, obtain COSE algorithm being used from credential public key + var coseAlg = (COSE.Algorithm)(int)CredentialPublicKey[COSE.KeyCommonParameter.Alg]; - // Next, build temporary CredentialPublicKey for comparison from credCert and COSE algorithm - var cpk = new CredentialPublicKey(credCert, coseAlg); + // Next, build temporary CredentialPublicKey for comparison from credCert and COSE algorithm + var cpk = new CredentialPublicKey(credCert, coseAlg); - // Finally, compare byte sequence of CredentialPublicKey built from credCert with byte sequence of CredentialPublicKey from AttestedCredentialData from authData - if (!cpk.GetBytes().AsSpan().SequenceEqual(AuthData.AttestedCredentialData.CredentialPublicKey.GetBytes())) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Credential public key in Apple attestation does not match subject public key of credCert"); + // Finally, compare byte sequence of CredentialPublicKey built from credCert with byte sequence of CredentialPublicKey from AttestedCredentialData from authData + if (!cpk.GetBytes().AsSpan().SequenceEqual(AuthData.AttestedCredentialData.CredentialPublicKey.GetBytes())) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Credential public key in Apple attestation does not match subject public key of credCert"); - // 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c. - return (AttestationType.Basic, trustPath); - } + // 7. If successful, return implementation-specific values representing attestation type Anonymous CA and attestation trust path x5c. + return (AttestationType.Basic, trustPath); } } diff --git a/Src/Fido2/AttestationFormat/AppleAppAttest.cs b/Src/Fido2/AttestationFormat/AppleAppAttest.cs index 657bff2ab..d3bd6b4ba 100644 --- a/Src/Fido2/AttestationFormat/AppleAppAttest.cs +++ b/Src/Fido2/AttestationFormat/AppleAppAttest.cs @@ -6,119 +6,118 @@ using Fido2NetLib.Cbor; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal sealed class AppleAppAttest : AttestationVerifier { - internal sealed class AppleAppAttest : AttestationVerifier + public static byte[] GetAppleAppIdFromCredCertExtValue(X509ExtensionCollection exts) { - public static byte[] GetAppleAppIdFromCredCertExtValue(X509ExtensionCollection exts) - { - var appleExtension = exts.Cast().FirstOrDefault(static e => e.Oid!.Value is "1.2.840.113635.100.8.5"); + var appleExtension = exts.Cast().FirstOrDefault(static e => e.Oid!.Value is "1.2.840.113635.100.8.5"); - if (appleExtension is null || appleExtension.RawData is null) - throw new Fido2VerificationException("Extension with OID 1.2.840.113635.100.8.5 not found on Apple AppAttest credCert"); + if (appleExtension is null || appleExtension.RawData is null) + throw new Fido2VerificationException("Extension with OID 1.2.840.113635.100.8.5 not found on Apple AppAttest credCert"); - var appleAttestationASN = Asn1Element.Decode(appleExtension.RawData); - appleAttestationASN.CheckTag(Asn1Tag.Sequence); - foreach (Asn1Element s in appleAttestationASN.Sequence) + var appleAttestationASN = Asn1Element.Decode(appleExtension.RawData); + appleAttestationASN.CheckTag(Asn1Tag.Sequence); + foreach (Asn1Element s in appleAttestationASN.Sequence) + { + if (s.TagValue is 1204) { - if (s.TagValue is 1204) - { - // App ID is the concatenation of your 10-digit team identifier, a period, and your app's CFBundleIdentifier value - s.CheckExactSequenceLength(1); - s[0].CheckTag(Asn1Tag.PrimitiveOctetString); - return s[0].GetOctetString(); - } + // App ID is the concatenation of your 10-digit team identifier, a period, and your app's CFBundleIdentifier value + s.CheckExactSequenceLength(1); + s[0].CheckTag(Asn1Tag.PrimitiveOctetString); + return s[0].GetOctetString(); } - throw new Fido2VerificationException("Apple AppAttest attestation extension 1.2.840.113635.100.8.5 has invalid data"); - } + } + throw new Fido2VerificationException("Apple AppAttest attestation extension 1.2.840.113635.100.8.5 has invalid data"); + } - // From https://www.apple.com/certificateauthority/Apple_App_Attestation_Root_CA.pem - internal static readonly string appleAppAttestationRootCA = "MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYwJAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNaFw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlvbiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdhNbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9auYen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijVoyFraWVIyd/dganmrduC1bmTBGwD"; + // From https://www.apple.com/certificateauthority/Apple_App_Attestation_Root_CA.pem + internal static readonly string appleAppAttestationRootCA = "MIICITCCAaegAwIBAgIQC/O+DvHN0uD7jG5yH2IXmDAKBggqhkjOPQQDAzBSMSYwJAYDVQQDDB1BcHBsZSBBcHAgQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODMyNTNaFw00NTAzMTUwMDAwMDBaMFIxJjAkBgNVBAMMHUFwcGxlIEFwcCBBdHRlc3RhdGlvbiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERTHhmLW07ATaFQIEVwTtT4dyctdhNbJhFs/Ii2FdCgAHGbpphY3+d8qjuDngIN3WVhQUBHAoMeQ/cLiP1sOUtgjqK9auYen1mMEvRq9Sk3Jm5X8U62H+xTD3FE9TgS41o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSskRBTM72+aEH/pwyp5frq5eWKoTAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwQgFGnByvsiVbpTKwSga0kP0e8EeDS4+sQmTvb7vn53O5+FRXgeLhpJ06ysC5PrOyAjEAp5U4xDgEgllF7En3VcE3iexZZtKeYnpqtijVoyFraWVIyd/dganmrduC1bmTBGwD"; - public static readonly X509Certificate2 AppleAppAttestRootCA = new(Convert.FromBase64String(appleAppAttestationRootCA)); + public static readonly X509Certificate2 AppleAppAttestRootCA = new(Convert.FromBase64String(appleAppAttestationRootCA)); - // From https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server - // "aaguid field is either appattestdevelop if operating in the development environment..." - // 61707061-7474-6573-7464-6576656c6f70 - public static readonly Guid devAaguid = new("61707061-7474-6573-7464-6576656c6f70"); + // From https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server + // "aaguid field is either appattestdevelop if operating in the development environment..." + // 61707061-7474-6573-7464-6576656c6f70 + public static readonly Guid devAaguid = new("61707061-7474-6573-7464-6576656c6f70"); - // "...or appattest followed by seven 0x00 bytes if operating in the production environment" - // 61707061-7474-6573-7400-000000000000 - public static readonly Guid prodAaguid = new("61707061-7474-6573-7400-000000000000"); + // "...or appattest followed by seven 0x00 bytes if operating in the production environment" + // 61707061-7474-6573-7400-000000000000 + public static readonly Guid prodAaguid = new("61707061-7474-6573-7400-000000000000"); - public override (AttestationType, X509Certificate2[]) Verify() + public override (AttestationType, X509Certificate2[]) Verify() + { + // 1. Verify that the x5c array contains the intermediate and leaf certificates for App Attest, starting from the credential certificate in the first data buffer in the array (credcert). + if (!(X5c is CborArray { Length: 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 } && x5cArray[1] is CborByteString { Length: > 0 })) { - // 1. Verify that the x5c array contains the intermediate and leaf certificates for App Attest, starting from the credential certificate in the first data buffer in the array (credcert). - if (!(X5c is CborArray { Length: 2 } x5cArray && x5cArray[0] is CborByteString { Length: > 0 } && x5cArray[1] is CborByteString { Length: > 0 })) - { - throw new Fido2VerificationException("Malformed x5c in Apple AppAttest attestation"); - } - - // Verify the validity of the certificates using Apple's App Attest root certificate. - var chain = new X509Chain(); - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - chain.ChainPolicy.CustomTrustStore.Add(AppleAppAttestRootCA); - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + throw new Fido2VerificationException("Malformed x5c in Apple AppAttest attestation"); + } - X509Certificate2 intermediateCert = new((byte[])x5cArray[1]); - chain.ChainPolicy.ExtraStore.Add(intermediateCert); + // Verify the validity of the certificates using Apple's App Attest root certificate. + var chain = new X509Chain(); + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.CustomTrustStore.Add(AppleAppAttestRootCA); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - X509Certificate2 credCert = new((byte[])x5cArray[0]); - if (AuthData.AttestedCredentialData.AaGuid.Equals(devAaguid)) - { - // Allow expired leaf cert in development environment - chain.ChainPolicy.VerificationTime = credCert.NotBefore.AddSeconds(1); - } + X509Certificate2 intermediateCert = new((byte[])x5cArray[1]); + chain.ChainPolicy.ExtraStore.Add(intermediateCert); - if (!chain.Build(credCert)) - { - throw new Fido2VerificationException("Failed to build chain in Apple AppAttest attestation: " + chain.ChainStatus.FirstOrDefault().StatusInformation.ToString()); - } + X509Certificate2 credCert = new((byte[])x5cArray[0]); + if (AuthData.AttestedCredentialData.AaGuid.Equals(devAaguid)) + { + // Allow expired leaf cert in development environment + chain.ChainPolicy.VerificationTime = credCert.NotBefore.AddSeconds(1); + } - // 2. Create clientDataHash as the SHA256 hash of the one-time challenge your server sends to your app before performing the attestation, and append that hash to the end of the authenticator data (authData from the decoded object). - // 3. Generate a new SHA256 hash of the composite item to create nonce. - // 4. Obtain the value of the credCert extension with OID 1.2.840.113635.100.8.2, which is a DER - encoded ASN.1 sequence.Decode the sequence and extract the single octet string that it contains. Verify that the string equals nonce. - // Steps 2 - 4 done in the "apple" format verifier - Apple apple = new(); - (var attType, var trustPath) = apple.Verify(attStmt, authenticatorData, clientDataHash); - - // 5. Create the SHA256 hash of the public key in credCert, and verify that it matches the key identifier from your app. - Span credCertPKHash = stackalloc byte[32]; - SHA256.HashData(credCert.GetPublicKey(), credCertPKHash); - var keyIdentifier = Convert.FromHexString(credCert.GetNameInfo(X509NameType.SimpleName, false)); - if (!credCertPKHash.SequenceEqual(keyIdentifier)) - { - throw new Fido2VerificationException("Public key hash does not match key identifier in Apple AppAttest attestation"); - } + if (!chain.Build(credCert)) + { + throw new Fido2VerificationException("Failed to build chain in Apple AppAttest attestation: " + chain.ChainStatus.FirstOrDefault().StatusInformation.ToString()); + } - // 6. Compute the SHA256 hash of your app's App ID, and verify that it’s the same as the authenticator data's RP ID hash. - var appId = GetAppleAppIdFromCredCertExtValue(credCert.Extensions); - Span appIdHash = stackalloc byte[32]; - SHA256.HashData(appId, appIdHash); - if (!appIdHash.SequenceEqual(AuthData.RpIdHash)) - { - throw new Fido2VerificationException("App ID hash does not match RP ID hash in Apple AppAttest attestation"); - } + // 2. Create clientDataHash as the SHA256 hash of the one-time challenge your server sends to your app before performing the attestation, and append that hash to the end of the authenticator data (authData from the decoded object). + // 3. Generate a new SHA256 hash of the composite item to create nonce. + // 4. Obtain the value of the credCert extension with OID 1.2.840.113635.100.8.2, which is a DER - encoded ASN.1 sequence.Decode the sequence and extract the single octet string that it contains. Verify that the string equals nonce. + // Steps 2 - 4 done in the "apple" format verifier + Apple apple = new(); + (var attType, var trustPath) = apple.Verify(attStmt, authenticatorData, clientDataHash); + + // 5. Create the SHA256 hash of the public key in credCert, and verify that it matches the key identifier from your app. + Span credCertPKHash = stackalloc byte[32]; + SHA256.HashData(credCert.GetPublicKey(), credCertPKHash); + var keyIdentifier = Convert.FromHexString(credCert.GetNameInfo(X509NameType.SimpleName, false)); + if (!credCertPKHash.SequenceEqual(keyIdentifier)) + { + throw new Fido2VerificationException("Public key hash does not match key identifier in Apple AppAttest attestation"); + } - // 7. Verify that the authenticator data's counter field equals 0. - if (AuthData.SignCount != 0) - { - throw new Fido2VerificationException("Sign count does not equal 0 in Apple AppAttest attestation"); - } + // 6. Compute the SHA256 hash of your app's App ID, and verify that it’s the same as the authenticator data's RP ID hash. + var appId = GetAppleAppIdFromCredCertExtValue(credCert.Extensions); + Span appIdHash = stackalloc byte[32]; + SHA256.HashData(appId, appIdHash); + if (!appIdHash.SequenceEqual(AuthData.RpIdHash)) + { + throw new Fido2VerificationException("App ID hash does not match RP ID hash in Apple AppAttest attestation"); + } - // 8. Verify that the authenticator data's aaguid field is either appattestdevelop if operating in the development environment, or appattest followed by seven 0x00 bytes if operating in the production environment. - if (!AuthData.AttestedCredentialData.AaGuid.Equals(devAaguid) && !AuthData.AttestedCredentialData.AaGuid.Equals(prodAaguid)) - { - throw new Fido2VerificationException("Invalid aaguid encountered in Apple AppAttest attestation"); - } + // 7. Verify that the authenticator data's counter field equals 0. + if (AuthData.SignCount != 0) + { + throw new Fido2VerificationException("Sign count does not equal 0 in Apple AppAttest attestation"); + } - // 9. Verify that the authenticator data's credentialId field is the same as the key identifier. - if (!keyIdentifier.SequenceEqual(AuthData.AttestedCredentialData.CredentialID)) - { - throw new Fido2VerificationException("Mismatch between credentialId and keyIdentifier in Apple AppAttest attestation"); - } + // 8. Verify that the authenticator data's aaguid field is either appattestdevelop if operating in the development environment, or appattest followed by seven 0x00 bytes if operating in the production environment. + if (!AuthData.AttestedCredentialData.AaGuid.Equals(devAaguid) && !AuthData.AttestedCredentialData.AaGuid.Equals(prodAaguid)) + { + throw new Fido2VerificationException("Invalid aaguid encountered in Apple AppAttest attestation"); + } - return (attType, trustPath); + // 9. Verify that the authenticator data's credentialId field is the same as the key identifier. + if (!keyIdentifier.SequenceEqual(AuthData.AttestedCredentialData.CredentialID)) + { + throw new Fido2VerificationException("Mismatch between credentialId and keyIdentifier in Apple AppAttest attestation"); } + + return (attType, trustPath); } } diff --git a/Src/Fido2/AttestationFormat/AttestationFormat.cs b/Src/Fido2/AttestationFormat/AttestationFormat.cs index d25c89e39..ef1cb1706 100644 --- a/Src/Fido2/AttestationFormat/AttestationFormat.cs +++ b/Src/Fido2/AttestationFormat/AttestationFormat.cs @@ -7,84 +7,83 @@ using Fido2NetLib.Cbor; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +public abstract class AttestationVerifier { - public abstract class AttestationVerifier + public CborMap attStmt; + public byte[] authenticatorData; + public byte[] clientDataHash; + + internal CborObject Sig => attStmt["sig"]; + internal CborObject X5c => attStmt["x5c"]; + internal CborObject Alg => attStmt["alg"]; + internal CborObject EcdaaKeyId => attStmt["ecdaaKeyId"]; + internal AuthenticatorData AuthData => new AuthenticatorData(authenticatorData); + internal CborMap CredentialPublicKey => AuthData.AttestedCredentialData.CredentialPublicKey.GetCborObject(); + internal byte[] Data => DataHelper.Concat(authenticatorData, clientDataHash); + + internal static byte[] AaguidFromAttnCertExts(X509ExtensionCollection exts) { - public CborMap attStmt; - public byte[] authenticatorData; - public byte[] clientDataHash; - - internal CborObject Sig => attStmt["sig"]; - internal CborObject X5c => attStmt["x5c"]; - internal CborObject Alg => attStmt["alg"]; - internal CborObject EcdaaKeyId => attStmt["ecdaaKeyId"]; - internal AuthenticatorData AuthData => new AuthenticatorData(authenticatorData); - internal CborMap CredentialPublicKey => AuthData.AttestedCredentialData.CredentialPublicKey.GetCborObject(); - internal byte[] Data => DataHelper.Concat(authenticatorData, clientDataHash); - - internal static byte[] AaguidFromAttnCertExts(X509ExtensionCollection exts) + byte[] aaguid = null; + var ext = exts.Cast().FirstOrDefault(e => e.Oid.Value is "1.3.6.1.4.1.45724.1.1.4"); // id-fido-gen-ce-aaguid + if (ext != null) { - byte[] aaguid = null; - var ext = exts.Cast().FirstOrDefault(e => e.Oid.Value is "1.3.6.1.4.1.45724.1.1.4"); // id-fido-gen-ce-aaguid - if (ext != null) - { - var decodedAaguid = Asn1Element.Decode(ext.RawData); - decodedAaguid.CheckTag(Asn1Tag.PrimitiveOctetString); - aaguid = decodedAaguid.GetOctetString(); - - // The extension MUST NOT be marked as critical - if (ext.Critical) - throw new Fido2VerificationException("extension MUST NOT be marked as critical"); - } + var decodedAaguid = Asn1Element.Decode(ext.RawData); + decodedAaguid.CheckTag(Asn1Tag.PrimitiveOctetString); + aaguid = decodedAaguid.GetOctetString(); - return aaguid; + // The extension MUST NOT be marked as critical + if (ext.Critical) + throw new Fido2VerificationException("extension MUST NOT be marked as critical"); } - internal static bool IsAttnCertCACert(X509ExtensionCollection exts) - { - var ext = exts.Cast().FirstOrDefault(e => e.Oid.Value is "2.5.29.19"); - if (ext is X509BasicConstraintsExtension baseExt) - { - return baseExt.CertificateAuthority; - } - - return true; - } + return aaguid; + } - internal static byte U2FTransportsFromAttnCert(X509ExtensionCollection exts) + internal static bool IsAttnCertCACert(X509ExtensionCollection exts) + { + var ext = exts.Cast().FirstOrDefault(e => e.Oid.Value is "2.5.29.19"); + if (ext is X509BasicConstraintsExtension baseExt) { - var u2ftransports = new byte(); - var ext = exts.Cast().FirstOrDefault(e => e.Oid.Value is "1.3.6.1.4.1.45724.2.1.1"); - if (ext != null) - { - var decodedU2Ftransports = Asn1Element.Decode(ext.RawData); - decodedU2Ftransports.CheckPrimitive(); + return baseExt.CertificateAuthority; + } - // some certificates seem to have this encoded as an octet string - // instead of a bit string, attempt to correct - if (decodedU2Ftransports.Tag == Asn1Tag.PrimitiveOctetString) - { - ext.RawData[0] = (byte)UniversalTagNumber.BitString; - decodedU2Ftransports = Asn1Element.Decode(ext.RawData); - } + return true; + } - decodedU2Ftransports.CheckTag(Asn1Tag.PrimitiveBitString); + internal static byte U2FTransportsFromAttnCert(X509ExtensionCollection exts) + { + var u2ftransports = new byte(); + var ext = exts.Cast().FirstOrDefault(e => e.Oid.Value is "1.3.6.1.4.1.45724.2.1.1"); + if (ext != null) + { + var decodedU2Ftransports = Asn1Element.Decode(ext.RawData); + decodedU2Ftransports.CheckPrimitive(); - u2ftransports = decodedU2Ftransports.GetBitString()[0]; + // some certificates seem to have this encoded as an octet string + // instead of a bit string, attempt to correct + if (decodedU2Ftransports.Tag == Asn1Tag.PrimitiveOctetString) + { + ext.RawData[0] = (byte)UniversalTagNumber.BitString; + decodedU2Ftransports = Asn1Element.Decode(ext.RawData); } - return u2ftransports; - } + decodedU2Ftransports.CheckTag(Asn1Tag.PrimitiveBitString); - public virtual (AttestationType, X509Certificate2[]) Verify(CborMap attStmt, byte[] authenticatorData, byte[] clientDataHash) - { - this.attStmt = attStmt; - this.authenticatorData = authenticatorData; - this.clientDataHash = clientDataHash; - return Verify(); + u2ftransports = decodedU2Ftransports.GetBitString()[0]; } - public abstract (AttestationType, X509Certificate2[]) Verify(); + return u2ftransports; } + + public virtual (AttestationType, X509Certificate2[]) Verify(CborMap attStmt, byte[] authenticatorData, byte[] clientDataHash) + { + this.attStmt = attStmt; + this.authenticatorData = authenticatorData; + this.clientDataHash = clientDataHash; + return Verify(); + } + + public abstract (AttestationType, X509Certificate2[]) Verify(); } diff --git a/Src/Fido2/AttestationFormat/MetadataAttestationType.cs b/Src/Fido2/AttestationFormat/MetadataAttestationType.cs index 3fc4a9e8e..306ed6bb7 100644 --- a/Src/Fido2/AttestationFormat/MetadataAttestationType.cs +++ b/Src/Fido2/AttestationFormat/MetadataAttestationType.cs @@ -1,50 +1,49 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +[JsonConverter(typeof(FidoEnumConverter))] +internal enum MetadataAttestationType { - [JsonConverter(typeof(FidoEnumConverter))] - internal enum MetadataAttestationType - { - /// - /// Indicates full basic attestation, based on an attestation private key shared among a class of authenticators (e.g. same model). - /// Authenticators must provide its attestation signature during the registration process for the same reason. - /// The attestation trust anchor is shared with FIDO Servers out of band (as part of the Metadata). - /// This sharing process shouldt be done according to [UAFMetadataService]. - /// - [EnumMember(Value = "basic_full")] - ATTESTATION_BASIC_FULL = 0x3e07, + /// + /// Indicates full basic attestation, based on an attestation private key shared among a class of authenticators (e.g. same model). + /// Authenticators must provide its attestation signature during the registration process for the same reason. + /// The attestation trust anchor is shared with FIDO Servers out of band (as part of the Metadata). + /// This sharing process shouldt be done according to [UAFMetadataService]. + /// + [EnumMember(Value = "basic_full")] + ATTESTATION_BASIC_FULL = 0x3e07, - /// - /// Just syntactically a Basic Attestation. - /// The attestation object self-signed, i.e. it is signed using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the attestation object. - /// As a consequence it does not provide a cryptographic proof of the security characteristics. - /// But it is the best thing we can do if the authenticator is not able to have an attestation private key. - /// - [EnumMember(Value = "basic_surrogate")] - ATTESTATION_BASIC_SURROGATE = 0x3e08, + /// + /// Just syntactically a Basic Attestation. + /// The attestation object self-signed, i.e. it is signed using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the attestation object. + /// As a consequence it does not provide a cryptographic proof of the security characteristics. + /// But it is the best thing we can do if the authenticator is not able to have an attestation private key. + /// + [EnumMember(Value = "basic_surrogate")] + ATTESTATION_BASIC_SURROGATE = 0x3e08, - /// - /// Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. - /// - [EnumMember(Value = "ecdaa")] - [Fido2Standard(Optional = true)] - ATTESTATION_ECDAA = 0x3e09, + /// + /// Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. + /// + [EnumMember(Value = "ecdaa")] + [Fido2Standard(Optional = true)] + ATTESTATION_ECDAA = 0x3e09, - /// - /// Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. - /// - [EnumMember(Value = "attca")] - [Fido2Standard(Optional = true)] - ATTESTATION_PRIVACY_CA = 0x3e10, + /// + /// Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. + /// + [EnumMember(Value = "attca")] + [Fido2Standard(Optional = true)] + ATTESTATION_PRIVACY_CA = 0x3e10, - /// - /// Anonymization CA (AnonCA) - /// - [EnumMember(Value = "anonca")] - ATTESTATION_ANONCA = 0x3e0c, - - [EnumMember(Value = "none")] - ATTESTATION_NONE = 0x3e0b - } + /// + /// Anonymization CA (AnonCA) + /// + [EnumMember(Value = "anonca")] + ATTESTATION_ANONCA = 0x3e0c, + + [EnumMember(Value = "none")] + ATTESTATION_NONE = 0x3e0b } diff --git a/Src/Fido2/AttestationFormat/None.cs b/Src/Fido2/AttestationFormat/None.cs index e28962f22..e987eb807 100644 --- a/Src/Fido2/AttestationFormat/None.cs +++ b/Src/Fido2/AttestationFormat/None.cs @@ -3,16 +3,15 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +public sealed class None : AttestationVerifier { - public sealed class None : AttestationVerifier + public override (AttestationType, X509Certificate2[]?) Verify() { - public override (AttestationType, X509Certificate2[]?) Verify() - { - if (attStmt.Count != 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation format none should have no attestation statement"); + if (attStmt.Count != 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation format none should have no attestation statement"); - return (AttestationType.None, null); - } + return (AttestationType.None, null); } } diff --git a/Src/Fido2/AttestationFormat/Packed.cs b/Src/Fido2/AttestationFormat/Packed.cs index 2cb1c4cc2..21a131736 100644 --- a/Src/Fido2/AttestationFormat/Packed.cs +++ b/Src/Fido2/AttestationFormat/Packed.cs @@ -6,150 +6,149 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal sealed class Packed : AttestationVerifier { - internal sealed class Packed : AttestationVerifier + public static bool IsValidPackedAttnCertSubject(string attnCertSubj) { - public static bool IsValidPackedAttnCertSubject(string attnCertSubj) - { - // parse the DN string using standard rules - var dictSubjectObj = new X500DistinguishedName(attnCertSubj); + // parse the DN string using standard rules + var dictSubjectObj = new X500DistinguishedName(attnCertSubj); - // form the string for splitting using new lines to avoid issues with commas - string dictSubjectString = dictSubjectObj.Decode(X500DistinguishedNameFlags.UseNewLines); + // form the string for splitting using new lines to avoid issues with commas + string dictSubjectString = dictSubjectObj.Decode(X500DistinguishedNameFlags.UseNewLines); - var dictSubject = new Dictionary(); - - foreach (var line in dictSubjectString.AsSpan().EnumerateLines()) - { - int equalIndex = line.IndexOf('='); + var dictSubject = new Dictionary(); - var lhs = line.Slice(0, equalIndex).ToString(); - var rhs = line.Slice(equalIndex + 1).ToString(); + foreach (var line in dictSubjectString.AsSpan().EnumerateLines()) + { + int equalIndex = line.IndexOf('='); - dictSubject[lhs] = rhs; - } + var lhs = line.Slice(0, equalIndex).ToString(); + var rhs = line.Slice(equalIndex + 1).ToString(); - return dictSubject["C"].Length != 0 - && dictSubject["O"].Length != 0 - && dictSubject["OU"].Length != 0 - && dictSubject["CN"].Length != 0 - && dictSubject["OU"].ToString() is "Authenticator Attestation"; + dictSubject[lhs] = rhs; } - public override (AttestationType, X509Certificate2[]?) Verify() - { - // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and - // perform CBOR decoding on it to extract the contained fields. - if (attStmt.Count is 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation format packed must have attestation statement"); + return dictSubject["C"].Length != 0 + && dictSubject["O"].Length != 0 + && dictSubject["OU"].Length != 0 + && dictSubject["CN"].Length != 0 + && dictSubject["OU"].ToString() is "Authenticator Attestation"; + } - if (!(Sig is CborByteString { Length: > 0 })) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid packed attestation signature"); + public override (AttestationType, X509Certificate2[]?) Verify() + { + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and + // perform CBOR decoding on it to extract the contained fields. + if (attStmt.Count is 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation format packed must have attestation statement"); - if (Alg is not CborInteger) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid packed attestation algorithm"); + if (!(Sig is CborByteString { Length: > 0 })) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid packed attestation signature"); - var alg = (COSE.Algorithm)(int)Alg; + if (Alg is not CborInteger) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid packed attestation algorithm"); - // 2. If x5c is present, this indicates that the attestation type is not ECDAA - if (X5c != null) - { - if (!(X5c is CborArray { Length: > 0 } x5cArray) || EcdaaKeyId != null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Malformed x5c array in packed attestation statement"); + var alg = (COSE.Algorithm)(int)Alg; + + // 2. If x5c is present, this indicates that the attestation type is not ECDAA + if (X5c != null) + { + if (!(X5c is CborArray { Length: > 0 } x5cArray) || EcdaaKeyId != null) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Malformed x5c array in packed attestation statement"); - var trustPath = new X509Certificate2[x5cArray.Length]; + var trustPath = new X509Certificate2[x5cArray.Length]; - for (int i = 0; i < trustPath.Length; i++) + for (int i = 0; i < trustPath.Length; i++) + { + if (X5c[i] is CborByteString { Length: > 0 } x5cObject) { - if (X5c[i] is CborByteString { Length: > 0 } x5cObject) - { - var x5cCert = new X509Certificate2(x5cObject.Value); - - // X509Certificate2.NotBefore/.NotAfter return LOCAL DateTimes, so - // it's correct to compare using DateTime.Now. - if (DateTime.Now < x5cCert.NotBefore || DateTime.Now > x5cCert.NotAfter) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Packed signing certificate expired or not yet valid"); - - trustPath[i] = x5cCert; - } - else - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Malformed x5c cert found in packed attestation statement"); - } - } + var x5cCert = new X509Certificate2(x5cObject.Value); - // The attestation certificate attestnCert MUST be the first element in the array. - X509Certificate2 attestnCert = trustPath[0]; + // X509Certificate2.NotBefore/.NotAfter return LOCAL DateTimes, so + // it's correct to compare using DateTime.Now. + if (DateTime.Now < x5cCert.NotBefore || DateTime.Now > x5cCert.NotAfter) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Packed signing certificate expired or not yet valid"); - // 2a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash - // using the attestation public key in attestnCert with the algorithm specified in alg - var cpk = new CredentialPublicKey(attestnCert, alg); + trustPath[i] = x5cCert; + } + else + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Malformed x5c cert found in packed attestation statement"); + } + } - if (!cpk.Verify(Data, (byte[])Sig)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid full packed signature"); + // The attestation certificate attestnCert MUST be the first element in the array. + X509Certificate2 attestnCert = trustPath[0]; - // Verify that attestnCert meets the requirements in https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements - // 2bi. Version MUST be set to 3 - if (attestnCert.Version != 3) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Packed x5c attestation certificate not V3"); + // 2a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // using the attestation public key in attestnCert with the algorithm specified in alg + var cpk = new CredentialPublicKey(attestnCert, alg); - // 2bii. Subject field MUST contain C, O, OU, CN - // OU must match "Authenticator Attestation" - if (!IsValidPackedAttnCertSubject(attestnCert.Subject)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.InvalidAttestationCertSubject); + if (!cpk.Verify(Data, (byte[])Sig)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid full packed signature"); - // 2biii. If the related attestation root certificate is used for multiple authenticator models, - // the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte OCTET STRING - // verify that the value of this extension matches the aaguid in authenticatorData - var aaguid = AaguidFromAttnCertExts(attestnCert.Extensions); + // Verify that attestnCert meets the requirements in https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements + // 2bi. Version MUST be set to 3 + if (attestnCert.Version != 3) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Packed x5c attestation certificate not V3"); - // 2biiii. The Basic Constraints extension MUST have the CA component set to false - if (IsAttnCertCACert(attestnCert.Extensions)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation certificate has CA cert flag present"); + // 2bii. Subject field MUST contain C, O, OU, CN + // OU must match "Authenticator Attestation" + if (!IsValidPackedAttnCertSubject(attestnCert.Subject)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.InvalidAttestationCertSubject); - // 2c. If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData - if (aaguid != null) - { - if (AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid) != 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aaguid present in packed attestation cert exts but does not match aaguid from authData"); - } + // 2biii. If the related attestation root certificate is used for multiple authenticator models, + // the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte OCTET STRING + // verify that the value of this extension matches the aaguid in authenticatorData + var aaguid = AaguidFromAttnCertExts(attestnCert.Extensions); - // id-fido-u2f-ce-transports - byte u2ftransports = U2FTransportsFromAttnCert(attestnCert.Extensions); + // 2biiii. The Basic Constraints extension MUST have the CA component set to false + if (IsAttnCertCACert(attestnCert.Extensions)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Attestation certificate has CA cert flag present"); - // 2d. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt conveys a Basic or AttCA attestation - - return (AttestationType.AttCa, trustPath); + // 2c. If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData + if (aaguid != null) + { + if (AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid) != 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aaguid present in packed attestation cert exts but does not match aaguid from authData"); } - // 3. If ecdaaKeyId is present, then the attestation type is ECDAA - else if (EcdaaKeyId != null) - { - throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, Fido2ErrorMessages.UnimplementedAlgorithm_Ecdaa_Packed); + // id-fido-u2f-ce-transports + byte u2ftransports = U2FTransportsFromAttnCert(attestnCert.Extensions); - // 3a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash - // using ECDAA-Verify with ECDAA-Issuer public key identified by ecdaaKeyId - // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm + // 2d. Optionally, inspect x5c and consult externally provided knowledge to determine whether attStmt conveys a Basic or AttCA attestation + + return (AttestationType.AttCa, trustPath); + } - // 3b. If successful, return attestation type ECDAA and attestation trust path ecdaaKeyId. - // attnType = AttestationType.ECDAA; - // trustPath = ecdaaKeyId; - } - // 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use - else - { - // 4a. Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData - if (!AuthData.AttestedCredentialData.CredentialPublicKey.IsSameAlg(alg)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Algorithm mismatch between credential public key and authenticator data in self attestation statement"); + // 3. If ecdaaKeyId is present, then the attestation type is ECDAA + else if (EcdaaKeyId != null) + { + throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, Fido2ErrorMessages.UnimplementedAlgorithm_Ecdaa_Packed); - // 4b. Verify that sig is a valid signature over the concatenation of authenticatorData and - // clientDataHash using the credential public key with alg - if (!AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, (byte[])Sig)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Failed to validate signature"); + // 3a. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // using ECDAA-Verify with ECDAA-Issuer public key identified by ecdaaKeyId + // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm - return (AttestationType.Self, null); - } + // 3b. If successful, return attestation type ECDAA and attestation trust path ecdaaKeyId. + // attnType = AttestationType.ECDAA; + // trustPath = ecdaaKeyId; + } + // 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use + else + { + // 4a. Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData + if (!AuthData.AttestedCredentialData.CredentialPublicKey.IsSameAlg(alg)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Algorithm mismatch between credential public key and authenticator data in self attestation statement"); + + // 4b. Verify that sig is a valid signature over the concatenation of authenticatorData and + // clientDataHash using the credential public key with alg + if (!AuthData.AttestedCredentialData.CredentialPublicKey.Verify(Data, (byte[])Sig)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Failed to validate signature"); + + return (AttestationType.Self, null); } } } diff --git a/Src/Fido2/AttestationFormat/Tpm.cs b/Src/Fido2/AttestationFormat/Tpm.cs index 40f5a5518..64317b9ed 100644 --- a/Src/Fido2/AttestationFormat/Tpm.cs +++ b/Src/Fido2/AttestationFormat/Tpm.cs @@ -10,646 +10,645 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal sealed class Tpm : AttestationVerifier { - internal sealed class Tpm : AttestationVerifier + public static readonly HashSet TPMManufacturers = new () { - public static readonly HashSet TPMManufacturers = new () - { - "id:FFFFF1D0", // FIDO testing TPM - // From https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-Vendor-ID-Registry-Version-1.02-Revision-1.00.pdf - "id:414D4400", // 'AMD' AMD - "id:41544D4C", // 'ATML' Atmel - "id:4252434D", // 'BRCM' Broadcom - "id:4353434F", // 'CSCO' Cisco - "id:464C5953", // 'FLYS' Flyslice Technologies - "id:48504500", // 'HPE' HPE - "id:49424d00", // 'IBM' IBM - "id:49465800", // 'IFX' Infinion - "id:494E5443", // 'INTC' Intel - "id:4C454E00", // 'LEN' Lenovo - "id:4D534654", // 'MSFT' Microsoft - "id:4E534D20", // 'NSM' National Semiconductor - "id:4E545A00", // 'NTZ' Nationz - "id:4E544300", // 'NTC' Nuvoton Technology - "id:51434F4D", // 'QCOM' Qualcomm - "id:534D5343", // 'SMSC' SMSC - "id:53544D20", // 'STM ' ST Microelectronics - "id:534D534E", // 'SMSN' Samsung - "id:534E5300", // 'SNS' Sinosun - "id:54584E00", // 'TXN' Texas Instruments - "id:57454300", // 'WEC' Winbond - "id:524F4343", // 'ROCC' Fuzhou Rockchip - "id:474F4F47", // 'GOOG' Google - }; - - public override (AttestationType, X509Certificate2[]) Verify() + "id:FFFFF1D0", // FIDO testing TPM + // From https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-Vendor-ID-Registry-Version-1.02-Revision-1.00.pdf + "id:414D4400", // 'AMD' AMD + "id:41544D4C", // 'ATML' Atmel + "id:4252434D", // 'BRCM' Broadcom + "id:4353434F", // 'CSCO' Cisco + "id:464C5953", // 'FLYS' Flyslice Technologies + "id:48504500", // 'HPE' HPE + "id:49424d00", // 'IBM' IBM + "id:49465800", // 'IFX' Infinion + "id:494E5443", // 'INTC' Intel + "id:4C454E00", // 'LEN' Lenovo + "id:4D534654", // 'MSFT' Microsoft + "id:4E534D20", // 'NSM' National Semiconductor + "id:4E545A00", // 'NTZ' Nationz + "id:4E544300", // 'NTC' Nuvoton Technology + "id:51434F4D", // 'QCOM' Qualcomm + "id:534D5343", // 'SMSC' SMSC + "id:53544D20", // 'STM ' ST Microelectronics + "id:534D534E", // 'SMSN' Samsung + "id:534E5300", // 'SNS' Sinosun + "id:54584E00", // 'TXN' Texas Instruments + "id:57454300", // 'WEC' Winbond + "id:524F4343", // 'ROCC' Fuzhou Rockchip + "id:474F4F47", // 'GOOG' Google + }; + + public override (AttestationType, X509Certificate2[]) Verify() + { + // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields. + // (handled in base class) + if (!(Sig is CborByteString { Length: > 0 })) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid TPM attestation signature"); + + if ((string)attStmt["ver"]! is not "2.0") + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "FIDO2 only supports TPM 2.0"); + + // 2. Verify that the public key specified by the parameters and unique fields of pubArea + // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData + PubArea? pubArea = null; + if (attStmt["pubArea"] is CborByteString { Length: > 0 } pubAreaObject) { - // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields. - // (handled in base class) - if (!(Sig is CborByteString { Length: > 0 })) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid TPM attestation signature"); - - if ((string)attStmt["ver"]! is not "2.0") - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "FIDO2 only supports TPM 2.0"); - - // 2. Verify that the public key specified by the parameters and unique fields of pubArea - // is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData - PubArea? pubArea = null; - if (attStmt["pubArea"] is CborByteString { Length: > 0 } pubAreaObject) - { - pubArea = new PubArea(pubAreaObject.Value); - } + pubArea = new PubArea(pubAreaObject.Value); + } - if (pubArea is null || pubArea.Unique is null || pubArea.Unique.Length is 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Missing or malformed pubArea"); + if (pubArea is null || pubArea.Unique is null || pubArea.Unique.Length is 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Missing or malformed pubArea"); - int coseKty = (int)CredentialPublicKey[COSE.KeyCommonParameter.KeyType]; - if (coseKty is 3) // RSA - { - ReadOnlySpan coseMod = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.N]; // modulus - ReadOnlySpan coseExp = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.E]; // exponent + int coseKty = (int)CredentialPublicKey[COSE.KeyCommonParameter.KeyType]; + if (coseKty is 3) // RSA + { + ReadOnlySpan coseMod = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.N]; // modulus + ReadOnlySpan coseExp = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.E]; // exponent - if (!coseMod.SequenceEqual(pubArea.Unique)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Public key mismatch between pubArea and credentialPublicKey"); + if (!coseMod.SequenceEqual(pubArea.Unique)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Public key mismatch between pubArea and credentialPublicKey"); - if ((coseExp[0] + (coseExp[1] << 8) + (coseExp[2] << 16)) != pubArea.Exponent) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Public key exponent mismatch between pubArea and credentialPublicKey"); - } - else if (coseKty is 2) // ECC - { - var curve = (int)CredentialPublicKey[COSE.KeyTypeParameter.Crv]; - var x = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.X]; - var y = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.Y]; + if ((coseExp[0] + (coseExp[1] << 8) + (coseExp[2] << 16)) != pubArea.Exponent) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Public key exponent mismatch between pubArea and credentialPublicKey"); + } + else if (coseKty is 2) // ECC + { + var curve = (int)CredentialPublicKey[COSE.KeyTypeParameter.Crv]; + var x = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.X]; + var y = (byte[])CredentialPublicKey[COSE.KeyTypeParameter.Y]; - if (pubArea.EccCurve != CoseCurveToTpm[curve]) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Curve mismatch between pubArea and credentialPublicKey"); + if (pubArea.EccCurve != CoseCurveToTpm[curve]) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Curve mismatch between pubArea and credentialPublicKey"); - if (!pubArea.ECPoint.X.AsSpan().SequenceEqual(x)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "X-coordinate mismatch between pubArea and credentialPublicKey"); + if (!pubArea.ECPoint.X.AsSpan().SequenceEqual(x)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "X-coordinate mismatch between pubArea and credentialPublicKey"); - if (!pubArea.ECPoint.Y.AsSpan().SequenceEqual(y)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Y-coordinate mismatch between pubArea and credentialPublicKey"); - } + if (!pubArea.ECPoint.Y.AsSpan().SequenceEqual(y)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Y-coordinate mismatch between pubArea and credentialPublicKey"); + } - // 3. Concatenate authenticatorData and clientDataHash to form attToBeSigned - // See Data field of base class + // 3. Concatenate authenticatorData and clientDataHash to form attToBeSigned + // See Data field of base class - // 4. Validate that certInfo is valid - var certInfo = attStmt["certInfo"] is CborByteString { Length: > 0 } certInfoObject - ? new CertInfo(certInfoObject.Value) - : throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "CertInfo invalid parsing TPM format attStmt"); + // 4. Validate that certInfo is valid + var certInfo = attStmt["certInfo"] is CborByteString { Length: > 0 } certInfoObject + ? new CertInfo(certInfoObject.Value) + : throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "CertInfo invalid parsing TPM format attStmt"); - // 4a. Verify that magic is set to TPM_GENERATED_VALUE - // Handled in CertInfo constructor, see CertInfo.Magic + // 4a. Verify that magic is set to TPM_GENERATED_VALUE + // Handled in CertInfo constructor, see CertInfo.Magic - // 4b. Verify that type is set to TPM_ST_ATTEST_CERTIFY - // Handled in CertInfo constructor, see CertInfo.Type + // 4b. Verify that type is set to TPM_ST_ATTEST_CERTIFY + // Handled in CertInfo constructor, see CertInfo.Type - // 4c. Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" - if (Alg is not CborInteger) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid TPM attestation algorithm"); + // 4c. Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg" + if (Alg is not CborInteger) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid TPM attestation algorithm"); - var alg = (COSE.Algorithm)(int)Alg; + var alg = (COSE.Algorithm)(int)Alg; - ReadOnlySpan dataHash = CryptoUtils.HashData(CryptoUtils.HashAlgFromCOSEAlg(alg), Data); + ReadOnlySpan dataHash = CryptoUtils.HashData(CryptoUtils.HashAlgFromCOSEAlg(alg), Data); - if (!dataHash.SequenceEqual(certInfo.ExtraData)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Hash value mismatch extraData and attToBeSigned"); + if (!dataHash.SequenceEqual(certInfo.ExtraData)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Hash value mismatch extraData and attToBeSigned"); - // 4d. Verify that attested contains a TPMS_CERTIFY_INFO structure, whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea - ReadOnlySpan pubAreaRawHash = CryptoUtils.HashData(CryptoUtils.HashAlgFromCOSEAlg((COSE.Algorithm)certInfo.Alg), pubArea.Raw); + // 4d. Verify that attested contains a TPMS_CERTIFY_INFO structure, whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea + ReadOnlySpan pubAreaRawHash = CryptoUtils.HashData(CryptoUtils.HashAlgFromCOSEAlg((COSE.Algorithm)certInfo.Alg), pubArea.Raw); - if (!pubAreaRawHash.SequenceEqual(certInfo.AttestedName)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Hash value mismatch attested and pubArea"); + if (!pubAreaRawHash.SequenceEqual(certInfo.AttestedName)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Hash value mismatch attested and pubArea"); - // 4e. Note that the remaining fields in the "Standard Attestation Structure" [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion are ignored. These fields MAY be used as an input to risk engines. + // 4e. Note that the remaining fields in the "Standard Attestation Structure" [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion are ignored. These fields MAY be used as an input to risk engines. - // 5. If x5c is present, this indicates that the attestation type is not ECDAA - if (X5c is CborArray { Length: > 0 } x5cArray) - { - var trustPath = new X509Certificate2[x5cArray.Length]; + // 5. If x5c is present, this indicates that the attestation type is not ECDAA + if (X5c is CborArray { Length: > 0 } x5cArray) + { + var trustPath = new X509Certificate2[x5cArray.Length]; - for (int i = 0; i < x5cArray.Length; i++) + for (int i = 0; i < x5cArray.Length; i++) + { + if (x5cArray[i] is CborByteString { Length: > 0 } x5cObject) { - if (x5cArray[i] is CborByteString { Length: > 0 } x5cObject) - { - trustPath[i] = new X509Certificate2(x5cObject.Value); - } - else - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_TpmAttestation); - } + trustPath[i] = new X509Certificate2(x5cObject.Value); } + else + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, Fido2ErrorMessages.MalformedX5c_TpmAttestation); + } + } - // 5a. Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg. - X509Certificate2 aikCert = trustPath[0]; - - var cpk = new CredentialPublicKey(aikCert, alg); + // 5a. Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg. + X509Certificate2 aikCert = trustPath[0]; - if (!cpk.Verify(certInfo.Raw, (byte[])Sig)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Bad signature in TPM with aikCert"); + var cpk = new CredentialPublicKey(aikCert, alg); - // 5b. Verify that aikCert meets the TPM attestation statement certificate requirements - // https://www.w3.org/TR/webauthn/#tpm-cert-requirements - // 5bi. Version MUST be set to 3 - if (aikCert.Version != 3) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert must be V3"); + if (!cpk.Verify(certInfo.Raw, (byte[])Sig)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Bad signature in TPM with aikCert"); - // 5bii. Subject field MUST be set to empty - they actually mean subject name - if (aikCert.SubjectName.Name.Length != 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert subject must be empty"); + // 5b. Verify that aikCert meets the TPM attestation statement certificate requirements + // https://www.w3.org/TR/webauthn/#tpm-cert-requirements + // 5bi. Version MUST be set to 3 + if (aikCert.Version != 3) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert must be V3"); - // 5biii. The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. - // https://www.w3.org/TR/webauthn/#tpm-cert-requirements - (string? tpmManufacturer, string? tpmModel, string? tpmVersion) = SANFromAttnCertExts(aikCert.Extensions); + // 5bii. Subject field MUST be set to empty - they actually mean subject name + if (aikCert.SubjectName.Name.Length != 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert subject must be empty"); - // From https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf - // "The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryName - // form within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device - // Attributes. In accordance with RFC 5280[11], this extension MUST be critical if subject is empty - // and SHOULD be non-critical if subject is non-empty" + // 5biii. The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. + // https://www.w3.org/TR/webauthn/#tpm-cert-requirements + (string? tpmManufacturer, string? tpmModel, string? tpmVersion) = SANFromAttnCertExts(aikCert.Extensions); - // Best I can figure to do for now? - if (string.IsNullOrEmpty(tpmManufacturer) || - string.IsNullOrEmpty(tpmModel) || - string.IsNullOrEmpty(tpmVersion)) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SAN missing TPMManufacturer, TPMModel, or TPMVersion from TPM attestation certificate"); - } + // From https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf + // "The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryName + // form within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device + // Attributes. In accordance with RFC 5280[11], this extension MUST be critical if subject is empty + // and SHOULD be non-critical if subject is non-empty" - if (!TPMManufacturers.Contains(tpmManufacturer)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid TPM manufacturer found parsing TPM attestation"); + // Best I can figure to do for now? + if (string.IsNullOrEmpty(tpmManufacturer) || + string.IsNullOrEmpty(tpmModel) || + string.IsNullOrEmpty(tpmVersion)) + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SAN missing TPMManufacturer, TPMModel, or TPMVersion from TPM attestation certificate"); + } - // 5biiii. The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID. - // OID is 2.23.133.8.3 - bool eku = EKUFromAttnCertExts(aikCert.Extensions, "2.23.133.8.3"); + if (!TPMManufacturers.Contains(tpmManufacturer)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Invalid TPM manufacturer found parsing TPM attestation"); - if (!eku) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert EKU missing tcg-kp-AIKCertificate OID"); + // 5biiii. The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID. + // OID is 2.23.133.8.3 + bool eku = EKUFromAttnCertExts(aikCert.Extensions, "2.23.133.8.3"); - // 5biiiii. The Basic Constraints extension MUST have the CA component set to false. - if (IsAttnCertCACert(aikCert.Extensions)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert Basic Constraints extension CA component must be false"); + if (!eku) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert EKU missing tcg-kp-AIKCertificate OID"); - // 5biiiiii. An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] - // are both OPTIONAL as the status of many attestation certificates is available through metadata services. - // See, for example, the FIDO Metadata Service [FIDOMetadataService]. + // 5biiiii. The Basic Constraints extension MUST have the CA component set to false. + if (IsAttnCertCACert(aikCert.Extensions)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "aikCert Basic Constraints extension CA component must be false"); - // 5c. If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData - if (AaguidFromAttnCertExts(aikCert.Extensions) is byte[] aaguid && - (!aaguid.AsSpan().SequenceEqual(Guid.Empty.ToByteArray())) && - (AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid) != 0)) - { - throw new Fido2VerificationException($"aaguid malformed, expected {AuthData.AttestedCredentialData.AaGuid}, got {new Guid(aaguid)}"); - } + // 5biiiiii. An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] + // are both OPTIONAL as the status of many attestation certificates is available through metadata services. + // See, for example, the FIDO Metadata Service [FIDOMetadataService]. - return (AttestationType.AttCa, trustPath); - } - // If ecdaaKeyId is present, then the attestation type is ECDAA - else if (EcdaaKeyId != null) - { - throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, Fido2ErrorMessages.UnimplementedAlgorithm_Ecdaa_Tpm); - - // Perform ECDAA-Verify on sig to verify that it is a valid signature over certInfo - // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm - - // If successful, return attestation type ECDAA and the identifier of the ECDAA-Issuer public key ecdaaKeyId. - // attnType = AttestationType.ECDAA; - // trustPath = ecdaaKeyId; - } - else + // 5c. If aikCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData + if (AaguidFromAttnCertExts(aikCert.Extensions) is byte[] aaguid && + (!aaguid.AsSpan().SequenceEqual(Guid.Empty.ToByteArray())) && + (AttestedCredentialData.FromBigEndian(aaguid).CompareTo(AuthData.AttestedCredentialData.AaGuid) != 0)) { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Neither x5c nor ECDAA were found in the TPM attestation statement"); + throw new Fido2VerificationException($"aaguid malformed, expected {AuthData.AttestedCredentialData.AaGuid}, got {new Guid(aaguid)}"); } - } - private static readonly Dictionary CoseCurveToTpm = new () + return (AttestationType.AttCa, trustPath); + } + // If ecdaaKeyId is present, then the attestation type is ECDAA + else if (EcdaaKeyId != null) { - { 1, TpmEccCurve.TPM_ECC_NIST_P256}, - { 2, TpmEccCurve.TPM_ECC_NIST_P384}, - { 3, TpmEccCurve.TPM_ECC_NIST_P521} - }; + throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, Fido2ErrorMessages.UnimplementedAlgorithm_Ecdaa_Tpm); - private static (string?, string?, string?) SANFromAttnCertExts(X509ExtensionCollection extensions) + // Perform ECDAA-Verify on sig to verify that it is a valid signature over certInfo + // https://www.w3.org/TR/webauthn/#biblio-fidoecdaaalgorithm + + // If successful, return attestation type ECDAA and the identifier of the ECDAA-Issuer public key ecdaaKeyId. + // attnType = AttestationType.ECDAA; + // trustPath = ecdaaKeyId; + } + else { - string? tpmManufacturer = null; - string? tpmModel = null; - string? tpmVersion = null; + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "Neither x5c nor ECDAA were found in the TPM attestation statement"); + } + } - var foundSAN = false; + private static readonly Dictionary CoseCurveToTpm = new () + { + { 1, TpmEccCurve.TPM_ECC_NIST_P256}, + { 2, TpmEccCurve.TPM_ECC_NIST_P384}, + { 3, TpmEccCurve.TPM_ECC_NIST_P521} + }; + + private static (string?, string?, string?) SANFromAttnCertExts(X509ExtensionCollection extensions) + { + string? tpmManufacturer = null; + string? tpmModel = null; + string? tpmVersion = null; - foreach (var extension in extensions) + var foundSAN = false; + + foreach (var extension in extensions) + { + if (extension.Oid!.Value is "2.5.29.17") // subject alternative name { - if (extension.Oid!.Value is "2.5.29.17") // subject alternative name - { - if (extension.RawData.Length is 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SAN missing from TPM attestation certificate"); + if (extension.RawData.Length is 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SAN missing from TPM attestation certificate"); - foundSAN = true; + foundSAN = true; - var subjectAlternativeName = Asn1Element.Decode(extension.RawData); - subjectAlternativeName.CheckTag(new Asn1Tag(UniversalTagNumber.Sequence, isConstructed: true)); - subjectAlternativeName.CheckMinimumSequenceLength(1); + var subjectAlternativeName = Asn1Element.Decode(extension.RawData); + subjectAlternativeName.CheckTag(new Asn1Tag(UniversalTagNumber.Sequence, isConstructed: true)); + subjectAlternativeName.CheckMinimumSequenceLength(1); - if (subjectAlternativeName.Sequence.FirstOrDefault(o => o is { TagClass: TagClass.ContextSpecific, TagValue: 4 /*Octet-String */ }) is Asn1Element generalName) + if (subjectAlternativeName.Sequence.FirstOrDefault(o => o is { TagClass: TagClass.ContextSpecific, TagValue: 4 /*Octet-String */ }) is Asn1Element generalName) + { + generalName.CheckConstructed(); + generalName.CheckExactSequenceLength(1); + + var nameSequence = generalName[0]; + nameSequence.CheckTag(new Asn1Tag(UniversalTagNumber.Sequence, isConstructed: true)); + nameSequence.CheckMinimumSequenceLength(1); + + /* + + Per Trusted Computing Group Endorsement Key Credential Profile section 3.2.9: + + "The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryName-form within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device Attributes." + + An example is provided in document section A.1 Example 1: + + // SEQUENCE + 30 49 + // SET + 31 16 + // SEQUENCE + 30 14 + // OBJECT IDENTIFER tcg-at-tpmManufacturer (2.23.133.2.1) + 06 05 67 81 05 02 01 + // UTF8 STRING id:54434700 (TCG) + 0C 0B 69 64 3A 35 34 34 33 34 37 30 30 + // SET + 31 17 + // SEQUENCE + 30 15 + // OBJECT IDENTIFER tcg-at-tpmModel (2.23.133.2.2) + 06 05 67 81 05 02 02 + // UTF8 STRING ABCDEF123456 + 0C 0C 41 42 43 44 45 46 31 32 33 34 35 36 + // SET + 31 16 + // SEQUENCE + 30 14 + // OBJECT IDENTIFER tcg-at-tpmVersion (2.23.133.2.3) + 06 05 67 81 05 02 03 + // UTF8 STRING id:00010023 + 0C 0B 69 64 3A 30 30 30 31 30 30 32 33 + + Some TPM implementations place each device attributes SEQUENCE within a single SET instead of each in its own SET. + + This detects this condition and repacks each devices attributes SEQUENCE into its own SET to conform with TCG spec. + + */ + + var deviceAttributes = nameSequence.Sequence; + + if (deviceAttributes[0].Sequence.Count != 1) { - generalName.CheckConstructed(); - generalName.CheckExactSequenceLength(1); - - var nameSequence = generalName[0]; - nameSequence.CheckTag(new Asn1Tag(UniversalTagNumber.Sequence, isConstructed: true)); - nameSequence.CheckMinimumSequenceLength(1); - - /* - - Per Trusted Computing Group Endorsement Key Credential Profile section 3.2.9: - - "The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryName-form within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device Attributes." - - An example is provided in document section A.1 Example 1: - - // SEQUENCE - 30 49 - // SET - 31 16 - // SEQUENCE - 30 14 - // OBJECT IDENTIFER tcg-at-tpmManufacturer (2.23.133.2.1) - 06 05 67 81 05 02 01 - // UTF8 STRING id:54434700 (TCG) - 0C 0B 69 64 3A 35 34 34 33 34 37 30 30 - // SET - 31 17 - // SEQUENCE - 30 15 - // OBJECT IDENTIFER tcg-at-tpmModel (2.23.133.2.2) - 06 05 67 81 05 02 02 - // UTF8 STRING ABCDEF123456 - 0C 0C 41 42 43 44 45 46 31 32 33 34 35 36 - // SET - 31 16 - // SEQUENCE - 30 14 - // OBJECT IDENTIFER tcg-at-tpmVersion (2.23.133.2.3) - 06 05 67 81 05 02 03 - // UTF8 STRING id:00010023 - 0C 0B 69 64 3A 30 30 30 31 30 30 32 33 - - Some TPM implementations place each device attributes SEQUENCE within a single SET instead of each in its own SET. - - This detects this condition and repacks each devices attributes SEQUENCE into its own SET to conform with TCG spec. - - */ - - var deviceAttributes = nameSequence.Sequence; - - if (deviceAttributes[0].Sequence.Count != 1) + var wrappedElements = new List(deviceAttributes[0].Sequence.Count); + + foreach (Asn1Element o in deviceAttributes[0].Sequence) { - var wrappedElements = new List(deviceAttributes[0].Sequence.Count); + wrappedElements.Add(Asn1Element.CreateSetOf(new List(1) { + Asn1Element.CreateSequence((List)o.Sequence) + })); + } - foreach (Asn1Element o in deviceAttributes[0].Sequence) - { - wrappedElements.Add(Asn1Element.CreateSetOf(new List(1) { - Asn1Element.CreateSequence((List)o.Sequence) - })); - } + deviceAttributes = wrappedElements; + } - deviceAttributes = wrappedElements; - } + foreach (Asn1Element propertySet in deviceAttributes) + { + propertySet.CheckTag(Asn1Tag.SetOf); + propertySet.CheckExactSequenceLength(1); + + var propertySequence = propertySet[0]; + propertySequence.CheckTag(Asn1Tag.Sequence); + propertySequence.CheckExactSequenceLength(2); + + var propertyOid = propertySequence[0]; + propertyOid.CheckTag(Asn1Tag.ObjectIdentifier); + + var propertyValue = propertySequence[1]; + propertyValue.CheckTag(new Asn1Tag(UniversalTagNumber.UTF8String)); - foreach (Asn1Element propertySet in deviceAttributes) + switch (propertyOid.GetOID()) { - propertySet.CheckTag(Asn1Tag.SetOf); - propertySet.CheckExactSequenceLength(1); - - var propertySequence = propertySet[0]; - propertySequence.CheckTag(Asn1Tag.Sequence); - propertySequence.CheckExactSequenceLength(2); - - var propertyOid = propertySequence[0]; - propertyOid.CheckTag(Asn1Tag.ObjectIdentifier); - - var propertyValue = propertySequence[1]; - propertyValue.CheckTag(new Asn1Tag(UniversalTagNumber.UTF8String)); - - switch (propertyOid.GetOID()) - { - case "2.23.133.2.1": - tpmManufacturer = propertyValue.GetString(); - break; - case "2.23.133.2.2": - tpmModel = propertyValue.GetString(); - break; - case "2.23.133.2.3": - tpmVersion = propertyValue.GetString(); - break; - default: - continue; - } + case "2.23.133.2.1": + tpmManufacturer = propertyValue.GetString(); + break; + case "2.23.133.2.2": + tpmModel = propertyValue.GetString(); + break; + case "2.23.133.2.3": + tpmVersion = propertyValue.GetString(); + break; + default: + continue; } } - - break; } + + break; } + } - if (!foundSAN) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SAN missing from TPM attestation certificate"); + if (!foundSAN) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestation, "SAN missing from TPM attestation certificate"); - return (tpmManufacturer, tpmModel, tpmVersion); - } + return (tpmManufacturer, tpmModel, tpmVersion); + } - private static bool EKUFromAttnCertExts(X509ExtensionCollection exts, string expectedEnhancedKeyUsages) + private static bool EKUFromAttnCertExts(X509ExtensionCollection exts, string expectedEnhancedKeyUsages) + { + foreach (var ext in exts) { - foreach (var ext in exts) + if (ext.Oid!.Value is "2.5.29.37" && ext is X509EnhancedKeyUsageExtension enhancedKeyUsageExtension) { - if (ext.Oid!.Value is "2.5.29.37" && ext is X509EnhancedKeyUsageExtension enhancedKeyUsageExtension) + foreach (var oid in enhancedKeyUsageExtension.EnhancedKeyUsages) { - foreach (var oid in enhancedKeyUsageExtension.EnhancedKeyUsages) - { - if (expectedEnhancedKeyUsages.Equals(oid.Value, StringComparison.Ordinal)) - return true; - } - + if (expectedEnhancedKeyUsages.Equals(oid.Value, StringComparison.Ordinal)) + return true; } + } - return false; } + return false; } +} - public enum TpmEccCurve : ushort - { - // TCG TPM Rev 2.0, part 2, structures, section 6.4, TPM_ECC_CURVE - TPM_ECC_NONE, // 0x0000 - TPM_ECC_NIST_P192, // 0x0001 - TPM_ECC_NIST_P224, // 0x0002 - TPM_ECC_NIST_P256, // 0x0003 - TPM_ECC_NIST_P384, // 0x0004 - TPM_ECC_NIST_P521, // 0x0005 - TPM_ECC_BN_P256, // 0x0010 curve to support ECDAA - TPM_ECC_BN_P638, // 0x0011 curve to support ECDAA - TPM_ECC_SM2_P256 // 0x0020 - } +public enum TpmEccCurve : ushort +{ + // TCG TPM Rev 2.0, part 2, structures, section 6.4, TPM_ECC_CURVE + TPM_ECC_NONE, // 0x0000 + TPM_ECC_NIST_P192, // 0x0001 + TPM_ECC_NIST_P224, // 0x0002 + TPM_ECC_NIST_P256, // 0x0003 + TPM_ECC_NIST_P384, // 0x0004 + TPM_ECC_NIST_P521, // 0x0005 + TPM_ECC_BN_P256, // 0x0010 curve to support ECDAA + TPM_ECC_BN_P638, // 0x0011 curve to support ECDAA + TPM_ECC_SM2_P256 // 0x0020 +} - public enum TpmAlg : ushort +public enum TpmAlg : ushort +{ + // TCG TPM Rev 2.0, part 2, structures, section 6.3, TPM_ALG_ID + TPM_ALG_ERROR, // 0 + TPM_ALG_RSA, // 1 + TPM_ALG_SHA1 = 4, // 4 + TPM_ALG_HMAC, // 5 + TPM_ALG_AES, // 6 + TPM_ALG_MGF1, // 7 + TPM_ALG_KEYEDHASH, // 8 + TPM_ALG_XOR = 0xA, // A + TPM_ALG_SHA256, // B + TPM_ALG_SHA384, // C + TPM_ALG_SHA512, // D + TPM_ALG_NULL = 0x10, // 10 + TPM_ALG_SM3_256 = 0x12, // 12 + TPM_ALG_SM4, // 13 + TPM_ALG_RSASSA, // 14 + TPM_ALG_RSAES, // 15 + TPM_ALG_RSAPSS, // 16 + TPM_ALG_OAEP, // 17 + TPM_ALG_ECDSA, // 18 + TPM_ALG_ECDH, // 19 + TPM_ALG_ECDAA, // 1A + TPM_ALG_SM2, // 1B + TPM_ALG_ECSCHNORR, // 1C + TPM_ALG_ECMQV, // 1D + TPM_ALG_KDF1_SP800_56A = 0x20, + TPM_ALG_KDF2, // 21 + TPM_ALG_KDF1_SP800_108, // 22 + TPM_ALG_ECC, // 23 + TPM_ALG_SYMCIPHER = 0x25, + TPM_ALG_CAMELLIA, // 26 + TPM_ALG_CTR = 0x40, + TPM_ALG_OFB, // 41 + TPM_ALG_CBC, // 42 + TPM_ALG_CFB, // 43 + TPM_ALG_ECB // 44 +}; + +// TPMS_ATTEST, TPMv2-Part2, section 10.12.8 +public class CertInfo +{ + private static readonly Dictionary tpmAlgToDigestSizeMap = new() { - // TCG TPM Rev 2.0, part 2, structures, section 6.3, TPM_ALG_ID - TPM_ALG_ERROR, // 0 - TPM_ALG_RSA, // 1 - TPM_ALG_SHA1 = 4, // 4 - TPM_ALG_HMAC, // 5 - TPM_ALG_AES, // 6 - TPM_ALG_MGF1, // 7 - TPM_ALG_KEYEDHASH, // 8 - TPM_ALG_XOR = 0xA, // A - TPM_ALG_SHA256, // B - TPM_ALG_SHA384, // C - TPM_ALG_SHA512, // D - TPM_ALG_NULL = 0x10, // 10 - TPM_ALG_SM3_256 = 0x12, // 12 - TPM_ALG_SM4, // 13 - TPM_ALG_RSASSA, // 14 - TPM_ALG_RSAES, // 15 - TPM_ALG_RSAPSS, // 16 - TPM_ALG_OAEP, // 17 - TPM_ALG_ECDSA, // 18 - TPM_ALG_ECDH, // 19 - TPM_ALG_ECDAA, // 1A - TPM_ALG_SM2, // 1B - TPM_ALG_ECSCHNORR, // 1C - TPM_ALG_ECMQV, // 1D - TPM_ALG_KDF1_SP800_56A = 0x20, - TPM_ALG_KDF2, // 21 - TPM_ALG_KDF1_SP800_108, // 22 - TPM_ALG_ECC, // 23 - TPM_ALG_SYMCIPHER = 0x25, - TPM_ALG_CAMELLIA, // 26 - TPM_ALG_CTR = 0x40, - TPM_ALG_OFB, // 41 - TPM_ALG_CBC, // 42 - TPM_ALG_CFB, // 43 - TPM_ALG_ECB // 44 + { TpmAlg.TPM_ALG_SHA1, (160/8) }, + { TpmAlg.TPM_ALG_SHA256, (256/8) }, + { TpmAlg.TPM_ALG_SHA384, (384/8) }, + { TpmAlg.TPM_ALG_SHA512, (512/8) } }; - // TPMS_ATTEST, TPMv2-Part2, section 10.12.8 - public class CertInfo + public static (ushort size, byte[] name) NameFromTPM2BName(ReadOnlySpan ab, ref int offset) { - private static readonly Dictionary tpmAlgToDigestSizeMap = new() + // TCG TPM Rev 2.0, part 2, structures, section 10.5.3, TPM2B_NAME + // This buffer holds a Name for any entity type. + // The type of Name in the structure is determined by context and the size parameter. + ushort totalSize = 0; + if (AuthDataHelper.GetSizedByteArray(ab, ref offset, 2) is byte[] totalBytes) { - { TpmAlg.TPM_ALG_SHA1, (160/8) }, - { TpmAlg.TPM_ALG_SHA256, (256/8) }, - { TpmAlg.TPM_ALG_SHA384, (384/8) }, - { TpmAlg.TPM_ALG_SHA512, (512/8) } - }; + totalSize = BinaryPrimitives.ReadUInt16BigEndian(totalBytes); + } - public static (ushort size, byte[] name) NameFromTPM2BName(ReadOnlySpan ab, ref int offset) + ushort size = 0; + var bytes = AuthDataHelper.GetSizedByteArray(ab, ref offset, 2); + if (bytes != null) { - // TCG TPM Rev 2.0, part 2, structures, section 10.5.3, TPM2B_NAME - // This buffer holds a Name for any entity type. - // The type of Name in the structure is determined by context and the size parameter. - ushort totalSize = 0; - if (AuthDataHelper.GetSizedByteArray(ab, ref offset, 2) is byte[] totalBytes) - { - totalSize = BinaryPrimitives.ReadUInt16BigEndian(totalBytes); - } + size = BinaryPrimitives.ReadUInt16BigEndian(bytes); + } - ushort size = 0; - var bytes = AuthDataHelper.GetSizedByteArray(ab, ref offset, 2); - if (bytes != null) - { - size = BinaryPrimitives.ReadUInt16BigEndian(bytes); - } + // If size is 4, then the Name is a handle. + if (size is 4) + throw new Fido2VerificationException("Unexpected handle in TPM2B_NAME"); + + // If size is 0, then no Name is present. + if (size is 0) + throw new Fido2VerificationException("Unexpected no name found in TPM2B_NAME"); - // If size is 4, then the Name is a handle. - if (size is 4) - throw new Fido2VerificationException("Unexpected handle in TPM2B_NAME"); - - // If size is 0, then no Name is present. - if (size is 0) - throw new Fido2VerificationException("Unexpected no name found in TPM2B_NAME"); - - // Otherwise, the size shall be the size of a TPM_ALG_ID plus the size of the digest produced by the indicated hash algorithm. - byte[] name; - if (Enum.IsDefined(typeof(TpmAlg), size)) + // Otherwise, the size shall be the size of a TPM_ALG_ID plus the size of the digest produced by the indicated hash algorithm. + byte[] name; + if (Enum.IsDefined(typeof(TpmAlg), size)) + { + var tpmalg = (TpmAlg)size; + if (tpmAlgToDigestSizeMap.TryGetValue(tpmalg, out ushort tplAlgDigestSize)) { - var tpmalg = (TpmAlg)size; - if (tpmAlgToDigestSizeMap.TryGetValue(tpmalg, out ushort tplAlgDigestSize)) - { - name = AuthDataHelper.GetSizedByteArray(ab, ref offset, tplAlgDigestSize); - } - else - { - throw new Fido2VerificationException("TPM_ALG_ID found in TPM2B_NAME not acceptable hash algorithm"); - } + name = AuthDataHelper.GetSizedByteArray(ab, ref offset, tplAlgDigestSize); } else { - throw new Fido2VerificationException("Invalid TPM_ALG_ID found in TPM2B_NAME"); + throw new Fido2VerificationException("TPM_ALG_ID found in TPM2B_NAME not acceptable hash algorithm"); } + } + else + { + throw new Fido2VerificationException("Invalid TPM_ALG_ID found in TPM2B_NAME"); + } - if (totalSize != bytes!.Length + name.Length) - throw new Fido2VerificationException("Unexpected extra bytes found in TPM2B_NAME"); + if (totalSize != bytes!.Length + name.Length) + throw new Fido2VerificationException("Unexpected extra bytes found in TPM2B_NAME"); - return (size, name); - } + return (size, name); + } - public CertInfo(byte[] certInfo) - { - if (certInfo is null || certInfo.Length is 0) - throw new Fido2VerificationException("Malformed certInfo bytes"); + public CertInfo(byte[] certInfo) + { + if (certInfo is null || certInfo.Length is 0) + throw new Fido2VerificationException("Malformed certInfo bytes"); - int offset = 0; + int offset = 0; - Raw = certInfo; + Raw = certInfo; - Magic = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 4); - if (0xff544347 != BinaryPrimitives.ReadUInt32BigEndian(Magic)) - throw new Fido2VerificationException("Bad magic number " + Convert.ToHexString(Magic)); + Magic = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 4); + if (0xff544347 != BinaryPrimitives.ReadUInt32BigEndian(Magic)) + throw new Fido2VerificationException("Bad magic number " + Convert.ToHexString(Magic)); - Type = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 2); - if (0x8017 != BinaryPrimitives.ReadUInt16BigEndian(Type)) - throw new Fido2VerificationException("Bad structure tag " + Convert.ToHexString(Type)); + Type = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 2); + if (0x8017 != BinaryPrimitives.ReadUInt16BigEndian(Type)) + throw new Fido2VerificationException("Bad structure tag " + Convert.ToHexString(Type)); - QualifiedSigner = AuthDataHelper.GetSizedByteArray(certInfo, ref offset); + QualifiedSigner = AuthDataHelper.GetSizedByteArray(certInfo, ref offset); - ExtraData = AuthDataHelper.GetSizedByteArray(certInfo, ref offset); - if (ExtraData is null || ExtraData.Length is 0) - throw new Fido2VerificationException("Bad extraData in certInfo"); + ExtraData = AuthDataHelper.GetSizedByteArray(certInfo, ref offset); + if (ExtraData is null || ExtraData.Length is 0) + throw new Fido2VerificationException("Bad extraData in certInfo"); - Clock = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 8); - ResetCount = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 4); - RestartCount = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 4); - Safe = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 1); - FirmwareVersion = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 8); + Clock = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 8); + ResetCount = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 4); + RestartCount = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 4); + Safe = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 1); + FirmwareVersion = AuthDataHelper.GetSizedByteArray(certInfo, ref offset, 8); - var (size, name) = NameFromTPM2BName(certInfo, ref offset); - Alg = size; // TPM_ALG_ID - AttestedName = name; - AttestedQualifiedNameBuffer = AuthDataHelper.GetSizedByteArray(certInfo, ref offset); + var (size, name) = NameFromTPM2BName(certInfo, ref offset); + Alg = size; // TPM_ALG_ID + AttestedName = name; + AttestedQualifiedNameBuffer = AuthDataHelper.GetSizedByteArray(certInfo, ref offset); - if (certInfo.Length != offset) - throw new Fido2VerificationException("Leftover bits decoding certInfo"); - } - public byte[] Raw { get; private set; } - public byte[] Magic { get; private set; } - public byte[] Type { get; private set; } - public byte[] QualifiedSigner { get; private set; } - public byte[] ExtraData { get; private set; } - public byte[] Clock { get; private set; } - public byte[] ResetCount { get; private set; } - public byte[] RestartCount { get; private set; } - public byte[] Safe { get; private set; } - public byte[] FirmwareVersion { get; private set; } - public ushort Alg { get; private set; } - public byte[] AttestedName { get; private set; } - public byte[] AttestedQualifiedNameBuffer { get; private set; } + if (certInfo.Length != offset) + throw new Fido2VerificationException("Leftover bits decoding certInfo"); } + public byte[] Raw { get; private set; } + public byte[] Magic { get; private set; } + public byte[] Type { get; private set; } + public byte[] QualifiedSigner { get; private set; } + public byte[] ExtraData { get; private set; } + public byte[] Clock { get; private set; } + public byte[] ResetCount { get; private set; } + public byte[] RestartCount { get; private set; } + public byte[] Safe { get; private set; } + public byte[] FirmwareVersion { get; private set; } + public ushort Alg { get; private set; } + public byte[] AttestedName { get; private set; } + public byte[] AttestedQualifiedNameBuffer { get; private set; } +} - // TPMT_PUBLIC, TPMv2-Part2, section 12.2.4 - public sealed class PubArea +// TPMT_PUBLIC, TPMv2-Part2, section 12.2.4 +public sealed class PubArea +{ + public PubArea(byte[] pubArea) { - public PubArea(byte[] pubArea) - { - Raw = pubArea; - var offset = 0; + Raw = pubArea; + var offset = 0; - // TPMI_ALG_PUBLIC - Type = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); - var tpmalg = (TpmAlg)Enum.ToObject(typeof(TpmAlg), BinaryPrimitives.ReadUInt16BigEndian(Type)); + // TPMI_ALG_PUBLIC + Type = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); + var tpmalg = (TpmAlg)Enum.ToObject(typeof(TpmAlg), BinaryPrimitives.ReadUInt16BigEndian(Type)); - // TPMI_ALG_HASH - Alg = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); + // TPMI_ALG_HASH + Alg = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); - // TPMA_OBJECT, attributes that, along with type, determine the manipulations of this object - Attributes = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 4); + // TPMA_OBJECT, attributes that, along with type, determine the manipulations of this object + Attributes = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 4); - // TPM2B_DIGEST, optional policy for using this key, computed using the alg of the object - Policy = AuthDataHelper.GetSizedByteArray(pubArea, ref offset); + // TPM2B_DIGEST, optional policy for using this key, computed using the alg of the object + Policy = AuthDataHelper.GetSizedByteArray(pubArea, ref offset); - // TPMU_PUBLIC_PARMS - Symmetric = null; - Scheme = null; + // TPMU_PUBLIC_PARMS + Symmetric = null; + Scheme = null; - if (tpmalg is TpmAlg.TPM_ALG_KEYEDHASH) - { - throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, "TPM_ALG_KEYEDHASH not yet supported"); - } - if (tpmalg is TpmAlg.TPM_ALG_SYMCIPHER) - { - throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, "TPM_ALG_SYMCIPHER not yet supported"); - } + if (tpmalg is TpmAlg.TPM_ALG_KEYEDHASH) + { + throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, "TPM_ALG_KEYEDHASH not yet supported"); + } + if (tpmalg is TpmAlg.TPM_ALG_SYMCIPHER) + { + throw new Fido2VerificationException(Fido2ErrorCode.UnimplementedAlgorithm, "TPM_ALG_SYMCIPHER not yet supported"); + } - // TPMS_ASYM_PARMS, for TPM_ALG_RSA and TPM_ALG_ECC - if (tpmalg is TpmAlg.TPM_ALG_RSA or TpmAlg.TPM_ALG_ECC) - { - Symmetric = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); - Scheme = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); - } + // TPMS_ASYM_PARMS, for TPM_ALG_RSA and TPM_ALG_ECC + if (tpmalg is TpmAlg.TPM_ALG_RSA or TpmAlg.TPM_ALG_ECC) + { + Symmetric = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); + Scheme = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); + } - // TPMI_RSA_KEY_BITS, number of bits in the public modulus - KeyBits = null; + // TPMI_RSA_KEY_BITS, number of bits in the public modulus + KeyBits = null; - // The public exponent, a prime number greater than 2. - Exponent = 0; + // The public exponent, a prime number greater than 2. + Exponent = 0; + + if (tpmalg is TpmAlg.TPM_ALG_RSA) + { + KeyBits = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); - if (tpmalg is TpmAlg.TPM_ALG_RSA) + if (AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 4) is byte[] tmp) { - KeyBits = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); + Exponent = BitConverter.ToUInt32(tmp, 0); - if (AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 4) is byte[] tmp) + // When zero, indicates that the exponent is the default of 2^16 + 1 + if (Exponent is 0) { - Exponent = BitConverter.ToUInt32(tmp, 0); - - // When zero, indicates that the exponent is the default of 2^16 + 1 - if (Exponent is 0) - { - Exponent = Convert.ToUInt32(Math.Pow(2, 16) + 1); - } + Exponent = Convert.ToUInt32(Math.Pow(2, 16) + 1); } - // TPM2B_PUBLIC_KEY_RSA - Unique = AuthDataHelper.GetSizedByteArray(pubArea, ref offset); } + // TPM2B_PUBLIC_KEY_RSA + Unique = AuthDataHelper.GetSizedByteArray(pubArea, ref offset); + } - // TPMI_ECC_CURVE - CurveID = null; - - // TPMT_KDF_SCHEME, an optional key derivation scheme for generating a symmetric key from a Z value - // If the kdf parameter associated with curveID is not TPM_ALG_NULL then this is required to be NULL. - // NOTE There are currently no commands where this parameter has effect and, in the reference code, this field needs to be set to TPM_ALG_NULL. - KDF = null; + // TPMI_ECC_CURVE + CurveID = null; - if (tpmalg is TpmAlg.TPM_ALG_ECC) - { - CurveID = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); - KDF = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); + // TPMT_KDF_SCHEME, an optional key derivation scheme for generating a symmetric key from a Z value + // If the kdf parameter associated with curveID is not TPM_ALG_NULL then this is required to be NULL. + // NOTE There are currently no commands where this parameter has effect and, in the reference code, this field needs to be set to TPM_ALG_NULL. + KDF = null; - // TPMS_ECC_POINT - ECPoint = new() - { - X = AuthDataHelper.GetSizedByteArray(pubArea, ref offset), - Y = AuthDataHelper.GetSizedByteArray(pubArea, ref offset), - }; - Unique = DataHelper.Concat(ECPoint.X, ECPoint.Y); - } + if (tpmalg is TpmAlg.TPM_ALG_ECC) + { + CurveID = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); + KDF = AuthDataHelper.GetSizedByteArray(pubArea, ref offset, 2); - if (pubArea.Length != offset) - throw new Fido2VerificationException("Leftover bytes decoding pubArea"); + // TPMS_ECC_POINT + ECPoint = new() + { + X = AuthDataHelper.GetSizedByteArray(pubArea, ref offset), + Y = AuthDataHelper.GetSizedByteArray(pubArea, ref offset), + }; + Unique = DataHelper.Concat(ECPoint.X, ECPoint.Y); } - public byte[] Raw { get; private set; } - public byte[] Type { get; private set; } - public byte[] Alg { get; private set; } - public byte[] Attributes { get; private set; } - public byte[] Policy { get; private set; } - public byte[]? Symmetric { get; private set; } - public byte[]? Scheme { get; private set; } - public byte[]? KeyBits { get; private set; } - public uint Exponent { get; private set; } - public byte[]? CurveID { get; private set; } - public byte[]? KDF { get; private set; } - public byte[]? Unique { get; private set; } - public TpmEccCurve EccCurve => (TpmEccCurve)Enum.ToObject(typeof(TpmEccCurve), BinaryPrimitives.ReadUInt16BigEndian(CurveID)); - public ECPoint ECPoint { get; private set; } + if (pubArea.Length != offset) + throw new Fido2VerificationException("Leftover bytes decoding pubArea"); } + + public byte[] Raw { get; private set; } + public byte[] Type { get; private set; } + public byte[] Alg { get; private set; } + public byte[] Attributes { get; private set; } + public byte[] Policy { get; private set; } + public byte[]? Symmetric { get; private set; } + public byte[]? Scheme { get; private set; } + public byte[]? KeyBits { get; private set; } + public uint Exponent { get; private set; } + public byte[]? CurveID { get; private set; } + public byte[]? KDF { get; private set; } + public byte[]? Unique { get; private set; } + public TpmEccCurve EccCurve => (TpmEccCurve)Enum.ToObject(typeof(TpmEccCurve), BinaryPrimitives.ReadUInt16BigEndian(CurveID)); + public ECPoint ECPoint { get; private set; } } diff --git a/Src/Fido2/Attributes/Fido2StandardAttribute.cs b/Src/Fido2/Attributes/Fido2StandardAttribute.cs index f9bd6d0e3..7381aa42b 100644 --- a/Src/Fido2/Attributes/Fido2StandardAttribute.cs +++ b/Src/Fido2/Attributes/Fido2StandardAttribute.cs @@ -1,10 +1,9 @@ using System; -namespace Fido2NetLib +namespace Fido2NetLib; + +[AttributeUsage(AttributeTargets.All, AllowMultiple = false)] +internal sealed class Fido2StandardAttribute : Attribute { - [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] - internal sealed class Fido2StandardAttribute : Attribute - { - public bool Optional { get; set; } - } + public bool Optional { get; set; } } diff --git a/Src/Fido2/AuthDataHelper.cs b/Src/Fido2/AuthDataHelper.cs index c4f3a9010..511949a2d 100644 --- a/Src/Fido2/AuthDataHelper.cs +++ b/Src/Fido2/AuthDataHelper.cs @@ -1,27 +1,26 @@ using System; using System.Buffers.Binary; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Helper functions that implements https://w3c.github.io/webauthn/#authenticator-data +/// +internal static class AuthDataHelper { - /// - /// Helper functions that implements https://w3c.github.io/webauthn/#authenticator-data - /// - internal static class AuthDataHelper + public static byte[] GetSizedByteArray(ReadOnlySpan ab, ref int offset, ushort len = 0) { - public static byte[] GetSizedByteArray(ReadOnlySpan ab, ref int offset, ushort len = 0) + if (len is 0 && ((offset + 2) <= ab.Length)) + { + len = BinaryPrimitives.ReadUInt16BigEndian(ab.Slice(offset, 2)); + offset += 2; + } + byte[] result = null!; + if ((0 < len) && ((offset + len) <= ab.Length)) { - if (len is 0 && ((offset + 2) <= ab.Length)) - { - len = BinaryPrimitives.ReadUInt16BigEndian(ab.Slice(offset, 2)); - offset += 2; - } - byte[] result = null!; - if ((0 < len) && ((offset + len) <= ab.Length)) - { - result = ab.Slice(offset, len).ToArray(); - offset += len; - } - return result; + result = ab.Slice(offset, len).ToArray(); + offset += len; } + return result; } } diff --git a/Src/Fido2/AuthenticatorAssertionResponse.cs b/Src/Fido2/AuthenticatorAssertionResponse.cs index 41013a198..375c3b25e 100644 --- a/Src/Fido2/AuthenticatorAssertionResponse.cs +++ b/Src/Fido2/AuthenticatorAssertionResponse.cs @@ -11,173 +11,172 @@ #nullable disable -namespace Fido2NetLib +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 { - /// - /// 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 + private AuthenticatorAssertionResponse(byte[] clientDataJson) : base(clientDataJson) { - private AuthenticatorAssertionResponse(byte[] clientDataJson) : base(clientDataJson) - { - } + } - public AuthenticatorAssertionRawResponse Raw { get; init; } + public AuthenticatorAssertionRawResponse Raw { get; init; } - public byte[] AuthenticatorData { get; init; } + public byte[] AuthenticatorData { get; init; } - public byte[] Signature { get; init; } + public byte[] Signature { get; init; } - public byte[] UserHandle { get; init; } + public byte[] UserHandle { get; init; } - public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResponse rawResponse) + public static AuthenticatorAssertionResponse Parse(AuthenticatorAssertionRawResponse rawResponse) + { + var response = new AuthenticatorAssertionResponse(rawResponse.Response.ClientDataJson) { - var response = new AuthenticatorAssertionResponse(rawResponse.Response.ClientDataJson) - { - Raw = rawResponse, // accessed in Verify() - AuthenticatorData = rawResponse.Response.AuthenticatorData, - Signature = rawResponse.Response.Signature, - UserHandle = rawResponse.Response.UserHandle - }; + Raw = rawResponse, // accessed in Verify() + AuthenticatorData = rawResponse.Response.AuthenticatorData, + Signature = rawResponse.Response.Signature, + UserHandle = rawResponse.Response.UserHandle + }; - return response; - } + return response; + } - /// - /// Implements alghoritm from https://www.w3.org/TR/webauthn/#verifying-assertion - /// - /// The assertionoptions 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, - HashSet fullyQualifiedExpectedOrigins, - byte[] storedPublicKey, - uint storedSignatureCounter, - IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId, - byte[] requestTokenBindingId, - CancellationToken cancellationToken = default) - { - BaseVerify(fullyQualifiedExpectedOrigins, options.Challenge, requestTokenBindingId); + /// + /// Implements alghoritm from https://www.w3.org/TR/webauthn/#verifying-assertion + /// + /// The assertionoptions 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, + HashSet fullyQualifiedExpectedOrigins, + byte[] storedPublicKey, + uint storedSignatureCounter, + IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredId, + byte[] requestTokenBindingId, + CancellationToken cancellationToken = default) + { + BaseVerify(fullyQualifiedExpectedOrigins, options.Challenge, requestTokenBindingId); - if (Raw.Type != PublicKeyCredentialType.PublicKey) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse type must be public-key"); + if (Raw.Type != PublicKeyCredentialType.PublicKey) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse type must be public-key"); - if (Raw.Id is null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "Id is missing"); + if (Raw.Id is null) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "Id is missing"); - if (Raw.RawId is null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "RawId is missing"); + if (Raw.RawId is null) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "RawId is missing"); - // 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials. - if (options.AllowCredentials != null && options.AllowCredentials.Any()) - { - // might need to transform x.Id and raw.id as described in https://www.w3.org/TR/webauthn/#publickeycredential - if (!options.AllowCredentials.Any(x => x.Id.SequenceEqual(Raw.Id))) - throw new Fido2VerificationException("Invalid"); - } + // 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that credential.id identifies one of the public key credentials that were listed in allowCredentials. + if (options.AllowCredentials != null && options.AllowCredentials.Any()) + { + // might need to transform x.Id and raw.id as described in https://www.w3.org/TR/webauthn/#publickeycredential + if (!options.AllowCredentials.Any(x => x.Id.SequenceEqual(Raw.Id))) + throw new Fido2VerificationException("Invalid"); + } - // 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 - if (UserHandle != null) - { - if (UserHandle.Length is 0) - throw new Fido2VerificationException(Fido2ErrorMessages.UserHandleIsEmpty); + // 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 + if (UserHandle != null) + { + if (UserHandle.Length is 0) + throw new Fido2VerificationException(Fido2ErrorMessages.UserHandleIsEmpty); - if (await isUserHandleOwnerOfCredId(new IsUserHandleOwnerOfCredentialIdParams(Raw.Id, UserHandle), cancellationToken) is false) - { - throw new Fido2VerificationException("User is not owner of the public key identified by the credential id"); - } + if (await isUserHandleOwnerOfCredId(new IsUserHandleOwnerOfCredentialIdParams(Raw.Id, UserHandle), cancellationToken) is false) + { + throw new Fido2VerificationException("User is not owner of the public key identified by the credential id"); } + } - // 3. Using credential’s id attribute(or the corresponding rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key. - // Credential public key passed in via storePublicKey parameter + // 3. Using credential’s id attribute(or the corresponding rawId, if base64url encoding is inappropriate for your use case), look up the corresponding credential public key. + // Credential public key passed in via storePublicKey parameter - // 4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively. - //var cData = Raw.Response.ClientDataJson; - var authData = new AuthenticatorData(AuthenticatorData); - //var sig = Raw.Response.Signature; + // 4. Let cData, authData and sig denote the value of credential’s response's clientDataJSON, authenticatorData, and signature respectively. + //var cData = Raw.Response.ClientDataJson; + var authData = new AuthenticatorData(AuthenticatorData); + //var sig = Raw.Response.Signature; - // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData. - // var JSONtext = Encoding.UTF8.GetBytes(cData.ToString()); + // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData. + // var JSONtext = Encoding.UTF8.GetBytes(cData.ToString()); - // 7. Verify that the value of C.type is the string webauthn.get. - if (Type is not "webauthn.get") - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse must be webauthn.get"); + // 7. Verify that the value of C.type is the string webauthn.get. + if (Type is not "webauthn.get") + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAssertionResponse, "AssertionResponse must be webauthn.get"); - // 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call. - // 9. Verify that the value of C.origin matches the Relying Party's origin. - // done in base class + // 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the PublicKeyCredentialRequestOptions passed to the get() call. + // 9. Verify that the value of C.origin matches the Relying Party's origin. + // done in base class - // 10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the attestation was obtained.If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection. - // Validated in BaseVerify. - // todo: Needs testing + // 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. + // 11. Verify that the rpIdHash in aData is the SHA - 256 hash of the RP ID expected by the Relying Party. - // https://www.w3.org/TR/webauthn/#sctn-appid-extension - // FIDO AppID Extension: - // If true, the AppID was used and thus, when verifying an assertion, the Relying Party MUST expect the rpIdHash to be the hash of the AppID, not the RP ID. - var rpid = Raw.Extensions?.AppID ?? false ? options.Extensions?.AppID : options.RpId; - byte[] hashedRpId = SHA256.HashData(Encoding.UTF8.GetBytes(rpid ?? string.Empty)); - byte[] hashedClientDataJson = SHA256.HashData(Raw.Response.ClientDataJson); + // 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(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash); + if (!authData.RpIdHash.SequenceEqual(hashedRpId)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash); - // 12. Verify that the User Present bit of the flags in authData is set. - // UNLESS...userVerification is set to preferred or discouraged? - // See Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData - // P-5 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "preferred", and check that server succeeds - // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds - // if ((!authData.UserPresent) && (options.UserVerification != UserVerificationRequirement.Discouraged && options.UserVerification != UserVerificationRequirement.Preferred)) throw new Fido2VerificationException("User Present flag not set in authenticator data"); + // 12. Verify that the User Present bit of the flags in authData is set. + // UNLESS...userVerification is set to preferred or discouraged? + // See Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData + // P-5 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "preferred", and check that server succeeds + // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds + // if ((!authData.UserPresent) && (options.UserVerification != UserVerificationRequirement.Discouraged && options.UserVerification != UserVerificationRequirement.Preferred)) throw new Fido2VerificationException("User Present flag not set in authenticator data"); - // 13 If user verification is required for this assertion, verify that the User Verified bit of the flags in aData is set. - // UNLESS...userPresent is true? - // see ee Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData - // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds - if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified) - throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet); + // 13 If user verification is required for this assertion, verify that the User Verified bit of the flags in aData is set. + // UNLESS...userPresent is true? + // see ee Server-ServerAuthenticatorAssertionResponse-Resp3 Test server processing authenticatorData + // P-8 Send a valid ServerAuthenticatorAssertionResponse both authenticatorData.flags.UV and authenticatorData.flags.UP are not set, for userVerification set to "discouraged", and check that server succeeds + if (options.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified) + throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet); - // 14. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the get() call.In particular, any extension identifier values in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use. - // todo: Verify this (and implement extensions on options) - if (authData.HasExtensionsData && (authData.Extensions is null || authData.Extensions.Length is 0)) - throw new Fido2VerificationException(Fido2ErrorCode.MalformedExtensionsDetected, Fido2ErrorMessages.MalformedExtensionsDetected); + // 14. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, considering the client extension input values that were given as the extensions option in the get() call.In particular, any extension identifier values in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use. + // todo: Verify this (and implement extensions on options) + if (authData.HasExtensionsData && (authData.Extensions is null || authData.Extensions.Length is 0)) + throw new Fido2VerificationException(Fido2ErrorCode.MalformedExtensionsDetected, Fido2ErrorMessages.MalformedExtensionsDetected); - if (!authData.HasExtensionsData && authData.Extensions != null) - throw new Fido2VerificationException(Fido2ErrorCode.UnexpectedExtensionsDetected, Fido2ErrorMessages.UnexpectedExtensionsDetected); + if (!authData.HasExtensionsData && authData.Extensions != null) + throw new Fido2VerificationException(Fido2ErrorCode.UnexpectedExtensionsDetected, Fido2ErrorMessages.UnexpectedExtensionsDetected); - // 15. - // Done earlier, hashedClientDataJson + // 15. + // Done earlier, hashedClientDataJson - // 16. Using the credential public key looked up in step 3, verify that sig is a valid signature over the binary concatenation of aData and hash. - byte[] data = DataHelper.Concat(Raw.Response.AuthenticatorData, hashedClientDataJson); - - if (storedPublicKey is null || storedPublicKey.Length is 0) - throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey); + // 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); + + if (storedPublicKey is null || storedPublicKey.Length is 0) + throw new Fido2VerificationException(Fido2ErrorCode.MissingStoredPublicKey, Fido2ErrorMessages.MissingStoredPublicKey); - var cpk = new CredentialPublicKey(storedPublicKey); + var cpk = new CredentialPublicKey(storedPublicKey); - if (!cpk.Verify(data, Signature)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignature, Fido2ErrorMessages.InvalidSignature); + if (!cpk.Verify(data, Signature)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignature, Fido2ErrorMessages.InvalidSignature); - // 17. - if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignCount, Fido2ErrorMessages.SignCountIsLessThanSignatureCounter); + // 17. + if (authData.SignCount > 0 && authData.SignCount <= storedSignatureCounter) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidSignCount, Fido2ErrorMessages.SignCountIsLessThanSignatureCounter); - return new AssertionVerificationResult - { - Status = "ok", - ErrorMessage = string.Empty, - CredentialId = Raw.Id, - Counter = authData.SignCount, - }; - } + return new AssertionVerificationResult + { + Status = "ok", + ErrorMessage = string.Empty, + CredentialId = Raw.Id, + Counter = authData.SignCount, + }; } } diff --git a/Src/Fido2/AuthenticatorAttestationResponse.cs b/Src/Fido2/AuthenticatorAttestationResponse.cs index e612dfe9f..0e67919c7 100644 --- a/Src/Fido2/AuthenticatorAttestationResponse.cs +++ b/Src/Fido2/AuthenticatorAttestationResponse.cs @@ -12,270 +12,269 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// The AuthenticatorAttestationResponse interface represents the authenticator's response +/// to a client’s request for the creation of a new public key credential. +/// 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 { - /// - /// The AuthenticatorAttestationResponse interface represents the authenticator's response - /// to a client’s request for the creation of a new public key credential. - /// 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 + private AuthenticatorAttestationResponse(byte[] clientDataJson) + : base(clientDataJson) { - private AuthenticatorAttestationResponse(byte[] clientDataJson) - : base(clientDataJson) - { - } + } - public ParsedAttestationObject AttestationObject { get; init; } + public ParsedAttestationObject AttestationObject { get; init; } - public AuthenticatorAttestationRawResponse Raw { get; private set; } + public AuthenticatorAttestationRawResponse Raw { get; private set; } - public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRawResponse rawResponse) - { - if (rawResponse?.Response is null) - throw new Fido2VerificationException("Expected rawResponse, got null"); + public static AuthenticatorAttestationResponse Parse(AuthenticatorAttestationRawResponse rawResponse) + { + if (rawResponse?.Response is null) + throw new Fido2VerificationException("Expected rawResponse, got null"); - if (rawResponse.Response.AttestationObject is null || rawResponse.Response.AttestationObject.Length is 0) - throw new Fido2VerificationException(Fido2ErrorMessages.MissingAttestationObject); + if (rawResponse.Response.AttestationObject is null || rawResponse.Response.AttestationObject.Length is 0) + throw new Fido2VerificationException(Fido2ErrorMessages.MissingAttestationObject); - // 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. - CborMap cborAttestation; - try - { - cborAttestation = (CborMap)CborObject.Decode(rawResponse.Response.AttestationObject); - } - catch (Exception ex) - { - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationObject, Fido2ErrorMessages.InvalidAttestationObject, ex); - } - - if (!( - cborAttestation["fmt"] is { Type: CborType.TextString } && - cborAttestation["attStmt"] is { Type: CborType.Map } && - cborAttestation["authData"] is { Type: CborType.ByteString })) - { - throw new Fido2VerificationException(Fido2ErrorCode.MalformedAttestationObject, Fido2ErrorMessages.MalformedAttestationObject); - } + // 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. + CborMap cborAttestation; + try + { + cborAttestation = (CborMap)CborObject.Decode(rawResponse.Response.AttestationObject); + } + catch (Exception ex) + { + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationObject, Fido2ErrorMessages.InvalidAttestationObject, ex); + } - return new AuthenticatorAttestationResponse(rawResponse.Response.ClientDataJson) - { - Raw = rawResponse, - AttestationObject = new ParsedAttestationObject( - fmt : (string)cborAttestation["fmt"], - attStmt : (CborMap)cborAttestation["attStmt"], - authData : (byte[])cborAttestation["authData"] - ) - }; + if (!( + cborAttestation["fmt"] is { Type: CborType.TextString } && + cborAttestation["attStmt"] is { Type: CborType.Map } && + cborAttestation["authData"] is { Type: CborType.ByteString })) + { + throw new Fido2VerificationException(Fido2ErrorCode.MalformedAttestationObject, Fido2ErrorMessages.MalformedAttestationObject); } - public async Task VerifyAsync( - CredentialCreateOptions originalOptions, - Fido2Configuration config, - IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, - IMetadataService metadataService, - byte[] requestTokenBindingId, - CancellationToken cancellationToken = default) + return new AuthenticatorAttestationResponse(rawResponse.Response.ClientDataJson) { - // https://www.w3.org/TR/webauthn/#registering-a-new-credential - // 1. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON. - // 2. Let C, the client data claimed as collected during the credential creation, be the result of running an implementation-specific JSON parser on JSONtext. - // Note: C may be any implementation-specific data structure representation, as long as C’s components are referenceable, as required by this algorithm. - // Above handled in base class constructor + Raw = rawResponse, + AttestationObject = new ParsedAttestationObject( + fmt : (string)cborAttestation["fmt"], + attStmt : (CborMap)cborAttestation["attStmt"], + authData : (byte[])cborAttestation["authData"] + ) + }; + } + + public async Task VerifyAsync( + CredentialCreateOptions 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. + // Note: C may be any implementation-specific data structure representation, as long as C’s components are referenceable, as required by this algorithm. + // Above handled in base class constructor - // 3. Verify that the value of C.type is webauthn.create - if (Type is not "webauthn.create") - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse type must be webauthn.create"); + // 3. Verify that the value of C.type is webauthn.create + if (Type is not "webauthn.create") + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse type must be webauthn.create"); - // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call. - // 5. Verify that the value of C.origin matches the Relying Party's origin. - // 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection over which the assertion was obtained. - // If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection. - BaseVerify(config.FullyQualifiedOrigins, originalOptions.Challenge, requestTokenBindingId); + // 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); - if (Raw.Id is null || Raw.Id.Length == 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse is missing Id"); + if (Raw.Id is null || Raw.Id.Length == 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse is missing Id"); - if (Raw.Type != PublicKeyCredentialType.PublicKey) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse type must be 'public-key'"); + if (Raw.Type != PublicKeyCredentialType.PublicKey) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestationResponse, "AttestationResponse type must be 'public-key'"); - var authData = new AuthenticatorData(AttestationObject.AuthData); + var authData = new AuthenticatorData(AttestationObject.AuthData); - // 7. Compute the hash of response.clientDataJSON using SHA-256. - byte[] clientDataHash = SHA256.HashData(Raw.Response.ClientDataJson); - byte[] rpIdHash = SHA256.HashData(Encoding.UTF8.GetBytes(originalOptions.Rp.Id)); + // 7. Compute the hash of response.clientDataJSON using SHA-256. + byte[] clientDataHash = SHA256.HashData(Raw.Response.ClientDataJson); + byte[] rpIdHash = SHA256.HashData(Encoding.UTF8.GetBytes(originalOptions.Rp.Id)); - // 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, - // the authenticator data authData, and the attestation statement attStmt. - // Handled in AuthenticatorAttestationResponse::Parse() + // 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to obtain the attestation statement format fmt, + // the authenticator data authData, and the attestation statement attStmt. + // Handled in AuthenticatorAttestationResponse::Parse() - // 9. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party - if (!authData.RpIdHash.AsSpan().SequenceEqual(rpIdHash)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash); + // 9. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party + if (!authData.RpIdHash.AsSpan().SequenceEqual(rpIdHash)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidRpidHash, Fido2ErrorMessages.InvalidRpidHash); - // 10. Verify that the User Present bit of the flags in authData is set. - if (!authData.UserPresent) - throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet); + // 10. Verify that the User Present bit of the flags in authData is set. + if (!authData.UserPresent) + throw new Fido2VerificationException(Fido2ErrorCode.UserPresentFlagNotSet, Fido2ErrorMessages.UserPresentFlagNotSet); - // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. - if (originalOptions.AuthenticatorSelection?.UserVerification is UserVerificationRequirement.Required && !authData.UserVerified) - throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet); + // 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) + throw new Fido2VerificationException(Fido2ErrorCode.UserVerificationRequirementNotMet, Fido2ErrorMessages.UserVerificationRequirementNotMet); - // 12. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, - // considering the client extension input values that were given as the extensions option in the create() call. In particular, any extension identifier values - // in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., - // no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use. + // 12. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator extension outputs in the extensions in authData are as expected, + // considering the client extension input values that were given as the extensions option in the create() call. In particular, any extension identifier values + // in the clientExtensionResults and the extensions in authData MUST be also be present as extension identifier values in the extensions member of options, i.e., + // no extensions are present that were not requested. In the general case, the meaning of "are as expected" is specific to the Relying Party and which extensions are in use. - // TODO?: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x); + // TODO?: Implement sort of like this: ClientExtensions.Keys.Any(x => options.extensions.contains(x); - if (!authData.HasAttestedCredentialData) - throw new Fido2VerificationException(Fido2ErrorCode.AttestedCredentialDataFlagNotSet, Fido2ErrorMessages.AttestedCredentialDataFlagNotSet); + if (!authData.HasAttestedCredentialData) + throw new Fido2VerificationException(Fido2ErrorCode.AttestedCredentialDataFlagNotSet, Fido2ErrorMessages.AttestedCredentialDataFlagNotSet); - if (string.IsNullOrEmpty(AttestationObject.Fmt)) + if (string.IsNullOrEmpty(AttestationObject.Fmt)) + { + throw new Fido2VerificationException(Fido2ErrorCode.MissingAttestationType, Fido2ErrorMessages.MissingAttestationType); + } + + // 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. + // An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same name + // https://www.w3.org/TR/webauthn/#defined-attestation-formats + AttestationVerifier verifier = AttestationObject.Fmt switch + { + // TODO: Better way to build these mappings? + "none" => new None(), // https://www.w3.org/TR/webauthn/#none-attestation + "tpm" => new Tpm(), // https://www.w3.org/TR/webauthn/#tpm-attestation + "android-key" => new AndroidKey(), // https://www.w3.org/TR/webauthn/#android-key-attestation + "android-safetynet" => new AndroidSafetyNet(), // https://www.w3.org/TR/webauthn/#android-safetynet-attestation + "fido-u2f" => new FidoU2f(), // https://www.w3.org/TR/webauthn/#fido-u2f-attestation + "packed" => new Packed(), // https://www.w3.org/TR/webauthn/#packed-attestation + "apple" => new Apple(), // https://www.w3.org/TR/webauthn/#apple-anonymous-attestation + "apple-appattest" => new AppleAppAttest(), // https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server + _ => throw new Fido2VerificationException(Fido2ErrorCode.UnknownAttestationType, $"Unknown attestation type. Was '{AttestationObject.Fmt}'") + }; + + // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, + // by using the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized client data computed in step 7 + (var attType, var trustPath) = verifier.Verify(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash); + + // 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or ECDAA-Issuer public keys) + // for that attestation type and attestation statement format fmt, from a trusted source or from policy. + // For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData. + + MetadataBLOBPayloadEntry metadataEntry = null; + if(metadataService != null) + metadataEntry = await metadataService.GetEntryAsync(authData.AttestedCredentialData.AaGuid, cancellationToken); + + // while conformance testing, we must reject any authenticator that we cannot get metadata for + if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && AttestationObject.Fmt is not "fido-u2f") + throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata"); + + if (trustPath != null && metadataEntry?.MetadataStatement?.AttestationTypes is not null) + { + static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAttestationType type) { - throw new Fido2VerificationException(Fido2ErrorCode.MissingAttestationType, Fido2ErrorMessages.MissingAttestationType); + return entry.MetadataStatement.AttestationTypes.Contains(type.ToEnumMemberValue()); } - // 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn Attestation Statement Format Identifier values. - // An up-to-date list of registered WebAuthn Attestation Statement Format Identifier values is maintained in the IANA registry of the same name - // https://www.w3.org/TR/webauthn/#defined-attestation-formats - AttestationVerifier verifier = AttestationObject.Fmt switch - { - // TODO: Better way to build these mappings? - "none" => new None(), // https://www.w3.org/TR/webauthn/#none-attestation - "tpm" => new Tpm(), // https://www.w3.org/TR/webauthn/#tpm-attestation - "android-key" => new AndroidKey(), // https://www.w3.org/TR/webauthn/#android-key-attestation - "android-safetynet" => new AndroidSafetyNet(), // https://www.w3.org/TR/webauthn/#android-safetynet-attestation - "fido-u2f" => new FidoU2f(), // https://www.w3.org/TR/webauthn/#fido-u2f-attestation - "packed" => new Packed(), // https://www.w3.org/TR/webauthn/#packed-attestation - "apple" => new Apple(), // https://www.w3.org/TR/webauthn/#apple-anonymous-attestation - "apple-appattest" => new AppleAppAttest(), // https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server - _ => throw new Fido2VerificationException(Fido2ErrorCode.UnknownAttestationType, $"Unknown attestation type. Was '{AttestationObject.Fmt}'") - }; - - // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, - // by using the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized client data computed in step 7 - (var attType, var trustPath) = verifier.Verify(AttestationObject.AttStmt, AttestationObject.AuthData, clientDataHash); - - // 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or ECDAA-Issuer public keys) - // for that attestation type and attestation statement format fmt, from a trusted source or from policy. - // For example, the FIDO Metadata Service [FIDOMetadataService] provides one way to obtain such information, using the aaguid in the attestedCredentialData in authData. - - MetadataBLOBPayloadEntry metadataEntry = null; - if(metadataService != null) - metadataEntry = await metadataService.GetEntryAsync(authData.AttestedCredentialData.AaGuid, cancellationToken); - - // while conformance testing, we must reject any authenticator that we cannot get metadata for - if (metadataService?.ConformanceTesting() is true && metadataEntry is null && attType != AttestationType.None && AttestationObject.Fmt is not "fido-u2f") - throw new Fido2VerificationException(Fido2ErrorCode.AaGuidNotFound, "AAGUID not found in MDS test metadata"); - - if (trustPath != null && metadataEntry?.MetadataStatement?.AttestationTypes is not null) + // If the authenticator's metadata requires basic full attestation, build and verify the chain + if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_BASIC_FULL) || + ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_PRIVACY_CA)) { - static bool ContainsAttestationType(MetadataBLOBPayloadEntry entry, MetadataAttestationType type) - { - return entry.MetadataStatement.AttestationTypes.Contains(type.ToEnumMemberValue()); - } + string[] certStrings = metadataEntry.MetadataStatement.AttestationRootCertificates; + var attestationRootCertificates = new X509Certificate2[certStrings.Length]; - // If the authenticator's metadata requires basic full attestation, build and verify the chain - if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_BASIC_FULL) || - ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_PRIVACY_CA)) + for (int i = 0; i < attestationRootCertificates.Length; i++) { - string[] certStrings = metadataEntry.MetadataStatement.AttestationRootCertificates; - var attestationRootCertificates = new X509Certificate2[certStrings.Length]; - - for (int i = 0; i < attestationRootCertificates.Length; i++) - { - attestationRootCertificates[i] = new X509Certificate2(Convert.FromBase64String(certStrings[i])); - } - - if (!CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates)) - { - throw new Fido2VerificationException(Fido2ErrorMessages.InvalidCertificateChain); - } + attestationRootCertificates[i] = new X509Certificate2(Convert.FromBase64String(certStrings[i])); } - else if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_ANONCA)) - { - // skip verification for Anonymization CA (AnonCA) - } - else // otherwise, ensure the certificate is self signed + if (!CryptoUtils.ValidateTrustChain(trustPath, attestationRootCertificates)) { - X509Certificate2 trustPath0 = trustPath[0]; - - if (!string.Equals(trustPath0.Subject, trustPath0.Issuer, StringComparison.Ordinal)) - { - // TODO: Improve this error message - throw new Fido2VerificationException("Attestation with full attestation from authenticator that does not support full attestation"); - } + throw new Fido2VerificationException(Fido2ErrorMessages.InvalidCertificateChain); } - - // TODO: Verify all MetadataAttestationTypes are correctly handled - - // [ ] ATTESTATION_ECDAA "ecdaa" | currently handled as self signed w/ no test coverage - // [ ] ATTESTATION_ANONCA "anonca" | currently not verified w/ no test coverage - // [ ] ATTESTATION_NONE "none" | currently handled as self signed w/ no test coverage } - // Check status resports for authenticator with undesirable status - var latestStatusReport = metadataEntry?.GetLatestStatusReport(); - if (latestStatusReport != null && config.UndesiredAuthenticatorMetadataStatuses.Contains(latestStatusReport.Status)) + else if (ContainsAttestationType(metadataEntry, MetadataAttestationType.ATTESTATION_ANONCA)) { - throw new UndesiredMetdatataStatusFido2VerificationException(latestStatusReport); + // skip verification for Anonymization CA (AnonCA) } - - // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows: - // If self attestation was used, check if self attestation is acceptable under Relying Party policy. - // If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in the set of acceptable trust anchors obtained in step 15. - // Otherwise, use the X.509 certificates returned by the verification procedure to verify that the attestation public key correctly chains up to an acceptable root certificate. - - // 17. Check that the credentialId is not yet registered to any other user. - // If registration is requested for a credential that is already registered to a different user, - // the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration - - if (await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User), cancellationToken) is false) + else // otherwise, ensure the certificate is self signed { - throw new Fido2VerificationException(Fido2ErrorCode.NonUniqueCredentialId, Fido2ErrorMessages.NonUniqueCredentialId); + X509Certificate2 trustPath0 = trustPath[0]; + + if (!string.Equals(trustPath0.Subject, trustPath0.Issuer, StringComparison.Ordinal)) + { + // TODO: Improve this error message + throw new Fido2VerificationException("Attestation with full attestation from authenticator that does not support full attestation"); + } } - // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, - // then register the new credential with the account that was denoted in the options.user passed to create(), - // by associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, - // as appropriate for the Relying Party's system. + // TODO: Verify all MetadataAttestationTypes are correctly handled - // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, - // the Relying Party SHOULD fail the registration ceremony. - // This implementation throws if the outputs are not trustworthy for a particular attestation type. + // [ ] ATTESTATION_ECDAA "ecdaa" | currently handled as self signed w/ no test coverage + // [ ] ATTESTATION_ANONCA "anonca" | currently not verified w/ no test coverage + // [ ] ATTESTATION_NONE "none" | currently handled as self signed w/ no test coverage + } - return new AttestationVerificationSuccess - { - CredentialId = authData.AttestedCredentialData.CredentialID, - PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(), - User = originalOptions.User, - Counter = authData.SignCount, - CredType = AttestationObject.Fmt, - Aaguid = authData.AttestedCredentialData.AaGuid, - AttestationCertificate = trustPath?.FirstOrDefault(), - AttestationCertificateChain = trustPath ?? Array.Empty(), - }; + // Check status resports for authenticator with undesirable status + var latestStatusReport = metadataEntry?.GetLatestStatusReport(); + if (latestStatusReport != null && config.UndesiredAuthenticatorMetadataStatuses.Contains(latestStatusReport.Status)) + { + throw new UndesiredMetdatataStatusFido2VerificationException(latestStatusReport); } - /// - /// The AttestationObject after CBOR parsing - /// - public sealed class ParsedAttestationObject + // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, as follows: + // If self attestation was used, check if self attestation is acceptable under Relying Party policy. + // If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in the set of acceptable trust anchors obtained in step 15. + // Otherwise, use the X.509 certificates returned by the verification procedure to verify that the attestation public key correctly chains up to an acceptable root certificate. + + // 17. Check that the credentialId is not yet registered to any other user. + // If registration is requested for a credential that is already registered to a different user, + // the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration + + if (await isCredentialIdUniqueToUser(new IsCredentialIdUniqueToUserParams(authData.AttestedCredentialData.CredentialID, originalOptions.User), cancellationToken) is false) { - public ParsedAttestationObject(string fmt, CborMap attStmt, byte[] authData) - { - Fmt = fmt; - AttStmt = attStmt; - AuthData = authData; - } + throw new Fido2VerificationException(Fido2ErrorCode.NonUniqueCredentialId, Fido2ErrorMessages.NonUniqueCredentialId); + } + + // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, + // then register the new credential with the account that was denoted in the options.user passed to create(), + // by associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, + // as appropriate for the Relying Party's system. - public string Fmt { get; } - - public CborMap AttStmt { get; } + // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, + // the Relying Party SHOULD fail the registration ceremony. + // This implementation throws if the outputs are not trustworthy for a particular attestation type. - public byte[] AuthData { get; } + return new AttestationVerificationSuccess + { + CredentialId = authData.AttestedCredentialData.CredentialID, + PublicKey = authData.AttestedCredentialData.CredentialPublicKey.GetBytes(), + User = originalOptions.User, + Counter = authData.SignCount, + CredType = AttestationObject.Fmt, + Aaguid = authData.AttestedCredentialData.AaGuid, + AttestationCertificate = trustPath?.FirstOrDefault(), + AttestationCertificateChain = trustPath ?? Array.Empty(), + }; + } + + /// + /// The AttestationObject after CBOR parsing + /// + public sealed class ParsedAttestationObject + { + public ParsedAttestationObject(string fmt, CborMap attStmt, byte[] authData) + { + Fmt = fmt; + AttStmt = attStmt; + AuthData = authData; } + + public string Fmt { get; } + + public CborMap AttStmt { get; } + + public byte[] AuthData { get; } } } diff --git a/Src/Fido2/AuthenticatorResponse.cs b/Src/Fido2/AuthenticatorResponse.cs index 9276b9983..346d0c584 100644 --- a/Src/Fido2/AuthenticatorResponse.cs +++ b/Src/Fido2/AuthenticatorResponse.cs @@ -9,92 +9,91 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +/// +/// Base class for responses sent by the Authenticator Client +/// +public class AuthenticatorResponse { - /// - /// Base class for responses sent by the Authenticator Client - /// - public class AuthenticatorResponse + protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson) { - protected AuthenticatorResponse(ReadOnlySpan utf8EncodedJson) + if (utf8EncodedJson.Length is 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, "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; + try + { + response = JsonSerializer.Deserialize(utf8EncodedJson, FidoSerializerContext.Default.AuthenticatorResponse)!; + } + catch (Exception e) when (e is JsonException) { - if (utf8EncodedJson.Length is 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, "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; - try - { - response = JsonSerializer.Deserialize(utf8EncodedJson, FidoSerializerContext.Default.AuthenticatorResponse)!; - } - catch (Exception e) when (e is JsonException) - { - throw new Fido2VerificationException(Fido2ErrorCode.MalformedAuthenticatorResponse, "Malformed clientDataJson"); - } - - if (response is null) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, "Deserialized authenticator response cannot be null"); - - Type = response.Type; - Challenge = response.Challenge; - Origin = response.Origin; + throw new Fido2VerificationException(Fido2ErrorCode.MalformedAuthenticatorResponse, "Malformed clientDataJson"); } + if (response is null) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, "Deserialized authenticator response cannot be null"); + + Type = response.Type; + Challenge = response.Challenge; + Origin = response.Origin; + } + #nullable disable - public AuthenticatorResponse() // for deserialization - { + public AuthenticatorResponse() // for deserialization + { - } + } #nullable enable - public const int MAX_ORIGINS_TO_PRINT = 5; + public const int MAX_ORIGINS_TO_PRINT = 5; - [JsonPropertyName("type")] - public string Type { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } - [JsonConverter(typeof(Base64UrlConverter))] - [JsonPropertyName("challenge")] - public byte[] Challenge { get; set; } + [JsonConverter(typeof(Base64UrlConverter))] + [JsonPropertyName("challenge")] + public byte[] Challenge { get; set; } - [JsonPropertyName("origin")] - public string Origin { get; set; } + [JsonPropertyName("origin")] + public string Origin { get; set; } - // todo: add TokenBinding https://www.w3.org/TR/webauthn/#dictdef-tokenbinding + // todo: add TokenBinding https://www.w3.org/TR/webauthn/#dictdef-tokenbinding - protected void BaseVerify(HashSet fullyQualifiedExpectedOrigins, ReadOnlySpan originalChallenge, ReadOnlySpan requestTokenBindingId) - { - if (Type is not "webauthn.create" && Type is not "webauthn.get") - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, $"Type must be 'webauthn.create' or 'webauthn.get'. Was '{Type}'"); + protected void BaseVerify(HashSet fullyQualifiedExpectedOrigins, ReadOnlySpan originalChallenge, ReadOnlySpan requestTokenBindingId) + { + if (Type is not "webauthn.create" && Type is not "webauthn.get") + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponse, $"Type must be 'webauthn.create' or 'webauthn.get'. Was '{Type}'"); - if (Challenge is null) - throw new Fido2VerificationException(Fido2ErrorCode.MissingAuthenticatorResponseChallenge, Fido2ErrorMessages.MissingAuthenticatorResponseChallange); + if (Challenge is null) + throw new Fido2VerificationException(Fido2ErrorCode.MissingAuthenticatorResponseChallenge, Fido2ErrorMessages.MissingAuthenticatorResponseChallange); - // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call - if (!Challenge.AsSpan().SequenceEqual(originalChallenge)) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponseChallenge, Fido2ErrorMessages.InvalidAuthenticatorResponseChallenge); + // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call + if (!Challenge.AsSpan().SequenceEqual(originalChallenge)) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorResponseChallenge, Fido2ErrorMessages.InvalidAuthenticatorResponseChallenge); - var fullyQualifiedOrigin = Origin.ToFullyQualifiedOrigin(); + var fullyQualifiedOrigin = Origin.ToFullyQualifiedOrigin(); - // 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})"); + // 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); + /* + 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}"; + if (UriHostNameType.Unknown != uri.HostNameType) + return uri.IsDefaultPort ? $"{uri.Scheme}://{uri.Host}" : $"{uri.Scheme}://{uri.Host}:{uri.Port}"; - return origin; - } - */ + return origin; } + */ } diff --git a/Src/Fido2/ConformanceMetadataService.cs b/Src/Fido2/ConformanceMetadataService.cs index 6c8f10159..a8ea6fd7a 100644 --- a/Src/Fido2/ConformanceMetadataService.cs +++ b/Src/Fido2/ConformanceMetadataService.cs @@ -5,94 +5,93 @@ using System.Threading; using System.Threading.Tasks; -namespace Fido2NetLib +namespace Fido2NetLib; + +public class ConformanceMetadataService : IMetadataService { - public class ConformanceMetadataService : IMetadataService + protected readonly List _repositories; + protected readonly ConcurrentDictionary _metadataStatements; + protected readonly ConcurrentDictionary _entries; + protected bool _initialized; + + public ConformanceMetadataService(IEnumerable repositories) { - protected readonly List _repositories; - protected readonly ConcurrentDictionary _metadataStatements; - protected readonly ConcurrentDictionary _entries; - protected bool _initialized; + _repositories = repositories.ToList(); + _metadataStatements = new ConcurrentDictionary(); + _entries = new ConcurrentDictionary(); + } - public ConformanceMetadataService(IEnumerable repositories) - { - _repositories = repositories.ToList(); - _metadataStatements = new ConcurrentDictionary(); - _entries = new ConcurrentDictionary(); - } + public bool ConformanceTesting() + { + return _repositories[0] is ConformanceMetadataRepository; + } - public bool ConformanceTesting() - { - return _repositories[0] is ConformanceMetadataRepository; - } + protected virtual MetadataBLOBPayloadEntry? GetEntry(Guid aaguid) + { + if (!IsInitialized()) + throw new InvalidOperationException("MetadataService must be initialized"); - protected virtual MetadataBLOBPayloadEntry? GetEntry(Guid aaguid) + if (_entries.TryGetValue(aaguid, out MetadataBLOBPayloadEntry? entry)) { - if (!IsInitialized()) - throw new InvalidOperationException("MetadataService must be initialized"); - - if (_entries.TryGetValue(aaguid, out MetadataBLOBPayloadEntry? entry)) + if (_metadataStatements.TryGetValue(aaguid, out var metadataStatement)) { - if (_metadataStatements.TryGetValue(aaguid, out var metadataStatement)) - { - entry.MetadataStatement = metadataStatement; - } - - return entry; - } - else - { - return null; + entry.MetadataStatement = metadataStatement; } - } - protected virtual async Task LoadEntryStatementAsync(IMetadataRepository repository, MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken) + return entry; + } + else { - if (entry.AaGuid != null) - { - var statement = await repository.GetMetadataStatementAsync(blob, entry, cancellationToken); - - if (!string.IsNullOrWhiteSpace(statement?.AaGuid)) - { - _metadataStatements.TryAdd(Guid.Parse(statement.AaGuid), statement); - } - } + return null; } + } - protected virtual async Task InitializeRepositoryAsync(IMetadataRepository repository, CancellationToken cancellationToken) + protected virtual async Task LoadEntryStatementAsync(IMetadataRepository repository, MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken) + { + if (entry.AaGuid != null) { - var blob = await repository.GetBLOBAsync(cancellationToken); + var statement = await repository.GetMetadataStatementAsync(blob, entry, cancellationToken); - foreach (var entry in blob.Entries) + if (!string.IsNullOrWhiteSpace(statement?.AaGuid)) { - if (!string.IsNullOrEmpty(entry.AaGuid)) - { - if (_entries.TryAdd(Guid.Parse(entry.AaGuid), entry)) - { - //Load if it doesn't already exist - await LoadEntryStatementAsync(repository, blob, entry, cancellationToken); - } - } + _metadataStatements.TryAdd(Guid.Parse(statement.AaGuid), statement); } } + } - public virtual async Task InitializeAsync(CancellationToken cancellationToken = default) + protected virtual async Task InitializeRepositoryAsync(IMetadataRepository repository, CancellationToken cancellationToken) + { + var blob = await repository.GetBLOBAsync(cancellationToken); + + foreach (var entry in blob.Entries) { - foreach (var repository in _repositories) + if (!string.IsNullOrEmpty(entry.AaGuid)) { - await InitializeRepositoryAsync(repository, cancellationToken); + if (_entries.TryAdd(Guid.Parse(entry.AaGuid), entry)) + { + //Load if it doesn't already exist + await LoadEntryStatementAsync(repository, blob, entry, cancellationToken); + } } - _initialized = true; } + } - public virtual bool IsInitialized() + public virtual async Task InitializeAsync(CancellationToken cancellationToken = default) + { + foreach (var repository in _repositories) { - return _initialized; + await InitializeRepositoryAsync(repository, cancellationToken); } + _initialized = true; + } - public virtual Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default) - { - return Task.FromResult(GetEntry(aaguid)); - } + public virtual bool IsInitialized() + { + return _initialized; + } + + public virtual Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default) + { + return Task.FromResult(GetEntry(aaguid)); } } diff --git a/Src/Fido2/CryptoUtils.cs b/Src/Fido2/CryptoUtils.cs index 182965502..549e71d2f 100644 --- a/Src/Fido2/CryptoUtils.cs +++ b/Src/Fido2/CryptoUtils.cs @@ -7,228 +7,227 @@ using Fido2NetLib.Exceptions; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +internal static class CryptoUtils { - internal static class CryptoUtils + public static byte[] HashData(HashAlgorithmName hashName, ReadOnlySpan data) { - public static byte[] HashData(HashAlgorithmName hashName, ReadOnlySpan data) + return hashName.Name switch { - return hashName.Name switch - { - "SHA1" => SHA1.HashData(data), - "SHA256" or "HS256" or "RS256" or "ES256" or "PS256" => SHA256.HashData(data), - "SHA384" or "HS384" or "RS384" or "ES384" or "PS384" => SHA384.HashData(data), - "SHA512" or "HS512" or "RS512" or "ES512" or "PS512" => SHA512.HashData(data), - _ => throw new ArgumentOutOfRangeException(nameof(hashName)), - }; - } + "SHA1" => SHA1.HashData(data), + "SHA256" or "HS256" or "RS256" or "ES256" or "PS256" => SHA256.HashData(data), + "SHA384" or "HS384" or "RS384" or "ES384" or "PS384" => SHA384.HashData(data), + "SHA512" or "HS512" or "RS512" or "ES512" or "PS512" => SHA512.HashData(data), + _ => throw new ArgumentOutOfRangeException(nameof(hashName)), + }; + } - public static HashAlgorithmName HashAlgFromCOSEAlg(COSE.Algorithm alg) + public static HashAlgorithmName HashAlgFromCOSEAlg(COSE.Algorithm alg) + { + return alg switch { - return alg switch - { - COSE.Algorithm.RS1 => HashAlgorithmName.SHA1, - COSE.Algorithm.ES256 => HashAlgorithmName.SHA256, - COSE.Algorithm.ES384 => HashAlgorithmName.SHA384, - COSE.Algorithm.ES512 => HashAlgorithmName.SHA512, - COSE.Algorithm.PS256 => HashAlgorithmName.SHA256, - COSE.Algorithm.PS384 => HashAlgorithmName.SHA384, - COSE.Algorithm.PS512 => HashAlgorithmName.SHA512, - COSE.Algorithm.RS256 => HashAlgorithmName.SHA256, - COSE.Algorithm.RS384 => HashAlgorithmName.SHA384, - COSE.Algorithm.RS512 => HashAlgorithmName.SHA512, - COSE.Algorithm.ES256K => HashAlgorithmName.SHA256, - (COSE.Algorithm)4 => HashAlgorithmName.SHA1, - (COSE.Algorithm)11 => HashAlgorithmName.SHA256, - (COSE.Algorithm)12 => HashAlgorithmName.SHA384, - (COSE.Algorithm)13 => HashAlgorithmName.SHA512, - COSE.Algorithm.EdDSA => HashAlgorithmName.SHA512, - _ => throw new Fido2VerificationException(Fido2ErrorMessages.InvalidCoseAlgorithmValue), - }; - } + COSE.Algorithm.RS1 => HashAlgorithmName.SHA1, + COSE.Algorithm.ES256 => HashAlgorithmName.SHA256, + COSE.Algorithm.ES384 => HashAlgorithmName.SHA384, + COSE.Algorithm.ES512 => HashAlgorithmName.SHA512, + COSE.Algorithm.PS256 => HashAlgorithmName.SHA256, + COSE.Algorithm.PS384 => HashAlgorithmName.SHA384, + COSE.Algorithm.PS512 => HashAlgorithmName.SHA512, + COSE.Algorithm.RS256 => HashAlgorithmName.SHA256, + COSE.Algorithm.RS384 => HashAlgorithmName.SHA384, + COSE.Algorithm.RS512 => HashAlgorithmName.SHA512, + COSE.Algorithm.ES256K => HashAlgorithmName.SHA256, + (COSE.Algorithm)4 => HashAlgorithmName.SHA1, + (COSE.Algorithm)11 => HashAlgorithmName.SHA256, + (COSE.Algorithm)12 => HashAlgorithmName.SHA384, + (COSE.Algorithm)13 => HashAlgorithmName.SHA512, + COSE.Algorithm.EdDSA => HashAlgorithmName.SHA512, + _ => throw new Fido2VerificationException(Fido2ErrorMessages.InvalidCoseAlgorithmValue), + }; + } - public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certificate2[] attestationRootCertificates) - { - // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-statement-v2.0-id-20180227.html#widl-MetadataStatement-attestationRootCertificates + public static bool ValidateTrustChain(X509Certificate2[] trustPath, X509Certificate2[] attestationRootCertificates) + { + // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-statement-v2.0-id-20180227.html#widl-MetadataStatement-attestationRootCertificates - // Each element of this array represents a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model. - // Multiple certificates might be used for different batches of the same model. - // The array does not represent a certificate chain, but only the trust anchor of that chain. - // A trust anchor can be a root certificate, an intermediate CA certificate or even the attestation certificate itself. + // Each element of this array represents a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model. + // Multiple certificates might be used for different batches of the same model. + // The array does not represent a certificate chain, but only the trust anchor of that chain. + // 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)) + // 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)) + { + foreach (X509Certificate2 cert in attestationRootCertificates) { - foreach (X509Certificate2 cert in attestationRootCertificates) + if (cert.Thumbprint.Equals(trustPath[0].Thumbprint, StringComparison.Ordinal)) { - if (cert.Thumbprint.Equals(trustPath[0].Thumbprint, StringComparison.Ordinal)) - { - return true; - } + return true; } - return false; } + return false; + } - // If the attestation cert is not self signed, we will need to build a chain - var chain = new X509Chain(); + // If the attestation cert is not self signed, we will need to build a chain + var chain = new X509Chain(); - // Put all potential trust anchors into extra store - chain.ChainPolicy.ExtraStore.AddRange(attestationRootCertificates); + // Put all potential trust anchors into extra store + chain.ChainPolicy.ExtraStore.AddRange(attestationRootCertificates); - // We don't know the root here, so allow unknown root, and turn off revocation checking - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + // We don't know the root here, so allow unknown root, and turn off revocation checking + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; - // trustPath[0] is the attestation cert, if there are more in the array than just that, add those to the extra store as well, but skip attestation cert - if (trustPath.Length > 1) + // trustPath[0] is the attestation cert, if there are more in the array than just that, add those to the extra store as well, but skip attestation cert + if (trustPath.Length > 1) + { + foreach (X509Certificate2 cert in trustPath.Skip(1)) // skip attestation cert { - foreach (X509Certificate2 cert in trustPath.Skip(1)) // skip attestation cert - { - chain.ChainPolicy.ExtraStore.Add(cert); - } + chain.ChainPolicy.ExtraStore.Add(cert); } + } - // try to build a chain with what we've got - if (chain.Build(trustPath[0])) - { - // if that validated, we should have a root for this chain now, add it to the custom trust store - chain.ChainPolicy.CustomTrustStore.Clear(); - chain.ChainPolicy.CustomTrustStore.Add(chain.ChainElements[^1].Certificate); + // try to build a chain with what we've got + if (chain.Build(trustPath[0])) + { + // if that validated, we should have a root for this chain now, add it to the custom trust store + chain.ChainPolicy.CustomTrustStore.Clear(); + chain.ChainPolicy.CustomTrustStore.Add(chain.ChainElements[^1].Certificate); - // explicitly trust the custom root we just added - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + // explicitly trust the custom root we just added + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - // if the attestation cert has a CDP extension, go ahead and turn on online revocation checking - if (!string.IsNullOrEmpty(CDPFromCertificateExts(trustPath[0].Extensions))) - chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + // if the attestation cert has a CDP extension, go ahead and turn on online revocation checking + if (!string.IsNullOrEmpty(CDPFromCertificateExts(trustPath[0].Extensions))) + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; - // don't allow unknown root now that we have a custom root - chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + // don't allow unknown root now that we have a custom root + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; - // now, verify chain again with all checks turned on - if (chain.Build(trustPath[0])) + // now, verify chain again with all checks turned on + if (chain.Build(trustPath[0])) + { + // if the chain validates, make sure one of the attestation root certificates is one of the chain elements + foreach (X509Certificate2? attestationRootCertificate in attestationRootCertificates) { - // if the chain validates, make sure one of the attestation root certificates is one of the chain elements - foreach (X509Certificate2? attestationRootCertificate in attestationRootCertificates) - { - // skip the first element, as that is the attestation cert - if (chain.ChainElements - .Cast() - .Skip(1) - .Any(x => x.Certificate.Thumbprint.Equals(attestationRootCertificate.Thumbprint, StringComparison.Ordinal))) - return true; - } + // skip the first element, as that is the attestation cert + if (chain.ChainElements + .Cast() + .Skip(1) + .Any(x => x.Certificate.Thumbprint.Equals(attestationRootCertificate.Thumbprint, StringComparison.Ordinal))) + return true; } } - - return false; } - public static byte[] SigFromEcDsaSig(byte[] ecDsaSig, int keySize) - { - var decoded = Asn1Element.Decode(ecDsaSig); - var r = decoded[0].GetIntegerBytes(); - var s = decoded[1].GetIntegerBytes(); + return false; + } - // .NET requires IEEE P-1363 fixed size unsigned big endian values for R and S - // ASN.1 requires storing positive integer values with any leading 0s removed - // Convert ASN.1 format to IEEE P-1363 format - // determine coefficient size + public static byte[] SigFromEcDsaSig(byte[] ecDsaSig, int keySize) + { + var decoded = Asn1Element.Decode(ecDsaSig); + var r = decoded[0].GetIntegerBytes(); + var s = decoded[1].GetIntegerBytes(); - // common coefficient sizes include: 32, 48, and 64 - var coefficientSize = (int)Math.Ceiling((decimal)keySize / 8); + // .NET requires IEEE P-1363 fixed size unsigned big endian values for R and S + // ASN.1 requires storing positive integer values with any leading 0s removed + // Convert ASN.1 format to IEEE P-1363 format + // determine coefficient size - // Create buffer to copy R into - Span p1363R = coefficientSize <= 64 - ? stackalloc byte[coefficientSize] - : new byte[coefficientSize]; + // common coefficient sizes include: 32, 48, and 64 + var coefficientSize = (int)Math.Ceiling((decimal)keySize / 8); - if (0x0 == r[0] && (r[1] & (1 << 7)) != 0) - { - r.Slice(1).CopyTo(p1363R.Slice(coefficientSize - r.Length + 1)); - } - else - { - r.CopyTo(p1363R.Slice(coefficientSize - r.Length)); - } + // Create buffer to copy R into + Span p1363R = coefficientSize <= 64 + ? stackalloc byte[coefficientSize] + : new byte[coefficientSize]; - // Create byte array to copy S into - Span p1363S = coefficientSize <= 64 - ? stackalloc byte[coefficientSize] - : new byte[coefficientSize]; + if (0x0 == r[0] && (r[1] & (1 << 7)) != 0) + { + r.Slice(1).CopyTo(p1363R.Slice(coefficientSize - r.Length + 1)); + } + else + { + r.CopyTo(p1363R.Slice(coefficientSize - r.Length)); + } - if (0x0 == s[0] && (s[1] & (1 << 7)) != 0) - { - s.Slice(1).CopyTo(p1363S.Slice(coefficientSize - s.Length + 1)); - } - else - { - s.CopyTo(p1363S.Slice(coefficientSize - s.Length)); - } + // Create byte array to copy S into + Span p1363S = coefficientSize <= 64 + ? stackalloc byte[coefficientSize] + : new byte[coefficientSize]; - // Concatenate R + S coordinates and return the raw signature - return DataHelper.Concat(p1363R, p1363S); + if (0x0 == s[0] && (s[1] & (1 << 7)) != 0) + { + s.Slice(1).CopyTo(p1363S.Slice(coefficientSize - s.Length + 1)); } - - /// - /// Convert PEM formated string into byte array. - /// - /// source string. - /// output byte array. - public static byte[] PemToBytes(ReadOnlySpan pemStr) + else { - var range = PemEncoding.Find(pemStr); + s.CopyTo(p1363S.Slice(coefficientSize - s.Length)); + } - byte[] data = new byte[range.DecodedDataLength]; + // Concatenate R + S coordinates and return the raw signature + return DataHelper.Concat(p1363R, p1363S); + } - Convert.TryFromBase64Chars(pemStr[range.Base64Data], data, out _); + /// + /// Convert PEM formated string into byte array. + /// + /// source string. + /// output byte array. + public static byte[] PemToBytes(ReadOnlySpan pemStr) + { + var range = PemEncoding.Find(pemStr); - return data; - } + byte[] data = new byte[range.DecodedDataLength]; - public static string CDPFromCertificateExts(X509ExtensionCollection exts) + Convert.TryFromBase64Chars(pemStr[range.Base64Data], data, out _); + + return data; + } + + public static string CDPFromCertificateExts(X509ExtensionCollection exts) + { + var cdp = ""; + foreach (var ext in exts) { - var cdp = ""; - foreach (var ext in exts) + if (ext.Oid!.Value is "2.5.29.31") // id-ce-CRLDistributionPoints { - if (ext.Oid!.Value is "2.5.29.31") // id-ce-CRLDistributionPoints - { - var asnData = Asn1Element.Decode(ext.RawData); + var asnData = Asn1Element.Decode(ext.RawData); - var el = asnData[0][0][0][0]; + var el = asnData[0][0][0][0]; - cdp = Encoding.ASCII.GetString(el.GetOctetString(el.Tag)); - } + cdp = Encoding.ASCII.GetString(el.GetOctetString(el.Tag)); } - return cdp; } + return cdp; + } - public static bool IsCertInCRL(byte[] crl, X509Certificate2 cert) - { - var asnData = Asn1Element.Decode(crl); + public static bool IsCertInCRL(byte[] crl, X509Certificate2 cert) + { + var asnData = Asn1Element.Decode(crl); - if (7 > asnData[0].Sequence.Count) - return false; // empty CRL + if (7 > asnData[0].Sequence.Count) + return false; // empty CRL - // Certificate users MUST be able to handle serialNumber values up to 20 octets. + // Certificate users MUST be able to handle serialNumber values up to 20 octets. - var certificateSerialNumber = cert.GetSerialNumber().ToArray(); // defensively copy + var certificateSerialNumber = cert.GetSerialNumber().ToArray(); // defensively copy - Array.Reverse(certificateSerialNumber); // convert to big-endian order + Array.Reverse(certificateSerialNumber); // convert to big-endian order - var revokedAsnSequence = asnData[0][5].Sequence; - - for (int i = 0; i < revokedAsnSequence.Count; i++) - { - ReadOnlySpan revokedSerialNumber = revokedAsnSequence[i][0].GetIntegerBytes(); + var revokedAsnSequence = asnData[0][5].Sequence; + + for (int i = 0; i < revokedAsnSequence.Count; i++) + { + ReadOnlySpan revokedSerialNumber = revokedAsnSequence[i][0].GetIntegerBytes(); - if (revokedSerialNumber.SequenceEqual(certificateSerialNumber)) - { - return true; - } + if (revokedSerialNumber.SequenceEqual(certificateSerialNumber)) + { + return true; } - - return false; } + + return false; } } diff --git a/Src/Fido2/DevelopmentInMemoryStore.cs b/Src/Fido2/DevelopmentInMemoryStore.cs index 799310a07..f67057ed6 100644 --- a/Src/Fido2/DevelopmentInMemoryStore.cs +++ b/Src/Fido2/DevelopmentInMemoryStore.cs @@ -4,76 +4,76 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; + using Fido2NetLib.Objects; -namespace Fido2NetLib.Development +namespace Fido2NetLib.Development; + +public class DevelopmentInMemoryStore { - public class DevelopmentInMemoryStore - { - private readonly ConcurrentDictionary _storedUsers = new(); - private readonly List _storedCredentials = new(); + private readonly ConcurrentDictionary _storedUsers = new(); + private readonly List _storedCredentials = new(); - public Fido2User GetOrAddUser(string username, Func addCallback) - { - return _storedUsers.GetOrAdd(username, addCallback()); - } + public Fido2User GetOrAddUser(string username, Func addCallback) + { + return _storedUsers.GetOrAdd(username, addCallback()); + } - public Fido2User? GetUser(string username) - { - _storedUsers.TryGetValue(username, out var user); - return user; - } + public Fido2User? GetUser(string username) + { + _storedUsers.TryGetValue(username, out var user); + return user; + } - public List GetCredentialsByUser(Fido2User user) - { - return _storedCredentials.Where(c => c.UserId.AsSpan().SequenceEqual(user.Id)).ToList(); - } + public List GetCredentialsByUser(Fido2User user) + { + return _storedCredentials.Where(c => c.UserId.AsSpan().SequenceEqual(user.Id)).ToList(); + } - public StoredCredential? GetCredentialById(byte[] id) - { - return _storedCredentials.FirstOrDefault(c => c.Descriptor.Id.AsSpan().SequenceEqual(id)); - } + public StoredCredential? GetCredentialById(byte[] id) + { + return _storedCredentials.FirstOrDefault(c => c.Descriptor.Id.AsSpan().SequenceEqual(id)); + } - public Task> GetCredentialsByUserHandleAsync(byte[] userHandle, CancellationToken cancellationToken = default) - { - return Task.FromResult(_storedCredentials.Where(c => c.UserHandle.AsSpan().SequenceEqual(userHandle)).ToList()); - } + public Task> GetCredentialsByUserHandleAsync(byte[] userHandle, CancellationToken cancellationToken = default) + { + return Task.FromResult(_storedCredentials.Where(c => c.UserHandle.AsSpan().SequenceEqual(userHandle)).ToList()); + } - public void UpdateCounter(byte[] credentialId, uint counter) - { - var cred = _storedCredentials.First(c => c.Descriptor.Id.AsSpan().SequenceEqual(credentialId)); - cred.SignatureCounter = counter; - } + public void UpdateCounter(byte[] credentialId, uint counter) + { + var cred = _storedCredentials.First(c => c.Descriptor.Id.AsSpan().SequenceEqual(credentialId)); + cred.SignatureCounter = counter; + } - public void AddCredentialToUser(Fido2User user, StoredCredential credential) - { - credential.UserId = user.Id; - _storedCredentials.Add(credential); - } + public void AddCredentialToUser(Fido2User user, StoredCredential credential) + { + credential.UserId = user.Id; + _storedCredentials.Add(credential); + } - public Task> GetUsersByCredentialIdAsync(byte[] credentialId, CancellationToken cancellationToken = default) - { - // our in-mem storage does not allow storing multiple users for a given credentialId. Yours shouldn't either. - var cred = _storedCredentials.FirstOrDefault(c => c.Descriptor.Id.AsSpan().SequenceEqual(credentialId)); + public Task> GetUsersByCredentialIdAsync(byte[] credentialId, CancellationToken cancellationToken = default) + { + // our in-mem storage does not allow storing multiple users for a given credentialId. Yours shouldn't either. + var cred = _storedCredentials.FirstOrDefault(c => c.Descriptor.Id.AsSpan().SequenceEqual(credentialId)); - if (cred is null) - return Task.FromResult(new List()); + if (cred is null) + return Task.FromResult(new List()); - return Task.FromResult(_storedUsers.Where(u => u.Value.Id.SequenceEqual(cred.UserId)).Select(u => u.Value).ToList()); - } + return Task.FromResult(_storedUsers.Where(u => u.Value.Id.SequenceEqual(cred.UserId)).Select(u => u.Value).ToList()); } +} #nullable disable - public class StoredCredential - { - public byte[] UserId { get; set; } - public PublicKeyCredentialDescriptor Descriptor { get; set; } - public byte[] PublicKey { get; set; } - public byte[] UserHandle { get; set; } - public uint SignatureCounter { get; set; } - public string CredType { get; set; } - public DateTime RegDate { get; set; } - public Guid AaGuid { get; set; } - } +public class StoredCredential +{ + public byte[] UserId { get; set; } + public PublicKeyCredentialDescriptor Descriptor { get; set; } + public byte[] PublicKey { get; set; } + public byte[] UserHandle { get; set; } + public uint SignatureCounter { get; set; } + public string CredType { get; set; } + public DateTime RegDate { get; set; } + public Guid AaGuid { get; set; } } diff --git a/Src/Fido2/IFido2.cs b/Src/Fido2/IFido2.cs index 51af95513..235d3277b 100644 --- a/Src/Fido2/IFido2.cs +++ b/Src/Fido2/IFido2.cs @@ -3,41 +3,40 @@ using System.Threading.Tasks; using Fido2NetLib.Objects; -namespace Fido2NetLib +namespace Fido2NetLib; + +public interface IFido2 { - public interface IFido2 - { - AssertionOptions GetAssertionOptions( - IEnumerable allowedCredentials, - UserVerificationRequirement? userVerification, - AuthenticationExtensionsClientInputs? extensions = null); + AssertionOptions GetAssertionOptions( + IEnumerable allowedCredentials, + UserVerificationRequirement? userVerification, + AuthenticationExtensionsClientInputs? extensions = null); - Task MakeAssertionAsync( - AuthenticatorAssertionRawResponse assertionResponse, - AssertionOptions originalOptions, - byte[] storedPublicKey, - uint storedSignatureCounter, - IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback, - byte[]? requestTokenBindingId = null, - CancellationToken cancellationToken = default); + Task MakeAssertionAsync( + AuthenticatorAssertionRawResponse assertionResponse, + AssertionOptions originalOptions, + byte[] storedPublicKey, + uint storedSignatureCounter, + IsUserHandleOwnerOfCredentialIdAsync isUserHandleOwnerOfCredentialIdCallback, + byte[]? requestTokenBindingId = null, + CancellationToken cancellationToken = default); - Task MakeNewCredentialAsync( - AuthenticatorAttestationRawResponse attestationResponse, - CredentialCreateOptions origChallenge, - IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, - byte[]? requestTokenBindingId = null, - CancellationToken cancellationToken = default); + Task MakeNewCredentialAsync( + AuthenticatorAttestationRawResponse attestationResponse, + CredentialCreateOptions origChallenge, + IsCredentialIdUniqueToUserAsyncDelegate isCredentialIdUniqueToUser, + byte[]? requestTokenBindingId = null, + CancellationToken cancellationToken = default); - CredentialCreateOptions RequestNewCredential( - Fido2User user, - List excludeCredentials, - AuthenticationExtensionsClientInputs? extensions = null); + CredentialCreateOptions RequestNewCredential( + Fido2User user, + List excludeCredentials, + AuthenticationExtensionsClientInputs? extensions = null); - CredentialCreateOptions RequestNewCredential( - Fido2User user, - List excludeCredentials, - AuthenticatorSelection authenticatorSelection, - AttestationConveyancePreference attestationPreference, - AuthenticationExtensionsClientInputs? extensions = null); - } + CredentialCreateOptions RequestNewCredential( + Fido2User user, + List excludeCredentials, + AuthenticatorSelection authenticatorSelection, + AttestationConveyancePreference attestationPreference, + AuthenticationExtensionsClientInputs? extensions = null); } diff --git a/Src/Fido2/IMetadataRepository.cs b/Src/Fido2/IMetadataRepository.cs index 2ff45975b..09b7e0920 100644 --- a/Src/Fido2/IMetadataRepository.cs +++ b/Src/Fido2/IMetadataRepository.cs @@ -1,12 +1,11 @@ using System.Threading; using System.Threading.Tasks; -namespace Fido2NetLib +namespace Fido2NetLib; + +public interface IMetadataRepository { - public interface IMetadataRepository - { - Task GetBLOBAsync(CancellationToken cancellationToken = default); + Task GetBLOBAsync(CancellationToken cancellationToken = default); - Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default); - } + Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default); } diff --git a/Src/Fido2/IMetadataService.cs b/Src/Fido2/IMetadataService.cs index 33f770629..be289bcdb 100644 --- a/Src/Fido2/IMetadataService.cs +++ b/Src/Fido2/IMetadataService.cs @@ -2,23 +2,22 @@ using System.Threading; using System.Threading.Tasks; -namespace Fido2NetLib +namespace Fido2NetLib; + +public interface IMetadataService { - public interface IMetadataService - { - /// - /// Gets the metadata payload entry by a guid asyncronously - /// - /// The Authenticator Attestation GUID. - /// Returns the entry; Otherwise null. - Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default); + /// + /// Gets the metadata payload entry by a guid asyncronously + /// + /// The Authenticator Attestation GUID. + /// Returns the entry; Otherwise null. + Task GetEntryAsync(Guid aaguid, CancellationToken cancellationToken = default); - /// - /// Gets a value indicating whether the internal access token is valid. - /// - /// - /// Returns true if access token is valid, or false if the access token is equal to an invalid token value. - /// - bool ConformanceTesting(); - } + /// + /// Gets a value indicating whether the internal access token is valid. + /// + /// + /// Returns true if access token is valid, or false if the access token is equal to an invalid token value. + /// + bool ConformanceTesting(); } diff --git a/Src/Fido2/Metadata/ConformanceMetadataRepository.cs b/Src/Fido2/Metadata/ConformanceMetadataRepository.cs index 2f4062691..99f032a44 100644 --- a/Src/Fido2/Metadata/ConformanceMetadataRepository.cs +++ b/Src/Fido2/Metadata/ConformanceMetadataRepository.cs @@ -14,230 +14,229 @@ using Microsoft.IdentityModel.Tokens; -namespace Fido2NetLib +namespace Fido2NetLib; + +public sealed class ConformanceMetadataRepository : IMetadataRepository { - public sealed class ConformanceMetadataRepository : IMetadataRepository + private const string ROOT_CERT = "MIICaDCCAe6gAwIBAgIPBCqih0DiJLW7+UHXx/o1MAoGCCqGSM49BAMDMGcxCzAJ" + + "BgNVBAYTAlVTMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMScwJQYDVQQLDB5GQUtF" + + "IE1ldGFkYXRhIDMgQkxPQiBST09UIEZBS0UxFzAVBgNVBAMMDkZBS0UgUm9vdCBG" + + "QUtFMB4XDTE3MDIwMTAwMDAwMFoXDTQ1MDEzMTIzNTk1OVowZzELMAkGA1UEBhMC" + + "VVMxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxJzAlBgNVBAsMHkZBS0UgTWV0YWRh" + + "dGEgMyBCTE9CIFJPT1QgRkFLRTEXMBUGA1UEAwwORkFLRSBSb290IEZBS0UwdjAQ" + + "BgcqhkjOPQIBBgUrgQQAIgNiAASKYiz3YltC6+lmxhPKwA1WFZlIqnX8yL5RybSL" + + "TKFAPEQeTD9O6mOz+tg8wcSdnVxHzwnXiQKJwhrav70rKc2ierQi/4QUrdsPes8T" + + "EirZOkCVJurpDFbXZOgs++pa4XmjYDBeMAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E" + + "BTADAQH/MB0GA1UdDgQWBBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAfBgNVHSMEGDAW" + + "gBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAKBggqhkjOPQQDAwNoADBlAjEA/xFsgri0" + + "xubSa3y3v5ormpPqCwfqn9s0MLBAtzCIgxQ/zkzPKctkiwoPtDzI51KnAjAmeMyg" + + "X2S5Ht8+e+EQnezLJBJXtnkRWY+Zt491wgt/AwSs5PHHMv5QgjELOuMxQBc="; + + private readonly HttpClient _httpClient; + + private readonly string _origin; + + private readonly string _getEndpointsUrl = "https://mds3.certinfra.fidoalliance.org/getEndpoints"; + + public ConformanceMetadataRepository(HttpClient? client, string origin) { - private const string ROOT_CERT = "MIICaDCCAe6gAwIBAgIPBCqih0DiJLW7+UHXx/o1MAoGCCqGSM49BAMDMGcxCzAJ" + - "BgNVBAYTAlVTMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMScwJQYDVQQLDB5GQUtF" + - "IE1ldGFkYXRhIDMgQkxPQiBST09UIEZBS0UxFzAVBgNVBAMMDkZBS0UgUm9vdCBG" + - "QUtFMB4XDTE3MDIwMTAwMDAwMFoXDTQ1MDEzMTIzNTk1OVowZzELMAkGA1UEBhMC" + - "VVMxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxJzAlBgNVBAsMHkZBS0UgTWV0YWRh" + - "dGEgMyBCTE9CIFJPT1QgRkFLRTEXMBUGA1UEAwwORkFLRSBSb290IEZBS0UwdjAQ" + - "BgcqhkjOPQIBBgUrgQQAIgNiAASKYiz3YltC6+lmxhPKwA1WFZlIqnX8yL5RybSL" + - "TKFAPEQeTD9O6mOz+tg8wcSdnVxHzwnXiQKJwhrav70rKc2ierQi/4QUrdsPes8T" + - "EirZOkCVJurpDFbXZOgs++pa4XmjYDBeMAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E" + - "BTADAQH/MB0GA1UdDgQWBBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAfBgNVHSMEGDAW" + - "gBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAKBggqhkjOPQQDAwNoADBlAjEA/xFsgri0" + - "xubSa3y3v5ormpPqCwfqn9s0MLBAtzCIgxQ/zkzPKctkiwoPtDzI51KnAjAmeMyg" + - "X2S5Ht8+e+EQnezLJBJXtnkRWY+Zt491wgt/AwSs5PHHMv5QgjELOuMxQBc="; - - private readonly HttpClient _httpClient; - - private readonly string _origin; - - private readonly string _getEndpointsUrl = "https://mds3.certinfra.fidoalliance.org/getEndpoints"; - - public ConformanceMetadataRepository(HttpClient? client, string origin) - { - _httpClient = client ?? new HttpClient(); - _origin = origin; - } + _httpClient = client ?? new HttpClient(); + _origin = origin; + } - public Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default) - { - return Task.FromResult(entry.MetadataStatement); - } + public Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default) + { + return Task.FromResult(entry.MetadataStatement); + } - public async Task GetBLOBAsync(CancellationToken cancellationToken = default) + public async Task GetBLOBAsync(CancellationToken cancellationToken = default) + { + var req = new GetBLOBRequest(_origin); + + var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(req, FidoSerializerContext.Default.GetBLOBRequest)) { - var req = new GetBLOBRequest(_origin); + Headers = { { "Content-Type", "application/json" } } + }; - var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(req, FidoSerializerContext.Default.GetBLOBRequest)) - { - Headers = { { "Content-Type", "application/json" } } - }; + using var response = await _httpClient.PostAsync(_getEndpointsUrl, content, cancellationToken); + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + MDSGetEndpointResponse? result = await JsonSerializer.DeserializeAsync(responseStream, FidoSerializerContext.Default.MDSGetEndpointResponse, cancellationToken: cancellationToken); + var conformanceEndpoints = result!.Result; - using var response = await _httpClient.PostAsync(_getEndpointsUrl, content, cancellationToken); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); - MDSGetEndpointResponse? result = await JsonSerializer.DeserializeAsync(responseStream, FidoSerializerContext.Default.MDSGetEndpointResponse, cancellationToken: cancellationToken); - var conformanceEndpoints = result!.Result; + var combinedBlob = new MetadataBLOBPayload + { + Number = -1, + NextUpdate = "2099-08-07" + }; - var combinedBlob = new MetadataBLOBPayload - { - Number = -1, - NextUpdate = "2099-08-07" - }; + var entries = new List(); - var entries = new List(); + foreach(var blobUrl in conformanceEndpoints) + { + var rawBlob = await DownloadStringAsync(blobUrl, cancellationToken); + + MetadataBLOBPayload blob; - foreach(var blobUrl in conformanceEndpoints) + try + { + blob = await DeserializeAndValidateBlob(rawBlob, cancellationToken); + } + catch { - var rawBlob = await DownloadStringAsync(blobUrl, cancellationToken); + continue; + } + + if(string.Compare(blob.NextUpdate, combinedBlob.NextUpdate, StringComparison.InvariantCulture) < 0) + combinedBlob.NextUpdate = blob.NextUpdate; + if (combinedBlob.Number < blob.Number) + combinedBlob.Number = blob.Number; - MetadataBLOBPayload blob; + foreach (var entry in blob.Entries) + { + entries.Add(entry); + } + combinedBlob.JwtAlg = blob.JwtAlg; + } - try - { - blob = await DeserializeAndValidateBlob(rawBlob, cancellationToken); - } - catch - { - continue; - } - - if(string.Compare(blob.NextUpdate, combinedBlob.NextUpdate, StringComparison.InvariantCulture) < 0) - combinedBlob.NextUpdate = blob.NextUpdate; - if (combinedBlob.Number < blob.Number) - combinedBlob.Number = blob.Number; + combinedBlob.Entries = entries.ToArray(); + return combinedBlob; + } - foreach (var entry in blob.Entries) - { - entries.Add(entry); - } - combinedBlob.JwtAlg = blob.JwtAlg; - } + private Task DownloadStringAsync(string url, CancellationToken cancellationToken) + { + return _httpClient.GetStringAsync(url, cancellationToken); + } - combinedBlob.Entries = entries.ToArray(); - return combinedBlob; - } + private Task DownloadDataAsync(string url, CancellationToken cancellationToken) + { + return _httpClient.GetByteArrayAsync(url, cancellationToken); + } - private Task DownloadStringAsync(string url, CancellationToken cancellationToken) + private X509Certificate2 GetX509Certificate(string key) + { + try { - return _httpClient.GetStringAsync(url, cancellationToken); + var certBytes = Convert.FromBase64String(key); + return new X509Certificate2(certBytes); } - - private Task DownloadDataAsync(string url, CancellationToken cancellationToken) + catch (Exception ex) { - return _httpClient.GetByteArrayAsync(url, cancellationToken); + throw new ArgumentException("Could not parse X509 certificate.", ex); } + } - private X509Certificate2 GetX509Certificate(string key) - { - try - { - var certBytes = Convert.FromBase64String(key); - return new X509Certificate2(certBytes); - } - catch (Exception ex) - { - throw new ArgumentException("Could not parse X509 certificate.", ex); - } - } + public async Task DeserializeAndValidateBlob(string rawBLOBJwt, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(rawBLOBJwt)) + throw new ArgumentNullException(nameof(rawBLOBJwt)); - public async Task DeserializeAndValidateBlob(string rawBLOBJwt, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(rawBLOBJwt)) - throw new ArgumentNullException(nameof(rawBLOBJwt)); + var jwtParts = rawBLOBJwt.Split('.'); - var jwtParts = rawBLOBJwt.Split('.'); + if (jwtParts.Length != 3) + throw new ArgumentException("The JWT does not have the 3 expected components"); - if (jwtParts.Length != 3) - throw new ArgumentException("The JWT does not have the 3 expected components"); + var blobHeader = jwtParts[0]; + using var jsonDoc = JsonDocument.Parse(Base64Url.Decode(blobHeader)); + var tokenHeader = jsonDoc.RootElement; - var blobHeader = jwtParts[0]; - using var jsonDoc = JsonDocument.Parse(Base64Url.Decode(blobHeader)); - var tokenHeader = jsonDoc.RootElement; + var blobAlg = tokenHeader.TryGetProperty("alg", out var algEl) + ? algEl.GetString()! + : throw new ArgumentNullException("No alg value was present in the BLOB header."); - var blobAlg = tokenHeader.TryGetProperty("alg", out var algEl) - ? algEl.GetString()! - : throw new ArgumentNullException("No alg value was present in the BLOB header."); + var blobCertStrings = tokenHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array + ? x5cEl.ToStringArray() + : throw new ArgumentException("No x5c array was present in the BLOB header."); - var blobCertStrings = tokenHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array - ? x5cEl.ToStringArray() - : throw new ArgumentException("No x5c array was present in the BLOB header."); + var rootCert = GetX509Certificate(ROOT_CERT); + var blobCertificates = new X509Certificate2[blobCertStrings.Length]; + var blobPublicKeys = new List(blobCertStrings.Length); - var rootCert = GetX509Certificate(ROOT_CERT); - var blobCertificates = new X509Certificate2[blobCertStrings.Length]; - var blobPublicKeys = new List(blobCertStrings.Length); + for (int i = 0; i < blobCertStrings.Length; i++) + { + var cert = GetX509Certificate(blobCertStrings[i]); + blobCertificates[i] = cert; - for (int i = 0; i < blobCertStrings.Length; i++) - { - var cert = GetX509Certificate(blobCertStrings[i]); - blobCertificates[i] = cert; + if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) + blobPublicKeys.Add(new ECDsaSecurityKey(ecdsaPublicKey)); + + else if (cert.GetRSAPublicKey() is RSA rsa) + blobPublicKeys.Add(new RsaSecurityKey(rsa)); - if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) - blobPublicKeys.Add(new ECDsaSecurityKey(ecdsaPublicKey)); - - else if (cert.GetRSAPublicKey() is RSA rsa) - blobPublicKeys.Add(new RsaSecurityKey(rsa)); + } - } - - var certChain = new X509Chain(); - certChain.ChainPolicy.ExtraStore.Add(rootCert); - certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + var certChain = new X509Chain(); + certChain.ChainPolicy.ExtraStore.Add(rootCert); + certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - ValidateIssuerSigningKey = true, - IssuerSigningKeys = blobPublicKeys, - }; - - var tokenHandler = new JwtSecurityTokenHandler() - { - // 250k isn't enough bytes for conformance test tool - // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1097 - MaximumTokenSizeInBytes = rawBLOBJwt.Length - }; + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = blobPublicKeys, + }; + + var tokenHandler = new JwtSecurityTokenHandler() + { + // 250k isn't enough bytes for conformance test tool + // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1097 + MaximumTokenSizeInBytes = rawBLOBJwt.Length + }; - tokenHandler.ValidateToken( - rawBLOBJwt, - validationParameters, - out var validatedToken); + tokenHandler.ValidateToken( + rawBLOBJwt, + validationParameters, + out var validatedToken); - if(blobCertificates.Length > 1) - { - certChain.ChainPolicy.ExtraStore.AddRange(blobCertificates.Skip(1).ToArray()); - } - - var certChainIsValid = certChain.Build(blobCertificates[0]); - - // if the root is trusted in the context we are running in, valid should be true here - if (!certChainIsValid) + if(blobCertificates.Length > 1) + { + certChain.ChainPolicy.ExtraStore.AddRange(blobCertificates.Skip(1).ToArray()); + } + + var certChainIsValid = certChain.Build(blobCertificates[0]); + + // if the root is trusted in the context we are running in, valid should be true here + if (!certChainIsValid) + { + foreach (var element in certChain.ChainElements) { - foreach (var element in certChain.ChainElements) + if (element.Certificate.Issuer != element.Certificate.Subject) { - if (element.Certificate.Issuer != element.Certificate.Subject) - { - var cdp = CryptoUtils.CDPFromCertificateExts(element.Certificate.Extensions); - var crlFile = await DownloadDataAsync(cdp, cancellationToken); - if (CryptoUtils.IsCertInCRL(crlFile, element.Certificate)) - throw new Fido2VerificationException($"Cert {element.Certificate.Subject} found in CRL {cdp}"); - } + var cdp = CryptoUtils.CDPFromCertificateExts(element.Certificate.Extensions); + var crlFile = await DownloadDataAsync(cdp, cancellationToken); + if (CryptoUtils.IsCertInCRL(crlFile, element.Certificate)) + throw new Fido2VerificationException($"Cert {element.Certificate.Subject} found in CRL {cdp}"); } + } - // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded - if (rootCert.Thumbprint.Equals(certChain.ChainElements[^1].Certificate.Thumbprint, StringComparison.Ordinal) && - // and that the number of elements in the chain accounts for what was in x5c plus the root we added - certChain.ChainElements.Count == (blobCertStrings.Length + 1) && - // and that the root cert has exactly one status listed against it - certChain.ChainElements[^1].ChainElementStatus.Length == 1 && - // and that that status is a status of exactly UntrustedRoot - certChain.ChainElements[^1].ChainElementStatus[0].Status == X509ChainStatusFlags.UntrustedRoot) + // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded + if (rootCert.Thumbprint.Equals(certChain.ChainElements[^1].Certificate.Thumbprint, StringComparison.Ordinal) && + // and that the number of elements in the chain accounts for what was in x5c plus the root we added + certChain.ChainElements.Count == (blobCertStrings.Length + 1) && + // and that the root cert has exactly one status listed against it + certChain.ChainElements[^1].ChainElementStatus.Length == 1 && + // and that that status is a status of exactly UntrustedRoot + certChain.ChainElements[^1].ChainElementStatus[0].Status == X509ChainStatusFlags.UntrustedRoot) + { + // if we are good so far, that is a good sign + certChainIsValid = true; + for (var i = 0; i < certChain.ChainElements.Count - 1; i++) { - // if we are good so far, that is a good sign - certChainIsValid = true; - for (var i = 0; i < certChain.ChainElements.Count - 1; i++) - { - // check each non-root cert to verify zero status listed against it, otherwise, invalidate chain - if (0 != certChain.ChainElements[i].ChainElementStatus.Length) - certChainIsValid = false; - } + // check each non-root cert to verify zero status listed against it, otherwise, invalidate chain + if (0 != certChain.ChainElements[i].ChainElementStatus.Length) + certChainIsValid = false; } } + } - if (!certChainIsValid) - throw new Fido2VerificationException("Failed to validate cert chain while parsing BLOB"); + if (!certChainIsValid) + throw new Fido2VerificationException("Failed to validate cert chain while parsing BLOB"); - var blobPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson(); + var blobPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson(); - MetadataBLOBPayload blob = JsonSerializer.Deserialize(blobPayload, FidoModelSerializerContext.Default.MetadataBLOBPayload)!; - blob.JwtAlg = blobAlg; - return blob; - } + MetadataBLOBPayload blob = JsonSerializer.Deserialize(blobPayload, FidoModelSerializerContext.Default.MetadataBLOBPayload)!; + blob.JwtAlg = blobAlg; + return blob; } } diff --git a/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs b/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs index ee8aafbab..a2c8e0b18 100644 --- a/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs +++ b/Src/Fido2/Metadata/Fido2MetadataServiceRepository.cs @@ -12,207 +12,206 @@ using Microsoft.IdentityModel.Tokens; -namespace Fido2NetLib +namespace Fido2NetLib; + +public sealed class Fido2MetadataServiceRepository : IMetadataRepository { - public sealed class Fido2MetadataServiceRepository : IMetadataRepository + private const string ROOT_CERT = + "MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G" + + "A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp" + + "Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4" + + "MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG" + + "A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI" + + "hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8" + + "RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT" + + "gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm" + + "KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd" + + "QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ" + + "XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw" + + "DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o" + + "LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU" + + "RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp" + + "jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK" + + "6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX" + + "mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs" + + "Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH" + + "WD9f"; + + private readonly string _blobUrl = "https://mds.fidoalliance.org/"; + private readonly IHttpClientFactory _httpClientFactory; + + public Fido2MetadataServiceRepository(IHttpClientFactory httpClientFactory) { - private const string ROOT_CERT = - "MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G" + - "A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp" + - "Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4" + - "MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG" + - "A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI" + - "hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8" + - "RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT" + - "gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm" + - "KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd" + - "QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ" + - "XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw" + - "DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o" + - "LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU" + - "RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp" + - "jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK" + - "6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX" + - "mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs" + - "Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH" + - "WD9f"; - - private readonly string _blobUrl = "https://mds.fidoalliance.org/"; - private readonly IHttpClientFactory _httpClientFactory; - - public Fido2MetadataServiceRepository(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } + _httpClientFactory = httpClientFactory; + } - public Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default) - { - return Task.FromResult(entry.MetadataStatement); - } + public Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default) + { + return Task.FromResult(entry.MetadataStatement); + } - public async Task GetBLOBAsync(CancellationToken cancellationToken = default) - { - var rawBLOB = await GetRawBlobAsync(cancellationToken); - return await DeserializeAndValidateBlobAsync(rawBLOB, cancellationToken); - } + public async Task GetBLOBAsync(CancellationToken cancellationToken = default) + { + var rawBLOB = await GetRawBlobAsync(cancellationToken); + return await DeserializeAndValidateBlobAsync(rawBLOB, cancellationToken); + } - private async Task GetRawBlobAsync(CancellationToken cancellationToken) - { - var url = _blobUrl; - return await DownloadStringAsync(url, cancellationToken); - } + private async Task GetRawBlobAsync(CancellationToken cancellationToken) + { + var url = _blobUrl; + return await DownloadStringAsync(url, cancellationToken); + } - private async Task DownloadStringAsync(string url, CancellationToken cancellationToken) - { - return await _httpClientFactory - .CreateClient(nameof(Fido2MetadataServiceRepository)) - .GetStringAsync(url, cancellationToken); - } + private async Task DownloadStringAsync(string url, CancellationToken cancellationToken) + { + return await _httpClientFactory + .CreateClient(nameof(Fido2MetadataServiceRepository)) + .GetStringAsync(url, cancellationToken); + } + + private async Task DownloadDataAsync(string url, CancellationToken cancellationToken) + { + return await _httpClientFactory + .CreateClient(nameof(Fido2MetadataServiceRepository)) + .GetByteArrayAsync(url, cancellationToken); + } - private async Task DownloadDataAsync(string url, CancellationToken cancellationToken) + private X509Certificate2 GetX509Certificate(string certString) + { + try { - return await _httpClientFactory - .CreateClient(nameof(Fido2MetadataServiceRepository)) - .GetByteArrayAsync(url, cancellationToken); + var certBytes = Convert.FromBase64String(certString); + return new X509Certificate2(certBytes); } - - private X509Certificate2 GetX509Certificate(string certString) + catch (Exception ex) { - try - { - var certBytes = Convert.FromBase64String(certString); - return new X509Certificate2(certBytes); - } - catch (Exception ex) - { - throw new ArgumentException("Could not parse X509 certificate.", ex); - } + throw new ArgumentException("Could not parse X509 certificate.", ex); } + } - private async Task DeserializeAndValidateBlobAsync(string rawBLOBJwt, CancellationToken cancellationToken) - { - - if (string.IsNullOrWhiteSpace(rawBLOBJwt)) - throw new ArgumentNullException(nameof(rawBLOBJwt)); + private async Task DeserializeAndValidateBlobAsync(string rawBLOBJwt, CancellationToken cancellationToken) + { + + if (string.IsNullOrWhiteSpace(rawBLOBJwt)) + throw new ArgumentNullException(nameof(rawBLOBJwt)); - var jwtParts = rawBLOBJwt.Split('.'); + var jwtParts = rawBLOBJwt.Split('.'); - if (jwtParts.Length != 3) - throw new ArgumentException("The JWT does not have the 3 expected components"); + if (jwtParts.Length != 3) + throw new ArgumentException("The JWT does not have the 3 expected components"); - var blobHeaderString = jwtParts[0]; - using var blobHeaderDoc = JsonDocument.Parse(Base64Url.Decode(blobHeaderString)); - var blobHeader = blobHeaderDoc.RootElement; + var blobHeaderString = jwtParts[0]; + using var blobHeaderDoc = JsonDocument.Parse(Base64Url.Decode(blobHeaderString)); + var blobHeader = blobHeaderDoc.RootElement; - string blobAlg = blobHeader.TryGetProperty("alg", out var algEl) - ? algEl.GetString()! - : throw new ArgumentNullException("No alg value was present in the BLOB header."); + string blobAlg = blobHeader.TryGetProperty("alg", out var algEl) + ? algEl.GetString()! + : throw new ArgumentNullException("No alg value was present in the BLOB header."); - string[] keyStrings = blobHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array - ? x5cEl.ToStringArray() - : throw new ArgumentNullException("No x5c array was present in the BLOB header."); + string[] keyStrings = blobHeader.TryGetProperty("x5c", out var x5cEl) && x5cEl.ValueKind is JsonValueKind.Array + ? x5cEl.ToStringArray() + : throw new ArgumentNullException("No x5c array was present in the BLOB header."); - if (keyStrings.Length is 0) - throw new ArgumentException("No keys were present in the BLOB header."); + if (keyStrings.Length is 0) + throw new ArgumentException("No keys were present in the BLOB header."); - var rootCert = GetX509Certificate(ROOT_CERT); - var blobCerts = new X509Certificate2[keyStrings.Length]; - var keys = new SecurityKey[keyStrings.Length]; + var rootCert = GetX509Certificate(ROOT_CERT); + var blobCerts = new X509Certificate2[keyStrings.Length]; + var keys = new SecurityKey[keyStrings.Length]; - for (int i = 0; i < blobCerts.Length; i++) - { - var cert = GetX509Certificate(keyStrings[i]); + for (int i = 0; i < blobCerts.Length; i++) + { + var cert = GetX509Certificate(keyStrings[i]); - blobCerts[i] = cert; + blobCerts[i] = cert; - if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) - { - keys[i] = new ECDsaSecurityKey(ecdsaPublicKey); - } - else if (cert.GetRSAPublicKey() is RSA rsaPublicKey) - { - keys[i] = new RsaSecurityKey(rsaPublicKey); - } - else - { - throw new Fido2MetadataException("Unknown certificate algorithm"); - } + if (cert.GetECDsaPublicKey() is ECDsa ecdsaPublicKey) + { + keys[i] = new ECDsaSecurityKey(ecdsaPublicKey); } - var blobPublicKeys = keys.ToArray(); // defensive copy - - var certChain = new X509Chain(); - certChain.ChainPolicy.ExtraStore.Add(rootCert); - certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - - var validationParameters = new TokenValidationParameters + else if (cert.GetRSAPublicKey() is RSA rsaPublicKey) { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = false, - ValidateIssuerSigningKey = true, - IssuerSigningKeys = blobPublicKeys - }; - - var tokenHandler = new JwtSecurityTokenHandler() + keys[i] = new RsaSecurityKey(rsaPublicKey); + } + else { - // 250k isn't enough bytes for conformance test tool - // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1097 - MaximumTokenSizeInBytes = rawBLOBJwt.Length - }; + throw new Fido2MetadataException("Unknown certificate algorithm"); + } + } + var blobPublicKeys = keys.ToArray(); // defensive copy - tokenHandler.ValidateToken( - rawBLOBJwt, - validationParameters, - out var validatedToken); + var certChain = new X509Chain(); + certChain.ChainPolicy.ExtraStore.Add(rootCert); + certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - if(blobCerts.Length > 1) - { - certChain.ChainPolicy.ExtraStore.AddRange(blobCerts.Skip(1).ToArray()); - } - - var certChainIsValid = certChain.Build(blobCerts[0]); - // if the root is trusted in the context we are running in, valid should be true here - if (!certChainIsValid) + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = true, + IssuerSigningKeys = blobPublicKeys + }; + + var tokenHandler = new JwtSecurityTokenHandler() + { + // 250k isn't enough bytes for conformance test tool + // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1097 + MaximumTokenSizeInBytes = rawBLOBJwt.Length + }; + + tokenHandler.ValidateToken( + rawBLOBJwt, + validationParameters, + out var validatedToken); + + if(blobCerts.Length > 1) + { + certChain.ChainPolicy.ExtraStore.AddRange(blobCerts.Skip(1).ToArray()); + } + + var certChainIsValid = certChain.Build(blobCerts[0]); + // if the root is trusted in the context we are running in, valid should be true here + if (!certChainIsValid) + { + foreach (var element in certChain.ChainElements) { - foreach (var element in certChain.ChainElements) + if (element.Certificate.Issuer != element.Certificate.Subject) { - if (element.Certificate.Issuer != element.Certificate.Subject) - { - var cdp = CryptoUtils.CDPFromCertificateExts(element.Certificate.Extensions); - var crlFile = await DownloadDataAsync(cdp, cancellationToken); - if (CryptoUtils.IsCertInCRL(crlFile, element.Certificate)) - throw new Fido2VerificationException($"Cert {element.Certificate.Subject} found in CRL {cdp}"); - } + var cdp = CryptoUtils.CDPFromCertificateExts(element.Certificate.Extensions); + var crlFile = await DownloadDataAsync(cdp, cancellationToken); + if (CryptoUtils.IsCertInCRL(crlFile, element.Certificate)) + throw new Fido2VerificationException($"Cert {element.Certificate.Subject} found in CRL {cdp}"); } + } - // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded - if (rootCert.Thumbprint == certChain.ChainElements[^1].Certificate.Thumbprint && - // and that the number of elements in the chain accounts for what was in x5c plus the root we added - certChain.ChainElements.Count == (keyStrings.Length + 1) && - // and that the root cert has exactly one status listed against it - certChain.ChainElements[^1].ChainElementStatus.Length == 1 && - // and that that status is a status of exactly UntrustedRoot - certChain.ChainElements[^1].ChainElementStatus[0].Status == X509ChainStatusFlags.UntrustedRoot) + // otherwise we have to manually validate that the root in the chain we are testing is the root we downloaded + if (rootCert.Thumbprint == certChain.ChainElements[^1].Certificate.Thumbprint && + // and that the number of elements in the chain accounts for what was in x5c plus the root we added + certChain.ChainElements.Count == (keyStrings.Length + 1) && + // and that the root cert has exactly one status listed against it + certChain.ChainElements[^1].ChainElementStatus.Length == 1 && + // and that that status is a status of exactly UntrustedRoot + certChain.ChainElements[^1].ChainElementStatus[0].Status == X509ChainStatusFlags.UntrustedRoot) + { + // if we are good so far, that is a good sign + certChainIsValid = true; + for (var i = 0; i < certChain.ChainElements.Count - 1; i++) { - // if we are good so far, that is a good sign - certChainIsValid = true; - for (var i = 0; i < certChain.ChainElements.Count - 1; i++) - { - // check each non-root cert to verify zero status listed against it, otherwise, invalidate chain - if (0 != certChain.ChainElements[i].ChainElementStatus.Length) - certChainIsValid = false; - } + // check each non-root cert to verify zero status listed against it, otherwise, invalidate chain + if (0 != certChain.ChainElements[i].ChainElementStatus.Length) + certChainIsValid = false; } } + } - if (!certChainIsValid) - throw new Fido2VerificationException("Failed to validate cert chain while parsing BLOB"); + if (!certChainIsValid) + throw new Fido2VerificationException("Failed to validate cert chain while parsing BLOB"); - var blobPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson(); + var blobPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson(); - MetadataBLOBPayload blob = JsonSerializer.Deserialize(blobPayload, FidoModelSerializerContext.Default.MetadataBLOBPayload)!; - blob.JwtAlg = blobAlg; - return blob; - } + MetadataBLOBPayload blob = JsonSerializer.Deserialize(blobPayload, FidoModelSerializerContext.Default.MetadataBLOBPayload)!; + blob.JwtAlg = blobAlg; + return blob; } } diff --git a/Src/Fido2/Metadata/FileSystemMetadataRepository.cs b/Src/Fido2/Metadata/FileSystemMetadataRepository.cs index c8196c76b..3c5168d55 100644 --- a/Src/Fido2/Metadata/FileSystemMetadataRepository.cs +++ b/Src/Fido2/Metadata/FileSystemMetadataRepository.cs @@ -8,67 +8,66 @@ using Fido2NetLib.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +public sealed class FileSystemMetadataRepository : IMetadataRepository { - public sealed class FileSystemMetadataRepository : IMetadataRepository + private readonly string _path; + private readonly IDictionary _entries; + private MetadataBLOBPayload? _blob; + + public FileSystemMetadataRepository(string path) { - private readonly string _path; - private readonly IDictionary _entries; - private MetadataBLOBPayload? _blob; + _path = path; + _entries = new Dictionary(); + } - public FileSystemMetadataRepository(string path) - { - _path = path; - _entries = new Dictionary(); - } + public async Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default) + { + if (_blob is null) + await GetBLOBAsync(cancellationToken); - public async Task GetMetadataStatementAsync(MetadataBLOBPayload blob, MetadataBLOBPayloadEntry entry, CancellationToken cancellationToken = default) + if (!string.IsNullOrEmpty(entry.AaGuid) && Guid.TryParse(entry.AaGuid, out Guid parsedAaGuid)) { - if (_blob is null) - await GetBLOBAsync(cancellationToken); - - if (!string.IsNullOrEmpty(entry.AaGuid) && Guid.TryParse(entry.AaGuid, out Guid parsedAaGuid)) - { - if (_entries.ContainsKey(parsedAaGuid)) - return _entries[parsedAaGuid].MetadataStatement; - } - - return null; + if (_entries.ContainsKey(parsedAaGuid)) + return _entries[parsedAaGuid].MetadataStatement; } - public async Task GetBLOBAsync(CancellationToken cancellationToken = default) + return null; + } + + public async Task GetBLOBAsync(CancellationToken cancellationToken = default) + { + if (Directory.Exists(_path)) { - if (Directory.Exists(_path)) + foreach (var filename in Directory.GetFiles(_path)) { - foreach (var filename in Directory.GetFiles(_path)) + await using var fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read); + MetadataStatement statement = await JsonSerializer.DeserializeAsync(fileStream, FidoModelSerializerContext.Default.MetadataStatement, cancellationToken:cancellationToken) ?? throw new NullReferenceException(nameof(statement)); + var conformanceEntry = new MetadataBLOBPayloadEntry { - await using var fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read); - MetadataStatement statement = await JsonSerializer.DeserializeAsync(fileStream, FidoModelSerializerContext.Default.MetadataStatement, cancellationToken:cancellationToken) ?? throw new NullReferenceException(nameof(statement)); - var conformanceEntry = new MetadataBLOBPayloadEntry - { - AaGuid = statement.AaGuid, - MetadataStatement = statement, - StatusReports = new StatusReport[] + AaGuid = statement.AaGuid, + MetadataStatement = statement, + StatusReports = new StatusReport[] + { + new StatusReport { - new StatusReport - { - Status = AuthenticatorStatus.NOT_FIDO_CERTIFIED - } - } - }; - if (null != conformanceEntry.AaGuid) _entries.Add(new Guid(conformanceEntry.AaGuid), conformanceEntry); - } + Status = AuthenticatorStatus.NOT_FIDO_CERTIFIED + } + } + }; + if (null != conformanceEntry.AaGuid) _entries.Add(new Guid(conformanceEntry.AaGuid), conformanceEntry); } + } - _blob = new MetadataBLOBPayload() - { - Entries = _entries.Select(static o => o.Value).ToArray(), - NextUpdate = "", //Empty means it won't get cached - LegalHeader = "Local FAKE", - Number = 1 - }; + _blob = new MetadataBLOBPayload() + { + Entries = _entries.Select(static o => o.Value).ToArray(), + NextUpdate = "", //Empty means it won't get cached + LegalHeader = "Local FAKE", + Number = 1 + }; - return _blob; - } + return _blob; } } diff --git a/Src/Fido2/Metadata/MDSGetEndpointResponse.cs b/Src/Fido2/Metadata/MDSGetEndpointResponse.cs index afd285568..b8c17f0b6 100644 --- a/Src/Fido2/Metadata/MDSGetEndpointResponse.cs +++ b/Src/Fido2/Metadata/MDSGetEndpointResponse.cs @@ -2,14 +2,13 @@ using System.Text.Json.Serialization; -namespace Fido2NetLib +namespace Fido2NetLib; + +public sealed class MDSGetEndpointResponse { - public sealed class MDSGetEndpointResponse - { - [JsonPropertyName("status")] - public string Status { get; set; } - - [JsonPropertyName("result")] - public string[] Result { get; set; } - } + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("result")] + public string[] Result { get; set; } } diff --git a/Src/Fido2/Objects/AttestedCredentialData.cs b/Src/Fido2/Objects/AttestedCredentialData.cs index 0309ffccc..3b44ea3e1 100644 --- a/Src/Fido2/Objects/AttestedCredentialData.cs +++ b/Src/Fido2/Objects/AttestedCredentialData.cs @@ -7,166 +7,165 @@ using Fido2NetLib.Exceptions; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +public sealed class AttestedCredentialData { - public sealed class AttestedCredentialData + /// + /// Minimum length of the attested credential data structure. AAGUID + credentialID length + credential ID + credential public key. + /// + /// + private readonly int _minLength = Marshal.SizeOf(typeof(Guid)) + sizeof(ushort) + sizeof(byte) + sizeof(byte); + + /// + /// Instantiates an AttestedCredentialData object from an aaguid, credentialID, and CredentialPublicKey + /// + /// + /// + /// + public AttestedCredentialData(Guid aaguid, byte[] credentialID, CredentialPublicKey cpk) { - /// - /// Minimum length of the attested credential data structure. AAGUID + credentialID length + credential ID + credential public key. - /// - /// - private readonly int _minLength = Marshal.SizeOf(typeof(Guid)) + sizeof(ushort) + sizeof(byte) + sizeof(byte); - - /// - /// Instantiates an AttestedCredentialData object from an aaguid, credentialID, and CredentialPublicKey - /// - /// - /// - /// - public AttestedCredentialData(Guid aaguid, byte[] credentialID, CredentialPublicKey cpk) - { - AaGuid = aaguid; - CredentialID = credentialID; - CredentialPublicKey = cpk; - } + AaGuid = aaguid; + CredentialID = credentialID; + CredentialPublicKey = cpk; + } - /// - /// Decodes attested credential data. - /// - public AttestedCredentialData(byte[] data) - : this(data, out _) - { - } - - internal AttestedCredentialData(ReadOnlyMemory data, out int bytesRead) - { - if (data.Length < _minLength) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestedCredentialData, Fido2ErrorMessages.InvalidAttestedCredentialData_TooShort); + /// + /// Decodes attested credential data. + /// + public AttestedCredentialData(byte[] data) + : this(data, out _) + { + } - int position = 0; + internal AttestedCredentialData(ReadOnlyMemory data, out int bytesRead) + { + if (data.Length < _minLength) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAttestedCredentialData, Fido2ErrorMessages.InvalidAttestedCredentialData_TooShort); - // First 16 bytes is AAGUID - var aaguidBytes = data[..16]; + int position = 0; - position += 16; + // First 16 bytes is AAGUID + var aaguidBytes = data[..16]; - if (BitConverter.IsLittleEndian) - { - // GUID from authenticator is big endian. If we are on a little endian system, convert. - AaGuid = FromBigEndian(aaguidBytes.ToArray()); - } - else - { - AaGuid = new Guid(aaguidBytes.Span); - } + position += 16; - // Byte length of Credential ID, 16-bit unsigned big-endian integer. - var credentialIDLen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(position, 2).Span); + if (BitConverter.IsLittleEndian) + { + // GUID from authenticator is big endian. If we are on a little endian system, convert. + AaGuid = FromBigEndian(aaguidBytes.ToArray()); + } + else + { + AaGuid = new Guid(aaguidBytes.Span); + } - position += 2; + // Byte length of Credential ID, 16-bit unsigned big-endian integer. + var credentialIDLen = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(position, 2).Span); - // Read the credential ID bytes - CredentialID = data.Slice(position, credentialIDLen).ToArray(); + position += 2; - position += credentialIDLen; + // Read the credential ID bytes + CredentialID = data.Slice(position, credentialIDLen).ToArray(); - // "Determining attested credential data's length, which is variable, involves determining - // credentialPublicKey's beginning location given the preceding credentialId's length, and - // then determining the credentialPublicKey's length" + position += credentialIDLen; + // "Determining attested credential data's length, which is variable, involves determining + // credentialPublicKey's beginning location given the preceding credentialId's length, and + // then determining the credentialPublicKey's length" - // Read the CBOR object from the stream - CredentialPublicKey = CredentialPublicKey.Decode(data[position..], out int read); - position += read; + // Read the CBOR object from the stream + CredentialPublicKey = CredentialPublicKey.Decode(data[position..], out int read); - bytesRead = position; - } + position += read; - /// - /// The AAGUID of the authenticator. Can be used to identify the make and model of the authenticator. - /// - /// - public Guid AaGuid { get; private set; } - - /// - /// A probabilistically-unique byte sequence identifying a public key credential source and its authentication assertions. - /// - /// - public byte[] CredentialID { get; private set; } - - /// - /// The credential public key encoded in COSE_Key format, as defined in - /// Section 7 of RFC8152, using the CTAP2 canonical CBOR encoding form. - /// - /// - public CredentialPublicKey CredentialPublicKey { get; private set; } - - internal static void SwapBytes(byte[] bytes, int index1, int index2) - { - var temp = bytes[index1]; - bytes[index1] = bytes[index2]; - bytes[index2] = temp; - } + bytesRead = position; + } - /// - /// AAGUID is sent as big endian byte array, this converter is for little endian systems. - /// - public static Guid FromBigEndian(byte[] Aaguid) - { - SwapBytes(Aaguid, 0, 3); - SwapBytes(Aaguid, 1, 2); - SwapBytes(Aaguid, 4, 5); - SwapBytes(Aaguid, 6, 7); + /// + /// The AAGUID of the authenticator. Can be used to identify the make and model of the authenticator. + /// + /// + public Guid AaGuid { get; private set; } + + /// + /// A probabilistically-unique byte sequence identifying a public key credential source and its authentication assertions. + /// + /// + public byte[] CredentialID { get; private set; } + + /// + /// The credential public key encoded in COSE_Key format, as defined in + /// Section 7 of RFC8152, using the CTAP2 canonical CBOR encoding form. + /// + /// + public CredentialPublicKey CredentialPublicKey { get; private set; } + + internal static void SwapBytes(byte[] bytes, int index1, int index2) + { + var temp = bytes[index1]; + bytes[index1] = bytes[index2]; + bytes[index2] = temp; + } - return new Guid(Aaguid); - } + /// + /// AAGUID is sent as big endian byte array, this converter is for little endian systems. + /// + public static Guid FromBigEndian(byte[] Aaguid) + { + SwapBytes(Aaguid, 0, 3); + SwapBytes(Aaguid, 1, 2); + SwapBytes(Aaguid, 4, 5); + SwapBytes(Aaguid, 6, 7); - /// - /// AAGUID is sent as big endian byte array, this converter is for little endian systems. - /// - public static byte[] AaGuidToBigEndian(Guid AaGuid) - { - var aaguid = AaGuid.ToByteArray(); + return new Guid(Aaguid); + } - SwapBytes(aaguid, 0, 3); - SwapBytes(aaguid, 1, 2); - SwapBytes(aaguid, 4, 5); - SwapBytes(aaguid, 6, 7); + /// + /// AAGUID is sent as big endian byte array, this converter is for little endian systems. + /// + public static byte[] AaGuidToBigEndian(Guid AaGuid) + { + var aaguid = AaGuid.ToByteArray(); - return aaguid; - } + SwapBytes(aaguid, 0, 3); + SwapBytes(aaguid, 1, 2); + SwapBytes(aaguid, 4, 5); + SwapBytes(aaguid, 6, 7); - public override string ToString() - { - return $"AAGUID: {AaGuid}, CredentialID: {Convert.ToHexString(CredentialID)}, CredentialPublicKey: {CredentialPublicKey}"; - } + return aaguid; + } - public byte[] ToByteArray() + public override string ToString() + { + return $"AAGUID: {AaGuid}, CredentialID: {Convert.ToHexString(CredentialID)}, CredentialPublicKey: {CredentialPublicKey}"; + } + + public byte[] ToByteArray() + { + using var ms = new MemoryStream(); + using (var writer = new BinaryWriter(ms)) { - using var ms = new MemoryStream(); - using (var writer = new BinaryWriter(ms)) + // Write the aaguid bytes out, reverse if we're on a little endian system + if (BitConverter.IsLittleEndian) { - // Write the aaguid bytes out, reverse if we're on a little endian system - if (BitConverter.IsLittleEndian) - { - writer.Write(AaGuidToBigEndian(AaGuid)); - } - else - { - writer.Write(AaGuid.ToByteArray()); - } - - // Write the length of credential ID, as big endian bytes of a 16-bit unsigned integer - writer.WriteUInt16BigEndian((ushort)CredentialID.Length); - - // Write CredentialID bytes - writer.Write(CredentialID); - - // Write credential public key bytes - writer.Write(CredentialPublicKey.GetBytes()); + writer.Write(AaGuidToBigEndian(AaGuid)); } - return ms.ToArray(); + else + { + writer.Write(AaGuid.ToByteArray()); + } + + // Write the length of credential ID, as big endian bytes of a 16-bit unsigned integer + writer.WriteUInt16BigEndian((ushort)CredentialID.Length); + + // Write CredentialID bytes + writer.Write(CredentialID); + + // Write credential public key bytes + writer.Write(CredentialPublicKey.GetBytes()); } + return ms.ToArray(); } } diff --git a/Src/Fido2/Objects/AuthenticatorData.cs b/Src/Fido2/Objects/AuthenticatorData.cs index 7600c74a8..b04861264 100644 --- a/Src/Fido2/Objects/AuthenticatorData.cs +++ b/Src/Fido2/Objects/AuthenticatorData.cs @@ -6,146 +6,145 @@ using Fido2NetLib.Cbor; using Fido2NetLib.Exceptions; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +public sealed class AuthenticatorData { - public sealed class AuthenticatorData + /// + /// Minimum length of the authenticator data structure. + /// + /// + internal const int MinLength = SHA256HashLenBytes + sizeof(AuthenticatorFlags) + sizeof(UInt32); + + private const int SHA256HashLenBytes = 32; // 256 bits, 8 bits per byte + + /// + /// SHA-256 hash of the RP ID the credential is scoped to. + /// + public byte[] RpIdHash; + + /// + /// Flags contains information from the authenticator about the authentication + /// and whether or not certain data is present in the authenticator data. + /// + private readonly AuthenticatorFlags _flags; + + /// + /// UserPresent indicates that the user presence test has completed successfully. + /// + /// + public bool UserPresent => _flags.HasFlag(AuthenticatorFlags.UP); + + /// + /// UserVerified indicates that the user verification process has completed successfully. + /// + /// + public bool UserVerified => _flags.HasFlag(AuthenticatorFlags.UV); + + /// + /// HasAttestedCredentialData indicates that the authenticator added attested credential data to the authenticator data. + /// + /// + public bool HasAttestedCredentialData => _flags.HasFlag(AuthenticatorFlags.AT); + + /// + /// HasExtensionsData indicates that the authenticator added extension data to the authenticator data. + /// + /// + public bool HasExtensionsData => _flags.HasFlag(AuthenticatorFlags.ED); + + /// + /// Signature counter, 32-bit unsigned big-endian integer. + /// + public uint SignCount; + + /// + /// Attested credential data is a variable-length byte array added to the + /// authenticator data when generating an attestation object for a given credential. + /// + public AttestedCredentialData AttestedCredentialData; + + /// + /// Optional extensions to suit particular use cases. + /// + public Extensions Extensions; + + public AuthenticatorData(byte[] rpIdHash, AuthenticatorFlags flags, uint signCount, AttestedCredentialData acd, Extensions exts = null) { - /// - /// Minimum length of the authenticator data structure. - /// - /// - internal const int MinLength = SHA256HashLenBytes + sizeof(AuthenticatorFlags) + sizeof(UInt32); - - private const int SHA256HashLenBytes = 32; // 256 bits, 8 bits per byte - - /// - /// SHA-256 hash of the RP ID the credential is scoped to. - /// - public byte[] RpIdHash; - - /// - /// Flags contains information from the authenticator about the authentication - /// and whether or not certain data is present in the authenticator data. - /// - private readonly AuthenticatorFlags _flags; - - /// - /// UserPresent indicates that the user presence test has completed successfully. - /// - /// - public bool UserPresent => _flags.HasFlag(AuthenticatorFlags.UP); - - /// - /// UserVerified indicates that the user verification process has completed successfully. - /// - /// - public bool UserVerified => _flags.HasFlag(AuthenticatorFlags.UV); - - /// - /// HasAttestedCredentialData indicates that the authenticator added attested credential data to the authenticator data. - /// - /// - public bool HasAttestedCredentialData => _flags.HasFlag(AuthenticatorFlags.AT); - - /// - /// HasExtensionsData indicates that the authenticator added extension data to the authenticator data. - /// - /// - public bool HasExtensionsData => _flags.HasFlag(AuthenticatorFlags.ED); - - /// - /// Signature counter, 32-bit unsigned big-endian integer. - /// - public uint SignCount; - - /// - /// Attested credential data is a variable-length byte array added to the - /// authenticator data when generating an attestation object for a given credential. - /// - public AttestedCredentialData AttestedCredentialData; - - /// - /// Optional extensions to suit particular use cases. - /// - public Extensions Extensions; - - public AuthenticatorData(byte[] rpIdHash, AuthenticatorFlags flags, uint signCount, AttestedCredentialData acd, Extensions exts = null) - { - RpIdHash = rpIdHash; - _flags = flags; - SignCount = signCount; - AttestedCredentialData = acd; - Extensions = exts; - } - - public AuthenticatorData(byte[] authData) - { - // Input validation - if (authData is null) - throw new Fido2VerificationException(Fido2ErrorCode.MissingAuthenticatorData, Fido2ErrorMessages.MissingAuthenticatorData); + RpIdHash = rpIdHash; + _flags = flags; + SignCount = signCount; + AttestedCredentialData = acd; + Extensions = exts; + } - if (authData.Length < MinLength) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorData, Fido2ErrorMessages.InvalidAuthenticatorData_TooShort); + public AuthenticatorData(byte[] authData) + { + // Input validation + if (authData is null) + throw new Fido2VerificationException(Fido2ErrorCode.MissingAuthenticatorData, Fido2ErrorMessages.MissingAuthenticatorData); - // Input parsing - var reader = new MemoryReader(authData); + if (authData.Length < MinLength) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorData, Fido2ErrorMessages.InvalidAuthenticatorData_TooShort); - RpIdHash = reader.ReadBytes(SHA256HashLenBytes); + // Input parsing + var reader = new MemoryReader(authData); - _flags = (AuthenticatorFlags)reader.ReadByte(); + RpIdHash = reader.ReadBytes(SHA256HashLenBytes); - SignCount = reader.ReadUInt32BigEndian(); + _flags = (AuthenticatorFlags)reader.ReadByte(); - // Attested credential data is only present if the AT flag is set - if (HasAttestedCredentialData) - { - // Decode attested credential data, which starts at the next byte past the minimum length of the structure - AttestedCredentialData = new AttestedCredentialData(authData.AsMemory(reader.Position), out int bytesRead); + SignCount = reader.ReadUInt32BigEndian(); - reader.Advance(bytesRead); - } + // Attested credential data is only present if the AT flag is set + if (HasAttestedCredentialData) + { + // Decode attested credential data, which starts at the next byte past the minimum length of the structure + AttestedCredentialData = new AttestedCredentialData(authData.AsMemory(reader.Position), out int bytesRead); - // Extensions data is only present if the ED flag is set - if (HasExtensionsData) - { - // Read the CBOR object - var ext = CborObject.Decode(authData.AsMemory(reader.Position), out int bytesRead); + reader.Advance(bytesRead); + } - reader.Advance(bytesRead); + // Extensions data is only present if the ED flag is set + if (HasExtensionsData) + { + // Read the CBOR object + var ext = CborObject.Decode(authData.AsMemory(reader.Position), out int bytesRead); - // Encode the CBOR object back to a byte array - Extensions = new Extensions(ext.Encode()); - } + reader.Advance(bytesRead); - // Ensure there are no remaining bytes left over after decoding the structure - if (reader.RemainingBytes != 0) - throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorData, "Leftover bytes decoding AuthenticatorData"); + // Encode the CBOR object back to a byte array + Extensions = new Extensions(ext.Encode()); } - public byte[] ToByteArray() - { - using var ms = new MemoryStream(); + // Ensure there are no remaining bytes left over after decoding the structure + if (reader.RemainingBytes != 0) + throw new Fido2VerificationException(Fido2ErrorCode.InvalidAuthenticatorData, "Leftover bytes decoding AuthenticatorData"); + } - using (var writer = new BinaryWriter(ms)) - { - writer.Write(RpIdHash); + public byte[] ToByteArray() + { + using var ms = new MemoryStream(); - writer.Write((byte)_flags); + using (var writer = new BinaryWriter(ms)) + { + writer.Write(RpIdHash); - writer.WriteUInt32BigEndian(SignCount); + writer.Write((byte)_flags); - if (HasAttestedCredentialData) - { - writer.Write(AttestedCredentialData.ToByteArray()); - } + writer.WriteUInt32BigEndian(SignCount); - if (HasExtensionsData) - { - writer.Write(Extensions.GetBytes()); - } + if (HasAttestedCredentialData) + { + writer.Write(AttestedCredentialData.ToByteArray()); } - return ms.ToArray(); + if (HasExtensionsData) + { + writer.Write(Extensions.GetBytes()); + } } + + return ms.ToArray(); } } diff --git a/Src/Fido2/Objects/AuthenticatorFlags.cs b/Src/Fido2/Objects/AuthenticatorFlags.cs index 8980f1873..642b02be5 100644 --- a/Src/Fido2/Objects/AuthenticatorFlags.cs +++ b/Src/Fido2/Objects/AuthenticatorFlags.cs @@ -1,56 +1,55 @@ using System; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// Authenticator data flags +/// +/// +[Flags] +public enum AuthenticatorFlags : byte { /// - /// Authenticator data flags - /// - /// - [Flags] - public enum AuthenticatorFlags : byte - { - /// - /// User Present indicates that the user presence test has completed successfully. - /// - /// - UP = 0x1, - - /// - /// Reserved for future use (RFU1) - /// - RFU1 = 0x2, - - /// - /// User Verified indicates that the user verification process has completed successfully. - /// - /// - UV = 0x4, - - /// - /// Reserved for future use (RFU2) - /// - RFU2 = 0x8, - - /// - /// Reserved for future use (RFU3) - /// - RFU3 = 0x10, - - /// - /// Reserved for future use (RFU4) - /// - RFU4 = 0x20, - - /// - /// Attested credential data included indicates that the authenticator added attested credential data to the authenticator data. - /// - /// - AT = 0x40, - - /// - /// Extension data included indicates that the authenticator added extension data to the authenticator data. - /// - /// - ED = 0x80, - } + /// User Present indicates that the user presence test has completed successfully. + /// + /// + UP = 0x1, + + /// + /// Reserved for future use (RFU1) + /// + RFU1 = 0x2, + + /// + /// User Verified indicates that the user verification process has completed successfully. + /// + /// + UV = 0x4, + + /// + /// Reserved for future use (RFU2) + /// + RFU2 = 0x8, + + /// + /// Reserved for future use (RFU3) + /// + RFU3 = 0x10, + + /// + /// Reserved for future use (RFU4) + /// + RFU4 = 0x20, + + /// + /// Attested credential data included indicates that the authenticator added attested credential data to the authenticator data. + /// + /// + AT = 0x40, + + /// + /// Extension data included indicates that the authenticator added extension data to the authenticator data. + /// + /// + ED = 0x80, } diff --git a/Src/Fido2/Objects/CredentialIdUserHandleParams.cs b/Src/Fido2/Objects/CredentialIdUserHandleParams.cs index 009850a85..9e6fbb074 100644 --- a/Src/Fido2/Objects/CredentialIdUserHandleParams.cs +++ b/Src/Fido2/Objects/CredentialIdUserHandleParams.cs @@ -1,18 +1,17 @@ -namespace Fido2NetLib.Objects -{ - /// - /// Paramters used for callback function - /// - public sealed class IsUserHandleOwnerOfCredentialIdParams - { - public IsUserHandleOwnerOfCredentialIdParams(byte[] credentialId, byte[] userHandle) - { - CredentialId = credentialId; - UserHandle = userHandle; - } +namespace Fido2NetLib.Objects; - public byte[] UserHandle { get; } - - public byte[] CredentialId { get; } +/// +/// Paramters used for callback function +/// +public sealed class IsUserHandleOwnerOfCredentialIdParams +{ + public IsUserHandleOwnerOfCredentialIdParams(byte[] credentialId, byte[] userHandle) + { + CredentialId = credentialId; + UserHandle = userHandle; } + + public byte[] UserHandle { get; } + + public byte[] CredentialId { get; } } diff --git a/Src/Fido2/Objects/CredentialIdUserParams.cs b/Src/Fido2/Objects/CredentialIdUserParams.cs index 8a4aef3c3..24a344b5d 100644 --- a/Src/Fido2/Objects/CredentialIdUserParams.cs +++ b/Src/Fido2/Objects/CredentialIdUserParams.cs @@ -1,18 +1,17 @@ -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// Paramters used for callback function to check that the CredentialId is unique user +/// +public sealed class IsCredentialIdUniqueToUserParams { - /// - /// Paramters used for callback function to check that the CredentialId is unique user - /// - public sealed class IsCredentialIdUniqueToUserParams + public IsCredentialIdUniqueToUserParams(byte[] credentialId, Fido2User user) { - public IsCredentialIdUniqueToUserParams(byte[] credentialId, Fido2User user) - { - CredentialId = credentialId; - User = user; - } + CredentialId = credentialId; + User = user; + } - public byte[] CredentialId { get; } + public byte[] CredentialId { get; } - public Fido2User User { get; } - } + public Fido2User User { get; } } diff --git a/Src/Fido2/Objects/CredentialPublicKey.cs b/Src/Fido2/Objects/CredentialPublicKey.cs index 2e7edf033..43df6a410 100644 --- a/Src/Fido2/Objects/CredentialPublicKey.cs +++ b/Src/Fido2/Objects/CredentialPublicKey.cs @@ -6,229 +6,228 @@ using Fido2NetLib.Cbor; using NSec.Cryptography; -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +public sealed class CredentialPublicKey { - public sealed class CredentialPublicKey + internal readonly COSE.KeyType _type; + internal readonly COSE.Algorithm _alg; + internal readonly CborMap _cpk; + + public CredentialPublicKey(byte[] cpk) + : this((CborMap)CborObject.Decode(cpk)) { } + + public CredentialPublicKey(CborMap cpk) + { + _cpk = cpk; + _type = (COSE.KeyType)(int)cpk[COSE.KeyCommonParameter.KeyType]; + _alg = (COSE.Algorithm)(int)cpk[COSE.KeyCommonParameter.Alg]; + } + + + public CredentialPublicKey(ECDsa ecdsaPublicKey, COSE.Algorithm alg) { - internal readonly COSE.KeyType _type; - internal readonly COSE.Algorithm _alg; - internal readonly CborMap _cpk; + _type = COSE.KeyType.EC2; + _alg = alg; + + var keyParams = ecdsaPublicKey.ExportParameters(false); + + _cpk = new CborMap + { + { COSE.KeyCommonParameter.KeyType, _type }, + { COSE.KeyCommonParameter.Alg, _alg }, + { COSE.KeyTypeParameter.Crv, keyParams.Curve.ToCoseCurve() }, + { COSE.KeyTypeParameter.X, keyParams.Q.X! }, + { COSE.KeyTypeParameter.Y, keyParams.Q.Y! } + }; + } - public CredentialPublicKey(byte[] cpk) - : this((CborMap)CborObject.Decode(cpk)) { } + public CredentialPublicKey(X509Certificate2 cert, COSE.Algorithm alg) + { + var keyAlg = cert.GetKeyAlgorithm(); + _type = CoseKeyTypeFromOid[keyAlg]; + _alg = alg; + _cpk = new CborMap + { + { COSE.KeyCommonParameter.KeyType, _type }, + { COSE.KeyCommonParameter.Alg, _alg } + }; - public CredentialPublicKey(CborMap cpk) + if (_type is COSE.KeyType.RSA) { - _cpk = cpk; - _type = (COSE.KeyType)(int)cpk[COSE.KeyCommonParameter.KeyType]; - _alg = (COSE.Algorithm)(int)cpk[COSE.KeyCommonParameter.Alg]; + var keyParams = cert.GetRSAPublicKey()!.ExportParameters(false); + _cpk.Add(COSE.KeyTypeParameter.N, keyParams.Modulus!); + _cpk.Add(COSE.KeyTypeParameter.E, keyParams.Exponent!); } + else if (_type is COSE.KeyType.EC2) + { + var ecDsaPubKey = cert.GetECDsaPublicKey()!; + var keyParams = ecDsaPubKey.ExportParameters(false); + _cpk.Add(COSE.KeyTypeParameter.Crv, keyParams.Curve.ToCoseCurve()); + _cpk.Add(COSE.KeyTypeParameter.X, keyParams.Q.X!); + _cpk.Add(COSE.KeyTypeParameter.Y, keyParams.Q.Y!); + } + } - public CredentialPublicKey(ECDsa ecdsaPublicKey, COSE.Algorithm alg) + public bool Verify(ReadOnlySpan data, byte[] sig) + { + switch (_type) { - _type = COSE.KeyType.EC2; - _alg = alg; + case COSE.KeyType.EC2: + using(ECDsa ecdsa = CreateECDsa()) + { + var ecsig = CryptoUtils.SigFromEcDsaSig(sig, ecdsa.KeySize); + return ecdsa.VerifyData(data, ecsig, CryptoUtils.HashAlgFromCOSEAlg(_alg)); + } - var keyParams = ecdsaPublicKey.ExportParameters(false); + case COSE.KeyType.RSA: + using (RSA rsa = CreateRsa()) + { + return rsa.VerifyData(data, sig, CryptoUtils.HashAlgFromCOSEAlg(_alg), Padding); + } - _cpk = new CborMap - { - { COSE.KeyCommonParameter.KeyType, _type }, - { COSE.KeyCommonParameter.Alg, _alg }, - { COSE.KeyTypeParameter.Crv, keyParams.Curve.ToCoseCurve() }, - { COSE.KeyTypeParameter.X, keyParams.Q.X! }, - { COSE.KeyTypeParameter.Y, keyParams.Q.Y! } - }; + case COSE.KeyType.OKP: + return SignatureAlgorithm.Ed25519.Verify(EdDSAPublicKey, data, sig); } + throw new InvalidOperationException($"Missing or unknown kty {_type}"); + } - public CredentialPublicKey(X509Certificate2 cert, COSE.Algorithm alg) + internal RSA CreateRsa() + { + if (_type != COSE.KeyType.RSA) { - var keyAlg = cert.GetKeyAlgorithm(); - _type = CoseKeyTypeFromOid[keyAlg]; - _alg = alg; - _cpk = new CborMap - { - { COSE.KeyCommonParameter.KeyType, _type }, - { COSE.KeyCommonParameter.Alg, _alg } - }; + throw new InvalidOperationException($"Must be a RSA key. Was {_type}"); + } - if (_type is COSE.KeyType.RSA) - { - var keyParams = cert.GetRSAPublicKey()!.ExportParameters(false); - _cpk.Add(COSE.KeyTypeParameter.N, keyParams.Modulus!); - _cpk.Add(COSE.KeyTypeParameter.E, keyParams.Exponent!); - } - else if (_type is COSE.KeyType.EC2) - { - var ecDsaPubKey = cert.GetECDsaPublicKey()!; - var keyParams = ecDsaPubKey.ExportParameters(false); + return RSA.Create(new RSAParameters + { + Modulus = (byte[])_cpk[COSE.KeyTypeParameter.N], + Exponent = (byte[])_cpk[COSE.KeyTypeParameter.E] + }); + } - _cpk.Add(COSE.KeyTypeParameter.Crv, keyParams.Curve.ToCoseCurve()); - _cpk.Add(COSE.KeyTypeParameter.X, keyParams.Q.X!); - _cpk.Add(COSE.KeyTypeParameter.Y, keyParams.Q.Y!); - } + public ECDsa CreateECDsa() + { + if (_type != COSE.KeyType.EC2) + { + throw new InvalidOperationException($"Must be a EC2 key. Was {_type}"); } - - public bool Verify(ReadOnlySpan data, byte[] sig) + + var point = new ECPoint { - switch (_type) - { - case COSE.KeyType.EC2: - using(ECDsa ecdsa = CreateECDsa()) - { - var ecsig = CryptoUtils.SigFromEcDsaSig(sig, ecdsa.KeySize); - return ecdsa.VerifyData(data, ecsig, CryptoUtils.HashAlgFromCOSEAlg(_alg)); - } + X = (byte[])_cpk[COSE.KeyTypeParameter.X], + Y = (byte[])_cpk[COSE.KeyTypeParameter.Y], + }; - case COSE.KeyType.RSA: - using (RSA rsa = CreateRsa()) - { - return rsa.VerifyData(data, sig, CryptoUtils.HashAlgFromCOSEAlg(_alg), Padding); - } + ECCurve curve; - case COSE.KeyType.OKP: - return SignatureAlgorithm.Ed25519.Verify(EdDSAPublicKey, data, sig); - } - throw new InvalidOperationException($"Missing or unknown kty {_type}"); + var crv = (COSE.EllipticCurve)(int)_cpk[COSE.KeyTypeParameter.Crv]!; + + // https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves + + switch ((_alg, crv)) + { + case (COSE.Algorithm.ES256K, COSE.EllipticCurve.P256K): + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // see https://github.com/dotnet/runtime/issues/47770 + { + throw new PlatformNotSupportedException($"No support currently for secP256k1 on macOS"); + } + + curve = ECCurve.CreateFromFriendlyName("secP256k1"); + break; + case (COSE.Algorithm.ES256, COSE.EllipticCurve.P256): + curve = ECCurve.NamedCurves.nistP256; + break; + case (COSE.Algorithm.ES384, COSE.EllipticCurve.P384): + curve = ECCurve.NamedCurves.nistP384; + break; + case (COSE.Algorithm.ES512, COSE.EllipticCurve.P521): + curve = ECCurve.NamedCurves.nistP521; + break; + default: + throw new InvalidOperationException($"Missing or unknown alg {_alg}"); } - internal RSA CreateRsa() + return ECDsa.Create(new ECParameters + { + Q = point, + Curve = curve + }); + } + + internal RSASignaturePadding Padding + { + get { if (_type != COSE.KeyType.RSA) { throw new InvalidOperationException($"Must be a RSA key. Was {_type}"); } - return RSA.Create(new RSAParameters + switch (_alg) // https://www.iana.org/assignments/cose/cose.xhtml#algorithms { - Modulus = (byte[])_cpk[COSE.KeyTypeParameter.N], - Exponent = (byte[])_cpk[COSE.KeyTypeParameter.E] - }); + case COSE.Algorithm.PS256: + case COSE.Algorithm.PS384: + case COSE.Algorithm.PS512: + return RSASignaturePadding.Pss; + + case COSE.Algorithm.RS1: + case COSE.Algorithm.RS256: + case COSE.Algorithm.RS384: + case COSE.Algorithm.RS512: + return RSASignaturePadding.Pkcs1; + default: + throw new InvalidOperationException($"Missing or unknown alg {_alg}"); + } } + } - public ECDsa CreateECDsa() + internal NSec.Cryptography.PublicKey EdDSAPublicKey + { + get { - if (_type != COSE.KeyType.EC2) + if (_type != COSE.KeyType.OKP) { - throw new InvalidOperationException($"Must be a EC2 key. Was {_type}"); + throw new InvalidOperationException($"Must be a OKP key. Was {_type}"); } - - var point = new ECPoint + + switch (_alg) // https://www.iana.org/assignments/cose/cose.xhtml#algorithms { - X = (byte[])_cpk[COSE.KeyTypeParameter.X], - Y = (byte[])_cpk[COSE.KeyTypeParameter.Y], - }; - - ECCurve curve; + case COSE.Algorithm.EdDSA: + var crv = (COSE.EllipticCurve)(int)_cpk[COSE.KeyTypeParameter.Crv]; - var crv = (COSE.EllipticCurve)(int)_cpk[COSE.KeyTypeParameter.Crv]!; - - // https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves - - switch ((_alg, crv)) - { - case (COSE.Algorithm.ES256K, COSE.EllipticCurve.P256K): - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // see https://github.com/dotnet/runtime/issues/47770 + // https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves + if (crv is COSE.EllipticCurve.Ed25519) { - throw new PlatformNotSupportedException($"No support currently for secP256k1 on macOS"); + return NSec.Cryptography.PublicKey.Import(SignatureAlgorithm.Ed25519, (byte[])_cpk[COSE.KeyTypeParameter.X], KeyBlobFormat.RawPublicKey); + } + else + { + throw new InvalidOperationException($"Missing or unknown crv {crv}"); } - - curve = ECCurve.CreateFromFriendlyName("secP256k1"); - break; - case (COSE.Algorithm.ES256, COSE.EllipticCurve.P256): - curve = ECCurve.NamedCurves.nistP256; - break; - case (COSE.Algorithm.ES384, COSE.EllipticCurve.P384): - curve = ECCurve.NamedCurves.nistP384; - break; - case (COSE.Algorithm.ES512, COSE.EllipticCurve.P521): - curve = ECCurve.NamedCurves.nistP521; - break; default: throw new InvalidOperationException($"Missing or unknown alg {_alg}"); } - - return ECDsa.Create(new ECParameters - { - Q = point, - Curve = curve - }); - } - - internal RSASignaturePadding Padding - { - get - { - if (_type != COSE.KeyType.RSA) - { - throw new InvalidOperationException($"Must be a RSA key. Was {_type}"); - } - - switch (_alg) // https://www.iana.org/assignments/cose/cose.xhtml#algorithms - { - case COSE.Algorithm.PS256: - case COSE.Algorithm.PS384: - case COSE.Algorithm.PS512: - return RSASignaturePadding.Pss; - - case COSE.Algorithm.RS1: - case COSE.Algorithm.RS256: - case COSE.Algorithm.RS384: - case COSE.Algorithm.RS512: - return RSASignaturePadding.Pkcs1; - default: - throw new InvalidOperationException($"Missing or unknown alg {_alg}"); - } - } - } - - internal NSec.Cryptography.PublicKey EdDSAPublicKey - { - get - { - if (_type != COSE.KeyType.OKP) - { - throw new InvalidOperationException($"Must be a OKP key. Was {_type}"); - } - - switch (_alg) // https://www.iana.org/assignments/cose/cose.xhtml#algorithms - { - case COSE.Algorithm.EdDSA: - var crv = (COSE.EllipticCurve)(int)_cpk[COSE.KeyTypeParameter.Crv]; - - // https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves - if (crv is COSE.EllipticCurve.Ed25519) - { - return NSec.Cryptography.PublicKey.Import(SignatureAlgorithm.Ed25519, (byte[])_cpk[COSE.KeyTypeParameter.X], KeyBlobFormat.RawPublicKey); - } - else - { - throw new InvalidOperationException($"Missing or unknown crv {crv}"); - } - default: - throw new InvalidOperationException($"Missing or unknown alg {_alg}"); - } - } } + } - internal static readonly Dictionary CoseKeyTypeFromOid = new () - { - { "1.2.840.10045.2.1", COSE.KeyType.EC2 }, - { "1.2.840.113549.1.1.1", COSE.KeyType.RSA} - }; + internal static readonly Dictionary CoseKeyTypeFromOid = new () + { + { "1.2.840.10045.2.1", COSE.KeyType.EC2 }, + { "1.2.840.113549.1.1.1", COSE.KeyType.RSA} + }; - public static CredentialPublicKey Decode(ReadOnlyMemory cpk, out int bytesRead) - { - var map = (CborMap)CborObject.Decode(cpk, out bytesRead); + public static CredentialPublicKey Decode(ReadOnlyMemory cpk, out int bytesRead) + { + var map = (CborMap)CborObject.Decode(cpk, out bytesRead); - return new CredentialPublicKey(map); - } + return new CredentialPublicKey(map); + } - public byte[] GetBytes() => _cpk.Encode(); + public byte[] GetBytes() => _cpk.Encode(); - public bool IsSameAlg(COSE.Algorithm alg) => _alg.Equals(alg); + public bool IsSameAlg(COSE.Algorithm alg) => _alg.Equals(alg); - public CborMap GetCborObject() => _cpk; - } + public CborMap GetCborObject() => _cpk; } diff --git a/Src/Fido2/Objects/Extensions.cs b/Src/Fido2/Objects/Extensions.cs index 54971f593..4e7d4f3c0 100644 --- a/Src/Fido2/Objects/Extensions.cs +++ b/Src/Fido2/Objects/Extensions.cs @@ -1,22 +1,21 @@ -namespace Fido2NetLib.Objects +namespace Fido2NetLib.Objects; + +/// +/// +/// +public sealed class Extensions { - /// - /// - /// - public sealed class Extensions + private readonly byte[] _extensionBytes; + public Extensions(byte[] extensions) { - private readonly byte[] _extensionBytes; - public Extensions(byte[] extensions) - { - _extensionBytes = extensions; - } + _extensionBytes = extensions; + } - public int Length => _extensionBytes.Length; + public int Length => _extensionBytes.Length; - public byte[] GetBytes() - { - return _extensionBytes; - } + public byte[] GetBytes() + { + return _extensionBytes; } }