1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-20 02:03:49 +00:00

[PM-5731] Create C# WebAuthn authenticator to support maui apps (#2951)

* [PM-5731] feat: implement get assertion params object

* [PM-5731] feat: add first test

* [PM-5731] feat: add rp mismatch test

* [PM-5731] feat: ask for credentials when found

* [PM-5731] feat: find discoverable credentials

* [PM-5731] feat: add tests for successful UV requests

* [PM-5731] feat: add user does not consent test

* [PM-5731] feat: check for UV when reprompt is active

* [PM-5731] fix: tests a bit, needed some additional "arrange" steps

* [PM-5731] feat: add support for counter

* [PM-5731] feat: implement assertion without signature

* [PM-5732] feat: finish authenticator assertion implementation

note: CryptoFunctionService still needs Sign implemenation

* [PM-5731] chore: minor clean up

* [PM-5731] feat: scaffold make credential

* [PM-5731] feat: start implementing attestation

* [PM-5731] feat: implement credential exclusion

* [PM-5731] feat: add new credential confirmaiton

* [PM-5731] feat: implement credential creation

* [PM-5731] feat: add user verification checks

* [PM-5731] feat: add unknown error handling

* [PM-5731] chore: clean up unusued params

* [PM-5731] feat: partial attestation implementation

* [PM-5731] feat: implement key generation

* [PM-5731] feat: return public key in DER format

* [PM-5731] feat: implement signing

* [PM-5731] feat: remove logging

* [PM-5731] chore: use primary constructor

* [PM-5731] chore: add Async to method names

* [PM-5731] feat: add support for silent discoverability

* [PM-5731] feat: add support for specifying user presence requirement

* [PM-5731] feat: ensure unlocked vault

* [PM-5731] chore: clean up and refactor assertion tests

* [PM-5731] chore: clean up and refactor attestation tests

* [PM-5731] chore: add user presence todo comment

* [PM-5731] feat: scaffold fido2 client

* PM-5731 Fix build updating discoverable flag

* [PM-5731] fix: failing test

* [PM-5731] feat: add sameOriginWithAncestor and user id length checks

* [PM-5731] feat: add incomplete rpId verification

* [PM-5731] chore: document uri helpers

* [PM-5731] feat: implement fido2 client createCredential

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

* fix wrong signature format

(cherry picked from commit a1c9ebf01f)

* [PM-5731] fix: issues after cherry-pick

* Fix incompatible GUID conversions

(cherry picked from commit c801b2fc3a)

* [PM-5731] chore: remove default constructor

* [PM-5731] feat: refactor user interface to increase flexibility

* [PM-5731] feat: implement generic assertion user interface class

* [PM-5731] feat: remove ability to make user presence optional

* [PM-5731] chore: remove logging comments

* [PM-5731] feat: add native reprompt support to the authenticator

* [PM-5731] feat: allow pre and post UV

* [PM-5731] chore: add `Async` to method name. Remove `I` from struct

* [PM-5731] fix: discoverable string repr lowercase

* [PM-5731] chore: don't use C# 12 features

* [PM-5731] fix: replace magic strings and numbers with contants and enums

* [PM-5731] fix: use UTC creation date

* [PM-5731] fix: formatting

* [PM-5731] chore: use properties for public fields

* [PM-5731] chore: remove TODO

* [PM-5731] fix: IsValidRpId

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: mpbw2 <59324545+mpbw2@users.noreply.github.com>
This commit is contained in:
Andreas Coroiu
2024-02-21 16:12:52 +01:00
committed by GitHub
parent d339514d9a
commit 71de3bedf4
45 changed files with 3166 additions and 21 deletions

View File

@@ -0,0 +1,252 @@
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
{
private readonly IStateService _stateService;
private readonly IEnvironmentService _environmentService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IFido2AuthenticatorService _fido2AuthenticatorService;
private readonly IFido2GetAssertionUserInterface _getAssertionUserInterface;
private readonly IFido2MakeCredentialUserInterface _makeCredentialUserInterface;
public Fido2ClientService(
IStateService stateService,
IEnvironmentService environmentService,
ICryptoFunctionService cryptoFunctionService,
IFido2AuthenticatorService fido2AuthenticatorService,
IFido2GetAssertionUserInterface getAssertionUserInterface,
IFido2MakeCredentialUserInterface makeCredentialUserInterface)
{
_stateService = stateService;
_environmentService = environmentService;
_cryptoFunctionService = cryptoFunctionService;
_fido2AuthenticatorService = fido2AuthenticatorService;
_getAssertionUserInterface = getAssertionUserInterface;
_makeCredentialUserInterface = makeCredentialUserInterface;
}
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");
}
if (createCredentialParams.User.Id.Length < 1 || createCredentialParams.User.Id.Length > 64)
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.TypeError,
"The length of user.id is not between 1 and 64 bytes (inclusive)");
}
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 == (int) Fido2AlgorithmIdentifier.ES256 && kp.Type == Constants.DefaultFido2CredentialType)
.ToArray();
}
else
{
// Assign default algorithms
credTypesAndPubKeyAlgs = [
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.ES256, Type = Constants.DefaultFido2CredentialType },
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.RS256, Type = Constants.DefaultFido2CredentialType }
];
}
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, _makeCredentialUserInterface);
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 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, _getAssertionUserInterface);
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,
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
};
}
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,
RequireUserVerification = requireUserVerification,
Hash = cliendDataHash
};
}
}
}