mirror of
https://github.com/bitwarden/mobile
synced 2025-12-14 15:23:35 +00:00
[PM-5731] feat: implement fido2 client createCredential
This commit is contained in:
@@ -15,7 +15,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams)
|
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams)
|
||||||
{
|
{
|
||||||
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Algorithm != (int) Fido2AlgorithmIdentifier.ES256))
|
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Alg != (int) Fido2AlgorithmIdentifier.ES256))
|
||||||
{
|
{
|
||||||
// var requestedAlgorithms = string.Join(", ", makeCredentialParams.CredTypesAndPubKeyAlgs.Select((p) => p.Algorithm).ToArray());
|
// var requestedAlgorithms = string.Join(", ", makeCredentialParams.CredTypesAndPubKeyAlgs.Select((p) => p.Algorithm).ToArray());
|
||||||
// _logService.Warning(
|
// _logService.Warning(
|
||||||
|
|||||||
@@ -1,17 +1,62 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
public class Fido2ClientService : IFido2ClientService
|
public class Fido2ClientService : IFido2ClientService
|
||||||
{
|
{
|
||||||
public Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IEnvironmentService _environmentService;
|
||||||
|
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||||
|
private readonly IFido2AuthenticatorService _fido2AuthenticatorService;
|
||||||
|
|
||||||
|
public Fido2ClientService(
|
||||||
|
IStateService stateService,
|
||||||
|
IEnvironmentService environmentService,
|
||||||
|
ICryptoFunctionService cryptoFunctionService,
|
||||||
|
IFido2AuthenticatorService fido2AuthenticatorService
|
||||||
|
)
|
||||||
{
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_environmentService = environmentService;
|
||||||
|
_cryptoFunctionService = cryptoFunctionService;
|
||||||
|
_fido2AuthenticatorService = fido2AuthenticatorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
|
||||||
|
{
|
||||||
|
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||||
|
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
|
||||||
|
if (blockedUris.Contains(domain))
|
||||||
|
{
|
||||||
|
throw new Fido2ClientException(
|
||||||
|
Fido2ClientException.ErrorCode.UriBlockedError,
|
||||||
|
"Origin is blocked by the user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _stateService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
throw new Fido2ClientException(
|
||||||
|
Fido2ClientException.ErrorCode.InvalidStateError,
|
||||||
|
"No user is logged in");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createCredentialParams.Origin == _environmentService.GetWebVaultUrl())
|
||||||
|
{
|
||||||
|
throw new Fido2ClientException(
|
||||||
|
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||||
|
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
if (!createCredentialParams.SameOriginWithAncestors)
|
if (!createCredentialParams.SameOriginWithAncestors)
|
||||||
{
|
{
|
||||||
throw new Fido2ClientException(
|
throw new Fido2ClientException(
|
||||||
Fido2ClientException.ErrorCode.NotAllowedError,
|
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||||
"Credential creation is now allowed from embedded contexts with different origins.");
|
"Credential creation is now allowed from embedded contexts with different origins");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createCredentialParams.User.Id.Length < 1 || createCredentialParams.User.Id.Length > 64)
|
if (createCredentialParams.User.Id.Length < 1 || createCredentialParams.User.Id.Length > 64)
|
||||||
@@ -19,12 +64,101 @@ namespace Bit.Core.Services
|
|||||||
// TODO: Should we use ArgumentException here instead?
|
// TODO: Should we use ArgumentException here instead?
|
||||||
throw new Fido2ClientException(
|
throw new Fido2ClientException(
|
||||||
Fido2ClientException.ErrorCode.TypeError,
|
Fido2ClientException.ErrorCode.TypeError,
|
||||||
"The length of user.id is not between 1 and 64 bytes (inclusive).");
|
"The length of user.id is not between 1 and 64 bytes (inclusive)");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotImplementedException();
|
if (!createCredentialParams.Origin.StartsWith("https://"))
|
||||||
|
{
|
||||||
|
throw new Fido2ClientException(
|
||||||
|
Fido2ClientException.ErrorCode.SecurityError,
|
||||||
|
"Origin is not a valid https origin");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Fido2DomainUtils.IsValidRpId(createCredentialParams.Rp.Id, createCredentialParams.Origin))
|
||||||
|
{
|
||||||
|
throw new Fido2ClientException(
|
||||||
|
Fido2ClientException.ErrorCode.SecurityError,
|
||||||
|
"RP ID cannot be used with this origin");
|
||||||
|
}
|
||||||
|
|
||||||
|
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs;
|
||||||
|
if (createCredentialParams.PubKeyCredParams?.Length > 0)
|
||||||
|
{
|
||||||
|
// Filter out all unsupported algorithms
|
||||||
|
credTypesAndPubKeyAlgs = createCredentialParams.PubKeyCredParams
|
||||||
|
.Where(kp => kp.Alg == -7 && kp.Type == "public-key")
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Assign default algorithms
|
||||||
|
credTypesAndPubKeyAlgs = [
|
||||||
|
new PublicKeyCredentialParameters { Alg = -7, Type = "public-key" },
|
||||||
|
new PublicKeyCredentialParameters { Alg = -257, Type = "public-key" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credTypesAndPubKeyAlgs.Length == 0)
|
||||||
|
{
|
||||||
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientDataJSON = JsonSerializer.Serialize(new {
|
||||||
|
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 makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams);
|
||||||
|
|
||||||
|
return new Fido2ClientCreateCredentialResult {
|
||||||
|
CredentialId = makeCredentialResult.CredentialId,
|
||||||
|
AttestationObject = makeCredentialResult.AttestationObject,
|
||||||
|
AuthData = makeCredentialResult.AuthData,
|
||||||
|
ClientDataJSON = clientDataJSONBytes,
|
||||||
|
PublicKey = makeCredentialResult.PublicKey,
|
||||||
|
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
|
||||||
|
Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
|
||||||
|
};
|
||||||
|
} catch (InvalidStateError) {
|
||||||
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||||
|
} catch (Exception) {
|
||||||
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) => throw new NotImplementedException();
|
public Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(
|
||||||
|
Fido2ClientCreateCredentialParams createCredentialParams,
|
||||||
|
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs,
|
||||||
|
byte[] clientDataHash)
|
||||||
|
{
|
||||||
|
var requireResidentKey = createCredentialParams.AuthenticatorSelection?.ResidentKey == "required" ||
|
||||||
|
createCredentialParams.AuthenticatorSelection?.ResidentKey == "preferred" ||
|
||||||
|
(createCredentialParams.AuthenticatorSelection?.ResidentKey == null &&
|
||||||
|
createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true);
|
||||||
|
|
||||||
|
var requireUserVerification = createCredentialParams.AuthenticatorSelection?.UserVerification == "required" ||
|
||||||
|
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
|
||||||
|
createCredentialParams.AuthenticatorSelection?.UserVerification == null;
|
||||||
|
|
||||||
|
return new Fido2AuthenticatorMakeCredentialParams {
|
||||||
|
RequireResidentKey = requireResidentKey,
|
||||||
|
RequireUserVerification = requireUserVerification,
|
||||||
|
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
||||||
|
CredTypesAndPubKeyAlgs = credTypesAndPubKeyAlgs,
|
||||||
|
Hash = clientDataHash,
|
||||||
|
RpEntity = createCredentialParams.Rp,
|
||||||
|
UserEntity = createCredentialParams.User,
|
||||||
|
Extensions = createCredentialParams.Extensions
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace Bit.Core.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the host (and not port) of the given uri.
|
/// Returns the host (and not port) of the given uri.
|
||||||
/// Does not support plain hostnames/only top level domain.
|
/// Does not support plain hostnames without a protocol.
|
||||||
///
|
///
|
||||||
/// Input => Output examples:
|
/// Input => Output examples:
|
||||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||||
@@ -59,7 +59,7 @@ namespace Bit.Core.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the host and port of the given uri.
|
/// Returns the host and port of the given uri.
|
||||||
/// Does not support plain hostnames/only top level domain.
|
/// Does not support plain hostnames without
|
||||||
///
|
///
|
||||||
/// Input => Output examples:
|
/// Input => Output examples:
|
||||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||||
@@ -89,7 +89,7 @@ namespace Bit.Core.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the second and top level domain of the given uri.
|
/// Returns the second and top level domain of the given uri.
|
||||||
/// Does not support plain hostnames/only top level domain.
|
/// Does not support plain hostnames without
|
||||||
///
|
///
|
||||||
/// Input => Output examples:
|
/// Input => Output examples:
|
||||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ namespace Bit.Core.Utilities.Fido2
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class InvalidStateError : Fido2AuthenticatorException
|
||||||
|
{
|
||||||
|
public InvalidStateError() : base("InvalidStateError")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class UnknownError : Fido2AuthenticatorException
|
public class UnknownError : Fido2AuthenticatorException
|
||||||
{
|
{
|
||||||
public UnknownError() : base("UnknownError")
|
public UnknownError() : base("UnknownError")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace Bit.Core.Utilities.Fido2
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A sequence of pairs of PublicKeyCredentialType and public key algorithms (COSEAlgorithmIdentifier) requested by the Relying Party. This sequence is ordered from most preferred to least preferred. The authenticator makes a best-effort to create the most preferred credential that it can.
|
/// A sequence of pairs of PublicKeyCredentialType and public key algorithms (COSEAlgorithmIdentifier) requested by the Relying Party. This sequence is ordered from most preferred to least preferred. The authenticator makes a best-effort to create the most preferred credential that it can.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PublicKeyCredentialAlgorithmDescriptor[] CredTypesAndPubKeyAlgs { get; set; }
|
public PublicKeyCredentialParameters[] CredTypesAndPubKeyAlgs { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
|
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ namespace Bit.Core.Utilities.Fido2
|
|||||||
/// the same account on a single authenticator. The client is requested to return an error if the new credential would
|
/// the same account on a single authenticator. The client is requested to return an error if the new credential would
|
||||||
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
|
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<PublicKeyCredentialDescriptor>? ExcludeCredentials { get; set; }
|
public PublicKeyCredentialDescriptor[]? ExcludeCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This member contains additional parameters requesting additional processing by the client and authenticator.
|
/// This member contains additional parameters requesting additional processing by the client and authenticator.
|
||||||
@@ -52,7 +52,7 @@ namespace Bit.Core.Utilities.Fido2
|
|||||||
/// The sequence is ordered from most preferred to least preferred.
|
/// The sequence is ordered from most preferred to least preferred.
|
||||||
/// The client makes a best-effort to create the most preferred credential that it can.
|
/// The client makes a best-effort to create the most preferred credential that it can.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required List<PublicKeyCredentialParameters> PubKeyCredParams { get; set; }
|
public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data about the Relying Party responsible for the request.
|
/// Data about the Relying Party responsible for the request.
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ namespace Bit.Core.Utilities.Fido2
|
|||||||
NotAllowedError,
|
NotAllowedError,
|
||||||
TypeError,
|
TypeError,
|
||||||
SecurityError,
|
SecurityError,
|
||||||
|
UriBlockedError,
|
||||||
|
NotSupportedError,
|
||||||
|
InvalidStateError,
|
||||||
UnknownError
|
UnknownError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Bit.Core.Models.View;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ExceptionExtensions;
|
using NSubstitute.ExceptionExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -49,9 +48,9 @@ namespace Bit.Core.Test.Services
|
|||||||
Name = "Bitwarden"
|
Name = "Bitwarden"
|
||||||
},
|
},
|
||||||
CredTypesAndPubKeyAlgs = [
|
CredTypesAndPubKeyAlgs = [
|
||||||
new PublicKeyCredentialAlgorithmDescriptor {
|
new PublicKeyCredentialParameters {
|
||||||
Type = "public-key",
|
Type = "public-key",
|
||||||
Algorithm = -7 // ES256
|
Alg = -7 // ES256
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
RequireResidentKey = false,
|
RequireResidentKey = false,
|
||||||
@@ -84,9 +83,9 @@ namespace Bit.Core.Test.Services
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_params.CredTypesAndPubKeyAlgs = [
|
_params.CredTypesAndPubKeyAlgs = [
|
||||||
new PublicKeyCredentialAlgorithmDescriptor {
|
new PublicKeyCredentialParameters {
|
||||||
Type = "public-key",
|
Type = "public-key",
|
||||||
Algorithm = -257 // RS256 which we do not support
|
Alg = -257 // RS256 which we do not support
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ExceptionExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services
|
namespace Bit.Core.Test.Services
|
||||||
@@ -36,6 +43,9 @@ namespace Bit.Core.Test.Services
|
|||||||
DisplayName = "User"
|
DisplayName = "User"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([]);
|
||||||
|
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -46,10 +56,13 @@ namespace Bit.Core.Test.Services
|
|||||||
// Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException.
|
// Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException.
|
||||||
public async Task CreateCredentialAsync_ThrowsNotAllowedError_SameOriginWithAncestorsIsFalse()
|
public async Task CreateCredentialAsync_ThrowsNotAllowedError_SameOriginWithAncestorsIsFalse()
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
_params.SameOriginWithAncestors = false;
|
_params.SameOriginWithAncestors = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
|
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +70,13 @@ namespace Bit.Core.Test.Services
|
|||||||
// Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError.
|
// 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()
|
public async Task CreateCredentialAsync_ThrowsTypeError_UserIdIsTooSmall()
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
_params.User.Id = RandomBytes(0);
|
_params.User.Id = RandomBytes(0);
|
||||||
|
|
||||||
|
// Act
|
||||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
|
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +84,216 @@ namespace Bit.Core.Test.Services
|
|||||||
// Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError.
|
// 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_UserIdIsTooLarge()
|
public async Task CreateCredentialAsync_ThrowsTypeError_UserIdIsTooLarge()
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
_params.User.Id = RandomBytes(65);
|
_params.User.Id = RandomBytes(65);
|
||||||
|
|
||||||
|
// Act
|
||||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
|
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Not sure how to check this, or if it matters.")]
|
||||||
|
// Spec: If callerOrigin is an opaque origin, return a DOMException whose name is "NotAllowedError", and terminate this algorithm.
|
||||||
|
public Task CreateCredentialAsync_ThrowsNotAllowedError_UserIdIsTooLarge() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
// Spec: Let effectiveDomain be the callerOrigin’s effective domain. If effective domain is not a valid domain,
|
||||||
|
// then return a DOMException whose name is "SecurityError" and terminate this algorithm.
|
||||||
|
public async Task CreateCredentialAsync_ThrowsSecurityError_OriginIsNotValidDomain()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.Origin = "invalid-domain-name";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
// Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain,
|
||||||
|
// return a DOMException whose name is "SecurityError", and terminate this algorithm.
|
||||||
|
public async Task CreateCredentialAsync_ThrowsSecurityError_RpIdIsNotValidForOrigin()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.Origin = "https://passwordless.dev";
|
||||||
|
_params.Rp.Id = "bitwarden.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
// Spec: The origin's scheme must be https.
|
||||||
|
public async Task CreateCredentialAsync_ThrowsSecurityError_OriginIsNotHttps()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.Origin = "http://bitwarden.com";
|
||||||
|
_params.Rp.Id = "bitwarden.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
// Spec: If the origin's hostname is a blocked uri, then return UriBlockedError.
|
||||||
|
public async Task CreateCredentialAsync_ThrowsUriBlockedError_OriginIsBlocked()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.Origin = "https://sub.bitwarden.com";
|
||||||
|
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([
|
||||||
|
"sub.bitwarden.com"
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.UriBlockedError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
// Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
|
||||||
|
public async Task CreateCredentialAsync_ThrowsNotSupportedError_CredTypesAndPubKeyAlgsIsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.PubKeyCredParams = [
|
||||||
|
new PublicKeyCredentialParameters {
|
||||||
|
Type = "not-supported",
|
||||||
|
Alg = -7
|
||||||
|
},
|
||||||
|
new PublicKeyCredentialParameters {
|
||||||
|
Type = "public-key",
|
||||||
|
Alg = -9001
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.NotSupportedError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Not implemented")]
|
||||||
|
// Spec: If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm.
|
||||||
|
public Task CreateCredentialAsync_ThrowsAbortError_AbortedByCaller() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateCredentialAsync_ReturnsNewCredential()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.AuthenticatorSelection = new AuthenticatorSelectionCriteria {
|
||||||
|
ResidentKey = "required",
|
||||||
|
UserVerification = "required"
|
||||||
|
};
|
||||||
|
var authenticatorResult = new Fido2AuthenticatorMakeCredentialResult {
|
||||||
|
CredentialId = RandomBytes(32),
|
||||||
|
AttestationObject = RandomBytes(32),
|
||||||
|
AuthData = RandomBytes(32),
|
||||||
|
PublicKey = RandomBytes(32),
|
||||||
|
PublicKeyAlgorithm = -7,
|
||||||
|
};
|
||||||
|
_sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||||
|
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>())
|
||||||
|
.Returns(authenticatorResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sutProvider.Sut.CreateCredentialAsync(_params);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||||
|
.Received()
|
||||||
|
.MakeCredentialAsync(Arg.Is<Fido2AuthenticatorMakeCredentialParams>(x =>
|
||||||
|
x.RequireResidentKey == true &&
|
||||||
|
x.RequireUserVerification == true &&
|
||||||
|
x.RpEntity.Id == _params.Rp.Id &&
|
||||||
|
x.UserEntity.DisplayName == _params.User.DisplayName
|
||||||
|
));
|
||||||
|
Assert.Equal(authenticatorResult.CredentialId, result.CredentialId);
|
||||||
|
Assert.Equal(authenticatorResult.AttestationObject, result.AttestationObject);
|
||||||
|
Assert.Equal(authenticatorResult.AuthData, result.AuthData);
|
||||||
|
Assert.Equal(authenticatorResult.PublicKey, result.PublicKey);
|
||||||
|
Assert.Equal(authenticatorResult.PublicKeyAlgorithm, result.PublicKeyAlgorithm);
|
||||||
|
Assert.Equal(["internal"], result.Transports);
|
||||||
|
|
||||||
|
var clientDataJSON = JsonSerializer.Deserialize<JsonObject>(Encoding.UTF8.GetString(result.ClientDataJSON));
|
||||||
|
Assert.Equal("webauthn.create", clientDataJSON["type"].GetValue<string>());
|
||||||
|
Assert.Equal(CoreHelpers.Base64UrlEncode(_params.Challenge), clientDataJSON["challenge"].GetValue<string>());
|
||||||
|
Assert.Equal(_params.Origin, clientDataJSON["origin"].GetValue<string>());
|
||||||
|
Assert.Equal(!_params.SameOriginWithAncestors, clientDataJSON["crossOrigin"].GetValue<bool>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateCredentialAsync_ThrowsInvalidStateError_AuthenticatorThrowsInvalidStateError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.AuthenticatorSelection = new AuthenticatorSelectionCriteria {
|
||||||
|
ResidentKey = "required",
|
||||||
|
UserVerification = "required"
|
||||||
|
};
|
||||||
|
_sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||||
|
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>())
|
||||||
|
.Throws(new InvalidStateError());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
// This keeps sensetive information form leaking
|
||||||
|
public async Task CreateCredentialAsync_ThrowsUnknownError_AuthenticatorThrowsUnknownError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||||
|
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>())
|
||||||
|
.Throws(new Exception("unknown error"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.UnknownError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateCredentialAsync_ThrowsInvalidStateError_UserIsLoggedOut()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateCredentialAsync_ThrowsNotAllowedError_OriginIsBitwardenVault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_params.Origin = "https://vault.bitwarden.com";
|
||||||
|
_sutProvider.GetDependency<IEnvironmentService>().GetWebVaultUrl().Returns("https://vault.bitwarden.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] RandomBytes(int length)
|
private byte[] RandomBytes(int length)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user