1
0
mirror of https://github.com/bitwarden/mobile synced 2026-02-28 10:23:40 +00:00

[PM-5731] feat: implement fido2 client createCredential

This commit is contained in:
Andreas Coroiu
2024-02-08 14:36:36 +01:00
parent 2075f8168c
commit b8c5ef501a
9 changed files with 378 additions and 16 deletions

View File

@@ -15,7 +15,7 @@ namespace Bit.Core.Services
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());
// _logService.Warning(

View File

@@ -1,17 +1,62 @@
using System.Text;
using System.Text.Json;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services
{
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)
{
throw new Fido2ClientException(
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)
@@ -19,12 +64,101 @@ namespace Bit.Core.Services
// TODO: Should we use ArgumentException here instead?
throw new Fido2ClientException(
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();
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
};
}
}
}

View File

@@ -40,7 +40,7 @@ namespace Bit.Core.Utilities
/// <summary>
/// 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:
/// <para>https://bitwarden.com => bitwarden.com</para>
@@ -59,7 +59,7 @@ namespace Bit.Core.Utilities
/// <summary>
/// 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:
/// <para>https://bitwarden.com => bitwarden.com</para>
@@ -89,7 +89,7 @@ namespace Bit.Core.Utilities
/// <summary>
/// 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:
/// <para>https://bitwarden.com => bitwarden.com</para>

View File

@@ -21,6 +21,13 @@ namespace Bit.Core.Utilities.Fido2
}
}
public class InvalidStateError : Fido2AuthenticatorException
{
public InvalidStateError() : base("InvalidStateError")
{
}
}
public class UnknownError : Fido2AuthenticatorException
{
public UnknownError() : base("UnknownError")

View File

@@ -20,7 +20,7 @@ namespace Bit.Core.Utilities.Fido2
/// <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.
/// </summary>
public PublicKeyCredentialAlgorithmDescriptor[] CredTypesAndPubKeyAlgs { get; set; }
public PublicKeyCredentialParameters[] CredTypesAndPubKeyAlgs { get; set; }
/// <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.

View File

@@ -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
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
/// </summary>
public List<PublicKeyCredentialDescriptor>? ExcludeCredentials { get; set; }
public PublicKeyCredentialDescriptor[]? ExcludeCredentials { get; set; }
/// <summary>
/// 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 client makes a best-effort to create the most preferred credential that it can.
/// </summary>
public required List<PublicKeyCredentialParameters> PubKeyCredParams { get; set; }
public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; }
/// <summary>
/// Data about the Relying Party responsible for the request.

View File

@@ -7,6 +7,9 @@ namespace Bit.Core.Utilities.Fido2
NotAllowedError,
TypeError,
SecurityError,
UriBlockedError,
NotSupportedError,
InvalidStateError,
UnknownError
}