From 694b8e111c90eee76a9bd79c220e4b604cd47458 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 3 Dec 2024 16:47:26 +0900 Subject: [PATCH] Update FIDO library --- BTCPayServer/BTCPayServer.csproj | 4 +- .../Controllers/UIAccountController.cs | 4 +- .../Controllers/UIInvoiceController.cs | 1 - BTCPayServer/Fido2/Fido2Service.cs | 32 +++----- .../Fido2/Models/Fido2CredentialBlob.cs | 80 ++++++++++++++++++- .../Fido2/Models/LoginWithFido2ViewModel.cs | 3 +- BTCPayServer/Hosting/MigrationStartupTask.cs | 34 ++++---- BTCPayServer/Hosting/Startup.cs | 5 +- .../Views/UIAccount/LoginWithFido2.cshtml | 3 +- BTCPayServer/Views/UIFido2/Create.cshtml | 3 +- BTCPayServer/wwwroot/js/webauthn/login.js | 2 +- BTCPayServer/wwwroot/js/webauthn/register.js | 4 +- 12 files changed, 122 insertions(+), 53 deletions(-) diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index aa649ba1e..439b45153 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -57,8 +57,8 @@ - - + + diff --git a/BTCPayServer/Controllers/UIAccountController.cs b/BTCPayServer/Controllers/UIAccountController.cs index 5e43e201a..1c8a5c52a 100644 --- a/BTCPayServer/Controllers/UIAccountController.cs +++ b/BTCPayServer/Controllers/UIAccountController.cs @@ -273,7 +273,7 @@ namespace BTCPayServer.Controllers } return new LoginWithFido2ViewModel { - Data = r, + Data = System.Text.Json.JsonSerializer.Serialize(r, r.GetType()), UserId = user.Id, RememberMe = rememberMe }; @@ -385,7 +385,7 @@ namespace BTCPayServer.Controllers try { - if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject())) + if (await _fido2Service.CompleteLogin(viewModel.UserId, System.Text.Json.JsonSerializer.Deserialize(viewModel.Response))) { await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2"); _logger.LogInformation("User {Email} logged in with FIDO2", user.Email); diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index 905b4f81d..97b56bed9 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -33,7 +33,6 @@ using NBitcoin; using Newtonsoft.Json.Linq; using StoreData = BTCPayServer.Data.StoreData; using Serilog.Filters; -using PeterO.Numbers; using BTCPayServer.Payouts; using Microsoft.Extensions.Localization; diff --git a/BTCPayServer/Fido2/Fido2Service.cs b/BTCPayServer/Fido2/Fido2Service.cs index e6e9cd6ae..2a2d64596 100644 --- a/BTCPayServer/Fido2/Fido2Service.cs +++ b/BTCPayServer/Fido2/Fido2Service.cs @@ -11,6 +11,7 @@ using Fido2NetLib.Objects; using Microsoft.EntityFrameworkCore; using NBitcoin; using Newtonsoft.Json.Linq; +using static BTCPayServer.Fido2.Models.Fido2CredentialBlob; namespace BTCPayServer.Fido2 { @@ -45,7 +46,7 @@ namespace BTCPayServer.Fido2 var existingKeys = user.Fido2Credentials .Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2) - .Select(c => c.GetFido2Blob().Descriptor).ToList(); + .Select(c => c.GetFido2Blob().Descriptor?.ToFido2()).ToList(); // 3. Create options var authenticatorSelection = new AuthenticatorSelection @@ -57,14 +58,7 @@ namespace BTCPayServer.Fido2 var exts = new AuthenticationExtensionsClientInputs() { Extensions = true, - UserVerificationIndex = true, - Location = true, - UserVerificationMethod = true, - BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds - { - FAR = float.MaxValue, - FRR = float.MaxValue - }, + UserVerificationMethod = true }; var options = _fido2.RequestNewCredential( @@ -81,7 +75,7 @@ namespace BTCPayServer.Fido2 try { - var attestationResponse = JObject.Parse(data).ToObject(); + var attestationResponse = System.Text.Json.JsonSerializer.Deserialize(data); await using var dbContext = _contextFactory.CreateContext(); var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials) .FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId); @@ -92,14 +86,14 @@ namespace BTCPayServer.Fido2 // 2. Verify and make the credentials var success = - await _fido2.MakeNewCredentialAsync(attestationResponse, options, args => Task.FromResult(true)); + await _fido2.MakeNewCredentialAsync(attestationResponse, options, (args, cancellation) => Task.FromResult(true)); // 3. Store the credentials in db var newCredential = new Fido2Credential() { Name = name, ApplicationUserId = userId }; newCredential.SetBlob(new Fido2CredentialBlob() { - Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId), + Descriptor = new DescriptorClass(success.Result.CredentialId), PublicKey = success.Result.PublicKey, UserHandle = success.Result.User.Id, SignatureCounter = success.Result.Counter, @@ -158,21 +152,13 @@ namespace BTCPayServer.Fido2 } var existingCredentials = user.Fido2Credentials .Where(credential => credential.Type == Fido2Credential.CredentialType.FIDO2) - .Select(c => c.GetFido2Blob().Descriptor) + .Select(c => c.GetFido2Blob().Descriptor?.ToFido2()) .ToList(); var exts = new AuthenticationExtensionsClientInputs() { - SimpleTransactionAuthorization = "FIDO", - GenericTransactionAuthorization = new TxAuthGenericArg - { - ContentType = "text/plain", - Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } - }, - UserVerificationIndex = true, - Location = true, UserVerificationMethod = true, Extensions = true, - AppID = _fido2Configuration.Origin + AppID = _fido2Configuration.Origins.First() }; // 3. Create options @@ -206,7 +192,7 @@ namespace BTCPayServer.Fido2 // 5. Make the assertion var res = await _fido2.MakeAssertionAsync(response, options, credential.Item2.PublicKey, - credential.Item2.SignatureCounter, x => Task.FromResult(true)); + credential.Item2.SignatureCounter, (x, cancellationToken) => Task.FromResult(true)); // 6. Store the updated counter credential.Item2.SignatureCounter = res.Counter; diff --git a/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs b/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs index da15df1a9..6f5344a5f 100644 --- a/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs +++ b/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs @@ -1,3 +1,4 @@ +using System; using Fido2NetLib; using Fido2NetLib.Objects; using Newtonsoft.Json; @@ -6,7 +7,84 @@ namespace BTCPayServer.Fido2.Models { public class Fido2CredentialBlob { - public PublicKeyCredentialDescriptor Descriptor { get; set; } + public class Base64UrlConverter : JsonConverter + { + private readonly Required _requirement = Required.DisallowNull; + + public Base64UrlConverter() + { + } + + public Base64UrlConverter(Required required = Required.DisallowNull) + { + _requirement = required; + } + + public override void WriteJson(JsonWriter writer, byte[] value, JsonSerializer serializer) + { + writer.WriteValue(Base64Url.Encode(value)); + } + + public override byte[] ReadJson(JsonReader reader, Type objectType, byte[] existingValue, bool hasExistingValue, JsonSerializer serializer) + { + byte[] ret = null; + + if (null == reader.Value && _requirement == Required.AllowNull) + return ret; + + if (null == reader.Value) + throw new Fido2VerificationException("json value must not be null"); + if (Type.GetType("System.String") != reader.ValueType) + throw new Fido2VerificationException("json valuetype must be string"); + try + { + ret = Base64Url.Decode((string)reader.Value); + } + catch (FormatException ex) + { + throw new Fido2VerificationException("json value must be valid base64 encoded string", ex); + } + return ret; + } + } + public class DescriptorClass + { + public DescriptorClass(byte[] credentialId) + { + Id = credentialId; + } + + public DescriptorClass() + { + + } + + /// + /// This member contains the type of the public key credential the caller is referring to. + /// + [JsonProperty("type")] + public string Type { get; set; } = "public-key"; + + /// + /// This member contains the credential ID of the public key credential the caller is referring to. + /// + [JsonConverter(typeof(Base64UrlConverter))] + [JsonProperty("id")] + public byte[] Id { 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. + /// + [JsonProperty("transports", NullValueHandling = NullValueHandling.Ignore)] + public string[] Transports { get; set; } + + public PublicKeyCredentialDescriptor ToFido2() + { + var str = JsonConvert.SerializeObject(this); + return System.Text.Json.JsonSerializer.Deserialize(str); + } + } + public DescriptorClass Descriptor { get; set; } [JsonConverter(typeof(Base64UrlConverter))] public byte[] PublicKey { get; set; } [JsonConverter(typeof(Base64UrlConverter))] diff --git a/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs b/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs index 23eb1a61a..9142f0261 100644 --- a/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs +++ b/BTCPayServer/Fido2/Models/LoginWithFido2ViewModel.cs @@ -1,4 +1,5 @@ using Fido2NetLib; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Fido2.Models { @@ -7,7 +8,7 @@ namespace BTCPayServer.Fido2.Models public string UserId { get; set; } public bool RememberMe { get; set; } - public AssertionOptions Data { get; set; } + public string Data { get; set; } public string Response { get; set; } } } diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index a45b10b0d..7092b76b8 100644 --- a/BTCPayServer/Hosting/MigrationStartupTask.cs +++ b/BTCPayServer/Hosting/MigrationStartupTask.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; @@ -22,14 +23,15 @@ using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Stores; using BTCPayServer.Storage.Models; using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration; +using Fido2NetLib.Cbor; using Fido2NetLib.Objects; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using PeterO.Cbor; using YamlDotNet.RepresentationModel; +using static BTCPayServer.Fido2.Models.Fido2CredentialBlob; using LightningAddressData = BTCPayServer.Data.LightningAddressData; namespace BTCPayServer.Hosting @@ -738,9 +740,9 @@ WHERE cte.""Id""=p.""Id"" fido2.SetBlob(new Fido2CredentialBlob() { SignatureCounter = (uint)u2FDevice.Counter, - PublicKey = CreatePublicKeyFromU2fRegistrationData(u2FDevice.PublicKey).EncodeToBytes(), + PublicKey = CreatePublicKeyFromU2fRegistrationData(u2FDevice.PublicKey).Encode(), UserHandle = u2FDevice.KeyHandle, - Descriptor = new PublicKeyCredentialDescriptor(u2FDevice.KeyHandle), + Descriptor = new DescriptorClass(u2FDevice.KeyHandle), CredType = "u2f" }); @@ -751,27 +753,29 @@ WHERE cte.""Id""=p.""Id"" await ctx.SaveChangesAsync(); } //from https://github.com/abergs/fido2-net-lib/blob/0fa7bb4b4a1f33f46c5f7ca4ee489b47680d579b/Test/ExistingU2fRegistrationDataTests.cs#L70 - private static CBORObject CreatePublicKeyFromU2fRegistrationData(byte[] publicKeyData) + private static CborMap CreatePublicKeyFromU2fRegistrationData(byte[] publicKeyData) { - if (publicKeyData.Length != 65) - { - throw new ArgumentException("u2f public key must be 65 bytes", nameof(publicKeyData)); - } var x = new byte[32]; var y = new byte[32]; Buffer.BlockCopy(publicKeyData, 1, x, 0, 32); Buffer.BlockCopy(publicKeyData, 33, y, 0, 32); + var point = new ECPoint + { + X = x, + Y = y, + }; - var coseKey = CBORObject.NewMap(); + var coseKey = new CborMap + { + { (long)COSE.KeyCommonParameter.KeyType, (long)COSE.KeyType.EC2 }, + { (long)COSE.KeyCommonParameter.Alg, -7L }, - coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2); - coseKey.Add(COSE.KeyCommonParameter.Alg, -7); + { (long)COSE.KeyTypeParameter.Crv, (long)COSE.EllipticCurve.P256 }, - coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256); - - coseKey.Add(COSE.KeyTypeParameter.X, x); - coseKey.Add(COSE.KeyTypeParameter.Y, y); + { (long)COSE.KeyTypeParameter.X, point.X }, + { (long)COSE.KeyTypeParameter.Y, point.Y } + }; return coseKey; } diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index f98368947..51b59a00d 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -122,8 +122,7 @@ namespace BTCPayServer.Hosting }) .AddCachedMetadataService(config => { - //They'll be used in a "first match wins" way in the order registered - config.AddStaticMetadataRepository(); + config.AddFidoMetadataRepository(); }); var descriptor = services.Single(descriptor => descriptor.ServiceType == typeof(Fido2Configuration)); services.Remove(descriptor); @@ -133,7 +132,7 @@ namespace BTCPayServer.Hosting return new Fido2Configuration() { ServerName = "BTCPay Server", - Origin = $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}", + Origins = new[] { $"{httpContext.HttpContext.Request.Scheme}://{httpContext.HttpContext.Request.Host}" }.ToHashSet(), ServerDomain = httpContext.HttpContext.Request.Host.Host }; }); diff --git a/BTCPayServer/Views/UIAccount/LoginWithFido2.cshtml b/BTCPayServer/Views/UIAccount/LoginWithFido2.cshtml index aaae6e61b..837dacd3b 100644 --- a/BTCPayServer/Views/UIAccount/LoginWithFido2.cshtml +++ b/BTCPayServer/Views/UIAccount/LoginWithFido2.cshtml @@ -1,3 +1,4 @@ +@using Newtonsoft.Json.Linq @model BTCPayServer.Fido2.Models.LoginWithFido2ViewModel
@@ -24,7 +25,7 @@ diff --git a/BTCPayServer/Views/UIFido2/Create.cshtml b/BTCPayServer/Views/UIFido2/Create.cshtml index c3670e4df..26d01d8e9 100644 --- a/BTCPayServer/Views/UIFido2/Create.cshtml +++ b/BTCPayServer/Views/UIFido2/Create.cshtml @@ -1,3 +1,4 @@ +@using Newtonsoft.Json.Linq @model Fido2NetLib.CredentialCreateOptions @{ ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, StringLocalizer["Register your security device"]); @@ -42,7 +43,7 @@ diff --git a/BTCPayServer/wwwroot/js/webauthn/login.js b/BTCPayServer/wwwroot/js/webauthn/login.js index 3ee87ab46..eb7325992 100644 --- a/BTCPayServer/wwwroot/js/webauthn/login.js +++ b/BTCPayServer/wwwroot/js/webauthn/login.js @@ -40,7 +40,7 @@ async function verifyAssertionWithServer(assertedCredential) { extensions: assertedCredential.getClientExtensionResults(), response: { authenticatorData: coerceToBase64Url(authData), - clientDataJson: coerceToBase64Url(clientDataJSON), + clientDataJSON: coerceToBase64Url(clientDataJSON), signature: coerceToBase64Url(sig) } }; diff --git a/BTCPayServer/wwwroot/js/webauthn/register.js b/BTCPayServer/wwwroot/js/webauthn/register.js index b6636625a..e73ad68a5 100644 --- a/BTCPayServer/wwwroot/js/webauthn/register.js +++ b/BTCPayServer/wwwroot/js/webauthn/register.js @@ -49,8 +49,8 @@ async function registerNewCredential(newCredential) { type: newCredential.type, extensions: newCredential.getClientExtensionResults(), response: { - AttestationObject: coerceToBase64Url(attestationObject), - clientDataJson: coerceToBase64Url(clientDataJSON) + attestationObject: coerceToBase64Url(attestationObject), + clientDataJSON: coerceToBase64Url(clientDataJSON) } };