mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Migrate existing U2F to Fido2 (#2484)
* Migrate existing U2F to Fido2 This seamlessly switches all u2f registrations over to the new FIDO2 support. Please note that I have not yet added a way to drop the u2f DB and its UI so that we can test the migration works properly for all. * add testing logic * fix u2f tests * remove duplicate status message * fix test and namespaces * fix test
This commit is contained in:
parent
c878f63f99
commit
02bf5afe0b
14 changed files with 149 additions and 59 deletions
|
@ -16,8 +16,7 @@ namespace BTCPayServer.Data
|
|||
public CredentialType Type { get; set; }
|
||||
public enum CredentialType
|
||||
{
|
||||
FIDO2,
|
||||
U2F
|
||||
FIDO2
|
||||
}
|
||||
public static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
|
|
@ -9,6 +9,7 @@ using BTCPayServer.Tests.Logging;
|
|||
using BTCPayServer.U2F;
|
||||
using BTCPayServer.U2F.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using U2F.Core.Models;
|
||||
using U2F.Core.Utils;
|
||||
using Xunit;
|
||||
|
@ -61,8 +62,8 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal("testdevice", addDeviceVM.Name);
|
||||
Assert.NotEmpty(addDeviceVM.Version);
|
||||
Assert.Null(addDeviceVM.DeviceResponse);
|
||||
|
||||
var devReg = new DeviceRegistration(Guid.NewGuid().ToByteArray(), Guid.NewGuid().ToByteArray(),
|
||||
|
||||
var devReg = new DeviceRegistration(Guid.NewGuid().ToByteArray(), RandomUtils.GetBytes(65),
|
||||
Guid.NewGuid().ToByteArray(), 1);
|
||||
|
||||
mock.GetDevReg = () => devReg;
|
||||
|
|
|
@ -19,6 +19,8 @@ using BTCPayServer.Configuration;
|
|||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Lightning;
|
||||
|
@ -42,9 +44,9 @@ using BTCPayServer.Services.Labels;
|
|||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.U2F.Models;
|
||||
using BTCPayServer.Validation;
|
||||
using ExchangeSharp;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
@ -3324,7 +3326,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
var accountController = tester.PayTester.GetController<AccountController>();
|
||||
|
||||
//no 2fa or u2f enabled, login should work
|
||||
//no 2fa or fido2 enabled, login should work
|
||||
Assert.Equal(nameof(HomeController.Index),
|
||||
Assert.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
|
||||
{
|
||||
|
@ -3332,48 +3334,46 @@ namespace BTCPayServer.Tests
|
|||
Password = user.RegisterDetails.Password
|
||||
})).ActionName);
|
||||
|
||||
var manageController = user.GetController<ManageController>();
|
||||
var manageController = user.GetController<Fido2Controller>();
|
||||
|
||||
//by default no u2f devices available
|
||||
//by default no fido2 devices available
|
||||
Assert.Empty(Assert
|
||||
.IsType<U2FAuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
var addRequest =
|
||||
Assert.IsType<AddU2FDeviceViewModel>(Assert
|
||||
.IsType<ViewResult>(manageController.AddU2FDevice("label")).Model);
|
||||
//name should match the one provided in beginning
|
||||
Assert.Equal("label", addRequest.Name);
|
||||
.IsType<Fido2AuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
|
||||
Assert.IsType<CredentialCreateOptions>(Assert
|
||||
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel()
|
||||
{
|
||||
Name = "label"
|
||||
})).Model);
|
||||
|
||||
//sending an invalid response model back to server, should error out
|
||||
Assert.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addRequest));
|
||||
Assert.IsType<RedirectToActionResult>(await manageController.CreateResponse("sdsdsa", "sds"));
|
||||
var statusModel = manageController.TempData.GetStatusMessageModel();
|
||||
Assert.Equal(StatusMessageModel.StatusSeverity.Error, statusModel.Severity);
|
||||
|
||||
var contextFactory = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
|
||||
//add a fake u2f device in db directly since emulating a u2f device is hard and annoying
|
||||
//add a fake fido2 device in db directly since emulating a fido2 device is hard and annoying
|
||||
using (var context = contextFactory.CreateContext())
|
||||
{
|
||||
var newDevice = new U2FDevice()
|
||||
var newDevice = new Fido2Credential()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Name = "fake",
|
||||
Counter = 0,
|
||||
KeyHandle = UTF8Encoding.UTF8.GetBytes("fake"),
|
||||
PublicKey = UTF8Encoding.UTF8.GetBytes("fake"),
|
||||
AttestationCert = UTF8Encoding.UTF8.GetBytes("fake"),
|
||||
Type = Fido2Credential.CredentialType.FIDO2,
|
||||
ApplicationUserId = user.UserId
|
||||
};
|
||||
await context.U2FDevices.AddAsync(newDevice);
|
||||
newDevice.SetBlob(new Fido2CredentialBlob() { });
|
||||
await context.Fido2Credentials.AddAsync(newDevice);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Assert.NotNull(newDevice.Id);
|
||||
Assert.NotEmpty(Assert
|
||||
.IsType<U2FAuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
.IsType<Fido2AuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
|
||||
}
|
||||
|
||||
//check if we are showing the u2f login screen now
|
||||
//check if we are showing the fido2 login screen now
|
||||
var secondLoginResult = Assert.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
|
||||
{
|
||||
Email = user.RegisterDetails.Email,
|
||||
|
@ -3384,7 +3384,7 @@ namespace BTCPayServer.Tests
|
|||
var vm = Assert.IsType<SecondaryLoginViewModel>(secondLoginResult.Model);
|
||||
//2fa was never enabled for user so this should be empty
|
||||
Assert.Null(vm.LoginWith2FaViewModel);
|
||||
Assert.NotNull(vm.LoginWithU2FViewModel);
|
||||
Assert.NotNull(vm.LoginWithFido2ViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Fido2" Version="2.0.0-preview2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.0-preview2" />
|
||||
<PackageReference Include="Fido2" Version="2.0.1" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
|
|
|
@ -2,7 +2,7 @@ using System.Threading.Tasks;
|
|||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.Models;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.U2F.Models
|
||||
namespace BTCPayServer.Fido2
|
||||
{
|
||||
|
||||
[Route("fido2")]
|
||||
|
@ -78,8 +78,7 @@ namespace BTCPayServer.U2F.Models
|
|||
[HttpPost("register")]
|
||||
public async Task<IActionResult> CreateResponse([FromForm] string data, [FromForm] string name)
|
||||
{
|
||||
var attestationResponse = JObject.Parse(data).ToObject<AuthenticatorAttestationRawResponse>();
|
||||
if (await _fido2Service.CompleteCreation(_userManager.GetUserId(User), name, attestationResponse))
|
||||
if (await _fido2Service.CompleteCreation(_userManager.GetUserId(User), name, data))
|
||||
{
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
|
|
|
@ -4,11 +4,13 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using ExchangeSharp;
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Fido2
|
||||
{
|
||||
|
@ -20,11 +22,13 @@ namespace BTCPayServer.Fido2
|
|||
new ConcurrentDictionary<string, AssertionOptions>();
|
||||
private readonly ApplicationDbContextFactory _contextFactory;
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly Fido2Configuration _fido2Configuration;
|
||||
|
||||
public Fido2Service(ApplicationDbContextFactory contextFactory, IFido2 fido2)
|
||||
public Fido2Service(ApplicationDbContextFactory contextFactory, IFido2 fido2, Fido2Configuration fido2Configuration)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_fido2 = fido2;
|
||||
_fido2Configuration = fido2Configuration;
|
||||
}
|
||||
|
||||
public async Task<CredentialCreateOptions> RequestCreation(string userId)
|
||||
|
@ -70,26 +74,27 @@ namespace BTCPayServer.Fido2
|
|||
return options;
|
||||
}
|
||||
|
||||
public async Task<bool> CompleteCreation(string userId, string name, AuthenticatorAttestationRawResponse attestationResponse)
|
||||
public async Task<bool> CompleteCreation(string userId, string name, string data)
|
||||
{
|
||||
await using var dbContext = _contextFactory.CreateContext();
|
||||
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
|
||||
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
|
||||
if (user == null || !CreationStore.TryGetValue(userId, out var options))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var attestationResponse = JObject.Parse(data).ToObject<AuthenticatorAttestationRawResponse>();
|
||||
await using var dbContext = _contextFactory.CreateContext();
|
||||
var user = await dbContext.Users.Include(applicationUser => applicationUser.Fido2Credentials)
|
||||
.FirstOrDefaultAsync(applicationUser => applicationUser.Id == userId);
|
||||
if (user == null || !CreationStore.TryGetValue(userId, out var options))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Verify and make the credentials
|
||||
var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, args => Task.FromResult(true));
|
||||
var success =
|
||||
await _fido2.MakeNewCredentialAsync(attestationResponse, options, args => Task.FromResult(true));
|
||||
|
||||
// 3. Store the credentials in db
|
||||
var newCredential = new Fido2Credential()
|
||||
{
|
||||
Name = name,
|
||||
ApplicationUserId = userId
|
||||
};
|
||||
|
||||
var newCredential = new Fido2Credential() {Name = name, ApplicationUserId = userId};
|
||||
|
||||
newCredential.SetBlob(new Fido2CredentialBlob()
|
||||
{
|
||||
Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),
|
||||
|
@ -104,8 +109,13 @@ namespace BTCPayServer.Fido2
|
|||
await dbContext.SaveChangesAsync();
|
||||
CreationStore.Remove(userId, out _);
|
||||
return true;
|
||||
|
||||
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Fido2Credential>> GetCredentials(string userId)
|
||||
|
@ -158,7 +168,9 @@ namespace BTCPayServer.Fido2
|
|||
},
|
||||
UserVerificationIndex = true,
|
||||
Location = true,
|
||||
UserVerificationMethod = true
|
||||
UserVerificationMethod = true ,
|
||||
Extensions = true,
|
||||
AppID = _fido2Configuration.Origin
|
||||
};
|
||||
|
||||
// 3. Create options
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
namespace BTCPayServer.Fido2
|
||||
{
|
||||
public static class Fido2Extensions
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
using Fido2NetLib.Objects;
|
||||
|
||||
namespace BTCPayServer.U2F.Models
|
||||
namespace BTCPayServer.Fido2.Models
|
||||
{
|
||||
public class AddFido2CredentialViewModel
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.U2F.Models
|
||||
namespace BTCPayServer.Fido2.Models
|
||||
{
|
||||
public class Fido2AuthenticationViewModel
|
||||
{
|
||||
|
|
|
@ -2,7 +2,7 @@ using Fido2NetLib;
|
|||
using Fido2NetLib.Objects;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
namespace BTCPayServer.Fido2.Models
|
||||
{
|
||||
public class Fido2CredentialBlob
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -9,11 +10,15 @@ using BTCPayServer.Abstractions.Contracts;
|
|||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -22,6 +27,8 @@ using NBitcoin.DataEncoders;
|
|||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Org.BouncyCastle.Math.EC;
|
||||
using PeterO.Cbor;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
|
@ -121,6 +128,13 @@ namespace BTCPayServer.Hosting
|
|||
settings.TransitionInternalNodeConnectionString = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
|
||||
if (true || !settings.MigrateU2FToFIDO2)
|
||||
{
|
||||
await MigrateU2FToFIDO2();
|
||||
settings.MigrateU2FToFIDO2 = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -129,6 +143,61 @@ namespace BTCPayServer.Hosting
|
|||
}
|
||||
}
|
||||
|
||||
private async Task MigrateU2FToFIDO2()
|
||||
{
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
ctx.RemoveRange(ctx.Fido2Credentials.ToList());
|
||||
var u2fDevices = await ctx.U2FDevices.ToListAsync();
|
||||
foreach (U2FDevice u2FDevice in u2fDevices)
|
||||
{
|
||||
var fido2 = new Fido2Credential()
|
||||
{
|
||||
ApplicationUserId = u2FDevice.ApplicationUserId,
|
||||
Name = u2FDevice.Name,
|
||||
Type = Fido2Credential.CredentialType.FIDO2
|
||||
};
|
||||
fido2.SetBlob(new Fido2CredentialBlob()
|
||||
{
|
||||
SignatureCounter = (uint)u2FDevice.Counter,
|
||||
PublicKey = CreatePublicKeyFromU2fRegistrationData( u2FDevice.PublicKey).EncodeToBytes() ,
|
||||
UserHandle = u2FDevice.KeyHandle,
|
||||
Descriptor = new PublicKeyCredentialDescriptor(u2FDevice.KeyHandle),
|
||||
CredType = "u2f"
|
||||
});
|
||||
|
||||
await ctx.AddAsync(fido2);
|
||||
|
||||
ctx.Remove(u2FDevice);
|
||||
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
//from https://github.com/abergs/fido2-net-lib/blob/0fa7bb4b4a1f33f46c5f7ca4ee489b47680d579b/Test/ExistingU2fRegistrationDataTests.cs#L70
|
||||
private static CBORObject 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 coseKey = CBORObject.NewMap();
|
||||
|
||||
coseKey.Add(COSE.KeyCommonParameter.KeyType, COSE.KeyType.EC2);
|
||||
coseKey.Add(COSE.KeyCommonParameter.Alg, -7);
|
||||
|
||||
coseKey.Add(COSE.KeyTypeParameter.Crv, COSE.EllipticCurve.P256);
|
||||
|
||||
coseKey.Add(COSE.KeyTypeParameter.X, x);
|
||||
coseKey.Add(COSE.KeyTypeParameter.Y, y);
|
||||
|
||||
return coseKey;
|
||||
}
|
||||
|
||||
private async Task TransitionInternalNodeConnectionString()
|
||||
{
|
||||
var nodes = LightningOptions.Value.InternalLightningByCryptoCode.Values.Select(c => c.ToString()).ToHashSet();
|
||||
|
|
|
@ -9,6 +9,7 @@ using BTCPayServer.Client;
|
|||
using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
@ -61,7 +62,16 @@ namespace BTCPayServer.Security.GreenField
|
|||
if (!result.Succeeded)
|
||||
return AuthenticateResult.Fail(result.ToString());
|
||||
|
||||
var user = await _userManager.FindByNameAsync(username);
|
||||
var user = await _userManager.Users
|
||||
.Include(applicationUser => applicationUser.U2FDevices)
|
||||
.Include(applicationUser => applicationUser.Fido2Credentials)
|
||||
.FirstOrDefaultAsync(applicationUser =>
|
||||
applicationUser.NormalizedUserName == _userManager.NormalizeName(username));
|
||||
|
||||
if (user.U2FDevices.Any() || user.Fido2Credentials.Any())
|
||||
{
|
||||
return AuthenticateResult.Fail("Cannot use Basic authentication with multi-factor is enabled.");
|
||||
}
|
||||
var claims = new List<Claim>()
|
||||
{
|
||||
new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id),
|
||||
|
|
|
@ -2,6 +2,7 @@ namespace BTCPayServer.Services
|
|||
{
|
||||
public class MigrationSettings
|
||||
{
|
||||
public bool MigrateU2FToFIDO2{ get; set; }
|
||||
public bool UnreachableStoreCheck { get; set; }
|
||||
public bool DeprecatedLightningConnectionStringCheck { get; set; }
|
||||
public bool ConvertMultiplierToSpread { get; set; }
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
@model BTCPayServer.U2F.Models.Fido2AuthenticationViewModel
|
||||
@model BTCPayServer.Fido2.Models.Fido2AuthenticationViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.Fido2, "Registered FIDO2 Credentials");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<table class="table table-lg mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
Loading…
Add table
Reference in a new issue