1
0
mirror of https://github.com/bitwarden/mobile.git synced 2025-02-17 01:21:25 +01:00

[PM-5731] feat: partial attestation implementation

This commit is contained in:
Andreas Coroiu 2024-01-26 14:57:44 +01:00
parent e1908d8eef
commit c87728027e
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
4 changed files with 186 additions and 14 deletions

View File

@ -34,6 +34,7 @@
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" />
<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="MessagePack.MSBuild.Tasks" Version="2.5.124">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -4,11 +4,15 @@ using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities.Fido2;
using Bit.Core.Utilities;
using System.Formats.Cbor;
namespace Bit.Core.Services
{
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 ICipherService _cipherService;
private ISyncService _syncService;
@ -83,6 +87,25 @@ namespace Bit.Core.Services
var reencrypted = await _cipherService.EncryptAsync(cipher);
await _cipherService.SaveWithServerAsync(reencrypted);
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) {
throw;
} catch (Exception e) {
@ -92,15 +115,6 @@ namespace Bit.Core.Services
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)
@ -293,16 +307,24 @@ namespace Bit.Core.Services
string rpId,
bool userVerification,
bool userPresence,
int counter
// byte[] credentialId,
// CryptoKey? cryptoKey - only needed for attestation
int counter,
byte[] credentialId = null,
byte[] publicKey = null,
byte[] privateKey = null
) {
var isAttestation = credentialId != null && publicKey != null && privateKey != null;
List<byte> authData = new List<byte>();
var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256);
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.AddRange([
@ -312,6 +334,40 @@ namespace Bit.Core.Services
(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();
}
@ -337,6 +393,21 @@ namespace Bit.Core.Services
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(
byte[] authData,
byte[] clientDataHash,

View File

@ -13,6 +13,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.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.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>

View File

@ -18,6 +18,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Security.Policy;
using NSubstitute.Extensions;
using System.Formats.Cbor;
namespace Bit.Core.Test.Services
{
@ -392,7 +393,69 @@ namespace Bit.Core.Test.Services
// Act & Assert
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
private byte[] RandomBytes(int length)
@ -430,5 +493,41 @@ namespace Bit.Core.Test.Services
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;
}
}
}