From ceca142c65c8e21faf66c76e5c8a8ab82f9ebef6 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 3 Apr 2024 17:52:39 +0200 Subject: [PATCH] feat: add support for `credProps.rk` extension (#3132) --- src/Core/Services/Fido2ClientService.cs | 19 +++- .../Fido2CreateCredentialExtensionsParams.cs | 9 ++ .../Fido2CreateCredentialExtensionsResult.cs | 9 ++ .../Fido2/Extensions/Fido2CredPropsResult.cs | 9 ++ .../Fido2AuthenticatorMakeCredentialParams.cs | 2 +- .../Fido2ClientCreateCredentialParams.cs | 7 +- .../Fido2ClientCreateCredentialResult.cs | 3 + .../Fido2ClientCreateCredentialTests.cs | 92 +++++++++++++++++-- 8 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsParams.cs create mode 100644 src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsResult.cs create mode 100644 src/Core/Utilities/Fido2/Extensions/Fido2CredPropsResult.cs diff --git a/src/Core/Services/Fido2ClientService.cs b/src/Core/Services/Fido2ClientService.cs index bf454bc73..a36ba61a5 100644 --- a/src/Core/Services/Fido2ClientService.cs +++ b/src/Core/Services/Fido2ClientService.cs @@ -4,6 +4,7 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; +using Bit.Core.Utilities.Fido2.Extensions; namespace Bit.Core.Services { @@ -124,6 +125,15 @@ namespace Bit.Core.Services { var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, _makeCredentialUserInterface); + Fido2CredPropsResult credProps = null; + if (createCredentialParams.Extensions?.CredProps == true) + { + credProps = new Fido2CredPropsResult + { + Rk = makeCredentialParams.RequireResidentKey + }; + } + return new Fido2ClientCreateCredentialResult { CredentialId = makeCredentialResult.CredentialId, @@ -132,7 +142,11 @@ namespace Bit.Core.Services ClientDataJSON = clientDataJSONBytes, PublicKey = makeCredentialResult.PublicKey, PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm, - Transports = createCredentialParams.Rp.Id == "google.com" ? new string[] { "internal", "usb" } : new string[] { "internal" } // workaround for a bug on Google's side + Transports = createCredentialParams.Rp.Id == "google.com" ? new string[] { "internal", "usb" } : new string[] { "internal" }, // workaround for a bug on Google's side + Extensions = new Fido2CreateCredentialExtensionsResult + { + CredProps = credProps + } }; } catch (InvalidStateError) @@ -249,7 +263,8 @@ namespace Bit.Core.Services Fido2ClientAssertCredentialParams assertCredentialParams, byte[] cliendDataHash) { - return new Fido2AuthenticatorGetAssertionParams { + return new Fido2AuthenticatorGetAssertionParams + { RpId = assertCredentialParams.RpId, Challenge = assertCredentialParams.Challenge, AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials, diff --git a/src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsParams.cs b/src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsParams.cs new file mode 100644 index 000000000..c2311fdb6 --- /dev/null +++ b/src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsParams.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Utilities.Fido2.Extensions +{ +#nullable enable + + public class Fido2CreateCredentialExtensionsParams + { + public bool CredProps { get; set; } = false; + } +} diff --git a/src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsResult.cs b/src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsResult.cs new file mode 100644 index 000000000..410f14418 --- /dev/null +++ b/src/Core/Utilities/Fido2/Extensions/Fido2CreateCredentialExtensionsResult.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Utilities.Fido2.Extensions +{ +#nullable enable + + public class Fido2CreateCredentialExtensionsResult + { + public Fido2CredPropsResult? CredProps { get; set; } + } +} diff --git a/src/Core/Utilities/Fido2/Extensions/Fido2CredPropsResult.cs b/src/Core/Utilities/Fido2/Extensions/Fido2CredPropsResult.cs new file mode 100644 index 000000000..b2fa1cce2 --- /dev/null +++ b/src/Core/Utilities/Fido2/Extensions/Fido2CredPropsResult.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Utilities.Fido2.Extensions +{ +#nullable enable + + public class Fido2CredPropsResult + { + public bool? Rk { get; set; } = false; + } +} diff --git a/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs b/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs index 5aebb8ab0..13e210745 100644 --- a/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs +++ b/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs @@ -36,7 +36,7 @@ namespace Bit.Core.Utilities.Fido2 /// The effective user verification preference for assertion, provided by the client. /// public Fido2UserVerificationPreference UserVerificationPreference { get; set; } - + /// /// CTAP2 authenticators support setting this to false, but we only support the WebAuthn authenticator model which does not have that option. /// diff --git a/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs b/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs index 52adbfff0..d730ed025 100644 --- a/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs +++ b/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs @@ -1,6 +1,8 @@ +using Bit.Core.Utilities.Fido2.Extensions; + namespace Bit.Core.Utilities.Fido2 { - #nullable enable +#nullable enable /// /// Parameters for creating a new credential. @@ -42,9 +44,8 @@ namespace Bit.Core.Utilities.Fido2 /// /// This member contains additional parameters requesting additional processing by the client and authenticator. - /// Not currently supported. /// - public object? Extensions { get; set; } + public Fido2CreateCredentialExtensionsParams? Extensions { get; set; } /// /// This member contains information about the desired properties of the credential to be created. diff --git a/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialResult.cs b/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialResult.cs index 21cd90357..65284c19e 100644 --- a/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialResult.cs +++ b/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialResult.cs @@ -1,3 +1,5 @@ +using Bit.Core.Utilities.Fido2.Extensions; + namespace Bit.Core.Utilities.Fido2 { /// @@ -15,5 +17,6 @@ namespace Bit.Core.Utilities.Fido2 public byte[] PublicKey { get; set; } public int PublicKeyAlgorithm { get; set; } public string[] Transports { get; set; } + public Fido2CreateCredentialExtensionsResult Extensions { get; set; } } } diff --git a/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs b/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs index 0310494cc..70fcdb240 100644 --- a/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs +++ b/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs @@ -8,6 +8,7 @@ using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; +using Bit.Core.Utilities.Fido2.Extensions; using Bit.Test.Common.AutoFixture; using NSubstitute; using NSubstitute.ExceptionExtensions; @@ -20,6 +21,7 @@ namespace Bit.Core.Test.Services private readonly SutProvider _sutProvider = new SutProvider().Create(); private Fido2ClientCreateCredentialParams _params; + private Fido2AuthenticatorMakeCredentialResult _authenticatorResult; public Fido2ClientCreateCredentialTests() { @@ -37,22 +39,33 @@ namespace Bit.Core.Test.Services Alg = (int) Fido2AlgorithmIdentifier.ES256 } }, - Rp = new PublicKeyCredentialRpEntity { + Rp = new PublicKeyCredentialRpEntity + { Id = "bitwarden.com", Name = "Bitwarden" }, - User = new PublicKeyCredentialUserEntity { + User = new PublicKeyCredentialUserEntity + { Id = RandomBytes(32), Name = "user@bitwarden.com", DisplayName = "User" } }; + _authenticatorResult = new Fido2AuthenticatorMakeCredentialResult + { + CredentialId = RandomBytes(32), + AttestationObject = RandomBytes(32), + AuthData = RandomBytes(32), + PublicKey = RandomBytes(32), + PublicKeyAlgorithm = (int)Fido2AlgorithmIdentifier.ES256, + }; + _sutProvider.GetDependency().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List())); _sutProvider.GetDependency().IsAuthenticatedAsync().Returns(true); } - public void Dispose() + public void Dispose() { } @@ -69,7 +82,7 @@ namespace Bit.Core.Test.Services // Assert Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code); } - + [Fact] // Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError. public async Task CreateCredentialAsync_ThrowsTypeError_UserIdIsTooSmall() @@ -198,16 +211,18 @@ namespace Bit.Core.Test.Services public async Task CreateCredentialAsync_ReturnsNewCredential() { // Arrange - _params.AuthenticatorSelection = new AuthenticatorSelectionCriteria { + _params.AuthenticatorSelection = new AuthenticatorSelectionCriteria + { ResidentKey = "required", UserVerification = "required" }; - var authenticatorResult = new Fido2AuthenticatorMakeCredentialResult { + var authenticatorResult = new Fido2AuthenticatorMakeCredentialResult + { CredentialId = RandomBytes(32), AttestationObject = RandomBytes(32), AuthData = RandomBytes(32), PublicKey = RandomBytes(32), - PublicKeyAlgorithm = (int) Fido2AlgorithmIdentifier.ES256, + PublicKeyAlgorithm = (int)Fido2AlgorithmIdentifier.ES256, }; _sutProvider.GetDependency() .MakeCredentialAsync(Arg.Any(), _sutProvider.GetDependency()) @@ -246,7 +261,8 @@ namespace Bit.Core.Test.Services public async Task CreateCredentialAsync_ThrowsInvalidStateError_AuthenticatorThrowsInvalidStateError() { // Arrange - _params.AuthenticatorSelection = new AuthenticatorSelectionCriteria { + _params.AuthenticatorSelection = new AuthenticatorSelectionCriteria + { ResidentKey = "required", UserVerification = "required" }; @@ -304,6 +320,66 @@ namespace Bit.Core.Test.Services Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code); } + [Fact] + public async Task CreateCredentialAsync_ReturnsCredPropsRkTrue_WhenCreatingDiscoverableCredential() + { + // Arrange + _params.AuthenticatorSelection = new AuthenticatorSelectionCriteria + { + ResidentKey = "required" + }; + _params.Extensions = new Fido2CreateCredentialExtensionsParams { CredProps = true }; + _sutProvider.GetDependency() + .MakeCredentialAsync(Arg.Any(), _sutProvider.GetDependency()) + .Returns(_authenticatorResult); + + // Act + var result = await _sutProvider.Sut.CreateCredentialAsync(_params); + + // Assert + Assert.True(result.Extensions.CredProps?.Rk); + } + + [Fact] + public async Task CreateCredentialAsync_ReturnsCredPropsRkFalse_WhenCreatingNonDiscoverableCredential() + { + // Arrange + _params.AuthenticatorSelection = new AuthenticatorSelectionCriteria + { + ResidentKey = "discouraged" + }; + _params.Extensions = new Fido2CreateCredentialExtensionsParams { CredProps = true }; + _sutProvider.GetDependency() + .MakeCredentialAsync(Arg.Any(), _sutProvider.GetDependency()) + .Returns(_authenticatorResult); + + // Act + var result = await _sutProvider.Sut.CreateCredentialAsync(_params); + + // Assert + Assert.False(result.Extensions.CredProps?.Rk); + } + + [Fact] + public async Task CreateCredentialAsync_ReturnsCredPropsUndefined_WhenExtensionIsNotRequested() + { + // Arrange + _params.AuthenticatorSelection = new AuthenticatorSelectionCriteria + { + ResidentKey = "required" + }; + _params.Extensions = new Fido2CreateCredentialExtensionsParams(); + _sutProvider.GetDependency() + .MakeCredentialAsync(Arg.Any(), _sutProvider.GetDependency()) + .Returns(_authenticatorResult); + + // Act + var result = await _sutProvider.Sut.CreateCredentialAsync(_params); + + // Assert + Assert.Null(result.Extensions.CredProps); + } + private byte[] RandomBytes(int length) { var bytes = new byte[length];