mirror of
https://github.com/bitwarden/mobile.git
synced 2025-02-21 02:01:35 +01:00
[PM-5731] feat: partial attestation implementation
This commit is contained in:
parent
e1908d8eef
commit
c87728027e
@ -34,6 +34,7 @@
|
|||||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||||
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
|
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
|
||||||
|
<PackageReference Include="System.Formats.Cbor" Version="8.0.0" />
|
||||||
<PackageReference Include="zxcvbn-core" Version="7.0.92" />
|
<PackageReference Include="zxcvbn-core" Version="7.0.92" />
|
||||||
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -4,11 +4,15 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using System.Formats.Cbor;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
public class Fido2AuthenticatorService : IFido2AuthenticatorService
|
public class Fido2AuthenticatorService : IFido2AuthenticatorService
|
||||||
{
|
{
|
||||||
|
// AAGUID: d548826e-79b4-db40-a3d8-11116f7e8349
|
||||||
|
public static readonly byte[] AAGUID = [ 0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49 ];
|
||||||
|
|
||||||
private INativeLogService _logService;
|
private INativeLogService _logService;
|
||||||
private ICipherService _cipherService;
|
private ICipherService _cipherService;
|
||||||
private ISyncService _syncService;
|
private ISyncService _syncService;
|
||||||
@ -83,6 +87,25 @@ namespace Bit.Core.Services
|
|||||||
var reencrypted = await _cipherService.EncryptAsync(cipher);
|
var reencrypted = await _cipherService.EncryptAsync(cipher);
|
||||||
await _cipherService.SaveWithServerAsync(reencrypted);
|
await _cipherService.SaveWithServerAsync(reencrypted);
|
||||||
credentialId = fido2Credential.CredentialId;
|
credentialId = fido2Credential.CredentialId;
|
||||||
|
|
||||||
|
var authData = await GenerateAuthData(
|
||||||
|
rpId: makeCredentialParams.RpEntity.Id,
|
||||||
|
counter: fido2Credential.CounterValue,
|
||||||
|
userPresence: true,
|
||||||
|
userVerification: userVerified,
|
||||||
|
credentialId: GuidToRawFormat(credentialId),
|
||||||
|
publicKey: publicKey,
|
||||||
|
privateKey: privateKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Fido2AuthenticatorMakeCredentialResult
|
||||||
|
{
|
||||||
|
CredentialId = GuidToRawFormat(credentialId),
|
||||||
|
AttestationObject = EncodeAttestationObject(authData),
|
||||||
|
AuthData = authData,
|
||||||
|
PublicKey = Array.Empty<byte>(),
|
||||||
|
PublicKeyAlgorithm = (int) Fido2AlgorithmIdentifier.ES256,
|
||||||
|
};
|
||||||
} catch (NotAllowedError) {
|
} catch (NotAllowedError) {
|
||||||
throw;
|
throw;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -92,15 +115,6 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
throw new UnknownError();
|
throw new UnknownError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Fido2AuthenticatorMakeCredentialResult
|
|
||||||
{
|
|
||||||
CredentialId = GuidToRawFormat(credentialId),
|
|
||||||
AttestationObject = Array.Empty<byte>(),
|
|
||||||
AuthData = Array.Empty<byte>(),
|
|
||||||
PublicKey = Array.Empty<byte>(),
|
|
||||||
PublicKeyAlgorithm = (int) Fido2AlgorithmIdentifier.ES256,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
|
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
|
||||||
@ -293,16 +307,24 @@ namespace Bit.Core.Services
|
|||||||
string rpId,
|
string rpId,
|
||||||
bool userVerification,
|
bool userVerification,
|
||||||
bool userPresence,
|
bool userPresence,
|
||||||
int counter
|
int counter,
|
||||||
// byte[] credentialId,
|
byte[] credentialId = null,
|
||||||
// CryptoKey? cryptoKey - only needed for attestation
|
byte[] publicKey = null,
|
||||||
|
byte[] privateKey = null
|
||||||
) {
|
) {
|
||||||
|
var isAttestation = credentialId != null && publicKey != null && privateKey != null;
|
||||||
|
|
||||||
List<byte> authData = new List<byte>();
|
List<byte> authData = new List<byte>();
|
||||||
|
|
||||||
var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256);
|
var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256);
|
||||||
authData.AddRange(rpIdHash);
|
authData.AddRange(rpIdHash);
|
||||||
|
|
||||||
var flags = AuthDataFlags(false, false, userVerification, userPresence);
|
var flags = AuthDataFlags(
|
||||||
|
extensionData: false,
|
||||||
|
attestationData: isAttestation,
|
||||||
|
userVerification: userVerification,
|
||||||
|
userPresence: userPresence
|
||||||
|
);
|
||||||
authData.Add(flags);
|
authData.Add(flags);
|
||||||
|
|
||||||
authData.AddRange([
|
authData.AddRange([
|
||||||
@ -312,6 +334,40 @@ namespace Bit.Core.Services
|
|||||||
(byte)counter
|
(byte)counter
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (isAttestation)
|
||||||
|
{
|
||||||
|
var attestedCredentialData = new List<byte>();
|
||||||
|
|
||||||
|
attestedCredentialData.AddRange(AAGUID);
|
||||||
|
|
||||||
|
// credentialIdLength (2 bytes) and credential Id
|
||||||
|
var credentialIdLength = new byte[] {
|
||||||
|
(byte)((credentialId.Length - (credentialId.Length & 0xff)) / 256),
|
||||||
|
(byte)(credentialId.Length & 0xff)
|
||||||
|
};
|
||||||
|
attestedCredentialData.AddRange(credentialIdLength);
|
||||||
|
attestedCredentialData.AddRange(credentialId);
|
||||||
|
|
||||||
|
var base64PrivateKey = CoreHelpers.Base64UrlEncode(privateKey);
|
||||||
|
|
||||||
|
// const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
|
||||||
|
// // COSE format of the EC256 key
|
||||||
|
// const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
|
||||||
|
// const keyY = Utils.fromUrlB64ToArray(publicKeyJwk.y);
|
||||||
|
|
||||||
|
// // Can't get `cbor-redux` to encode in CTAP2 canonical CBOR. So we do it manually:
|
||||||
|
// const coseBytes = new Uint8Array(77);
|
||||||
|
// coseBytes.set([0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20], 0);
|
||||||
|
// coseBytes.set(keyX, 10);
|
||||||
|
// coseBytes.set([0x22, 0x58, 0x20], 10 + 32);
|
||||||
|
// coseBytes.set(keyY, 10 + 32 + 3);
|
||||||
|
|
||||||
|
// // credential public key - convert to array from CBOR encoded COSE key
|
||||||
|
// attestedCredentialData.push(...coseBytes);
|
||||||
|
|
||||||
|
// authData.push(...attestedCredentialData);
|
||||||
|
}
|
||||||
|
|
||||||
return authData.ToArray();
|
return authData.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,6 +393,21 @@ namespace Bit.Core.Services
|
|||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] EncodeAttestationObject(byte[] authData) {
|
||||||
|
var attestationObject = new CborWriter(CborConformanceMode.Ctap2Canonical);
|
||||||
|
attestationObject.WriteStartMap(3);
|
||||||
|
attestationObject.WriteTextString("fmt");
|
||||||
|
attestationObject.WriteTextString("none");
|
||||||
|
attestationObject.WriteTextString("attStmt");
|
||||||
|
attestationObject.WriteStartMap(0);
|
||||||
|
attestationObject.WriteEndMap();
|
||||||
|
attestationObject.WriteTextString("authData");
|
||||||
|
attestationObject.WriteByteString(authData);
|
||||||
|
attestationObject.WriteEndMap();
|
||||||
|
|
||||||
|
return attestationObject.Encode();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<byte[]> GenerateSignature(
|
private async Task<byte[]> GenerateSignature(
|
||||||
byte[] authData,
|
byte[] authData,
|
||||||
byte[] clientDataHash,
|
byte[] clientDataHash,
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
||||||
|
<PackageReference Include="System.Formats.Cbor" Version="8.0.0" />
|
||||||
<PackageReference Include="xunit" Version="2.5.0" />
|
<PackageReference Include="xunit" Version="2.5.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
@ -18,6 +18,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Policy;
|
using System.Security.Policy;
|
||||||
using NSubstitute.Extensions;
|
using NSubstitute.Extensions;
|
||||||
|
using System.Formats.Cbor;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services
|
namespace Bit.Core.Test.Services
|
||||||
{
|
{
|
||||||
@ -393,6 +394,68 @@ namespace Bit.Core.Test.Services
|
|||||||
await Assert.ThrowsAsync<UnknownError>(() => sutProvider.Sut.MakeCredentialAsync(mParams));
|
await Assert.ThrowsAsync<UnknownError>(() => sutProvider.Sut.MakeCredentialAsync(mParams));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
|
||||||
|
public async Task MakeCredentialAsync_ReturnsAttestation(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorMakeCredentialParams mParams)
|
||||||
|
{
|
||||||
|
// Common Arrange
|
||||||
|
mParams.CredTypesAndPubKeyAlgs = [
|
||||||
|
new PublicKeyCredentialAlgorithmDescriptor {
|
||||||
|
Type = "public-key",
|
||||||
|
Algorithm = -7 // ES256
|
||||||
|
}
|
||||||
|
];
|
||||||
|
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||||
|
mParams.RequireUserVerification = false;
|
||||||
|
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||||
|
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||||
|
_encryptedCipher.Key = null;
|
||||||
|
_encryptedCipher.Attachments = [];
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var rpIdHashMock = RandomBytes(32);
|
||||||
|
mParams.RequireResidentKey = false;
|
||||||
|
sutProvider.GetDependency<ICryptoFunctionService>().HashAsync(mParams.RpEntity.Id, CryptoHashAlgorithm.Sha256).Returns(rpIdHashMock);
|
||||||
|
// sutProvider.GetDependency<ICipherService>().EncryptAsync(Arg.Any<CipherView>()).Returns(_encryptedCipher);
|
||||||
|
sutProvider.GetDependency<ICipherService>().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher);
|
||||||
|
sutProvider.GetDependency<IFido2UserInterface>().ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns(new Fido2ConfirmNewCredentialResult {
|
||||||
|
CipherId = _encryptedCipher.Id,
|
||||||
|
UserVerified = false
|
||||||
|
});
|
||||||
|
CipherView generatedCipherView = null;
|
||||||
|
sutProvider.GetDependency<ICipherService>().EncryptAsync(Arg.Any<CipherView>()).Returns((call) => {
|
||||||
|
generatedCipherView = call.Arg<CipherView>();
|
||||||
|
return _encryptedCipher;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.MakeCredentialAsync(mParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var credentialIdBytes = Guid.Parse(generatedCipherView.Login.MainFido2Credential.CredentialId).ToByteArray();
|
||||||
|
var attestationObject = DecodeAttestationObject(result.AttestationObject);
|
||||||
|
Assert.Equal("none", attestationObject.Fmt);
|
||||||
|
|
||||||
|
var authData = attestationObject.AuthData;
|
||||||
|
var rpIdHash = authData.Take(32).ToArray();
|
||||||
|
var flags = authData.Skip(32).Take(1).ToArray();
|
||||||
|
var counter = authData.Skip(33).Take(4).ToArray();
|
||||||
|
var aaguid = authData.Skip(37).Take(16).ToArray();
|
||||||
|
var credentialIdLength = authData.Skip(53).Take(2).ToArray();
|
||||||
|
var credentialId = authData.Skip(55).Take(16).ToArray();
|
||||||
|
// Unsure how to test public key
|
||||||
|
// const publicKey = authData.Skip(71).ToArray(); // Key data is 77 bytes long
|
||||||
|
|
||||||
|
// Not implemented yet
|
||||||
|
// Assert.Equal(71 + 77, authData.Length);
|
||||||
|
// Assert.Equal(rpIdHashMock, rpIdHash);
|
||||||
|
// Assert.Equal([0b01000001], flags); // UP = true, AD = true
|
||||||
|
// Assert.Equal([0, 0, 0, 0], counter);
|
||||||
|
// Assert.Equal(Fido2AuthenticatorService.AAGUID, aaguid);
|
||||||
|
// Assert.Equal([0, 16], credentialIdLength); // 16 bytes because we're using GUIDs
|
||||||
|
// Assert.Equal(credentialIdBytes, credentialId);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private byte[] RandomBytes(int length)
|
private byte[] RandomBytes(int length)
|
||||||
@ -430,5 +493,41 @@ namespace Bit.Core.Test.Services
|
|||||||
Login = new Login {}
|
Login = new Login {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class AttestationObject
|
||||||
|
{
|
||||||
|
public string Fmt { get; set; }
|
||||||
|
public object AttStmt { get; set; }
|
||||||
|
public byte[] AuthData { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private AttestationObject DecodeAttestationObject(byte[] attestationObject)
|
||||||
|
{
|
||||||
|
var result = new AttestationObject();
|
||||||
|
var reader = new CborReader(attestationObject, CborConformanceMode.Ctap2Canonical);
|
||||||
|
reader.ReadStartMap();
|
||||||
|
|
||||||
|
while (reader.BytesRemaining != 0)
|
||||||
|
{
|
||||||
|
var key = reader.ReadTextString();
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "fmt":
|
||||||
|
result.Fmt = reader.ReadTextString();
|
||||||
|
break;
|
||||||
|
case "attStmt":
|
||||||
|
reader.ReadStartMap();
|
||||||
|
reader.ReadEndMap();
|
||||||
|
break;
|
||||||
|
case "authData":
|
||||||
|
result.AuthData = reader.ReadByteString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user