1
0
mirror of https://github.com/bitwarden/mobile synced 2026-02-25 08:53:20 +00:00

[PM-5731] feat: implement credential assertion in client

This commit is contained in:
Andreas Coroiu
2024-02-12 13:45:41 +01:00
parent e9d1792dd7
commit 3c848a3dcc
9 changed files with 350 additions and 12 deletions

View File

@@ -192,7 +192,7 @@ namespace Bit.Core.Services
var signature = GenerateSignature(
authData: authenticatorData,
clientDataHash: assertionParams.ClientDataHash,
clientDataHash: assertionParams.Hash,
privateKey: selectedFido2Credential.KeyBytes
);

View File

@@ -18,8 +18,7 @@ namespace Bit.Core.Services
IStateService stateService,
IEnvironmentService environmentService,
ICryptoFunctionService cryptoFunctionService,
IFido2AuthenticatorService fido2AuthenticatorService
)
IFido2AuthenticatorService fido2AuthenticatorService)
{
_stateService = stateService;
_environmentService = environmentService;
@@ -133,7 +132,74 @@ namespace Bit.Core.Services
}
}
public Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) => throw new NotImplementedException();
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams)
{
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(assertCredentialParams.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 (assertCredentialParams.Origin == _environmentService.GetWebVaultUrl())
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.NotAllowedError,
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
}
if (!assertCredentialParams.Origin.StartsWith("https://"))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError,
"Origin is not a valid https origin");
}
if (!Fido2DomainUtils.IsValidRpId(assertCredentialParams.RpId, assertCredentialParams.Origin))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError,
"RP ID cannot be used with this origin");
}
var clientDataJSON = JsonSerializer.Serialize(new {
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 getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
try {
var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams);
return new Fido2ClientAssertCredentialResult {
AuthenticatorData = getAssertionResult.AuthenticatorData,
ClientDataJSON = clientDataJSONBytes,
Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id),
RawId = getAssertionResult.SelectedCredential.Id,
Signature = getAssertionResult.Signature,
UserHandle = getAssertionResult.SelectedCredential.UserHandle
};
} catch (InvalidStateError) {
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
} catch (Exception) {
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
}
throw new NotImplementedException();
}
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(
Fido2ClientCreateCredentialParams createCredentialParams,
@@ -160,5 +226,23 @@ namespace Bit.Core.Services
Extensions = createCredentialParams.Extensions
};
}
private Fido2AuthenticatorGetAssertionParams MapToGetAssertionParams(
Fido2ClientAssertCredentialParams assertCredentialParams,
byte[] cliendDataHash)
{
var requireUserVerification = assertCredentialParams.UserVerification == "required" ||
assertCredentialParams.UserVerification == "preferred" ||
assertCredentialParams.UserVerification == null;
return new Fido2AuthenticatorGetAssertionParams {
RpId = assertCredentialParams.RpId,
Challenge = assertCredentialParams.Challenge,
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
RequireUserPresence = true,
RequireUserVerification = requireUserVerification,
Hash = cliendDataHash
};
}
}
}

View File

@@ -6,7 +6,7 @@
public string RpId { get; set; }
/** The hash of the serialized client data, provided by the client. */
public byte[] ClientDataHash { get; set; }
public byte[] Hash { get; set; }
public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; }
@@ -20,6 +20,11 @@
/// </summary>
public bool RequireUserPresence { get; set; }
/// <summary>
/// The challenge to be signed by the authenticator.
/// </summary>
public byte[] Challenge { get; set; }
public object Extensions { get; set; }
}
}

View File

@@ -11,7 +11,13 @@ namespace Bit.Core.Utilities.Fido2
public class Fido2ClientAssertCredentialParams
{
/// <summary>
/// S challenge that the selected authenticator signs, along with other data, when producing an authentication
/// A value which is true if and only if the callers environment settings object is same-origin with its ancestors.
/// It is false if caller is cross-origin.
/// </summary>
public bool SameOriginWithAncestors { get; set; }
/// <summary>
/// The challenge that the selected authenticator signs, along with other data, when producing an authentication
/// assertion.
/// </summary>
public required byte[] Challenge { get; set; }

View File

@@ -16,5 +16,27 @@ namespace Bit.Core.Utilities.Fido2
/// The credential identifier.
/// </summary>
public required byte[] RawId { get; set; }
/// <summary>
/// The JSON-compatible serialization of client datapassed to the authenticator by the client in
/// order to generate this assertion.
/// </summary>
public required byte[] ClientDataJSON { get; set; }
/// <summary>
/// The authenticator data returned by the authenticator.
/// </summary>
public required byte[] AuthenticatorData { get; set; }
/// <summary>
/// The raw signature returned from the authenticator.
/// </summary>
public required byte[] Signature { get; set; }
/// <summary>
/// The user handle returned from the authenticator, or null if the authenticator did not
/// return a user handle.
/// </summary>
public byte[]? UserHandle { get; set; }
}
}

View File

@@ -12,7 +12,6 @@ namespace Bit.Core.Utilities.Fido2
/// </summary>
public required string Origin { get; set; }
// TODO: Check if we actually need this
/// <summary>
/// A value which is true if and only if the callers environment settings object is same-origin with its ancestors.
/// It is false if caller is cross-origin.