1
0
mirror of https://github.com/bitwarden/mobile synced 2026-02-13 14:53:19 +00:00

[PM-7576] Implemented digital asset links verification on Fido2 flows (#3191)

* PM-7553 Fix native apps passkeys autofill and creation

* PM-7658 Implemented Fido2 priviliged apps verification

* PM-7576 Implemented digital asset links verification on Fido2 flows for native apps.

* PM-7576 Renamed to ValidateAssetLinksAndGetOriginAsync to go along with Google naming and also changed method to private given that public is not necessary

* PM-7576 Moved digital asset links verification to a Core service AssetLinksService and added unit tests for it.
This commit is contained in:
Federico Maccaroni
2024-04-25 15:00:01 -03:00
committed by GitHub
parent ea098c92d3
commit 299899f952
13 changed files with 598 additions and 49 deletions

View File

@@ -10,6 +10,7 @@ using Bit.App.Abstractions;
using Bit.App.Droid.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Bit.Core.Utilities.Fido2.Extensions;
@@ -252,7 +253,7 @@ namespace Bit.App.Platforms.Android.Autofill
return reader.ReadToEnd();
}
catch (Exception ex)
catch
{
return null;
}
@@ -262,7 +263,7 @@ namespace Bit.App.Platforms.Android.Autofill
{
if (callingAppInfo.Origin is null)
{
return await ValidateNativeAppAndGetOriginAsync(callingAppInfo, rpId);
return await ValidateAssetLinksAndGetOriginAsync(callingAppInfo, rpId);
}
var priviligedAllowedList = await LoadFido2PriviligedAllowedListAsync();
@@ -285,10 +286,18 @@ namespace Bit.App.Platforms.Android.Autofill
}
}
private static async Task<string> ValidateNativeAppAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
private static async Task<string> ValidateAssetLinksAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
{
// TODO: do asset links verification
return callingAppInfo.GetAndroidOrigin();
if (!ServiceContainer.TryResolve<IAssetLinksService>(out var assetLinksService))
{
throw new InvalidOperationException("Can't resolve IAssetLinksService");
}
var normalizedFingerprint = callingAppInfo.GetLatestCertificationFingerprint();
var isValid = await assetLinksService.ValidateAssetLinksAsync(rpId, callingAppInfo.PackageName, normalizedFingerprint);
return isValid ? callingAppInfo.GetAndroidOrigin() : null;
}
}
}

View File

@@ -19,5 +19,19 @@ namespace Bit.App.Droid.Utilities
var certHash = md.Digest(cert);
return $"android:apk-key-hash:{CoreHelpers.Base64UrlEncode(certHash)}";
}
public static string GetLatestCertificationFingerprint(this CallingAppInfo callingAppInfo)
{
if (callingAppInfo.SigningInfo.HasMultipleSigners)
{
return null;
}
var signature = callingAppInfo.SigningInfo.GetSigningCertificateHistory()[0].ToByteArray();
var md = MessageDigest.GetInstance("SHA-256");
var digestedSignature = md.Digest(signature);
var normalizedFingerprint = string.Join(":", digestedSignature.Select(b => b.ToString("X2")));
return normalizedFingerprint;
}
}
}

View File

@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
@@ -100,5 +95,6 @@ namespace Bit.Core.Abstractions
Task<bool> GetDevicesExistenceByTypes(DeviceType[] deviceTypes);
Task<ConfigResponse> GetConfigsAsync();
Task<string> GetFastmailAccountIdAsync(string apiKey);
Task<List<Utilities.DigitalAssetLinks.Statement>> GetDigitalAssetLinksForRpAsync(string rpId);
}
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.Services
{
public interface IAssetLinksService
{
Task<bool> ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint);
}
}

View File

