1
0
mirror of https://github.com/bitwarden/mobile synced 2026-02-23 16:12:57 +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

@@ -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);