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:
parent
e1908d8eef
commit
c87728027e
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user