1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-20 10:13:42 +00:00

Fix FIDO2 client bugs (#3056)

* fix: blockedUris null issue

* fix: trailing slash in origin breaking check
This commit is contained in:
Andreas Coroiu
2024-03-06 11:58:48 +01:00
committed by GitHub
parent a10481603d
commit 4c2932f4d0
3 changed files with 48 additions and 21 deletions

View File

@@ -32,11 +32,11 @@ namespace Bit.Core.Services
_makeCredentialUserInterface = makeCredentialUserInterface; _makeCredentialUserInterface = makeCredentialUserInterface;
} }
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams) public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
{ {
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin); var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
if (blockedUris.Contains(domain)) if (blockedUris != null && blockedUris.Contains(domain))
{ {
throw new Fido2ClientException( throw new Fido2ClientException(
Fido2ClientException.ErrorCode.UriBlockedError, Fido2ClientException.ErrorCode.UriBlockedError,
@@ -90,7 +90,7 @@ namespace Bit.Core.Services
{ {
// Filter out all unsupported algorithms // Filter out all unsupported algorithms
credTypesAndPubKeyAlgs = createCredentialParams.PubKeyCredParams credTypesAndPubKeyAlgs = createCredentialParams.PubKeyCredParams
.Where(kp => kp.Alg == (int) Fido2AlgorithmIdentifier.ES256 && kp.Type == Constants.DefaultFido2CredentialType) .Where(kp => kp.Alg == (int)Fido2AlgorithmIdentifier.ES256 && kp.Type == Constants.DefaultFido2CredentialType)
.ToArray(); .ToArray();
} }
else else
@@ -107,7 +107,8 @@ namespace Bit.Core.Services
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found"); throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
} }
var clientDataJSON = JsonSerializer.Serialize(new { var clientDataJSON = JsonSerializer.Serialize(new
{
type = "webauthn.create", type = "webauthn.create",
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge), challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge),
origin = createCredentialParams.Origin, origin = createCredentialParams.Origin,
@@ -118,10 +119,12 @@ namespace Bit.Core.Services
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash); var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
try { try
{
var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, _makeCredentialUserInterface); var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, _makeCredentialUserInterface);
return new Fido2ClientCreateCredentialResult { return new Fido2ClientCreateCredentialResult
{
CredentialId = makeCredentialResult.CredentialId, CredentialId = makeCredentialResult.CredentialId,
AttestationObject = makeCredentialResult.AttestationObject, AttestationObject = makeCredentialResult.AttestationObject,
AuthData = makeCredentialResult.AuthData, AuthData = makeCredentialResult.AuthData,
@@ -130,9 +133,13 @@ namespace Bit.Core.Services
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm, PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
}; };
} catch (InvalidStateError) { }
catch (InvalidStateError)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered"); throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
} catch (Exception) { }
catch (Exception)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred"); throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
} }
} }
@@ -141,7 +148,7 @@ namespace Bit.Core.Services
{ {
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin); var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
if (blockedUris.Contains(domain)) if (blockedUris != null && blockedUris.Contains(domain))
{ {
throw new Fido2ClientException( throw new Fido2ClientException(
Fido2ClientException.ErrorCode.UriBlockedError, Fido2ClientException.ErrorCode.UriBlockedError,
@@ -176,7 +183,8 @@ namespace Bit.Core.Services
"RP ID cannot be used with this origin"); "RP ID cannot be used with this origin");
} }
var clientDataJSON = JsonSerializer.Serialize(new { var clientDataJSON = JsonSerializer.Serialize(new
{
type = "webauthn.get", type = "webauthn.get",
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge), challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge),
origin = assertCredentialParams.Origin, origin = assertCredentialParams.Origin,
@@ -186,10 +194,12 @@ namespace Bit.Core.Services
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash); var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
try { try
{
var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams, _getAssertionUserInterface); var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams, _getAssertionUserInterface);
return new Fido2ClientAssertCredentialResult { return new Fido2ClientAssertCredentialResult
{
AuthenticatorData = getAssertionResult.AuthenticatorData, AuthenticatorData = getAssertionResult.AuthenticatorData,
ClientDataJSON = clientDataJSONBytes, ClientDataJSON = clientDataJSONBytes,
Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id), Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id),
@@ -197,9 +207,13 @@ namespace Bit.Core.Services
Signature = getAssertionResult.Signature, Signature = getAssertionResult.Signature,
UserHandle = getAssertionResult.SelectedCredential.UserHandle UserHandle = getAssertionResult.SelectedCredential.UserHandle
}; };
} catch (InvalidStateError) { }
catch (InvalidStateError)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered"); throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
} catch (Exception) { }
catch (Exception)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred"); throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
} }
@@ -215,12 +229,13 @@ namespace Bit.Core.Services
createCredentialParams.AuthenticatorSelection?.ResidentKey == "preferred" || createCredentialParams.AuthenticatorSelection?.ResidentKey == "preferred" ||
(createCredentialParams.AuthenticatorSelection?.ResidentKey == null && (createCredentialParams.AuthenticatorSelection?.ResidentKey == null &&
createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true); createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true);
var requireUserVerification = createCredentialParams.AuthenticatorSelection?.UserVerification == "required" || var requireUserVerification = createCredentialParams.AuthenticatorSelection?.UserVerification == "required" ||
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" || createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
createCredentialParams.AuthenticatorSelection?.UserVerification == null; createCredentialParams.AuthenticatorSelection?.UserVerification == null;
return new Fido2AuthenticatorMakeCredentialParams { return new Fido2AuthenticatorMakeCredentialParams
{
RequireResidentKey = requireResidentKey, RequireResidentKey = requireResidentKey,
RequireUserVerification = requireUserVerification, RequireUserVerification = requireUserVerification,
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials, ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
@@ -240,7 +255,8 @@ namespace Bit.Core.Services
assertCredentialParams.UserVerification == "preferred" || assertCredentialParams.UserVerification == "preferred" ||
assertCredentialParams.UserVerification == null; assertCredentialParams.UserVerification == null;
return new Fido2AuthenticatorGetAssertionParams { return new Fido2AuthenticatorGetAssertionParams
{
RpId = assertCredentialParams.RpId, RpId = assertCredentialParams.RpId,
Challenge = assertCredentialParams.Challenge, Challenge = assertCredentialParams.Challenge,
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials, AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,

View File

@@ -16,18 +16,18 @@ namespace Bit.Core.Utilities.Fido2
// We only care about the domain part of the origin, not the protocol or port so we remove them here, // We only care about the domain part of the origin, not the protocol or port so we remove them here,
// while still keeping ipv6 intact. // while still keeping ipv6 intact.
// https is enforced in the client, so we don't need to worry about that here // https is enforced in the client, so we don't need to worry about that here
var originWithoutProtocolOrPort = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?", "$2$4"); var originWithoutProtocolPortOrPath = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?$", "$2");
if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolOrPort) != UriHostNameType.Dns) if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolPortOrPath) != UriHostNameType.Dns)
{ {
return false; return false;
} }
if (rpId == originWithoutProtocolOrPort) if (rpId == originWithoutProtocolPortOrPath)
{ {
return true; return true;
} }
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolOrPort, out var parsedOrgin)) if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolPortOrPath, out var parsedOrgin))
{ {
return false; return false;
} }

