diff --git a/BTCPayServer.Data/Data/Fido2Credential.cs b/BTCPayServer.Data/Data/Fido2Credential.cs index fe57f3fd5..59fb75bc8 100644 --- a/BTCPayServer.Data/Data/Fido2Credential.cs +++ b/BTCPayServer.Data/Data/Fido2Credential.cs @@ -16,8 +16,7 @@ namespace BTCPayServer.Data public CredentialType Type { get; set; } public enum CredentialType { - FIDO2, - U2F + FIDO2 } public static void OnModelCreating(ModelBuilder builder) { diff --git a/BTCPayServer.Tests/U2FTests.cs b/BTCPayServer.Tests/U2FTests.cs index d1c285dbd..fe39b5d70 100644 --- a/BTCPayServer.Tests/U2FTests.cs +++ b/BTCPayServer.Tests/U2FTests.cs @@ -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; diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 8204c474d..9a4ad8c1f 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -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(); - //no 2fa or u2f enabled, login should work + //no 2fa or fido2 enabled, login should work Assert.Equal(nameof(HomeController.Index), Assert.IsType(await accountController.Login(new LoginViewModel() { @@ -3332,48 +3334,46 @@ namespace BTCPayServer.Tests Password = user.RegisterDetails.Password })).ActionName); - var manageController = user.GetController(); + var manageController = user.GetController(); - //by default no u2f devices available + //by default no fido2 devices available Assert.Empty(Assert - .IsType(Assert - .IsType(await manageController.U2FAuthentication()).Model).Devices); - var addRequest = - Assert.IsType(Assert - .IsType(manageController.AddU2FDevice("label")).Model); - //name should match the one provided in beginning - Assert.Equal("label", addRequest.Name); + .IsType(Assert + .IsType(await manageController.List()).Model).Credentials); + Assert.IsType(Assert + .IsType(await manageController.Create(new AddFido2CredentialViewModel() + { + Name = "label" + })).Model); //sending an invalid response model back to server, should error out - Assert.IsType(await manageController.AddU2FDevice(addRequest)); + Assert.IsType(await manageController.CreateResponse("sdsdsa", "sds")); var statusModel = manageController.TempData.GetStatusMessageModel(); Assert.Equal(StatusMessageModel.StatusSeverity.Error, statusModel.Severity); var contextFactory = tester.PayTester.GetService(); - //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(Assert - .IsType(await manageController.U2FAuthentication()).Model).Devices); + .IsType(Assert + .IsType(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(await accountController.Login(new LoginViewModel() { Email = user.RegisterDetails.Email, @@ -3384,7 +3384,7 @@ namespace BTCPayServer.Tests var vm = Assert.IsType(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); } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index d5b1cf8ba..3772aabb2 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -51,8 +51,8 @@ - - + + diff --git a/BTCPayServer/Fido2/Fido2Controller.cs b/BTCPayServer/Fido2/Fido2Controller.cs index 98b776594..e9da7200b 100644 --- a/BTCPayServer/Fido2/Fido2Controller.cs +++ b/BTCPayServer/Fido2/Fido2Controller.cs @@ -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 CreateResponse([FromForm] string data, [FromForm] string name) { - var attestationResponse = JObject.Parse(data).ToObject(); - if (await _fido2Service.CompleteCreation(_userManager.GetUserId(User), name, attestationResponse)) + if (await _fido2Service.CompleteCreation(_userManager.GetUserId(User), name, data)) { TempData.SetStatusMessageModel(new StatusMessageModel diff --git a/BTCPayServer/Fido2/Fido2Service.cs b/BTCPayServer/Fido2/Fido2Service.cs index 8da2f009e..1f7c7d9df 100644 --- a/BTCPayServer/Fido2/Fido2Service.cs +++ b/BTCPayServer/Fido2/Fido2Service.cs @@ -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(); 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 RequestCreation(string userId) @@ -70,26 +74,27 @@ namespace BTCPayServer.Fido2 return options; } - public async Task CompleteCreation(string userId, string name, AuthenticatorAttestationRawResponse attestationResponse) + public async Task 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(); + 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> 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 diff --git a/BTCPayServer/Fido2/FidoExtensions.cs b/BTCPayServer/Fido2/FidoExtensions.cs index 774f98931..903d965f8 100644 --- a/BTCPayServer/Fido2/FidoExtensions.cs +++ b/BTCPayServer/Fido2/FidoExtensions.cs @@ -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 { diff --git a/BTCPayServer/Fido2/Models/AddFido2CredentialViewModel.cs b/BTCPayServer/Fido2/Models/AddFido2CredentialViewModel.cs index 0819f7724..f51224937 100644 --- a/BTCPayServer/Fido2/Models/AddFido2CredentialViewModel.cs +++ b/BTCPayServer/Fido2/Models/AddFido2CredentialViewModel.cs @@ -1,6 +1,6 @@ using Fido2NetLib.Objects; -namespace BTCPayServer.U2F.Models +namespace BTCPayServer.Fido2.Models { public class AddFido2CredentialViewModel { diff --git a/BTCPayServer/Fido2/Models/Fido2AuthenticationViewModel.cs b/BTCPayServer/Fido2/Models/Fido2AuthenticationViewModel.cs index 4b5402be9..dea279c10 100644 --- a/BTCPayServer/Fido2/Models/Fido2AuthenticationViewModel.cs +++ b/BTCPayServer/Fido2/Models/Fido2AuthenticationViewModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using BTCPayServer.Data; -namespace BTCPayServer.U2F.Models +namespace BTCPayServer.Fido2.Models { public class Fido2AuthenticationViewModel { diff --git a/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs b/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs index 3523933a9..da15df1a9 100644 --- a/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs +++ b/BTCPayServer/Fido2/Models/Fido2CredentialBlob.cs @@ -2,7 +2,7 @@ using Fido2NetLib; using Fido2NetLib.Objects; using Newtonsoft.Json; -namespace BTCPayServer.Data +namespace BTCPayServer.Fido2.Models { public class Fido2CredentialBlob { diff --git a/BTCPayServer/Hosting/MigrationStartupTask.cs b/BTCPayServer/Hosting/MigrationStartupTask.cs index 2dee902a9..1a5311914 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.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(); diff --git a/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs b/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs index 53f35b36a..dd26c7d53 100644 --- a/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs +++ b/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs @@ -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() { new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, user.Id), diff --git a/BTCPayServer/Services/MigrationSettings.cs b/BTCPayServer/Services/MigrationSettings.cs index bdc06d426..56d63ff68 100644 --- a/BTCPayServer/Services/MigrationSettings.cs +++ b/BTCPayServer/Services/MigrationSettings.cs @@ -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; } diff --git a/BTCPayServer/Views/Fido2/List.cshtml b/BTCPayServer/Views/Fido2/List.cshtml index 994daf6ed..d29e4a98f 100644 --- a/BTCPayServer/Views/Fido2/List.cshtml +++ b/BTCPayServer/Views/Fido2/List.cshtml @@ -1,10 +1,8 @@ -@model BTCPayServer.U2F.Models.Fido2AuthenticationViewModel +@model BTCPayServer.Fido2.Models.Fido2AuthenticationViewModel @{ ViewData.SetActivePageAndTitle(ManageNavPages.Fido2, "Registered FIDO2 Credentials"); } - -