From c1522e249d99681507ac96d38be65340d0faae92 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 19 Apr 2024 15:52:19 +0200 Subject: [PATCH] [PM-7257] android add support for web authn resident key credential property in our net mobile app 2 (#3170) * [PM-7257] feat: add ability to override `clientDataHash` * [PM-7257] feat: add support for clientDataHash and extensions * PM-7257 Updated the origin to be the correct one and not the android one to be passed to the Fido2Client --------- Co-authored-by: Federico Maccaroni --- .../Android/Autofill/CredentialHelpers.cs | 45 +++++++++-- .../CredentialProviderSelectionActivity.cs | 19 ++--- src/Core/Abstractions/IFido2ClientService.cs | 4 +- .../Abstractions/IFido2MediatorService.cs | 4 +- .../Services/Fido2AuthenticatorService.cs | 4 +- src/Core/Services/Fido2ClientService.cs | 50 +++++++----- src/Core/Services/Fido2MediatorService.cs | 12 +-- .../Fido2AuthenticatorGetAssertionResult.cs | 11 +-- .../Fido2ClientAssertCredentialResult.cs | 15 ++-- .../Fido2/Fido2SelectedCredential.cs | 10 +++ .../Fido2ClientAssertCredentialTests.cs | 81 +++++++++++++++---- .../Fido2ClientCreateCredentialTests.cs | 44 ++++++++++ 12 files changed, 217 insertions(+), 82 deletions(-) create mode 100644 src/Core/Utilities/Fido2/Fido2SelectedCredential.cs diff --git a/src/App/Platforms/Android/Autofill/CredentialHelpers.cs b/src/App/Platforms/Android/Autofill/CredentialHelpers.cs index c2c33c97e..34060cc1f 100644 --- a/src/App/Platforms/Android/Autofill/CredentialHelpers.cs +++ b/src/App/Platforms/Android/Autofill/CredentialHelpers.cs @@ -7,6 +7,7 @@ using AndroidX.Credentials.Provider; using AndroidX.Credentials.WebAuthn; using Bit.Core.Abstractions; using Bit.Core.Utilities; +using Bit.Core.Utilities.Fido2.Extensions; using Bit.Droid; using Org.Json; using Activity = Android.App.Activity; @@ -84,7 +85,7 @@ namespace Bit.App.Platforms.Android.Autofill var excludeCredentials = new List(); foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials) { - excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor(){ Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() }); + excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor() { Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() }); } var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria() @@ -95,7 +96,7 @@ namespace Bit.App.Platforms.Android.Autofill }; var timeout = Convert.ToInt32(credentialCreationOptions.Timeout); - + var credentialCreateParams = new Bit.Core.Utilities.Fido2.Fido2ClientCreateCredentialParams() { Challenge = credentialCreationOptions.GetChallenge(), @@ -107,7 +108,7 @@ namespace Bit.App.Platforms.Android.Autofill Attestation = credentialCreationOptions.Attestation, AuthenticatorSelection = authenticatorSelection, ExcludeCredentials = excludeCredentials.ToArray(), - //Extensions = // Can be improved later to add support for 'credProps' + Extensions = MapExtensionsFromJson(credentialCreationOptions), SameOriginWithAncestors = true }; @@ -121,7 +122,7 @@ namespace Bit.App.Platforms.Android.Autofill activity.Finish(); return; } - + var transportsArray = new JSONArray(); if (clientCreateCredentialResult.Transports != null) { @@ -130,7 +131,7 @@ namespace Bit.App.Platforms.Android.Autofill transportsArray.Put(transport); } } - + var responseInnerAndroidJson = new JSONObject(); responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON)); responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData)); @@ -144,7 +145,7 @@ namespace Bit.App.Platforms.Android.Autofill rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId)); rootAndroidJson.Put("authenticatorAttachment", "platform"); rootAndroidJson.Put("type", "public-key"); - rootAndroidJson.Put("clientExtensionResults", new JSONObject()); + rootAndroidJson.Put("clientExtensionResults", MapExtensionsToJson(clientCreateCredentialResult.Extensions)); rootAndroidJson.Put("response", responseInnerAndroidJson); var responseAndroidJson = rootAndroidJson.ToString(); @@ -158,5 +159,37 @@ namespace Bit.App.Platforms.Android.Autofill activity.SetResult(Result.Ok, result); activity.Finish(); } + + private static Fido2CreateCredentialExtensionsParams MapExtensionsFromJson(PublicKeyCredentialCreationOptions options) + { + if (options == null || !options.Json.Has("extensions")) + { + return null; + } + + var extensions = options.Json.GetJSONObject("extensions"); + return new Fido2CreateCredentialExtensionsParams + { + CredProps = extensions.Has("credProps") && extensions.GetBoolean("credProps") + }; + } + + private static JSONObject MapExtensionsToJson(Fido2CreateCredentialExtensionsResult extensions) + { + if (extensions == null) + { + return null; + } + + var extensionsJson = new JSONObject(); + if (extensions.CredProps != null) + { + var credPropsJson = new JSONObject(); + credPropsJson.Put("rk", extensions.CredProps.Rk); + extensionsJson.Put("credProps", credPropsJson); + } + + return extensionsJson; + } } } diff --git a/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs index 30622f108..dcf7963c7 100644 --- a/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs +++ b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs @@ -68,6 +68,7 @@ namespace Bit.Droid.Autofill var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo); var packageName = getRequest?.CallingAppInfo.PackageName; + var appInfoOrigin = getRequest?.CallingAppInfo.Origin; var userInterface = new Fido2GetAssertionUserInterface( cipherId: cipherId, @@ -76,17 +77,17 @@ namespace Bit.Droid.Autofill hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction, verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction)); - var assertParams = new Fido2AuthenticatorGetAssertionParams + var clientAssertParams = new Fido2ClientAssertCredentialParams { Challenge = requestOptions.GetChallenge(), RpId = RpId, - UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(requestOptions.UserVerification), - Hash = credentialPublic.GetClientDataHash(), - AllowCredentialDescriptorList = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } }, - Extensions = new object() + AllowCredentials = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } }, + Origin = appInfoOrigin, + SameOriginWithAncestors = true, + UserVerification = requestOptions.UserVerification }; - var assertResult = await _fido2MediatorService.Value.GetAssertionAsync(assertParams, userInterface); + var assertResult = await _fido2MediatorService.Value.AssertCredentialAsync(clientAssertParams, credentialPublic.GetClientDataHash()); var response = new AuthenticatorAssertionResponse( requestOptions, @@ -98,7 +99,7 @@ namespace Bit.Droid.Autofill false, assertResult.SelectedCredential.UserHandle, packageName, - credentialPublic.GetClientDataHash() //clientDataHash + assertResult.ClientDataHash ); response.SetAuthenticatorData(assertResult.AuthenticatorData); response.SetSignature(assertResult.Signature); @@ -117,7 +118,7 @@ namespace Bit.Droid.Autofill } catch (NotAllowedError) { - await MainThread.InvokeOnMainThreadAsync(async() => + await MainThread.InvokeOnMainThreadAsync(async () => { await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok); Finish(); @@ -126,7 +127,7 @@ namespace Bit.Droid.Autofill catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); - await MainThread.InvokeOnMainThreadAsync(async() => + await MainThread.InvokeOnMainThreadAsync(async () => { await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok); Finish(); diff --git a/src/Core/Abstractions/IFido2ClientService.cs b/src/Core/Abstractions/IFido2ClientService.cs index a690faacf..d3ca04dd9 100644 --- a/src/Core/Abstractions/IFido2ClientService.cs +++ b/src/Core/Abstractions/IFido2ClientService.cs @@ -21,7 +21,7 @@ namespace Bit.Core.Abstractions /// /// The parameters for the credential creation operation /// The new credential - Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams); + Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash); /// /// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s consent. @@ -30,6 +30,6 @@ namespace Bit.Core.Abstractions /// /// The parameters for the credential assertion operation /// The asserted credential - Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams); + Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash); } } diff --git a/src/Core/Abstractions/IFido2MediatorService.cs b/src/Core/Abstractions/IFido2MediatorService.cs index 46071b6f3..7464f5423 100644 --- a/src/Core/Abstractions/IFido2MediatorService.cs +++ b/src/Core/Abstractions/IFido2MediatorService.cs @@ -4,8 +4,8 @@ namespace Bit.Core.Abstractions { public interface IFido2MediatorService { - Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams); - Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams); + Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null); + Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null); Task MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface); Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface); diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 46867feba..e40159a0f 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -184,7 +184,7 @@ namespace Bit.Core.Services { throw new NotAllowedError(); } - + if (!userVerified && await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions( @@ -224,7 +224,7 @@ namespace Bit.Core.Services return new Fido2AuthenticatorGetAssertionResult { - SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential + SelectedCredential = new Fido2SelectedCredential { Id = selectedCredentialId.GuidToRawFormat(), UserHandle = selectedFido2Credential.UserHandleValue, diff --git a/src/Core/Services/Fido2ClientService.cs b/src/Core/Services/Fido2ClientService.cs index 03f090abd..bf8be680b 100644 --- a/src/Core/Services/Fido2ClientService.cs +++ b/src/Core/Services/Fido2ClientService.cs @@ -33,7 +33,7 @@ namespace Bit.Core.Services _makeCredentialUserInterface = makeCredentialUserInterface; } - public async Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams) + public async Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null) { var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var domain = CoreHelpers.GetHostname(createCredentialParams.Origin); @@ -109,16 +109,20 @@ namespace Bit.Core.Services throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found"); } - var clientDataJSON = JsonSerializer.Serialize(new + byte[] clientDataJSONBytes = null; + if (clientDataHash == null) { - type = "webauthn.create", - challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge), - origin = createCredentialParams.Origin, - crossOrigin = !createCredentialParams.SameOriginWithAncestors, - // tokenBinding: {} // Not supported - }); - var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); - var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); + var clientDataJSON = JsonSerializer.Serialize(new + { + type = "webauthn.create", + challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge), + origin = createCredentialParams.Origin, + crossOrigin = !createCredentialParams.SameOriginWithAncestors, + // tokenBinding: {} // Not supported + }); + clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); + clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); + } var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash); try @@ -159,7 +163,7 @@ namespace Bit.Core.Services } } - public async Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) + public async Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null) { var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin); @@ -198,15 +202,19 @@ namespace Bit.Core.Services "RP ID cannot be used with this origin"); } - var clientDataJSON = JsonSerializer.Serialize(new + byte[] clientDataJSONBytes = null; + if (clientDataHash == null) { - type = "webauthn.get", - challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge), - origin = assertCredentialParams.Origin, - crossOrigin = !assertCredentialParams.SameOriginWithAncestors, - }); - var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); - var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); + var clientDataJSON = JsonSerializer.Serialize(new + { + type = "webauthn.get", + challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge), + origin = assertCredentialParams.Origin, + crossOrigin = !assertCredentialParams.SameOriginWithAncestors, + }); + clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); + clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); + } var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash); try @@ -220,8 +228,8 @@ namespace Bit.Core.Services Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id), RawId = getAssertionResult.SelectedCredential.Id, Signature = getAssertionResult.Signature, - UserHandle = getAssertionResult.SelectedCredential.UserHandle, - Cipher = getAssertionResult.SelectedCredential.Cipher + SelectedCredential = getAssertionResult.SelectedCredential, + ClientDataHash = clientDataHash }; } catch (InvalidStateError) diff --git a/src/Core/Services/Fido2MediatorService.cs b/src/Core/Services/Fido2MediatorService.cs index 12917a8c9..35d49f173 100644 --- a/src/Core/Services/Fido2MediatorService.cs +++ b/src/Core/Services/Fido2MediatorService.cs @@ -18,21 +18,21 @@ namespace Bit.Core.Services _cipherService = cipherService; } - public async Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) + public async Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null) { - var result = await _fido2ClientService.AssertCredentialAsync(assertCredentialParams); + var result = await _fido2ClientService.AssertCredentialAsync(assertCredentialParams, clientDataHash); - if (result?.Cipher != null) + if (result?.SelectedCredential?.Cipher != null) { - await _cipherService.CopyTotpCodeIfNeededAsync(result.Cipher); + await _cipherService.CopyTotpCodeIfNeededAsync(result.SelectedCredential.Cipher); } return result; } - public Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams) + public Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null) { - return _fido2ClientService.CreateCredentialAsync(createCredentialParams); + return _fido2ClientService.CreateCredentialAsync(createCredentialParams, clientDataHash); } public async Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface) diff --git a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs index ff23d0668..91f95f975 100644 --- a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs +++ b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs @@ -8,16 +8,7 @@ namespace Bit.Core.Utilities.Fido2 public byte[] Signature { get; set; } - public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { get; set; } - } - - public class Fido2AuthenticatorGetAssertionSelectedCredential { - public byte[] Id { get; set; } - - #nullable enable - public byte[]? UserHandle { get; set; } - - public CipherView? Cipher { get; set; } + public Fido2SelectedCredential SelectedCredential { get; set; } } } diff --git a/src/Core/Utilities/Fido2/Fido2ClientAssertCredentialResult.cs b/src/Core/Utilities/Fido2/Fido2ClientAssertCredentialResult.cs index 48afc9681..0857cc945 100644 --- a/src/Core/Utilities/Fido2/Fido2ClientAssertCredentialResult.cs +++ b/src/Core/Utilities/Fido2/Fido2ClientAssertCredentialResult.cs @@ -25,6 +25,11 @@ namespace Bit.Core.Utilities.Fido2 /// public required byte[] ClientDataJSON { get; set; } + /// + /// The hash of the serialized client data used to generate the assertion. + /// + public required byte[] ClientDataHash { get; set; } + /// /// The authenticator data returned by the authenticator. /// @@ -36,14 +41,8 @@ namespace Bit.Core.Utilities.Fido2 public required byte[] Signature { get; set; } /// - /// The user handle returned from the authenticator, or null if the authenticator did not - /// return a user handle. + /// The selected credential that was used to generate the assertion. /// - public byte[]? UserHandle { get; set; } - - /// - /// The selected cipher login item that has the credential - /// - public CipherView? Cipher { get; set; } + public Fido2SelectedCredential SelectedCredential { get; set; } } } diff --git a/src/Core/Utilities/Fido2/Fido2SelectedCredential.cs b/src/Core/Utilities/Fido2/Fido2SelectedCredential.cs new file mode 100644 index 000000000..e714d93f1 --- /dev/null +++ b/src/Core/Utilities/Fido2/Fido2SelectedCredential.cs @@ -0,0 +1,10 @@ +using Bit.Core.Models.View; + +public class Fido2SelectedCredential +{ + public byte[] Id { get; set; } + + public byte[] UserHandle { get; set; } + + public CipherView Cipher { get; set; } +} diff --git a/test/Core.Test/Services/Fido2ClientAssertCredentialTests.cs b/test/Core.Test/Services/Fido2ClientAssertCredentialTests.cs index d2b85181f..ba79a1bac 100644 --- a/test/Core.Test/Services/Fido2ClientAssertCredentialTests.cs +++ b/test/Core.Test/Services/Fido2ClientAssertCredentialTests.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; @@ -20,10 +21,12 @@ namespace Bit.Core.Test.Services private readonly SutProvider _sutProvider = new SutProvider().Create(); private Fido2ClientAssertCredentialParams _params; + private Fido2AuthenticatorGetAssertionResult _authenticatorResult; public Fido2ClientAssertCredentialTests() { - _params = new Fido2ClientAssertCredentialParams { + _params = new Fido2ClientAssertCredentialParams + { Origin = "https://bitwarden.com", Challenge = RandomBytes(32), RpId = "bitwarden.com", @@ -39,11 +42,22 @@ namespace Bit.Core.Test.Services Timeout = 60000, }; + _authenticatorResult = new Fido2AuthenticatorGetAssertionResult + { + AuthenticatorData = RandomBytes(32), + SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential + { + Id = RandomBytes(16), + UserHandle = RandomBytes(32) + }, + Signature = RandomBytes(32) + }; + _sutProvider.GetDependency().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List())); _sutProvider.GetDependency().IsAuthenticatedAsync().Returns(true); } - public void Dispose() + public void Dispose() { } @@ -174,22 +188,57 @@ namespace Bit.Core.Test.Services Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code); } + [Fact] + public async Task AssertCredentialAsync_ConstructsClientDataHash_WhenHashIsNotProvided() + { + // Arrange + var mockHash = RandomBytes(32); + _sutProvider.GetDependency() + .HashAsync(Arg.Any(), Arg.Is(CryptoHashAlgorithm.Sha256)) + .Returns(Task.FromResult(mockHash)); + _sutProvider.GetDependency() + .GetAssertionAsync(Arg.Any(), _sutProvider.GetDependency()) + .Returns(_authenticatorResult); + + // Act + await _sutProvider.Sut.AssertCredentialAsync(_params); + + // Assert + await _sutProvider.GetDependency().Received() + .GetAssertionAsync( + Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash), + Arg.Any() + ); + } + + [Fact] + public async Task AssertCredentialAsync_UsesProvidedClientDataHash_WhenHashIsProvided() + { + // Arrange + var mockHash = RandomBytes(32); + _sutProvider.GetDependency() + .GetAssertionAsync(Arg.Any(), _sutProvider.GetDependency()) + .Returns(_authenticatorResult); + + // Act + await _sutProvider.Sut.AssertCredentialAsync(_params, mockHash); + + // Assert + await _sutProvider.GetDependency().Received() + .GetAssertionAsync( + Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash), + Arg.Any() + ); + } + [Fact] public async Task AssertCredentialAsync_ReturnsAssertion() { // Arrange _params.UserVerification = "required"; - var authenticatorResult = new Fido2AuthenticatorGetAssertionResult { - AuthenticatorData = RandomBytes(32), - SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential { - Id = RandomBytes(16), - UserHandle = RandomBytes(32) - }, - Signature = RandomBytes(32) - }; _sutProvider.GetDependency() .GetAssertionAsync(Arg.Any(), _sutProvider.GetDependency()) - .Returns(authenticatorResult); + .Returns(_authenticatorResult); // Act var result = await _sutProvider.Sut.AssertCredentialAsync(_params); @@ -203,14 +252,14 @@ namespace Bit.Core.Test.Services x.UserVerificationPreference == Fido2UserVerificationPreference.Required && x.AllowCredentialDescriptorList.Length == 1 && x.AllowCredentialDescriptorList[0].Id == _params.AllowCredentials[0].Id - ), + ), _sutProvider.GetDependency() ); - Assert.Equal(authenticatorResult.SelectedCredential.Id, result.RawId); - Assert.Equal(CoreHelpers.Base64UrlEncode(authenticatorResult.SelectedCredential.Id), result.Id); - Assert.Equal(authenticatorResult.AuthenticatorData, result.AuthenticatorData); - Assert.Equal(authenticatorResult.Signature, result.Signature); + Assert.Equal(_authenticatorResult.SelectedCredential.Id, result.RawId); + Assert.Equal(CoreHelpers.Base64UrlEncode(_authenticatorResult.SelectedCredential.Id), result.Id); + Assert.Equal(_authenticatorResult.AuthenticatorData, result.AuthenticatorData); + Assert.Equal(_authenticatorResult.Signature, result.Signature); var clientDataJSON = JsonSerializer.Deserialize(Encoding.UTF8.GetString(result.ClientDataJSON)); Assert.Equal("webauthn.get", clientDataJSON["type"].GetValue()); diff --git a/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs b/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs index 70fcdb240..202e93c28 100644 --- a/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs +++ b/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; @@ -320,6 +321,49 @@ namespace Bit.Core.Test.Services Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code); } + [Fact] + public async Task AssertCredentialAsync_ConstructsClientDataHash_WhenHashIsNotProvided() + { + // Arrange + var mockHash = RandomBytes(32); + _sutProvider.GetDependency() + .HashAsync(Arg.Any(), Arg.Is(CryptoHashAlgorithm.Sha256)) + .Returns(Task.FromResult(mockHash)); + _sutProvider.GetDependency() + .MakeCredentialAsync(Arg.Any(), _sutProvider.GetDependency()) + .Returns(_authenticatorResult); + + // Act + await _sutProvider.Sut.CreateCredentialAsync(_params); + + // Assert + await _sutProvider.GetDependency().Received() + .GetAssertionAsync( + Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash), + Arg.Any() + ); + } + + [Fact] + public async Task AssertCredentialAsync_UsesProvidedClientDataHash_WhenHashIsProvided() + { + // Arrange + var mockHash = RandomBytes(32); + _sutProvider.GetDependency() + .MakeCredentialAsync(Arg.Any(), _sutProvider.GetDependency()) + .Returns(_authenticatorResult); + + // Act + await _sutProvider.Sut.CreateCredentialAsync(_params, mockHash); + + // Assert + await _sutProvider.GetDependency().Received() + .GetAssertionAsync( + Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash), + Arg.Any() + ); + } + [Fact] public async Task CreateCredentialAsync_ReturnsCredPropsRkTrue_WhenCreatingDiscoverableCredential() {