View File

@@ -37,6 +37,17 @@ namespace Bit.Core.Test.Utilities.Fido2
[InlineData("bitwarden.com", "https://login.bitwarden.com:1337", true)] [InlineData("bitwarden.com", "https://login.bitwarden.com:1337", true)]
[InlineData("login.bitwarden.com", "https://login.bitwarden.com:1337", true)] [InlineData("login.bitwarden.com", "https://login.bitwarden.com:1337", true)]
[InlineData("login.bitwarden.com", "https://sub.login.bitwarden.com:1337", true)] [InlineData("login.bitwarden.com", "https://sub.login.bitwarden.com:1337", true)]
// Origin with trailing slash
[InlineData("sub.login.bitwarden.com", "https://login.bitwarden.com:1337/", false)]
[InlineData("passwordless.dev", "https://login.bitwarden.com:1337/", false)]
[InlineData("login.passwordless.dev", "https://login.bitwarden.com:1337/", false)]
[InlineData("bitwarden", "localhost/", false)]
[InlineData("bitwarden", "bitwarden/", true)]
[InlineData("localhost", "https://localhost:8080/", true)]
[InlineData("bitwarden.com", "https://bitwarden.com/", true)]
[InlineData("bitwarden.com", "https://login.bitwarden.com:1337/", true)]
[InlineData("login.bitwarden.com", "https://login.bitwarden.com:1337/", true)]
[InlineData("login.bitwarden.com", "https://sub.login.bitwarden.com:1337/", true)]
public void ValidateRpId(string rpId, string origin, bool isValid) public void ValidateRpId(string rpId, string origin, bool isValid)
{ {
Assert.Equal(isValid, Fido2DomainUtils.IsValidRpId(rpId, origin)); Assert.Equal(isValid, Fido2DomainUtils.IsValidRpId(rpId, origin));