Update FIDO library

This commit is contained in:
nicolas.dorier
2024-12-03 16:47:26 +09:00
parent 7d65817acd
commit 694b8e111c
12 changed files with 122 additions and 53 deletions

View File

@@ -57,8 +57,8 @@
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.9" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="Fido2" Version="3.0.1" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="LNURL" Version="0.0.36" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />

View File

@@ -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<AuthenticatorAssertionRawResponse>()))
if (await _fido2Service.CompleteLogin(viewModel.UserId, System.Text.Json.JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(viewModel.Response)))
{
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
_logger.LogInformation("User {Email} logged in with FIDO2", user.Email);

View File

@@ -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;

View File

@@ -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<AuthenticatorAttestationRawResponse>();
var attestationResponse = System.Text.Json.JsonSerializer.Deserialize<AuthenticatorAttestationRawResponse>(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;

View File

@@ -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<byte[]>
{
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()
{
}
/// <summary>
/// This member contains the type of the public key credential the caller is referring to.
/// </summary>
[JsonProperty("type")]
public string Type { get; set; } = "public-key";
/// <summary>
/// This member contains the credential ID of the public key credential the caller is referring to.
/// </summary>
[JsonConverter(typeof(Base64UrlConverter))]
[JsonProperty("id")]
public byte[] Id { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty("transports", NullValueHandling = NullValueHandling.Ignore)]
public string[] Transports { get; set; }
public PublicKeyCredentialDescriptor ToFido2()
{
var str = JsonConvert.SerializeObject(this);
return System.Text.Json.JsonSerializer.Deserialize<PublicKeyCredentialDescriptor>(str);
}
}
public DescriptorClass Descriptor { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]
public byte[] PublicKey { get; set; }
[JsonConverter(typeof(Base64UrlConverter))]

View File

@@ -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; }
}
}

View File

@@ -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;
}

View File

@@ -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
};
});

View File

@@ -1,3 +1,4 @@
@using Newtonsoft.Json.Linq
@model BTCPayServer.Fido2.Models.LoginWithFido2ViewModel
<div class="twoFaBox">
@@ -24,7 +25,7 @@
<script>
document.getElementById('btn-retry').addEventListener('click', () => window.location.reload())
// send to server for registering
window.makeAssertionOptions = @Safe.Json(Model.Data);
window.makeAssertionOptions = @Safe.Json(JObject.Parse(Model.Data));
</script>
<script src="~/js/webauthn/helpers.js" asp-append-version="true"></script>
<script src="~/js/webauthn/login.js" asp-append-version="true"></script>

View File

@@ -1,3 +1,4 @@
@using Newtonsoft.Json.Linq
@model Fido2NetLib.CredentialCreateOptions
@{
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, StringLocalizer["Register your security device"]);
@@ -42,7 +43,7 @@
<script>
document.getElementById('btn-retry').addEventListener('click', function () { window.location.reload() });
// send to server for registering
window.makeCredentialOptions = @Json.Serialize(Model);
window.makeCredentialOptions = @Json.Serialize(JToken.Parse(Model.ToJson()));
</script>
<script src="~/js/webauthn/helpers.js"></script>
<script src="~/js/webauthn/register.js"></script>

View File

@@ -40,7 +40,7 @@ async function verifyAssertionWithServer(assertedCredential) {
extensions: assertedCredential.getClientExtensionResults(),
response: {
authenticatorData: coerceToBase64Url(authData),
clientDataJson: coerceToBase64Url(clientDataJSON),
clientDataJSON: coerceToBase64Url(clientDataJSON),
signature: coerceToBase64Url(sig)
}
};

View File

@@ -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)
}
};