From ca250c53ad5350127d475d5fd634d77448414b2f Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 30 Jan 2024 14:19:41 +0100 Subject: [PATCH] [PM-5731] feat: add support for specifying user presence requirement --- .../Services/Fido2AuthenticatorService.cs | 43 ++++++++++++----- .../Fido2AuthenticatorGetAssertionParams.cs | 18 ++++--- .../Fido2AuthenticatorGetAssertionTests.cs | 47 +++++++++++++++++-- 3 files changed, 86 insertions(+), 22 deletions(-) diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index d3a252c47..3c5f1a8fe 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -126,14 +126,27 @@ namespace Bit.Core.Services throw new NotAllowedError(); } - var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams { - CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(), - UserVerification = assertionParams.RequireUserVerification - }); - var selectedCipherId = response.CipherId; - var userVerified = response.UserVerified; - var selectedCipher = cipherOptions.FirstOrDefault((c) => c.Id == selectedCipherId); + string selectedCipherId; + bool userVerified; + bool userPresence; + if (assertionParams.AllowCredentialDescriptorList?.Length == 1 && assertionParams.RequireUserPresence == false) + { + selectedCipherId = cipherOptions[0].Id; + userVerified = false; + userPresence = false; + } + else + { + var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams { + CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(), + UserVerification = assertionParams.RequireUserVerification + }); + selectedCipherId = response.CipherId; + userVerified = response.UserVerified; + userPresence = true; + } + var selectedCipher = cipherOptions.FirstOrDefault((c) => c.Id == selectedCipherId); if (selectedCipher == null) { // _logService.Info( // "[Fido2Authenticator] Aborting because the selected credential could not be found." @@ -142,6 +155,14 @@ namespace Bit.Core.Services throw new NotAllowedError(); } + if (!userPresence && assertionParams.RequireUserPresence) { + // _logService.Info( + // "[Fido2Authenticator] Aborting because user presence was required but not detected." + // ); + + throw new NotAllowedError(); + } + if (!userVerified && (assertionParams.RequireUserVerification || selectedCipher.Reprompt != CipherRepromptType.None)) { // _logService.Info( // "[Fido2Authenticator] Aborting because user verification was unsuccessful." @@ -164,14 +185,14 @@ namespace Bit.Core.Services var authenticatorData = await GenerateAuthDataAsync( rpId: selectedFido2Credential.RpId, - userPresence: true, + userPresence: userPresence, userVerification: userVerified, counter: selectedFido2Credential.CounterValue ); var signature = GenerateSignature( authData: authenticatorData, - clientDataHash: assertionParams.Hash, + clientDataHash: assertionParams.ClientDataHash, privateKey: selectedFido2Credential.KeyBytes ); @@ -207,9 +228,9 @@ namespace Bit.Core.Services return credentials; } - /// + /// /// Finds existing crendetials and returns the `CipherId` for each one - /// + /// private async Task FindExcludedCredentialsAsync( PublicKeyCredentialDescriptor[] credentials ) { diff --git a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionParams.cs b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionParams.cs index 803815c92..4abeaa3d6 100644 --- a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionParams.cs +++ b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionParams.cs @@ -6,17 +6,21 @@ public string RpId { get; set; } /** The hash of the serialized client data, provided by the client. */ - public byte[] Hash {get; set;} + public byte[] ClientDataHash { get; set; } - public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList {get; set;} + public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; } - /** The effective user verification requirement for assertion, a Boolean value provided by the client. */ - public bool RequireUserVerification {get; set;} + /// + /// Instructs the authenticator to require a user-verifying gesture in order to complete the request. Examples of such gestures are fingerprint scan or a PIN. + /// + public bool RequireUserVerification { get; set; } - /** CTAP2 authenticators support setting this to false, but we only support the WebAuthn authenticator model which does not have that option. */ - // public bool RequireUserPresence {get; set;} // Always required + /// + /// Instructs the authenticator to require user consent to complete the operation. + /// + public bool RequireUserPresence { get; set; } - public object Extensions {get; set;} + public object Extensions { get; set; } } } diff --git a/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs b/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs index 6b2190d17..2f6cde821 100644 --- a/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs +++ b/test/Core.Test/Services/Fido2AuthenticatorGetAssertionTests.cs @@ -6,7 +6,6 @@ using Bit.Core.Services; using Bit.Core.Models.Domain; using Bit.Core.Models.View; using Bit.Core.Enums; -using Bit.Core.Test.AutoFixture; using Bit.Core.Utilities.Fido2; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -34,7 +33,7 @@ namespace Bit.Core.Test.Services [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] - public async Task GetAssertionAsync_Throws_CredentialExistsButRpIdDoesNotMatch(SutProvider sutProvider, Fido2AuthenticatorGetAssertionParams aParams) + public async Task GetAssertionAsync_ThrowsNotAllowed_CredentialExistsButRpIdDoesNotMatch(SutProvider sutProvider, Fido2AuthenticatorGetAssertionParams aParams) { var credentialId = Guid.NewGuid(); aParams.RpId = "bitwarden.com"; @@ -305,7 +304,7 @@ namespace Bit.Core.Test.Services // Arrange var keyPair = GenerateKeyPair(); var rpIdHashMock = RandomBytes(32); - aParams.Hash = RandomBytes(32); + aParams.ClientDataHash = RandomBytes(32); sutProvider.GetDependency().HashAsync(aParams.RpId, CryptoHashAlgorithm.Sha256).Returns(rpIdHashMock); cipherView.Login.MainFido2Credential.CounterValue = 9000; cipherView.Login.MainFido2Credential.KeyValue = CoreHelpers.Base64UrlEncode(keyPair.ExportPkcs8PrivateKey()); @@ -324,7 +323,47 @@ namespace Bit.Core.Test.Services Assert.Equal(rpIdHashMock, rpIdHash); Assert.Equal(new byte[] { 0b00000101 }, flags); // UP = true, UV = true Assert.Equal(new byte[] { 0, 0, 0x23, 0x29 }, counter); // 9001 in binary big-endian format - Assert.True(keyPair.VerifyData(authData.Concat(aParams.Hash).ToArray(), result.Signature, HashAlgorithmName.SHA256), "Signature verification failed"); + Assert.True(keyPair.VerifyData(authData.Concat(aParams.ClientDataHash).ToArray(), result.Signature, HashAlgorithmName.SHA256), "Signature verification failed"); + } + + [Theory] + [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] + public async Task GetAssertionAsync_DoesNotAskForConfirmation_ParamsContainsOneAllowedCredentialAndUserPresenceIsFalse(SutProvider sutProvider, Fido2AuthenticatorGetAssertionParams aParams) + { + // 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) + ]; + var cipherView = ciphers[0]; + aParams.RpId = "bitwarden.com"; + aParams.RequireUserVerification = false; + aParams.RequireUserPresence = false; + aParams.AllowCredentialDescriptorList = [ + new PublicKeyCredentialDescriptor { + Id = credentialIds[1].ToByteArray(), + Type = "public-key" + } + ]; + sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); + + // Arrange + var keyPair = GenerateKeyPair(); + var rpIdHashMock = RandomBytes(32); + aParams.ClientDataHash = RandomBytes(32); + sutProvider.GetDependency().HashAsync(aParams.RpId, CryptoHashAlgorithm.Sha256).Returns(rpIdHashMock); + cipherView.Login.MainFido2Credential.CounterValue = 9000; + cipherView.Login.MainFido2Credential.KeyValue = CoreHelpers.Base64UrlEncode(keyPair.ExportPkcs8PrivateKey()); + + // Act + var result = await sutProvider.Sut.GetAssertionAsync(aParams); + + // Assert + await sutProvider.GetDependency().DidNotReceive().PickCredentialAsync(Arg.Any()); + var authData = result.AuthenticatorData; + var flags = authData.Skip(32).Take(1); + Assert.Equal(new byte[] { 0b00000000 }, flags); // UP = false, UV = false } [Theory]