diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 7cdc0b377..baa410d8b 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -34,6 +34,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs
index 15a5a23cd..3f9fd1942 100644
--- a/src/Core/Services/Fido2AuthenticatorService.cs
+++ b/src/Core/Services/Fido2AuthenticatorService.cs
@@ -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(),
+ 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(),
- AuthData = Array.Empty(),
- PublicKey = Array.Empty(),
- PublicKeyAlgorithm = (int) Fido2AlgorithmIdentifier.ES256,
- };
}
public async Task 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 authData = new List();
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();
+
+ 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 GenerateSignature(
byte[] authData,
byte[] clientDataHash,
diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj
index 6062ea90c..3e99ced0e 100644
--- a/test/Core.Test/Core.Test.csproj
+++ b/test/Core.Test/Core.Test.csproj
@@ -13,6 +13,7 @@
+
all
diff --git a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs
index 8d8a20f32..226bb14cb 100644
--- a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs
+++ b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs
@@ -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(() => sutProvider.Sut.MakeCredentialAsync(mParams));
}
-
+
+ [Theory]
+ [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
+ public async Task MakeCredentialAsync_ReturnsAttestation(SutProvider 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().EcdsaGenerateKeyPairAsync(Arg.Any())
+ .Returns((RandomBytes(32), RandomBytes(32)));
+ _encryptedCipher.Key = null;
+ _encryptedCipher.Attachments = [];
+
+ // Arrange
+ var rpIdHashMock = RandomBytes(32);
+ mParams.RequireResidentKey = false;
+ sutProvider.GetDependency().HashAsync(mParams.RpEntity.Id, CryptoHashAlgorithm.Sha256).Returns(rpIdHashMock);
+ // sutProvider.GetDependency().EncryptAsync(Arg.Any()).Returns(_encryptedCipher);
+ sutProvider.GetDependency().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher);
+ sutProvider.GetDependency().ConfirmNewCredentialAsync(Arg.Any()).Returns(new Fido2ConfirmNewCredentialResult {
+ CipherId = _encryptedCipher.Id,
+ UserVerified = false
+ });
+ CipherView generatedCipherView = null;
+ sutProvider.GetDependency().EncryptAsync(Arg.Any()).Returns((call) => {
+ generatedCipherView = call.Arg();
+ 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;
+ }
}
}