@@ -839,6 +839,33 @@ namespace Bit.Core.Services
}
}
public async Task<List<Utilities.DigitalAssetLinks.Statement>> GetDigitalAssetLinksForRpAsync(string rpId)
{
using (var httpclient = new HttpClient())
{
HttpResponseMessage response;
try
{
httpclient.DefaultRequestHeaders.Add("Accept", "application/json");
response = await httpclient.GetAsync(new Uri($"https://{rpId}/.well-known/assetlinks.json"));
}
catch (Exception e)
{
throw new ApiException(HandleWebError(e));
}
if (!response.IsSuccessStatusCode)
{
throw new ApiException(new ErrorResponse
{
StatusCode = response.StatusCode,
Message = $"Digital Asset links Rp error: {(int)response.StatusCode} {response.ReasonPhrase}."
});
}
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<Utilities.DigitalAssetLinks.Statement>>(json);
}
}
private ErrorResponse HandleWebError(Exception e)
{
return new ErrorResponse

View File

@@ -0,0 +1,35 @@
using Bit.Core.Abstractions;
namespace Bit.Core.Services
{
public class AssetLinksService : IAssetLinksService
{
private readonly IApiService _apiService;
public AssetLinksService(IApiService apiService)
{
_apiService = apiService;
}
/// <summary>
/// Gets the digital asset links file associated with the <paramref name="rpId"/> and
/// validates that the <paramref name="packageName"/> and <paramref name="normalizedFingerprint"/> matches.
/// </summary>
/// <returns><c>True</c> if matches, <c>False</c> otherwise.</returns>
public async Task<bool> ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint)
{
var statementList = await _apiService.GetDigitalAssetLinksForRpAsync(rpId);
return statementList
.Any(s => s.Target.Namespace == "android_app"
&&
s.Target.PackageName == packageName
&&
s.Relation.Contains("delegate_permission/common.get_login_creds")
&&
s.Relation.Contains("delegate_permission/common.handle_all_urls")
&&
s.Target.Sha256CertFingerprints.Contains(normalizedFingerprint));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Bit.Core.Utilities.DigitalAssetLinks
{
public class Statement
{
public IEnumerable<string> Relation { get; set; }
public Target Target { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Bit.Core.Utilities.DigitalAssetLinks
{
public class Target
{
public string Namespace { get; set; }
[JsonProperty("package_name")]
public string PackageName { get; set; }
[JsonProperty("sha256_cert_fingerprints")]
public IEnumerable<string> Sha256CertFingerprints { get; set; }
}
}

View File

@@ -116,6 +116,9 @@ namespace Bit.Core.Utilities
Register<IUsernameGenerationService>(usernameGenerationService);
Register<IDeviceTrustCryptoService>(deviceTrustCryptoService);
Register<IPasswordResetEnrollmentService>(passwordResetEnrollmentService);
#if ANDROID
Register<IAssetLinksService>(new AssetLinksService(apiService));
#endif
}
public static void Register<T>(string serviceName, T obj)

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities.DigitalAssetLinks;
using Bit.Test.Common.AutoFixture;
using Newtonsoft.Json;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services
{
public class AssetLinksServiceTest : IDisposable
{
private readonly SutProvider<AssetLinksService> _sutProvider = new SutProvider<AssetLinksService>().Create();
private readonly string _validRpId = "example.com";
private readonly string _validPackageName = "com.example.app";
private readonly string _validFingerprint = "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00";
private List<Statement> Deserialize(string json)
{
return JsonConvert.DeserializeObject<List<Statement>>(json);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_True_When_Data_Has_One_Statement_And_One_Fingerprint()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
// Assert
Assert.True(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_True_When_Data_Has_One_Statement_And_Multiple_Fingerprints()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementMultipleFingerprintsJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
// Assert
Assert.True(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_True_When_Data_Has_Multiple_Statements()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.MultipleStatementsJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
// Assert
Assert.True(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_No_GetLoginCreds_Relation()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoGetLoginCredsRelationJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
// Assert
Assert.False(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_No_HandleAllUrls_Relation()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoHandleAllUrlsRelationJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
// Assert
Assert.False(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_Wrong_Namespace()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementWrongNamespaceJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
// Assert
Assert.False(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_No_Fingerprints()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoFingerprintsJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
// Assert
Assert.False(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_PackageName_Doesnt_Match()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, "com.foo.another", _validFingerprint);
// Assert
Assert.False(isValid);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Fingerprint_Doesnt_Match()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint.Replace("00", "33"));
// Assert
Assert.False(isValid);
}
public void Dispose() {}
}
}

View File

@@ -0,0 +1,179 @@
public static class BasicAssetLinksTestData
{
#region Valid statements
public static string OneStatementOneFingerprintJson()
{
return
"""
[
{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
]
}
}
]
""";
}
public static string OneStatementMultipleFingerprintsJson()
{
return
"""
[
{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00",
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01",
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:02"
]
}
}
]
""";
}
public static string MultipleStatementsJson()
{
return
"""
[
{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00",
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01",
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:02"
]
}
},
{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.foo.app",
"sha256_cert_fingerprints": [
"10:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00",
"10:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01",
"10:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:02"
]
}
}
]
""";
}
#endregion
#region Invalid statements
public static string OneStatementNoGetLoginCredsRelationJson()
{
return
"""
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
]
}
}
]
""";
}
public static string OneStatementNoHandleAllUrlsRelationJson()
{
return
"""
[
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
]
}
}
]
""";
}
public static string OneStatementWrongNamespaceJson()
{
return
"""
[
{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "NOT_android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
]
}
}
]
""";
}
public static string OneStatementNoFingerprintsJson()
{
return
"""
[
{
"relation": [
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": []
}
}
]
""";
}
#endregion
}

View File

@@ -45,7 +45,7 @@ namespace Bit.Core.Test.Services
_authenticatorResult = new Fido2AuthenticatorGetAssertionResult
{
AuthenticatorData = RandomBytes(32),
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential
SelectedCredential = new Fido2SelectedCredential
{
Id = RandomBytes(16),
UserHandle = RandomBytes(32)
@@ -74,7 +74,7 @@ namespace Bit.Core.Test.Services
_params.Origin = "invalid-domain-name";
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
@@ -90,7 +90,7 @@ namespace Bit.Core.Test.Services
_params.RpId = "bitwarden.com";
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
@@ -105,7 +105,7 @@ namespace Bit.Core.Test.Services
_params.RpId = "bitwarden.com";
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
@@ -124,7 +124,7 @@ namespace Bit.Core.Test.Services
}));
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.UriBlockedError, exception.Code);
@@ -139,7 +139,7 @@ namespace Bit.Core.Test.Services
.Throws(new InvalidStateError());
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
@@ -155,7 +155,7 @@ namespace Bit.Core.Test.Services
.Throws(new Exception("unknown error"));
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.UnknownError, exception.Code);
@@ -168,7 +168,7 @@ namespace Bit.Core.Test.Services
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(false);
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
@@ -182,7 +182,7 @@ namespace Bit.Core.Test.Services
_sutProvider.GetDependency<IEnvironmentService>().GetWebVaultUrl().Returns("https://vault.bitwarden.com");
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
@@ -201,7 +201,7 @@ namespace Bit.Core.Test.Services
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.AssertCredentialAsync(_params);
await _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams());
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
@@ -216,12 +216,13 @@ namespace Bit.Core.Test.Services
{
// Arrange
var mockHash = RandomBytes(32);
var extraParams = new Fido2ExtraAssertCredentialParams(mockHash, null);
_sutProvider.GetDependency<IFido2AuthenticatorService>()
.GetAssertionAsync(Arg.Any<Fido2AuthenticatorGetAssertionParams>(), _sutProvider.GetDependency<IFido2GetAssertionUserInterface>())
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.AssertCredentialAsync(_params, mockHash);
await _sutProvider.Sut.AssertCredentialAsync(_params, extraParams);
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
@@ -241,7 +242,7 @@ namespace Bit.Core.Test.Services
.Returns(_authenticatorResult);
// Act
var result = await _sutProvider.Sut.AssertCredentialAsync(_params);
var result = await _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams());
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>()
@@ -268,6 +269,45 @@ namespace Bit.Core.Test.Services
Assert.Equal(!_params.SameOriginWithAncestors, clientDataJSON["crossOrigin"].GetValue<bool>());
}
[Fact]
public async Task AssertCredentialAsync_ReturnsAssertionWithAndroidPackage()
{
// Arrange
var packageName = "com.example.app";
_params.UserVerification = "required";
_sutProvider.GetDependency<IFido2AuthenticatorService>()
.GetAssertionAsync(Arg.Any<Fido2AuthenticatorGetAssertionParams>(), _sutProvider.GetDependency<IFido2GetAssertionUserInterface>())
.Returns(_authenticatorResult);
// Act
var result = await _sutProvider.Sut.AssertCredentialAsync(_params, new Fido2ExtraAssertCredentialParams(null, packageName));
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>()
.Received()
.GetAssertionAsync(
Arg.Is<Fido2AuthenticatorGetAssertionParams>(x =>
x.RpId == _params.RpId &&
x.UserVerificationPreference == Fido2UserVerificationPreference.Required &&
x.AllowCredentialDescriptorList.Length == 1 &&
x.AllowCredentialDescriptorList[0].Id == _params.AllowCredentials[0].Id
),
_sutProvider.GetDependency<IFido2GetAssertionUserInterface>()
);
Assert.Equal(_authenticatorResult.SelectedCredential.Id, result.RawId);
Assert.Equal(CoreHelpers.Base64UrlEncode(_authenticatorResult.SelectedCredential.Id), result.Id);
Assert.Equal(_authenticatorResult.AuthenticatorData, result.AuthenticatorData);
Assert.Equal(_authenticatorResult.Signature, result.Signature);
var clientDataJSON = JsonSerializer.Deserialize<JsonObject>(Encoding.UTF8.GetString(result.ClientDataJSON));
Assert.Equal("webauthn.get", 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>());
Assert.Equal(packageName, clientDataJSON["androidPackageName"].GetValue<string>());
}
private byte[] RandomBytes(int length)
{
var bytes = new byte[length];

View File

@@ -78,7 +78,7 @@ namespace Bit.Core.Test.Services
_params.SameOriginWithAncestors = false;
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
@@ -92,7 +92,7 @@ namespace Bit.Core.Test.Services
_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, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
@@ -106,7 +106,7 @@ namespace Bit.Core.Test.Services
_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, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
@@ -125,7 +125,7 @@ namespace Bit.Core.Test.Services
_params.Origin = "invalid-domain-name";
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
@@ -141,7 +141,7 @@ namespace Bit.Core.Test.Services
_params.Rp.Id = "bitwarden.com";
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
@@ -156,7 +156,7 @@ namespace Bit.Core.Test.Services
_params.Rp.Id = "bitwarden.com";
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
@@ -174,7 +174,7 @@ namespace Bit.Core.Test.Services
}));
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.UriBlockedError, exception.Code);
@@ -198,7 +198,7 @@ namespace Bit.Core.Test.Services
};
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.NotSupportedError, exception.Code);
@@ -230,7 +230,7 @@ namespace Bit.Core.Test.Services
.Returns(authenticatorResult);
// Act
var result = await _sutProvider.Sut.CreateCredentialAsync(_params);
var result = await _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams());
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>()
@@ -258,6 +258,58 @@ namespace Bit.Core.Test.Services
Assert.Equal(!_params.SameOriginWithAncestors, clientDataJSON["crossOrigin"].GetValue<bool>());
}
[Fact]
public async Task CreateCredentialAsync_ReturnsNewCredential_WithAndroidPackageName()
{
// 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 = (int)Fido2AlgorithmIdentifier.ES256,
};
_sutProvider.GetDependency<IFido2AuthenticatorService>()
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>(), _sutProvider.GetDependency<IFido2MakeCredentialUserInterface>())
.Returns(authenticatorResult);
var packageName = "com.example.app";
// Act
var result = await _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams(null, packageName));
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>()
.Received()
.MakeCredentialAsync(
Arg.Is<Fido2AuthenticatorMakeCredentialParams>(x =>
x.RequireResidentKey == true &&
x.UserVerificationPreference == Fido2UserVerificationPreference.Required &&
x.RpEntity.Id == _params.Rp.Id &&
x.UserEntity.DisplayName == _params.User.DisplayName
),
_sutProvider.GetDependency<IFido2MakeCredentialUserInterface>()
);
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(new string[] { "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>());
Assert.Equal(packageName, clientDataJSON["androidPackageName"].GetValue<string>());
}
[Fact]
public async Task CreateCredentialAsync_ThrowsInvalidStateError_AuthenticatorThrowsInvalidStateError()
{
@@ -272,7 +324,7 @@ namespace Bit.Core.Test.Services
.Throws(new InvalidStateError());
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
@@ -288,7 +340,7 @@ namespace Bit.Core.Test.Services
.Throws(new Exception("unknown error"));
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.UnknownError, exception.Code);
@@ -301,7 +353,7 @@ namespace Bit.Core.Test.Services
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(false);
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
@@ -315,14 +367,14 @@ namespace Bit.Core.Test.Services
_sutProvider.GetDependency<IEnvironmentService>().GetWebVaultUrl().Returns("https://vault.bitwarden.com");
// Act
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams()));
// Assert
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
}
[Fact]
public async Task AssertCredentialAsync_ConstructsClientDataHash_WhenHashIsNotProvided()
public async Task CreateCredentialAsync_ConstructsClientDataHash_WhenHashIsNotProvided()
{
// Arrange
var mockHash = RandomBytes(32);
@@ -334,18 +386,18 @@ namespace Bit.Core.Test.Services
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.CreateCredentialAsync(_params);
await _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams());
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
.GetAssertionAsync(
Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash),
Arg.Any<IFido2GetAssertionUserInterface>()
.MakeCredentialAsync(
Arg.Is((Fido2AuthenticatorMakeCredentialParams x) => x.Hash == mockHash),
Arg.Any<IFido2MakeCredentialUserInterface>()
);
}
[Fact]
public async Task AssertCredentialAsync_UsesProvidedClientDataHash_WhenHashIsProvided()
public async Task CreateCredentialAsync_UsesProvidedClientDataHash_WhenHashIsProvided()
{
// Arrange
var mockHash = RandomBytes(32);
@@ -354,13 +406,13 @@ namespace Bit.Core.Test.Services
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.CreateCredentialAsync(_params, mockHash);
await _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams(mockHash));
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
.GetAssertionAsync(
Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash),
Arg.Any<IFido2GetAssertionUserInterface>()
.MakeCredentialAsync(
Arg.Is((Fido2AuthenticatorMakeCredentialParams x) => x.Hash == mockHash),
Arg.Any<IFido2MakeCredentialUserInterface>()
);
}
@@ -378,7 +430,7 @@ namespace Bit.Core.Test.Services
.Returns(_authenticatorResult);
// Act
var result = await _sutProvider.Sut.CreateCredentialAsync(_params);
var result = await _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams());
// Assert
Assert.True(result.Extensions.CredProps?.Rk);
@@ -398,7 +450,7 @@ namespace Bit.Core.Test.Services
.Returns(_authenticatorResult);
// Act
var result = await _sutProvider.Sut.CreateCredentialAsync(_params);
var result = await _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams());
// Assert
Assert.False(result.Extensions.CredProps?.Rk);
@@ -418,7 +470,7 @@ namespace Bit.Core.Test.Services
.Returns(_authenticatorResult);
// Act
var result = await _sutProvider.Sut.CreateCredentialAsync(_params);
var result = await _sutProvider.Sut.CreateCredentialAsync(_params, new Fido2ExtraCreateCredentialParams());
// Assert
Assert.Null(result.Extensions.CredProps);