diff --git a/src/Core/Abstractions/ICryptoFunctionService.cs b/src/Core/Abstractions/ICryptoFunctionService.cs index 062c0c3bf..90fb33ad4 100644 --- a/src/Core/Abstractions/ICryptoFunctionService.cs +++ b/src/Core/Abstractions/ICryptoFunctionService.cs @@ -32,7 +32,6 @@ namespace Bit.Core.Abstractions Task RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm); Task RsaExtractPublicKeyAsync(byte[] privateKey); Task> RsaGenerateKeyPairAsync(int length); - Task<(byte[] PublicKey, byte[] PrivateKey)> EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm algorithm); Task RandomBytesAsync(int length); byte[] RandomBytes(int length); Task RandomNumberAsync(); diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 3f9fd1942..b8c675d06 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -70,8 +70,8 @@ namespace Bit.Core.Services } try { - var (publicKey, privateKey) = await _cryptoFunctionService.EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm.P256Sha256); - var fido2Credential = CreateCredentialView(makeCredentialParams, privateKey); + var keyPair = GenerateKeyPair(); + var fido2Credential = CreateCredentialView(makeCredentialParams, keyPair.privateKey); var encrypted = await _cipherService.GetAsync(cipherId); var cipher = await encrypted.DecryptAsync(); @@ -94,8 +94,7 @@ namespace Bit.Core.Services userPresence: true, userVerification: userVerified, credentialId: GuidToRawFormat(credentialId), - publicKey: publicKey, - privateKey: privateKey + publicKey: keyPair.publicKey ); return new Fido2AuthenticatorMakeCredentialResult @@ -284,6 +283,24 @@ namespace Bit.Core.Services ); } + // TODO: Move this to a separate service + private (PublicKey publicKey, byte[] privateKey) GenerateKeyPair() + { + using (System.Security.Cryptography.ECDsa dsa = System.Security.Cryptography.ECDsa.Create()) + { + dsa.GenerateKey(System.Security.Cryptography.ECCurve.NamedCurves.nistP256); + var privateKey = dsa.ExportPkcs8PrivateKey(); + + System.Security.Cryptography.ECParameters parameters = dsa.ExportParameters(true); + + return ( + new PublicKey { + X = parameters.Q.X, + Y = parameters.Q.Y + }, privateKey); + } + } + private Fido2CredentialView CreateCredentialView(Fido2AuthenticatorMakeCredentialParams makeCredentialsParams, byte[] privateKey) { return new Fido2CredentialView { @@ -309,10 +326,9 @@ namespace Bit.Core.Services bool userPresence, int counter, byte[] credentialId = null, - byte[] publicKey = null, - byte[] privateKey = null + PublicKey? publicKey = null ) { - var isAttestation = credentialId != null && publicKey != null && privateKey != null; + var isAttestation = credentialId != null && publicKey.HasValue; List authData = new List(); @@ -347,25 +363,9 @@ namespace Bit.Core.Services }; attestedCredentialData.AddRange(credentialIdLength); attestedCredentialData.AddRange(credentialId); + attestedCredentialData.AddRange(publicKey.Value.ToCose()); - 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); + authData.AddRange(attestedCredentialData); } return authData.ToArray(); @@ -434,5 +434,40 @@ namespace Bit.Core.Services return Guid.Parse(guid).ToByteArray(); } + private struct PublicKey + { + public byte[] X { get; set; } + public byte[] Y { get; set; } + + public byte[] ToCose() + { + var result = new CborWriter(CborConformanceMode.Ctap2Canonical); + result.WriteStartMap(5); + + // kty = EC2 + result.WriteInt32(1); + result.WriteInt32(2); + + // alg = ES256 + result.WriteInt32(3); + result.WriteInt32(-7); + + // crv = P-256 + result.WriteInt32(-1); + result.WriteInt32(1); + + // x + result.WriteInt32(-2); + result.WriteByteString(X); + + // y + result.WriteInt32(-3); + result.WriteByteString(Y); + + result.WriteEndMap(); + + return result.Encode(); + } + } } } diff --git a/src/Core/Services/PclCryptoFunctionService.cs b/src/Core/Services/PclCryptoFunctionService.cs index e822075a7..3a55db662 100644 --- a/src/Core/Services/PclCryptoFunctionService.cs +++ b/src/Core/Services/PclCryptoFunctionService.cs @@ -228,20 +228,6 @@ namespace Bit.Core.Services return Task.FromResult(new Tuple(publicKey, privateKey)); } - public Task<(byte[], byte[])> EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm algorithm) - { - if (algorithm != CryptoEcdsaAlgorithm.P256Sha256) - { - throw new ArgumentException("Unsupported algorithm."); - } - - var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.EcdsaP256Sha256); - var cryptoKey = provider.CreateKeyPair(256); - var publicKey = cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); - var privateKey = cryptoKey.Export(CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); - return Task.FromResult((publicKey, privateKey)); - } - public Task RandomBytesAsync(int length) { return Task.FromResult(CryptographicBuffer.GenerateRandom(length)); diff --git a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs index 226bb14cb..61854c3e7 100644 --- a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs +++ b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs @@ -1,12 +1,10 @@ using System; using System.Threading.Tasks; using Bit.Core.Abstractions; -using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Models.Domain; using Bit.Core.Models.View; using Bit.Core.Enums; -using Bit.Core.Test.AutoFixture; using Bit.Core.Utilities.Fido2; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -16,8 +14,6 @@ using Xunit; using Bit.Core.Utilities; using System.Collections.Generic; using System.Linq; -using System.Security.Policy; -using NSubstitute.Extensions; using System.Formats.Cbor; namespace Bit.Core.Test.Services @@ -183,8 +179,6 @@ namespace Bit.Core.Test.Services ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; mParams.RequireUserVerification = false; - sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) - .Returns((RandomBytes(32), RandomBytes(32))); sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); sutProvider.GetDependency().ConfirmNewCredentialAsync(Arg.Any()).Returns(new Fido2ConfirmNewCredentialResult { CipherId = null, @@ -220,8 +214,6 @@ namespace Bit.Core.Test.Services } ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; - sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) - .Returns((RandomBytes(32), RandomBytes(32))); sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); // Arrange @@ -249,8 +241,6 @@ namespace Bit.Core.Test.Services ]; 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 = []; @@ -297,8 +287,6 @@ namespace Bit.Core.Test.Services ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; mParams.RequireUserVerification = false; - sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) - .Returns((RandomBytes(32), RandomBytes(32))); // Arrange sutProvider.GetDependency().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher); @@ -324,8 +312,6 @@ namespace Bit.Core.Test.Services ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; mParams.RequireUserVerification = true; - sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) - .Returns((RandomBytes(32), RandomBytes(32))); // Arrange sutProvider.GetDependency().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher); @@ -351,8 +337,6 @@ namespace Bit.Core.Test.Services ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; mParams.RequireUserVerification = false; - sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) - .Returns((RandomBytes(32), RandomBytes(32))); _encryptedCipher.Reprompt = CipherRepromptType.Password; // Arrange @@ -379,8 +363,6 @@ namespace Bit.Core.Test.Services ]; mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" }; mParams.RequireUserVerification = false; - sutProvider.GetDependency().EcdsaGenerateKeyPairAsync(Arg.Any()) - .Returns((RandomBytes(32), RandomBytes(32))); // Arrange sutProvider.GetDependency().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher); @@ -407,8 +389,6 @@ namespace Bit.Core.Test.Services ]; 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 = []; @@ -416,7 +396,6 @@ namespace Bit.Core.Test.Services 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, @@ -446,14 +425,13 @@ namespace Bit.Core.Test.Services // 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); + 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 @@ -494,16 +472,19 @@ namespace Bit.Core.Test.Services }; } - private class AttestationObject + private struct AttestationObject { - public string Fmt { get; set; } - public object AttStmt { get; set; } - public byte[] AuthData { get; set; } + public string? Fmt { get; set; } + public object? AttStmt { get; set; } + public byte[]? AuthData { get; set; } } private AttestationObject DecodeAttestationObject(byte[] attestationObject) { - var result = new AttestationObject(); + string? fmt = null; + object? attStmt = null; + byte[]? authData = null; + var reader = new CborReader(attestationObject, CborConformanceMode.Ctap2Canonical); reader.ReadStartMap(); @@ -513,21 +494,25 @@ namespace Bit.Core.Test.Services switch (key) { case "fmt": - result.Fmt = reader.ReadTextString(); + fmt = reader.ReadTextString(); break; case "attStmt": reader.ReadStartMap(); reader.ReadEndMap(); break; case "authData": - result.AuthData = reader.ReadByteString(); + authData = reader.ReadByteString(); break; default: throw new Exception("Unknown key"); } } - return result; + return new AttestationObject { + Fmt = fmt, + AttStmt = attStmt, + AuthData = authData + }; } } }