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; + } } }