From 32c43afae20e596a9779d6040ad3ce9db4fbf796 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 25 Jan 2024 16:29:26 +0100 Subject: [PATCH] [PM-5731] feat: implement credential creation --- .../Abstractions/ICryptoFunctionService.cs | 1 + .../Models/Domain/CryptoEcdsaAlgorithm.cs | 6 ++ .../Models/Domain/CryptoSignEcdsaOptions.cs | 6 +- src/Core/Models/View/Fido2CredentialView.cs | 6 +- .../Services/Fido2AuthenticatorService.cs | 61 +++++++++++++- src/Core/Services/PclCryptoFunctionService.cs | 14 ++++ .../Fido2AuthenticatorGetAssertionTests.cs | 14 ++-- .../Fido2AuthenticatorMakeCredentialTests.cs | 82 +++++++++++++++---- 8 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 src/Core/Models/Domain/CryptoEcdsaAlgorithm.cs diff --git a/src/Core/Abstractions/ICryptoFunctionService.cs b/src/Core/Abstractions/ICryptoFunctionService.cs index 90fb33ad4..062c0c3bf 100644 --- a/src/Core/Abstractions/ICryptoFunctionService.cs +++ b/src/Core/Abstractions/ICryptoFunctionService.cs @@ -32,6 +32,7 @@ namespace Bit.Core.Abstractions Task RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm); Task RsaExtractPublicKeyAsync(byte[] privateKey); Task> RsaGenerateKeyPairAsync(int length); + Task<(byte[] PublicKey, byte[] PrivateKey)> EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm algorithm); Task RandomBytesAsync(int length); byte[] RandomBytes(int length); Task RandomNumberAsync(); diff --git a/src/Core/Models/Domain/CryptoEcdsaAlgorithm.cs b/src/Core/Models/Domain/CryptoEcdsaAlgorithm.cs new file mode 100644 index 000000000..fb813495b --- /dev/null +++ b/src/Core/Models/Domain/CryptoEcdsaAlgorithm.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Models.Domain +{ + public enum CryptoEcdsaAlgorithm : byte { + P256Sha256 = 0, + } +} diff --git a/src/Core/Models/Domain/CryptoSignEcdsaOptions.cs b/src/Core/Models/Domain/CryptoSignEcdsaOptions.cs index 6a944c5d4..eee718ff6 100644 --- a/src/Core/Models/Domain/CryptoSignEcdsaOptions.cs +++ b/src/Core/Models/Domain/CryptoSignEcdsaOptions.cs @@ -2,16 +2,12 @@ { public struct CryptoSignEcdsaOptions : ICryptoSignOptions { - public enum EcdsaAlgorithm : byte { - EcdsaP256Sha256 = 0, - } - public enum DsaSignatureFormat : byte { IeeeP1363FixedFieldConcatenation = 0, Rfc3279DerSequence = 1 } - public EcdsaAlgorithm Algorithm { get; set; } + public CryptoEcdsaAlgorithm Algorithm { get; set; } public DsaSignatureFormat SignatureFormat { get; set; } } } diff --git a/src/Core/Models/View/Fido2CredentialView.cs b/src/Core/Models/View/Fido2CredentialView.cs index c0e4b6c49..67c05dce5 100644 --- a/src/Core/Models/View/Fido2CredentialView.cs +++ b/src/Core/Models/View/Fido2CredentialView.cs @@ -43,9 +43,13 @@ namespace Bit.Core.Models.View set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value); } + public bool DiscoverableValue { + get => bool.TryParse(Discoverable, out var discoverable) && discoverable; + set => Discoverable = value.ToString(); + } + public override string SubTitle => UserName; public override List> LinkedFieldOptions => new List>(); - public bool IsDiscoverable => bool.TryParse(Discoverable, out var isDiscoverable) && isDiscoverable; public bool CanLaunch => !string.IsNullOrEmpty(RpId); public string LaunchUri => $"https://{RpId}"; diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 84ba74aa4..db535173f 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -3,6 +3,7 @@ using Bit.Core.Models.View; using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Utilities.Fido2; +using Bit.Core.Utilities; namespace Bit.Core.Services { @@ -54,9 +55,44 @@ namespace Bit.Core.Services UserVerification = makeCredentialParams.RequireUserVerification }); + var cipherId = response.CipherId; + string credentialId; + // if (cipherId === undefined) { + // this.logService?.warning( + // `[Fido2Authenticator] Aborting because user confirmation was not recieved.`, + // ); + // throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.NotAllowed); + // } + + try { + var (publicKey, privateKey) = await _cryptoFunctionService.EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm.P256Sha256); + var fido2Credential = CreateCredentialView(makeCredentialParams, privateKey); + + var encrypted = await _cipherService.GetAsync(cipherId); + var cipher = await encrypted.DecryptAsync(); + + // if ( + // !userVerified && + // (params.requireUserVerification || cipher.reprompt !== CipherRepromptType.None) + // ) { + // this.logService?.warning( + // `[Fido2Authenticator] Aborting because user verification was unsuccessful.`, + // ); + // throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.NotAllowed); + // } + + cipher.Login.Fido2Credentials = [fido2Credential]; + var reencrypted = await _cipherService.EncryptAsync(cipher); + await _cipherService.SaveWithServerAsync(reencrypted); + credentialId = fido2Credential.CredentialId; + } catch { + throw; + // throw new NotImplementedException(); + } + return new Fido2AuthenticatorMakeCredentialResult { - CredentialId = GuidToRawFormat(Guid.NewGuid().ToString()), + CredentialId = GuidToRawFormat(credentialId), AttestationObject = Array.Empty(), AuthData = Array.Empty(), PublicKey = Array.Empty(), @@ -227,10 +263,29 @@ namespace Bit.Core.Services cipher.Type == CipherType.Login && cipher.Login.HasFido2Credentials && cipher.Login.MainFido2Credential.RpId == rpId && - cipher.Login.MainFido2Credential.IsDiscoverable + cipher.Login.MainFido2Credential.DiscoverableValue ); } + private Fido2CredentialView CreateCredentialView(Fido2AuthenticatorMakeCredentialParams makeCredentialsParams, byte[] privateKey) + { + return new Fido2CredentialView { + CredentialId = Guid.NewGuid().ToString(), + KeyType = "public-key", + KeyAlgorithm = "ECDSA", + KeyCurve = "P-256", + KeyValue = CoreHelpers.Base64UrlEncode(privateKey), + RpId = makeCredentialsParams.RpEntity.Id, + UserHandle = CoreHelpers.Base64UrlEncode(makeCredentialsParams.UserEntity.Id), + UserName = makeCredentialsParams.UserEntity.Name, + CounterValue = 0, + RpName = makeCredentialsParams.RpEntity.Name, + // UserDisplayName = makeCredentialsParams.UserEntity.DisplayName, + DiscoverableValue = makeCredentialsParams.RequireResidentKey, + CreationDate = DateTime.Now + }; + } + private async Task GenerateAuthData( string rpId, bool userVerification, @@ -288,7 +343,7 @@ namespace Bit.Core.Services var sigBase = authData.Concat(clientDataHash).ToArray(); var signature = await _cryptoFunctionService.SignAsync(sigBase, privateKey, new CryptoSignEcdsaOptions { - Algorithm = CryptoSignEcdsaOptions.EcdsaAlgorithm.EcdsaP256Sha256, + Algorithm = CryptoEcdsaAlgorithm.P256Sha256, SignatureFormat = CryptoSignEcdsaOptions.DsaSignatureFormat.Rfc3279DerSequence }); diff --git a/src/Core/Services/PclCryptoFunctionService.cs b/src/Core/Services/PclCryptoFunctionService.cs index 3a55db662..e822075a7 100644 --- a/src/Core/Services/PclCryptoFunctionService.cs +++ b/src/Core/Services/PclCryptoFunctionService.cs @@ -228,6 +228,20 @@ namespace Bit.Core.Services return Task.FromResult(new Tuple(publicKey, privateKey)); } + public Task<(byte[], byte[])> EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm algorithm) + { + if (algorithm != CryptoEcdsaAlgorithm.P256Sha256) + { + throw new ArgumentException("Unsupported algorithm."); + } + + var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.EcdsaP256Sha256); + var cryptoKey = provider.CreateKeyPair(256); + var publicKey = cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); + var privateKey = cryptoKey.Export(CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); + return Task.FromResult((publicKey, privateKey)); + } + public Task RandomBytesAsync(int length) { return Task.FromResult(CryptographicBuffer.GenerateRandom(length)); diff --git a/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs b/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs index 20b3fb5cc..81e6d48b8 100644 --- a/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs +++ b/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs @@ -91,7 +91,7 @@ namespace Bit.Core.Test.Services CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) ]; - var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.IsDiscoverable).ToList(); + var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.DiscoverableValue).ToList(); aParams.RpId = "bitwarden.com"; aParams.RequireUserVerification = false; aParams.AllowCredentialDescriptorList = null; @@ -118,7 +118,7 @@ namespace Bit.Core.Test.Services CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) ]; - var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.IsDiscoverable).ToList(); + var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.DiscoverableValue).ToList(); aParams.RpId = "bitwarden.com"; aParams.AllowCredentialDescriptorList = null; aParams.RequireUserVerification = true; @@ -146,7 +146,7 @@ namespace Bit.Core.Test.Services CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) ]; - var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.IsDiscoverable).ToList(); + var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.DiscoverableValue).ToList(); aParams.RpId = "bitwarden.com"; aParams.AllowCredentialDescriptorList = null; aParams.RequireUserVerification = false; @@ -172,7 +172,7 @@ namespace Bit.Core.Test.Services CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) ]; - var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.IsDiscoverable).ToList(); + var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.DiscoverableValue).ToList(); aParams.RpId = "bitwarden.com"; aParams.AllowCredentialDescriptorList = null; sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); @@ -193,7 +193,7 @@ namespace Bit.Core.Test.Services CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) ]; - var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.IsDiscoverable).ToList(); + var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.DiscoverableValue).ToList(); aParams.RequireUserVerification = true; aParams.RpId = "bitwarden.com"; aParams.AllowCredentialDescriptorList = null; @@ -216,7 +216,7 @@ namespace Bit.Core.Test.Services CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) ]; ciphers[0].Reprompt = CipherRepromptType.Password; - var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.IsDiscoverable).ToList(); + var discoverableCiphers = ciphers.Where((cipher) => cipher.Login.MainFido2Credential.DiscoverableValue).ToList(); aParams.RpId = "bitwarden.com"; aParams.AllowCredentialDescriptorList = null; sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); @@ -311,7 +311,7 @@ namespace Bit.Core.Test.Services Arg.Any(), Arg.Any(), new CryptoSignEcdsaOptions { - Algorithm = CryptoSignEcdsaOptions.EcdsaAlgorithm.EcdsaP256Sha256, + Algorithm = CryptoEcdsaAlgorithm.P256Sha256, SignatureFormat = CryptoSignEcdsaOptions.DsaSignatureFormat.Rfc3279DerSequence } ).Returns(signatureMock); diff --git a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs index 6170c4429..608189376 100644 --- a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs +++ b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs @@ -16,6 +16,8 @@ using Xunit; using Bit.Core.Utilities; using System.Collections.Generic; using System.Linq; +using System.Security.Policy; +using NSubstitute.Extensions; namespace Bit.Core.Test.Services { @@ -50,8 +52,8 @@ namespace Bit.Core.Test.Services { var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; List ciphers = [ - CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), - CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + CreateCipherView(true, credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(true, credentialIds[1].ToString(), "bitwarden.com", true) ]; mParams.CredTypesAndPubKeyAlgs = [ new PublicKeyCredentialAlgorithmDescriptor { @@ -87,8 +89,8 @@ namespace Bit.Core.Test.Services { var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; List ciphers = [ - CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), - CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + CreateCipherView(true, credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(true, credentialIds[1].ToString(), "bitwarden.com", true) ]; mParams.CredTypesAndPubKeyAlgs = [ new PublicKeyCredentialAlgorithmDescriptor { @@ -116,8 +118,8 @@ namespace Bit.Core.Test.Services { var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; List ciphers = [ - CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), - CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + CreateCipherView(false, credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(false, credentialIds[1].ToString(), "bitwarden.com", true) ]; ciphers[0].OrganizationId = "someOrganizationId"; mParams.CredTypesAndPubKeyAlgs = [ @@ -155,8 +157,8 @@ namespace Bit.Core.Test.Services // Common Arrange var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; List ciphers = [ - CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), - CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + CreateCipherView(false, credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(false, credentialIds[1].ToString(), "bitwarden.com", true) ]; mParams.CredTypesAndPubKeyAlgs = [ new PublicKeyCredentialAlgorithmDescriptor { @@ -166,6 +168,8 @@ namespace Bit.Core.Test.Services ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; mParams.RequireUserVerification = false; + sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) + .Returns((RandomBytes(32), RandomBytes(32))); sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); // Arrange @@ -187,8 +191,8 @@ namespace Bit.Core.Test.Services // Common Arrange var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; List ciphers = [ - CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), - CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + CreateCipherView(false, credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(false, credentialIds[1].ToString(), "bitwarden.com", true) ]; mParams.CredTypesAndPubKeyAlgs = [ new PublicKeyCredentialAlgorithmDescriptor { @@ -197,7 +201,8 @@ namespace Bit.Core.Test.Services } ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; - mParams.RequireUserVerification = false; + sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) + .Returns((RandomBytes(32), RandomBytes(32))); sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); // Arrange @@ -211,6 +216,55 @@ namespace Bit.Core.Test.Services (p) => p.UserVerification == false )); } + + [Theory] + [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] + public async Task MakeCredentialAsync_RequestsUserVerification_RequestConfirmedByUser(SutProvider sutProvider, Fido2AuthenticatorMakeCredentialParams mParams, Cipher encryptedCipher) + { + // Common Arrange + mParams.CredTypesAndPubKeyAlgs = [ + new PublicKeyCredentialAlgorithmDescriptor { + Type = "public-key", + Algorithm = -7 // ES256 + } + ]; + mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; + mParams.RequireUserVerification = false; + sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) + .Returns((RandomBytes(32), RandomBytes(32))); + var cryptoServiceMock = Substitute.For(); + ServiceContainer.Register(typeof(CryptoService), cryptoServiceMock); + encryptedCipher.Key = null; + encryptedCipher.Attachments = []; + + // Arrange + mParams.RequireResidentKey = false; + sutProvider.GetDependency().EncryptAsync(Arg.Any()).Returns(encryptedCipher); + sutProvider.GetDependency().GetAsync(Arg.Is(encryptedCipher.Id)).Returns(encryptedCipher); + sutProvider.GetDependency().ConfirmNewCredentialAsync(Arg.Any()).Returns(new Fido2ConfirmNewCredentialResult { + CipherId = encryptedCipher.Id, + UserVerified = false + }); + + // Act + await sutProvider.Sut.MakeCredentialAsync(mParams); + + // Assert + await sutProvider.GetDependency().Received().EncryptAsync(Arg.Is( + (c) => + c.Login.MainFido2Credential.KeyType == "public-key" && + c.Login.MainFido2Credential.KeyAlgorithm == "ECDSA" && + c.Login.MainFido2Credential.KeyCurve == "P-256" && + c.Login.MainFido2Credential.RpId == mParams.RpEntity.Id && + c.Login.MainFido2Credential.RpName == mParams.RpEntity.Name && + c.Login.MainFido2Credential.UserHandle == CoreHelpers.Base64UrlEncode(mParams.UserEntity.Id) && + c.Login.MainFido2Credential.UserName == mParams.UserEntity.Name && + c.Login.MainFido2Credential.CounterValue == 0 && + // c.Login.MainFido2Credential.UserDisplayName == mParams.UserEntity.DisplayName && + c.Login.MainFido2Credential.DiscoverableValue == false + )); + await sutProvider.GetDependency().Received().SaveWithServerAsync(encryptedCipher); + } #endregion @@ -222,21 +276,21 @@ namespace Bit.Core.Test.Services } #nullable enable - private CipherView CreateCipherView(string? credentialId, string? rpId, bool? discoverable) + private CipherView CreateCipherView(bool? withFido2Credential, string? credentialId = null, string? rpId = null, bool? discoverable = null) { return new CipherView { Type = CipherType.Login, Id = Guid.NewGuid().ToString(), Reprompt = CipherRepromptType.None, Login = new LoginView { - Fido2Credentials = new List { + Fido2Credentials = withFido2Credential.HasValue && withFido2Credential.Value ? new List { new Fido2CredentialView { CredentialId = credentialId ?? Guid.NewGuid().ToString(), RpId = rpId ?? "bitwarden.com", Discoverable = discoverable.HasValue ? discoverable.ToString() : "true", UserHandleValue = RandomBytes(32), } - } + } : null } }; }