From 77189879c75805fc485f69690f74631b9b57478a Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Thu, 31 Jul 2025 01:31:09 -0700 Subject: [PATCH 1/6] EC-DSA Composite ML-DSA for S.S.C. --- .../CompositeMLDsaManaged.ECDsa.cs | 247 ++++++++++++++++++ .../Cryptography/CompositeMLDsaManaged.RSA.cs | 8 + .../Cryptography/CompositeMLDsaManaged.cs | 52 +--- .../CompositeMLDsaFactoryTests.cs | 120 +++++++++ .../CompositeMLDsaTestHelpers.cs | 19 +- .../CompositeMLDsa/CompositeMLDsaTestsBase.cs | 24 +- .../src/Microsoft.Bcl.Cryptography.csproj | 4 +- .../src/System.Security.Cryptography.csproj | 4 + 8 files changed, 420 insertions(+), 58 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs new file mode 100644 index 00000000000000..46c585aa12083d --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs @@ -0,0 +1,247 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + internal sealed partial class CompositeMLDsaManaged + { + private sealed class ECDsaComponent : ComponentAlgorithm +#if DESIGNTIMEINTERFACES +#pragma warning disable SA1001 // Commas should be spaced correctly + , IComponentAlgorithmFactory +#pragma warning restore SA1001 // Commas should be spaced correctly +#endif + { + private readonly ECDsaAlgorithm _algorithm; + + private ECDsa _ecdsa; + + private ECDsaComponent(ECDsa ecdsa, ECDsaAlgorithm algorithm) + { + Debug.Assert(ecdsa != null); + + _ecdsa = ecdsa; + _algorithm = algorithm; + } + + // OpenSSL supports the brainpool curves so this can be relaxed on a per-platform basis in the future if desired. + public static bool IsAlgorithmSupported(ECDsaAlgorithm algorithm) => +#if NET + algorithm.CurveOid is Oids.secp256r1 or Oids.secp384r1 or Oids.secp521r1; +#else + false; +#endif + + public static ECDsaComponent GenerateKey(ECDsaAlgorithm algorithm) + { +#if NET + ECDsa? ecdsa = null; + + try + { + ecdsa = algorithm.CurveOid switch + { + Oids.secp256r1 => ECDsa.Create(ECCurve.NamedCurves.nistP256), + Oids.secp384r1 => ECDsa.Create(ECCurve.NamedCurves.nistP384), + Oids.secp521r1 => ECDsa.Create(ECCurve.NamedCurves.nistP521), + string oid => FailAndThrow(oid) + }; + + static T FailAndThrow(string oid) + { + Debug.Fail($"EC-DSA curve not supported ({oid})"); + throw new CryptographicException(); + } + + // DSA key generation is lazy, so we need to force it to happen eagerly. + ecdsa.ExportParameters(includePrivateParameters: false); + + return new ECDsaComponent(ecdsa, algorithm); + } + catch (CryptographicException) + { + ecdsa?.Dispose(); + throw; + } +#else + throw new PlatformNotSupportedException(); +#endif + } + + public static ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) + { +#if NET + ECDsa? ecdsa = null; + + try + { + ecdsa = ECDsa.Create(); + ecdsa.ImportECPrivateKey(source, out int bytesRead); + + if (bytesRead != source.Length) + { + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } + + ECParameters parameters = ecdsa.ExportParameters(includePrivateParameters: false); + + if (!parameters.Curve.IsNamed || parameters.Curve.Oid.Value != algorithm.CurveOid) + { + // The curve specified in ECDomainParameters of ECPrivateKey do not match the required curve for + // the Composite ML-DSA algorithm. + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return new ECDsaComponent(ecdsa, algorithm); + } + catch (CryptographicException) + { + ecdsa?.Dispose(); + throw; + } +#else + throw new PlatformNotSupportedException(); +#endif + } + + public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) + { +#if NET + int fieldWidth = (algorithm.KeySizeInBits + 7) / 8; + + if (source.Length != 1 + fieldWidth * 2) + { + Debug.Fail("Public key format is fixed size, so caller needs to provide exactly correct sized buffer."); + throw new CryptographicException(); + } + + // Implementation limitation. + // 04 (Uncompressed ECPoint) is almost always used. + if (source[0] != 0x04) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + ECParameters parameters = new ECParameters() + { + Curve = ECCurve.CreateFromValue(algorithm.CurveOid), + Q = new ECPoint() + { + X = source.Slice(1, fieldWidth).ToArray(), + Y = source.Slice(1 + fieldWidth).ToArray(), + } + }; + + ECDsa? ecdsa = null; + + try + { + ecdsa = ECDsa.Create(parameters); + return new ECDsaComponent(ecdsa, algorithm); + } + catch (CryptographicException) + { + ecdsa?.Dispose(); + throw; + } +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal override bool TryExportPrivateKey(Span destination, out int bytesWritten) + { +#if NET + return _ecdsa.TryExportECPrivateKey(destination, out bytesWritten); +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal override bool TryExportPublicKey(Span destination, out int bytesWritten) + { +#if NET + int fieldWidth = (_algorithm.KeySizeInBits + 7) / 8; + + if (destination.Length < 1 + 2 * fieldWidth) + { + Debug.Fail("Public key format is fixed size, so caller needs to provide exactly correct sized buffer."); + + bytesWritten = 0; + return false; + } + + ECParameters parameters = _ecdsa.ExportParameters(includePrivateParameters: false); + + if (parameters.Q.X is not byte[] x || + parameters.Q.Y is not byte[] y || + x.Length != fieldWidth || + y.Length != fieldWidth) + { + throw new CryptographicException(); + } + + // Uncompressed ECPoint format + destination[0] = 0x04; + + x.CopyTo(destination.Slice(1, fieldWidth)); + y.CopyTo(destination.Slice(1 + fieldWidth)); + + bytesWritten = 1 + 2 * fieldWidth; + return true; +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal override bool VerifyData( +#if NET + ReadOnlySpan data, +#else + byte[] data, +#endif + ReadOnlySpan signature) + { +#if NET + return _ecdsa.VerifyData(data, signature, _algorithm.HashAlgorithmName, DSASignatureFormat.Rfc3279DerSequence); +#else + throw new PlatformNotSupportedException(); +#endif + } + + internal override int SignData( +#if NET + ReadOnlySpan data, +#else + byte[] data, +#endif + Span destination) + { +#if NET + if (!_ecdsa.TrySignData(data, destination, _algorithm.HashAlgorithmName, DSASignatureFormat.Rfc3279DerSequence, out int bytesWritten)) + { + Debug.Fail("Buffer size should have been validated by caller."); + throw new CryptographicException(); + } + + return bytesWritten; +#else + throw new PlatformNotSupportedException(); +#endif + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _ecdsa?.Dispose(); + _ecdsa = null!; + } + + base.Dispose(disposing); + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs index 7388ba650e9bbe..7d2269ebb58b89 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs @@ -156,6 +156,10 @@ public static RsaComponent ImportPrivateKey(RsaAlgorithm algorithm, ReadOnlySpan throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } #endif + if (rsa.KeySize != algorithm.KeySizeInBits) + { + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } if (bytesRead != source.Length) { @@ -207,6 +211,10 @@ public static RsaComponent ImportPublicKey(RsaAlgorithm algorithm, ReadOnlySpan< throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } #endif + if (rsa.KeySize != algorithm.KeySizeInBits) + { + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } if (bytesRead != source.Length) { diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs index 0468bdee6253ce..eef2c52074cf25 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using Internal.Cryptography; @@ -547,38 +546,6 @@ protected virtual void Dispose(bool disposing) } } - private sealed class ECDsaComponent : ComponentAlgorithm -#if DESIGNTIMEINTERFACES -#pragma warning disable SA1001 // Commas should be spaced correctly - , IComponentAlgorithmFactory -#pragma warning restore SA1001 // Commas should be spaced correctly -#endif - { - public static bool IsAlgorithmSupported(ECDsaAlgorithm _) => false; - public static ECDsaComponent GenerateKey(ECDsaAlgorithm algorithm) => throw new NotImplementedException(); - public static ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) => throw new NotImplementedException(); - public static ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) => throw new NotImplementedException(); - - internal override bool TryExportPrivateKey(Span destination, out int bytesWritten) => throw new NotImplementedException(); - internal override bool TryExportPublicKey(Span destination, out int bytesWritten) => throw new NotImplementedException(); - - internal override bool VerifyData( -#if NET - ReadOnlySpan data, -#else - byte[] data, -#endif - ReadOnlySpan signature) => throw new NotImplementedException(); - - internal override int SignData( -#if NET - ReadOnlySpan data, -#else - byte[] data, -#endif - Span destination) => throw new NotImplementedException(); - } - private static Dictionary CreateAlgorithmMetadata() { const int count = 18; @@ -613,7 +580,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa44, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(256, Oids.secp256r1, HashAlgorithmName.SHA256), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x03], HashAlgorithmName.SHA256) }, @@ -653,7 +620,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa65, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(256, Oids.secp256r1, HashAlgorithmName.SHA256), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x08], HashAlgorithmName.SHA512) }, @@ -661,7 +628,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa65, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(384, Oids.secp384r1, HashAlgorithmName.SHA384), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x09], HashAlgorithmName.SHA512) }, @@ -669,7 +636,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa65WithECDsaBrainpoolP256r1, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa65, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(256, "1.3.36.3.3.2.8.1.1.7", HashAlgorithmName.SHA256), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0A], HashAlgorithmName.SHA512) }, @@ -685,7 +652,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa87WithECDsaP384, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa87, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(384, Oids.secp384r1, HashAlgorithmName.SHA384), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0C], HashAlgorithmName.SHA512) }, @@ -693,7 +660,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa87WithECDsaBrainpoolP384r1, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa87, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(384, "1.3.36.3.3.2.8.1.1.11", HashAlgorithmName.SHA384), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x0D], HashAlgorithmName.SHA512) }, @@ -725,7 +692,7 @@ private static Dictionary CreateAlgo CompositeMLDsaAlgorithm.MLDsa87WithECDsaP521, new AlgorithmMetadata( MLDsaAlgorithm.MLDsa87, - new ECDsaAlgorithm(), + new ECDsaAlgorithm(521, Oids.secp521r1, HashAlgorithmName.SHA512), [0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x09, 0x01, 0x11], HashAlgorithmName.SHA512) } @@ -755,8 +722,11 @@ private sealed class RsaAlgorithm(int keySizeInBits, HashAlgorithmName hashAlgor internal RSASignaturePadding Padding { get; } = padding; } - private sealed class ECDsaAlgorithm + private sealed class ECDsaAlgorithm(int keySizeInBits, string curveOid, HashAlgorithmName hashAlgorithmName) { + internal int KeySizeInBits { get; } = keySizeInBits; + internal HashAlgorithmName HashAlgorithmName { get; } = hashAlgorithmName; + internal string CurveOid { get; } = curveOid; } private sealed class EdDsaAlgorithm diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs index d5997d722d8b85..d8d591eb96bb4e 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs @@ -58,6 +58,50 @@ public static void ImportBadPrivateKey_ShortTradKey(CompositeMLDsaAlgorithm algo AssertImportBadPrivateKey(algorithm, shortTradKey); } + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ImportBadPrivateKey_TrailingData(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) + { + byte[] key = vector.SecretKey; + Array.Resize(ref key, key.Length + 1); + + AssertImportBadPrivateKey(vector.Algorithm, key); + } + + [Fact] + public static void ImportBadPrivateKey_Rsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithRSA3072Pss + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); + + // But use MLDsa65WithRSA4096Pss + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.SecretKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); + + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.SecretKey); + } + + [Fact] + public static void ImportBadPrivateKey_ECDsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithECDsaP256 + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); + + // But use MLDsa65WithECDsaP384 + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.SecretKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); + + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.SecretKey); + } + [Theory] [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void ImportPrivateKey_LowerBound(CompositeMLDsaAlgorithm algorithm) @@ -131,6 +175,78 @@ public static void ImportBadPublicKey_ShortTradKey(CompositeMLDsaAlgorithm algor AssertImportBadPublicKey(algorithm, shortTradKey); } + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ImportBadPublicKey_TrailingData(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) + { + byte[] key = vector.PublicKey; + Array.Resize(ref key, key.Length + 1); + + AssertImportBadPublicKey(vector.Algorithm, key); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ImportBadPublicKey_ECDsa_Uncompressed(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) + { + if (CompositeMLDsaTestHelpers.ExecuteComponentFunc(vector.Algorithm, _ => true, ecdsa => false, _ => true)) + { + return; + } + + byte[] key = vector.PublicKey.AsSpan().ToArray(); + int formatIndex = CompositeMLDsaTestHelpers.MLDsaAlgorithms[vector.Algorithm].PublicKeySizeInBytes; + + // Uncompressed + Assert.Equal(4, key[formatIndex]); + + key[formatIndex] = 0; + AssertImportBadPublicKey(vector.Algorithm, key); + + key[formatIndex] = 1; + AssertImportBadPublicKey(vector.Algorithm, key); + + key[formatIndex] = 2; + AssertImportBadPublicKey(vector.Algorithm, key); + + key[formatIndex] = 3; + AssertImportBadPublicKey(vector.Algorithm, key); + } + + [Fact] + public static void ImportBadPublicKey_Rsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithRSA3072Pss + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); + + // But use MLDsa65WithRSA4096Pss + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.PublicKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); + + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.PublicKey); + } + + [Fact] + public static void ImportBadPublicKey_ECDsa_WrongAlgorithm() + { + // Get vector for MLDsa65WithECDsaP256 + CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); + + // But use MLDsa65WithECDsaP384 + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.PublicKey); + + // And flip + differentTradKey = + CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); + + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.PublicKey); + } + [Theory] [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void ImportPublicKey_LowerBound(CompositeMLDsaAlgorithm algorithm) @@ -208,7 +324,11 @@ public static void IsAlgorithmSupported_AgreesWithPlatform(CompositeMLDsaAlgorit bool supported = CompositeMLDsaTestHelpers.ExecuteComponentFunc( algorithm, _ => MLDsa.IsSupported, +#if NET + ecdsa => ecdsa.IsSec && MLDsa.IsSupported, +#else _ => false, +#endif _ => false); Assert.Equal( diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs index e169c8edc66ba9..b98fc014a19a63 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs @@ -70,9 +70,10 @@ internal class RsaAlgorithm(int keySizeInBits) internal int KeySizeInBits { get; } = keySizeInBits; } - internal class ECDsaAlgorithm(int keySizeInBits) + internal class ECDsaAlgorithm(int keySizeInBits, bool isSec) { internal int KeySizeInBits { get; } = keySizeInBits; + internal bool IsSec { get; } = isSec; } internal class EdDsaAlgorithm(int keySizeInBits) @@ -117,20 +118,26 @@ internal static T ExecuteComponentFunc( return rsaFunc(new RsaAlgorithm(4096)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256 || - algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaBrainpoolP256r1 || algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256) { - return ecdsaFunc(new ECDsaAlgorithm(256)); + return ecdsaFunc(new ECDsaAlgorithm(256, isSec: true)); + } + else if (algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaBrainpoolP256r1) + { + return ecdsaFunc(new ECDsaAlgorithm(256, isSec: false)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384 || - algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaBrainpoolP384r1 || algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaP384) { - return ecdsaFunc(new ECDsaAlgorithm(384)); + return ecdsaFunc(new ECDsaAlgorithm(384, isSec: true)); + } + else if (algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaBrainpoolP384r1) + { + return ecdsaFunc(new ECDsaAlgorithm(384, isSec: false)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa87WithECDsaP521) { - return ecdsaFunc(new ECDsaAlgorithm(521)); + return ecdsaFunc(new ECDsaAlgorithm(521, isSec: true)); } else if (algo == CompositeMLDsaAlgorithm.MLDsa44WithEd25519 || algo == CompositeMLDsaAlgorithm.MLDsa65WithEd25519) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs index a7d9fe129b90e8..17606e753a1e97 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestsBase.cs @@ -55,8 +55,8 @@ public void GenerateSignVerifyWithPrivateKey(CompositeMLDsaAlgorithm algorithm) { ExerciseSuccessfulVerify(privateKey, data, signature, []); - signature.AsSpan().Clear(); - privateKey.SignData(data, signature, []); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, privateKey.SignData(data, signature, [])); ExerciseSuccessfulVerify(privateKey, data, signature, []); } @@ -71,8 +71,8 @@ public void GenerateSignVerifyNoContext(CompositeMLDsaAlgorithm algorithm) byte[] signature = dsa.SignData(data); ExerciseSuccessfulVerify(dsa, data, signature, []); - signature.AsSpan().Clear(); - dsa.SignData(data, signature, Array.Empty()); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(data, signature, Array.Empty())); ExerciseSuccessfulVerify(dsa, data, signature, Array.Empty()); } @@ -86,6 +86,10 @@ public void GenerateSignVerifyWithContext(CompositeMLDsaAlgorithm algorithm) byte[] signature = dsa.SignData(data, context); ExerciseSuccessfulVerify(dsa, data, signature, context); + + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(data, signature, context)); + ExerciseSuccessfulVerify(dsa, data, signature, context); } [Theory] @@ -96,8 +100,8 @@ public void GenerateSignVerifyEmptyMessageNoContext(CompositeMLDsaAlgorithm algo byte[] signature = dsa.SignData([]); ExerciseSuccessfulVerify(dsa, [], signature, []); - signature.AsSpan().Clear(); - dsa.SignData(Array.Empty(), signature, Array.Empty()); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(Array.Empty(), signature, Array.Empty())); ExerciseSuccessfulVerify(dsa, [], signature, []); } @@ -110,8 +114,8 @@ public void GenerateSignVerifyEmptyMessageWithContext(CompositeMLDsaAlgorithm al byte[] signature = dsa.SignData([], context); ExerciseSuccessfulVerify(dsa, [], signature, context); - signature.AsSpan().Clear(); - dsa.SignData(Array.Empty(), signature, context); + signature = new byte[algorithm.MaxSignatureSizeInBytes]; + Array.Resize(ref signature, dsa.SignData(Array.Empty(), signature, context)); ExerciseSuccessfulVerify(dsa, [], signature, context); } @@ -154,14 +158,14 @@ public void ImportSignVerify(CompositeMLDsaTestData.CompositeMLDsaTestVector vec { signature = privateKey.SignData(vector.Message, null); - Assert.Equal(vector.Signature.Length, signature.Length); - ExerciseSuccessfulVerify(privateKey, vector.Message, signature, []); + ExerciseSuccessfulVerify(privateKey, vector.Message, vector.Signature, []); } using (CompositeMLDsa publicKey = ImportPublicKey(vector.Algorithm, vector.PublicKey)) { ExerciseSuccessfulVerify(publicKey, vector.Message, signature, []); + ExerciseSuccessfulVerify(publicKey, vector.Message, vector.Signature, []); } } diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index d193ac0db3c1e0..86d90e9a0c35d4 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -207,7 +207,7 @@ + Link="Common\System\Security\Cryptography\Oids.cs" /> + + + Date: Thu, 31 Jul 2025 10:24:15 -0700 Subject: [PATCH 2/6] Change private key to public key in message Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs index 7d2269ebb58b89..1aed30d805a0a6 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.RSA.cs @@ -213,7 +213,7 @@ public static RsaComponent ImportPublicKey(RsaAlgorithm algorithm, ReadOnlySpan< #endif if (rsa.KeySize != algorithm.KeySizeInBits) { - throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + throw new CryptographicException(SR.Argument_PublicKeyWrongSizeForAlgorithm); } if (bytesRead != source.Length) From 82b90b12d397b5e2bdd0cfeeb7973ec79740b815 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Thu, 31 Jul 2025 20:19:57 -0700 Subject: [PATCH 3/6] Add validation during private key import --- .../CompositeMLDsaManaged.ECDsa.cs | 122 +-- .../Cryptography/CompositeMLDsaManaged.cs | 22 + .../Cryptography/ECCurve.ECCurveType.cs | 7 +- .../Cryptography/ECCurve.NamedCurves.cs | 7 +- .../System/Security/Cryptography/ECCurve.cs | 8 +- .../Security/Cryptography/ECParameters.cs | 7 +- .../System/Security/Cryptography/ECPoint.cs | 7 +- .../Cryptography/EccKeyFormatHelper.cs | 849 +----------------- .../src/Microsoft.Bcl.Cryptography.csproj | 49 +- .../src/Resources/Strings.resx | 18 + .../src/System.Security.Cryptography.csproj | 16 +- .../Cryptography/EccKeyFormatHelper.cs | 835 +++++++++++++++++ 12 files changed, 1042 insertions(+), 905 deletions(-) rename src/libraries/{System.Security.Cryptography => Common}/src/System/Security/Cryptography/ECCurve.ECCurveType.cs (87%) rename src/libraries/{System.Security.Cryptography => Common}/src/System/Security/Cryptography/ECCurve.NamedCurves.cs (98%) rename src/libraries/{System.Security.Cryptography => Common}/src/System/Security/Cryptography/ECCurve.cs (97%) rename src/libraries/{System.Security.Cryptography => Common}/src/System/Security/Cryptography/ECParameters.cs (96%) rename src/libraries/{System.Security.Cryptography => Common}/src/System/Security/Cryptography/ECPoint.cs (81%) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs index 46c585aa12083d..0127ddabc9192b 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; +using System.Formats.Asn1; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Asn1; namespace System.Security.Cryptography { @@ -37,79 +41,88 @@ public static bool IsAlgorithmSupported(ECDsaAlgorithm algorithm) => public static ECDsaComponent GenerateKey(ECDsaAlgorithm algorithm) { #if NET - ECDsa? ecdsa = null; - - try - { - ecdsa = algorithm.CurveOid switch - { - Oids.secp256r1 => ECDsa.Create(ECCurve.NamedCurves.nistP256), - Oids.secp384r1 => ECDsa.Create(ECCurve.NamedCurves.nistP384), - Oids.secp521r1 => ECDsa.Create(ECCurve.NamedCurves.nistP521), - string oid => FailAndThrow(oid) - }; - - static T FailAndThrow(string oid) - { - Debug.Fail($"EC-DSA curve not supported ({oid})"); - throw new CryptographicException(); - } - - // DSA key generation is lazy, so we need to force it to happen eagerly. - ecdsa.ExportParameters(includePrivateParameters: false); - - return new ECDsaComponent(ecdsa, algorithm); - } - catch (CryptographicException) - { - ecdsa?.Dispose(); - throw; - } + return new ECDsaComponent(ECDsa.Create(algorithm.Curve), algorithm); #else throw new PlatformNotSupportedException(); #endif } - public static ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) + public static unsafe ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) { -#if NET - ECDsa? ecdsa = null; - try { - ecdsa = ECDsa.Create(); - ecdsa.ImportECPrivateKey(source, out int bytesRead); - - if (bytesRead != source.Length) + AsnDecoder.ReadEncodedValue( + source, + AsnEncodingRules.BER, + out _, + out _, + out int firstValueLength); + + if (firstValueLength != source.Length) { - throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - ECParameters parameters = ecdsa.ExportParameters(includePrivateParameters: false); - - if (!parameters.Curve.IsNamed || parameters.Curve.Oid.Value != algorithm.CurveOid) + fixed (byte* ptr = &MemoryMarshal.GetReference(source)) { - // The curve specified in ECDomainParameters of ECPrivateKey do not match the required curve for - // the Composite ML-DSA algorithm. - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } + using (MemoryManager manager = new PointerMemoryManager(ptr, firstValueLength)) + { + ECPrivateKey ecPrivateKey = ECPrivateKey.Decode(manager.Memory, AsnEncodingRules.BER); + + if (ecPrivateKey.Version != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // If domain parameters are present, validate that they match the composite ML-DSA algorithm. + if (ecPrivateKey.Parameters is ECDomainParameters domainParameters) + { + if (domainParameters.Named is not string curveOid || curveOid != algorithm.CurveOid) + { + // The curve specified must be named and match the required curve for the composite ML-DSA algorithm. + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + + ECParameters parameters = new ECParameters + { + Curve = algorithm.Curve, + }; + + // If public key is present, add it to the parameters. + if (ecPrivateKey.PublicKey is ReadOnlyMemory publicKey) + { + parameters.Q = EccKeyFormatHelper.GetECPointFromUncompressedPublicKey(publicKey.Span, algorithm.KeySizeInBytes); + } + + byte[] d = new byte[ecPrivateKey.PrivateKey.Length]; + + using (PinAndClear.Track(d)) + { + ecPrivateKey.PrivateKey.CopyTo(d); + parameters.D = d; + + parameters.Validate(); - return new ECDsaComponent(ecdsa, algorithm); +#if NET + return new ECDsaComponent(ECDsa.Create(parameters), algorithm); +#else + throw new PlatformNotSupportedException(); +#endif + } + } + } } - catch (CryptographicException) + catch (AsnContentException e) { - ecdsa?.Dispose(); - throw; + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } -#else - throw new PlatformNotSupportedException(); -#endif + } public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) { -#if NET - int fieldWidth = (algorithm.KeySizeInBits + 7) / 8; + int fieldWidth = algorithm.KeySizeInBytes; if (source.Length != 1 + fieldWidth * 2) { @@ -134,6 +147,7 @@ public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, Re } }; +#if NET ECDsa? ecdsa = null; try @@ -163,7 +177,7 @@ internal override bool TryExportPrivateKey(Span destination, out int bytes internal override bool TryExportPublicKey(Span destination, out int bytesWritten) { #if NET - int fieldWidth = (_algorithm.KeySizeInBits + 7) / 8; + int fieldWidth = _algorithm.KeySizeInBytes; if (destination.Length < 1 + 2 * fieldWidth) { diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs index eef2c52074cf25..0fab448bb21ce6 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs @@ -727,6 +727,28 @@ private sealed class ECDsaAlgorithm(int keySizeInBits, string curveOid, HashAlgo internal int KeySizeInBits { get; } = keySizeInBits; internal HashAlgorithmName HashAlgorithmName { get; } = hashAlgorithmName; internal string CurveOid { get; } = curveOid; + + internal int KeySizeInBytes => (KeySizeInBits + 7) / 8; + + internal ECCurve Curve + { + get + { + return CurveOid switch + { + Oids.secp256r1 => ECCurve.NamedCurves.nistP256, + Oids.secp384r1 => ECCurve.NamedCurves.nistP384, + Oids.secp521r1 => ECCurve.NamedCurves.nistP521, + string oid => FailAndThrow(oid) + }; + + static ECCurve FailAndThrow(string oid) + { + Debug.Fail($"EC-DSA curve not supported ({oid})"); + throw new CryptographicException(); + } + } + } } private sealed class EdDsaAlgorithm diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.ECCurveType.cs b/src/libraries/Common/src/System/Security/Cryptography/ECCurve.ECCurveType.cs similarity index 87% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.ECCurveType.cs rename to src/libraries/Common/src/System/Security/Cryptography/ECCurve.ECCurveType.cs index f935be1e6b29a6..32db002736239e 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.ECCurveType.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECCurve.ECCurveType.cs @@ -5,7 +5,12 @@ namespace System.Security.Cryptography { - public partial struct ECCurve +#if NETFRAMEWORK + internal +#else + public +#endif + partial struct ECCurve { /// /// Represents the type of elliptic curve. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.NamedCurves.cs b/src/libraries/Common/src/System/Security/Cryptography/ECCurve.NamedCurves.cs similarity index 98% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.NamedCurves.cs rename to src/libraries/Common/src/System/Security/Cryptography/ECCurve.NamedCurves.cs index b5e99ff782ae84..2aae6ebf7080d8 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.NamedCurves.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECCurve.NamedCurves.cs @@ -5,7 +5,12 @@ namespace System.Security.Cryptography { - public partial struct ECCurve +#if NETFRAMEWORK + internal +#else + public +#endif + partial struct ECCurve { /// /// Factory class for creating named curves. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.cs b/src/libraries/Common/src/System/Security/Cryptography/ECCurve.cs similarity index 97% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.cs rename to src/libraries/Common/src/System/Security/Cryptography/ECCurve.cs index f8c66ee70f67c6..f220560f666316 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECCurve.cs @@ -14,7 +14,13 @@ namespace System.Security.Cryptography /// which is either a prime curve or a characteristic-2 curve. /// [DebuggerDisplay("ECCurve = {Oid}")] - public partial struct ECCurve +#if NETFRAMEWORK +#pragma warning disable CS0649 // Field 'ECCurve.G' is never assigned to, and will always have its default value + internal +#else + public +#endif + partial struct ECCurve { /// /// Coefficient A. Applies only to Explicit curves. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECParameters.cs b/src/libraries/Common/src/System/Security/Cryptography/ECParameters.cs similarity index 96% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECParameters.cs rename to src/libraries/Common/src/System/Security/Cryptography/ECParameters.cs index 165f9877fcef84..e1fe0e32f08b22 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECParameters.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECParameters.cs @@ -6,7 +6,12 @@ namespace System.Security.Cryptography /// /// Represents the public and private key of the specified elliptic curve. /// - public struct ECParameters +#if NETFRAMEWORK + internal +#else + public +#endif + struct ECParameters { /// /// Public point. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECPoint.cs b/src/libraries/Common/src/System/Security/Cryptography/ECPoint.cs similarity index 81% rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECPoint.cs rename to src/libraries/Common/src/System/Security/Cryptography/ECPoint.cs index 26af121e9eb46d..73c60374f3b418 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECPoint.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECPoint.cs @@ -6,7 +6,12 @@ namespace System.Security.Cryptography /// /// Represents a point on an elliptic curve. /// - public struct ECPoint +#if NETFRAMEWORK + internal +#else + public +#endif + struct ECPoint { public byte[]? X; public byte[]? Y; diff --git a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs index 1b5059af55b09d..c881231a85e50a 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs @@ -1,866 +1,35 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; -using System.Collections; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Formats.Asn1; -using System.Runtime.InteropServices; -using System.Security.Cryptography.Asn1; - namespace System.Security.Cryptography { - internal static class EccKeyFormatHelper + internal static partial class EccKeyFormatHelper { - // This is the same limit that OpenSSL 1.0.2-1.1.1 has, - // there's no real point reading anything bigger than this (for now). - private const int MaxFieldBitSize = 661; - - private static readonly string[] s_validOids = - { - Oids.EcPublicKey, - }; - - internal static void ReadSubjectPublicKeyInfo( - ReadOnlySpan source, - out int bytesRead, - out ECParameters key) - { - KeyFormatHelper.ReadSubjectPublicKeyInfo( - s_validOids, - source, - FromECPublicKey, - out bytesRead, - out key); - } - - internal static ReadOnlyMemory ReadSubjectPublicKeyInfo( - ReadOnlyMemory source, - out int bytesRead) - { - return KeyFormatHelper.ReadSubjectPublicKeyInfo( - s_validOids, - source, - out bytesRead); - } - - internal static void ReadEncryptedPkcs8( - ReadOnlySpan source, - ReadOnlySpan password, - out int bytesRead, - out ECParameters key) - { - KeyFormatHelper.ReadEncryptedPkcs8( - s_validOids, - source, - password, - FromECPrivateKey, - out bytesRead, - out key); - } - - internal static void ReadEncryptedPkcs8( - ReadOnlySpan source, - ReadOnlySpan passwordBytes, - out int bytesRead, - out ECParameters key) - { - KeyFormatHelper.ReadEncryptedPkcs8( - s_validOids, - source, - passwordBytes, - FromECPrivateKey, - out bytesRead, - out key); - } - - internal static unsafe ECParameters FromECPrivateKey(ReadOnlySpan key, out int bytesRead) - { - try - { - AsnDecoder.ReadEncodedValue( - key, - AsnEncodingRules.BER, - out _, - out _, - out int firstValueLength); - - fixed (byte* ptr = &MemoryMarshal.GetReference(key)) - { - using (MemoryManager manager = new PointerMemoryManager(ptr, firstValueLength)) - { - AlgorithmIdentifierAsn algId = default; - FromECPrivateKey(manager.Memory, algId, out ECParameters ret); - bytesRead = firstValueLength; - return ret; - } - } - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } - - internal static void FromECPrivateKey( - ReadOnlyMemory keyData, - in AlgorithmIdentifierAsn algId, - out ECParameters ret) - { - ECPrivateKey key = ECPrivateKey.Decode(keyData, AsnEncodingRules.BER); - FromECPrivateKey(key, algId, out ret); - } - - internal static void FromECPrivateKey( - ECPrivateKey key, - in AlgorithmIdentifierAsn algId, - out ECParameters ret) - { - ValidateParameters(key.Parameters, algId); - - if (key.Version != 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - byte[]? x = null; - byte[]? y = null; - - if (key.PublicKey != null) - { - ReadOnlySpan publicKeyBytes = key.PublicKey.Value.Span; - - if (publicKeyBytes.Length == 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - // Implementation limitation - // 04 (Uncompressed ECPoint) is almost always used. - if (publicKeyBytes[0] != 0x04) - { - throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); - } - - // https://www.secg.org/sec1-v2.pdf, 2.3.4, #3 (M has length 2 * CEIL(log2(q)/8) + 1) - if (publicKeyBytes.Length != 2 * key.PrivateKey.Length + 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - x = publicKeyBytes.Slice(1, key.PrivateKey.Length).ToArray(); - y = publicKeyBytes.Slice(1 + key.PrivateKey.Length).ToArray(); - } - - ECDomainParameters domainParameters; - - if (key.Parameters != null) - { - domainParameters = key.Parameters.Value; - } - else - { - domainParameters = ECDomainParameters.Decode(algId.Parameters!.Value, AsnEncodingRules.DER); - } - - Debug.Assert((x == null) == (y == null)); - - ret = new ECParameters - { - Curve = GetCurve(domainParameters), - Q = - { - X = x, - Y = y, - }, - D = key.PrivateKey.ToArray(), - }; - - ret.Validate(); - } - - internal static void FromECPublicKey( - ReadOnlyMemory key, - in AlgorithmIdentifierAsn algId, - out ECParameters ret) + internal static ECPoint GetECPointFromUncompressedPublicKey(ReadOnlySpan publicKey, int fieldWidthInBytes) { - if (algId.Parameters == null) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - ReadOnlySpan publicKeyBytes = key.Span; - - if (publicKeyBytes.Length == 0) + if (publicKey.Length == 0) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - // Implementation limitation. + // Implementation limitation // 04 (Uncompressed ECPoint) is almost always used. - if (publicKeyBytes[0] != 0x04) + if (publicKey[0] != 0x04) { throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); } // https://www.secg.org/sec1-v2.pdf, 2.3.4, #3 (M has length 2 * CEIL(log2(q)/8) + 1) - if ((publicKeyBytes.Length & 0x01) != 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - int fieldWidth = publicKeyBytes.Length / 2; - - ECDomainParameters domainParameters = ECDomainParameters.Decode( - algId.Parameters.Value, - AsnEncodingRules.DER); - - ret = new ECParameters - { - Curve = GetCurve(domainParameters), - Q = - { - X = publicKeyBytes.Slice(1, fieldWidth).ToArray(), - Y = publicKeyBytes.Slice(1 + fieldWidth).ToArray(), - }, - }; - - ret.Validate(); - } - - private static void ValidateParameters(ECDomainParameters? keyParameters, in AlgorithmIdentifierAsn algId) - { - // At least one is required - if (keyParameters == null && algId.Parameters == null) + if (publicKey.Length != 2 * fieldWidthInBytes + 1) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - // If they are both specified they must match. - if (keyParameters != null && algId.Parameters != null) - { - ReadOnlySpan algIdParameters = algId.Parameters.Value.Span; - - // X.509 SubjectPublicKeyInfo specifies DER encoding. - // RFC 5915 specifies DER encoding for EC Private Keys. - // So we can compare as DER. - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - keyParameters.Value.Encode(writer); - - if (!writer.EncodedValueEquals(algIdParameters)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - } - } - - private static ECCurve GetCurve(ECDomainParameters domainParameters) - { - if (domainParameters.Specified.HasValue) + return new ECPoint { - return GetSpecifiedECCurve(domainParameters.Specified.Value); - } - - if (domainParameters.Named == null) - { - throw new CryptographicException(SR.Cryptography_ECC_NamedCurvesOnly); - } - - Oid curveOid = domainParameters.Named switch { - Oids.secp256r1 => Oids.secp256r1Oid, - Oids.secp384r1 => Oids.secp384r1Oid, - Oids.secp521r1 => Oids.secp521r1Oid, - _ => new Oid(domainParameters.Named, null) + X = publicKey.Slice(1, fieldWidthInBytes).ToArray(), + Y = publicKey.Slice(1 + fieldWidthInBytes).ToArray(), }; - - return ECCurve.CreateFromOid(curveOid); - } - - private static ECCurve GetSpecifiedECCurve(SpecifiedECDomain specifiedParameters) - { - try - { - return GetSpecifiedECCurveCore(specifiedParameters); - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } - - private static ECCurve GetSpecifiedECCurveCore(SpecifiedECDomain specifiedParameters) - { - // sec1-v2 C.3: - // - // Versions 1, 2, and 3 are defined. - // 1 is just data, 2 and 3 mean that a seed is required (with different reasons for why, - // but they're human-reasons, not technical ones). - if (specifiedParameters.Version < 1 || specifiedParameters.Version > 3) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - if (specifiedParameters.Version > 1 && !specifiedParameters.Curve.Seed.HasValue) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - byte[] primeOrPoly; - bool prime; - - switch (specifiedParameters.FieldID.FieldType) - { - case Oids.EcPrimeField: - prime = true; - AsnReader primeReader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); - ReadOnlySpan primeValue = primeReader.ReadIntegerBytes().Span; - primeReader.ThrowIfNotEmpty(); - - if (primeValue[0] == 0) - { - primeValue = primeValue.Slice(1); - } - - if (primeValue.Length > (MaxFieldBitSize / 8)) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - primeOrPoly = primeValue.ToArray(); - break; - case Oids.EcChar2Field: - prime = false; - AsnReader char2Reader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); - AsnReader innerReader = char2Reader.ReadSequence(); - char2Reader.ThrowIfNotEmpty(); - - // Characteristic-two ::= SEQUENCE - // { - // m INTEGER, -- Field size - // basis CHARACTERISTIC-TWO.&id({BasisTypes}), - // parameters CHARACTERISTIC-TWO.&Type({BasisTypes}{@basis}) - // } - - if (!innerReader.TryReadInt32(out int m) || m > MaxFieldBitSize || m < 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - int k1; - int k2 = -1; - int k3 = -1; - - switch (innerReader.ReadObjectIdentifier()) - { - case Oids.EcChar2TrinomialBasis: - // Trinomial ::= INTEGER - if (!innerReader.TryReadInt32(out k1) || k1 >= m || k1 < 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - break; - case Oids.EcChar2PentanomialBasis: - // Pentanomial ::= SEQUENCE - // { - // k1 INTEGER, -- k1 > 0 - // k2 INTEGER, -- k2 > k1 - // k3 INTEGER -- k3 > k2 - // } - AsnReader pentanomialReader = innerReader.ReadSequence(); - - if (!pentanomialReader.TryReadInt32(out k1) || - !pentanomialReader.TryReadInt32(out k2) || - !pentanomialReader.TryReadInt32(out k3) || - k1 < 1 || - k2 <= k1 || - k3 <= k2 || - k3 >= m) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - pentanomialReader.ThrowIfNotEmpty(); - - break; - default: - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - innerReader.ThrowIfNotEmpty(); - - BitArray poly = new BitArray(m + 1); - poly.Set(m, true); - poly.Set(k1, true); - poly.Set(0, true); - - if (k2 > 0) - { - poly.Set(k2, true); - poly.Set(k3, true); - } - - primeOrPoly = new byte[(m + 7) / 8]; - poly.CopyTo(primeOrPoly, 0); - Array.Reverse(primeOrPoly); - break; - default: - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - ECCurve curve; - - if (prime) - { - curve = new ECCurve - { - CurveType = ECCurve.ECCurveType.PrimeShortWeierstrass, - Prime = primeOrPoly, - }; - } - else - { - curve = new ECCurve - { - CurveType = ECCurve.ECCurveType.Characteristic2, - Polynomial = primeOrPoly, - }; - } - - curve.A = specifiedParameters.Curve.A.ToUnsignedIntegerBytes(primeOrPoly.Length); - curve.B = specifiedParameters.Curve.B.ToUnsignedIntegerBytes(primeOrPoly.Length); - curve.Order = specifiedParameters.Order.ToUnsignedIntegerBytes(primeOrPoly.Length); - - ReadOnlySpan baseSpan = specifiedParameters.Base.Span; - - // We only understand the uncompressed point encoding, but that's almost always what's used. - if (baseSpan[0] != 0x04 || baseSpan.Length != 2 * primeOrPoly.Length + 1) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - curve.G.X = baseSpan.Slice(1, primeOrPoly.Length).ToArray(); - curve.G.Y = baseSpan.Slice(1 + primeOrPoly.Length).ToArray(); - - if (specifiedParameters.Cofactor.HasValue) - { - curve.Cofactor = specifiedParameters.Cofactor.Value.ToUnsignedIntegerBytes(); - } - - return curve; - } - - internal static AsnWriter WriteSubjectPublicKeyInfo(ECParameters ecParameters) - { - ecParameters.Validate(); - - // Since the public key format for EC keys is not ASN.1, - // write the SPKI structure manually. - - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - // SubjectPublicKeyInfo - writer.PushSequence(); - - // algorithm - WriteAlgorithmIdentifier(ecParameters, writer); - - // subjectPublicKey - WriteUncompressedPublicKey(ecParameters, writer); - - writer.PopSequence(); - return writer; - } - - private static AsnWriter WriteAlgorithmIdentifier(in ECParameters ecParameters) - { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - WriteAlgorithmIdentifier(ecParameters, writer); - return writer; - } - - private static void WriteAlgorithmIdentifier(in ECParameters ecParameters, AsnWriter writer) - { - writer.PushSequence(); - - writer.WriteObjectIdentifier(Oids.EcPublicKey); - WriteEcParameters(ecParameters, writer); - - writer.PopSequence(); - } - - internal static AsnWriter WritePkcs8PrivateKey(ECParameters ecParameters, AttributeAsn[]? attributes = null) - { - ecParameters.Validate(); - - if (ecParameters.D == null) - { - throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); - } - - // Don't need the domain parameters because they're contained in the algId. - AsnWriter ecPrivateKey = WriteEcPrivateKey(ecParameters, includeDomainParameters: false); - AsnWriter algorithmIdentifier = WriteAlgorithmIdentifier(ecParameters); - AsnWriter? attributeWriter = WritePrivateKeyInfoAttributes(attributes); - - return KeyFormatHelper.WritePkcs8(algorithmIdentifier, ecPrivateKey, attributeWriter); - } - - [return: NotNullIfNotNull(nameof(attributes))] - private static AsnWriter? WritePrivateKeyInfoAttributes(AttributeAsn[]? attributes) - { - if (attributes == null) - return null; - - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 0); - writer.PushSetOf(tag); - - for (int i = 0; i < attributes.Length; i++) - { - attributes[i].Encode(writer); - } - - writer.PopSetOf(tag); - return writer; - } - - private static void WriteEcParameters(ECParameters ecParameters, AsnWriter writer) - { - if (ecParameters.Curve.IsNamed) - { - Oid oid = ecParameters.Curve.Oid; - - // On Windows the FriendlyName is populated in places where the Value mightn't be. - if (string.IsNullOrEmpty(oid.Value)) - { - Debug.Assert(oid.FriendlyName != null); - oid = Oid.FromFriendlyName(oid.FriendlyName, OidGroup.All); - } - - writer.WriteObjectIdentifier(oid.Value!); - } - else if (ecParameters.Curve.IsExplicit) - { - Debug.Assert(ecParameters.Curve.IsPrime || ecParameters.Curve.IsCharacteristic2); - WriteSpecifiedECDomain(ecParameters, writer); - } - else - { - throw new CryptographicException( - SR.Format(SR.Cryptography_CurveNotSupported, ecParameters.Curve.CurveType.ToString())); - } - } - - private static void WriteSpecifiedECDomain(ECParameters ecParameters, AsnWriter writer) - { - int m; - int k1; - int k2; - int k3; - m = k1 = k2 = k3 = -1; - - if (ecParameters.Curve.IsCharacteristic2) - { - DetermineChar2Parameters(ecParameters, ref m, ref k1, ref k2, ref k3); - } - - // SpecifiedECDomain - writer.PushSequence(); - { - // version - // We don't know if the seed (if present) is verifiably random (2). - // We also don't know if the base point is verifiably random (3). - // So just be version 1. - writer.WriteInteger(1); - - // fieldId - writer.PushSequence(); - { - if (ecParameters.Curve.IsPrime) - { - writer.WriteObjectIdentifier(Oids.EcPrimeField); - writer.WriteIntegerUnsigned(ecParameters.Curve.Prime); - } - else - { - Debug.Assert(ecParameters.Curve.IsCharacteristic2); - - // id - writer.WriteObjectIdentifier(Oids.EcChar2Field); - - // Parameters (Characteristic-two) - writer.PushSequence(); - { - // m - writer.WriteInteger(m); - - if (k3 > 0) - { - writer.WriteObjectIdentifier(Oids.EcChar2PentanomialBasis); - - writer.PushSequence(); - { - writer.WriteInteger(k1); - writer.WriteInteger(k2); - writer.WriteInteger(k3); - - writer.PopSequence(); - } - } - else - { - Debug.Assert(k2 < 0); - Debug.Assert(k1 > 0); - - writer.WriteObjectIdentifier(Oids.EcChar2TrinomialBasis); - writer.WriteInteger(k1); - } - - writer.PopSequence(); - } - } - - writer.PopSequence(); - } - - // curve - WriteCurve(ecParameters.Curve, writer); - - // base - WriteUncompressedBasePoint(ecParameters, writer); - - // order - writer.WriteIntegerUnsigned(ecParameters.Curve.Order); - - // cofactor - if (ecParameters.Curve.Cofactor != null) - { - writer.WriteIntegerUnsigned(ecParameters.Curve.Cofactor); - } - - // hash is omitted. - - writer.PopSequence(); - } - } - - private static void DetermineChar2Parameters( - in ECParameters ecParameters, - ref int m, - ref int k1, - ref int k2, - ref int k3) - { - byte[] polynomial = ecParameters.Curve.Polynomial!; - int lastIndex = polynomial.Length - 1; - - // The most significant byte needs a set bit, and the least significant bit must be set. - if (polynomial[0] == 0 || (polynomial[lastIndex] & 1) != 1) - { - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - - for (int localBitIndex = 7; localBitIndex >= 0; localBitIndex--) - { - int test = 1 << localBitIndex; - - if ((polynomial[0] & test) == test) - { - m = checked(8 * lastIndex + localBitIndex); - } - } - - // Find the other set bits. Since we've already found m and 0, there is either - // one remaining (trinomial) or 3 (pentanomial). - for (int inverseIndex = 0; inverseIndex < polynomial.Length; inverseIndex++) - { - int forwardIndex = lastIndex - inverseIndex; - byte val = polynomial[forwardIndex]; - - for (int localBitIndex = 0; localBitIndex < 8; localBitIndex++) - { - int test = 1 << localBitIndex; - - if ((val & test) == test) - { - int bitIndex = 8 * inverseIndex + localBitIndex; - - if (bitIndex == 0) - { - // The bottom bit is always set, it's not considered a parameter. - } - else if (bitIndex == m) - { - break; - } - else if (k1 < 0) - { - k1 = bitIndex; - } - else if (k2 < 0) - { - k2 = bitIndex; - } - else if (k3 < 0) - { - k3 = bitIndex; - } - else - { - // More than pentanomial. - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - } - } - } - - if (k3 > 0) - { - // Pentanomial - } - else if (k2 > 0) - { - // There is no quatranomial - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - else if (k1 > 0) - { - // Trinomial - } - else - { - // No smaller bases exist - throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); - } - } - - private static void WriteCurve(in ECCurve curve, AsnWriter writer) - { - writer.PushSequence(); - WriteFieldElement(curve.A!, writer); - WriteFieldElement(curve.B!, writer); - - if (curve.Seed != null) - { - writer.WriteBitString(curve.Seed); - } - - writer.PopSequence(); - } - - private static void WriteFieldElement(byte[] fieldElement, AsnWriter writer) - { - int start = 0; - - while (start < fieldElement.Length - 1 && fieldElement[start] == 0) - { - start++; - } - - writer.WriteOctetString(fieldElement.AsSpan(start)); - } - - private static void WriteUncompressedBasePoint(in ECParameters ecParameters, AsnWriter writer) - { - int basePointLength = ecParameters.Curve.G.X!.Length * 2 + 1; - - // A NIST P-521 G will be at most 133 bytes (NIST 186-4 defines G.) - // 256 should be plenty for all but very atypical uses. - const int MaxStackAllocSize = 256; - Span basePointBytes = stackalloc byte[MaxStackAllocSize]; - byte[]? rented = null; - - if (basePointLength > MaxStackAllocSize) - { - basePointBytes = rented = CryptoPool.Rent(basePointLength); - } - - basePointBytes[0] = 0x04; - ecParameters.Curve.G.X.CopyTo(basePointBytes.Slice(1)); - ecParameters.Curve.G.Y.CopyTo(basePointBytes.Slice(1 + ecParameters.Curve.G.X.Length)); - - writer.WriteOctetString(basePointBytes.Slice(0, basePointLength)); - - if (rented is not null) - { - // G contains public EC parameters that are not sensitive. - CryptoPool.Return(rented, clearSize: 0); - } - } - - private static void WriteUncompressedPublicKey(in ECParameters ecParameters, AsnWriter writer) - { - int publicKeyLength = ecParameters.Q.X!.Length * 2 + 1; - - // A NIST P-521 Q will encode to 133 bytes: (521 + 7)/8 * 2 + 1. - // 256 should be plenty for all but very atypical uses. - const int MaxStackAllocSize = 256; - Span publicKeyBytes = stackalloc byte[MaxStackAllocSize]; - byte[]? rented = null; - - if (publicKeyLength > MaxStackAllocSize) - { - publicKeyBytes = rented = CryptoPool.Rent(publicKeyLength); - } - - publicKeyBytes[0] = 0x04; - ecParameters.Q.X.CopyTo(publicKeyBytes.Slice(1)); - ecParameters.Q.Y.CopyTo(publicKeyBytes.Slice(1 + ecParameters.Q.X!.Length)); - - writer.WriteBitString(publicKeyBytes.Slice(0, publicKeyLength)); - - if (rented is not null) - { - // Q contains public EC parameters that are not sensitive. - CryptoPool.Return(rented, clearSize: 0); - } - } - - internal static AsnWriter WriteECPrivateKey(in ECParameters ecParameters) - { - return WriteEcPrivateKey(ecParameters, includeDomainParameters: true); - } - - private static AsnWriter WriteEcPrivateKey(in ECParameters ecParameters, bool includeDomainParameters) - { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - // ECPrivateKey - writer.PushSequence(); - - // version 1 - writer.WriteInteger(1); - - // privateKey - writer.WriteOctetString(ecParameters.D); - - // domainParameters - if (includeDomainParameters) - { - Asn1Tag explicit0 = new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true); - writer.PushSequence(explicit0); - - WriteEcParameters(ecParameters, writer); - - writer.PopSequence(explicit0); - } - - // publicKey - if (ecParameters.Q.X != null) - { - Debug.Assert(ecParameters.Q.Y != null); - Asn1Tag explicit1 = new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true); - writer.PushSequence(explicit1); - - WriteUncompressedPublicKey(ecParameters, writer); - - writer.PopSequence(explicit1); - } - - writer.PopSequence(); - return writer; } } } diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index 86d90e9a0c35d4..b67aa8daef64f3 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -112,6 +112,27 @@ Common\System\Security\Cryptography\Asn1\AttributeAsn.manual.cs Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + Common\System\Security\Cryptography\Asn1\CurveAsn.xml + + + Common\System\Security\Cryptography\Asn1\CurveAsn.xml.cs + Common\System\Security\Cryptography\Asn1\CurveAsn.xml + + + Common\System\Security\Cryptography\Asn1\ECDomainParameters.xml + + + Common\System\Security\Cryptography\Asn1\ECDomainParameters.xml.cs + Common\System\Security\Cryptography\Asn1\ECDomainParameters.xml + + + Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml + + + Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml.cs + Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml @@ -119,6 +140,13 @@ Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml.cs Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml + + Common\System\Security\Cryptography\Asn1\FieldID.xml + + + Common\System\Security\Cryptography\Asn1\FieldID.xml.cs + Common\System\Security\Cryptography\Asn1\FieldID.xml + Common\System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml.cs Common\System\Security\Cryptography\Asn1\MLDsaPrivateKeyAsn.xml @@ -187,6 +215,13 @@ Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml.cs Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml + + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml + + + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml.cs + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml + Common\System\Security\Cryptography\Asn1\SubjectPublicKeyInfoAsn.xml @@ -198,6 +233,8 @@ Link="Common\System\Memory\PointerMemoryManager.cs" /> + + Link="Common\System\Security\Cryptography\Oids.cs" /> + + + + + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx index 0cc70b1e6d7bce..eef274248873fe 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -159,6 +159,9 @@ Composite signature generation failed due to an error in one or both of the components. + + The specified curve '{0}' or its parameters are not valid for this platform. + ASN1 corrupted data. @@ -174,6 +177,21 @@ The size of the specified tag does not match the expected size of {0}. + + The specified key parameters are not valid. Q.X and Q.Y, or D, must be specified. Q.X, Q.Y must be the same length. If D is specified it must be the same length as Q.X and Q.Y if also specified for named curves or the same length as Order for explicit curves. + + + The specified Oid ({0}) is not valid. The Oid.FriendlyName or Oid.Value property must be set. + + + The specified Characteristic2 curve parameters are not valid. Polynomial, A, B, G.X, G.Y, and Order are required. A, B, G.X, G.Y must be the same length, and the same length as Q.X, Q.Y and D if those are specified. Cofactor is required. Seed and Hash are optional. Other parameters are not allowed. + + + The specified named curve parameters are not valid. Only the Oid parameter must be set. + + + The specified prime curve parameters are not valid. Prime, A, B, G.X, G.Y and Order are required and must be the same length, and the same length as Q.X, Q.Y and D if those are specified. Cofactor is required. Seed and Hash are optional. Other parameters are not allowed. + Specified key is not a valid size for this algorithm. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index d36474698f2bc2..9a42cddaa5085c 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -396,6 +396,16 @@ Link="Common\System\Security\Cryptography\DSAKeyFormatHelper.cs" /> + + + + + - - - + @@ -582,8 +590,6 @@ - - diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs new file mode 100644 index 00000000000000..019ff9b950e6d9 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs @@ -0,0 +1,835 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Formats.Asn1; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Asn1; + +namespace System.Security.Cryptography +{ + internal static partial class EccKeyFormatHelper + { + // This is the same limit that OpenSSL 1.0.2-1.1.1 has, + // there's no real point reading anything bigger than this (for now). + private const int MaxFieldBitSize = 661; + + private static readonly string[] s_validOids = + { + Oids.EcPublicKey, + }; + + internal static void ReadSubjectPublicKeyInfo( + ReadOnlySpan source, + out int bytesRead, + out ECParameters key) + { + KeyFormatHelper.ReadSubjectPublicKeyInfo( + s_validOids, + source, + FromECPublicKey, + out bytesRead, + out key); + } + + internal static ReadOnlyMemory ReadSubjectPublicKeyInfo( + ReadOnlyMemory source, + out int bytesRead) + { + return KeyFormatHelper.ReadSubjectPublicKeyInfo( + s_validOids, + source, + out bytesRead); + } + + internal static void ReadEncryptedPkcs8( + ReadOnlySpan source, + ReadOnlySpan password, + out int bytesRead, + out ECParameters key) + { + KeyFormatHelper.ReadEncryptedPkcs8( + s_validOids, + source, + password, + FromECPrivateKey, + out bytesRead, + out key); + } + + internal static void ReadEncryptedPkcs8( + ReadOnlySpan source, + ReadOnlySpan passwordBytes, + out int bytesRead, + out ECParameters key) + { + KeyFormatHelper.ReadEncryptedPkcs8( + s_validOids, + source, + passwordBytes, + FromECPrivateKey, + out bytesRead, + out key); + } + + internal static unsafe ECParameters FromECPrivateKey(ReadOnlySpan key, out int bytesRead) + { + try + { + AsnDecoder.ReadEncodedValue( + key, + AsnEncodingRules.BER, + out _, + out _, + out int firstValueLength); + + fixed (byte* ptr = &MemoryMarshal.GetReference(key)) + { + using (MemoryManager manager = new PointerMemoryManager(ptr, firstValueLength)) + { + AlgorithmIdentifierAsn algId = default; + FromECPrivateKey(manager.Memory, algId, out ECParameters ret); + bytesRead = firstValueLength; + return ret; + } + } + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + + internal static void FromECPrivateKey( + ReadOnlyMemory keyData, + in AlgorithmIdentifierAsn algId, + out ECParameters ret) + { + ECPrivateKey key = ECPrivateKey.Decode(keyData, AsnEncodingRules.BER); + FromECPrivateKey(key, algId, out ret); + } + + internal static void FromECPrivateKey( + ECPrivateKey key, + in AlgorithmIdentifierAsn algId, + out ECParameters ret) + { + ValidateParameters(key.Parameters, algId); + + if (key.Version != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ECPoint publicKey = key.PublicKey is not null + ? GetECPointFromUncompressedPublicKey(key.PublicKey.Value.Span, key.PrivateKey.Length) + : default(ECPoint); + + ECDomainParameters domainParameters; + + if (key.Parameters != null) + { + domainParameters = key.Parameters.Value; + } + else + { + domainParameters = ECDomainParameters.Decode(algId.Parameters!.Value, AsnEncodingRules.DER); + } + + ret = new ECParameters + { + Curve = GetCurve(domainParameters), + Q = publicKey, + D = key.PrivateKey.ToArray(), + }; + + ret.Validate(); + } + + internal static void FromECPublicKey( + ReadOnlyMemory key, + in AlgorithmIdentifierAsn algId, + out ECParameters ret) + { + if (algId.Parameters == null) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlySpan publicKeyBytes = key.Span; + + if (publicKeyBytes.Length == 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // Implementation limitation. + // 04 (Uncompressed ECPoint) is almost always used. + if (publicKeyBytes[0] != 0x04) + { + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + // https://www.secg.org/sec1-v2.pdf, 2.3.4, #3 (M has length 2 * CEIL(log2(q)/8) + 1) + if ((publicKeyBytes.Length & 0x01) != 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + int fieldWidth = publicKeyBytes.Length / 2; + + ECDomainParameters domainParameters = ECDomainParameters.Decode( + algId.Parameters.Value, + AsnEncodingRules.DER); + + ret = new ECParameters + { + Curve = GetCurve(domainParameters), + Q = + { + X = publicKeyBytes.Slice(1, fieldWidth).ToArray(), + Y = publicKeyBytes.Slice(1 + fieldWidth).ToArray(), + }, + }; + + ret.Validate(); + } + + private static void ValidateParameters(ECDomainParameters? keyParameters, in AlgorithmIdentifierAsn algId) + { + // At least one is required + if (keyParameters == null && algId.Parameters == null) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // If they are both specified they must match. + if (keyParameters != null && algId.Parameters != null) + { + ReadOnlySpan algIdParameters = algId.Parameters.Value.Span; + + // X.509 SubjectPublicKeyInfo specifies DER encoding. + // RFC 5915 specifies DER encoding for EC Private Keys. + // So we can compare as DER. + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + keyParameters.Value.Encode(writer); + + if (!writer.EncodedValueEquals(algIdParameters)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + } + + private static ECCurve GetCurve(ECDomainParameters domainParameters) + { + if (domainParameters.Specified.HasValue) + { + return GetSpecifiedECCurve(domainParameters.Specified.Value); + } + + if (domainParameters.Named == null) + { + throw new CryptographicException(SR.Cryptography_ECC_NamedCurvesOnly); + } + + Oid curveOid = domainParameters.Named switch { + Oids.secp256r1 => Oids.secp256r1Oid, + Oids.secp384r1 => Oids.secp384r1Oid, + Oids.secp521r1 => Oids.secp521r1Oid, + _ => new Oid(domainParameters.Named, null) + }; + + return ECCurve.CreateFromOid(curveOid); + } + + private static ECCurve GetSpecifiedECCurve(SpecifiedECDomain specifiedParameters) + { + try + { + return GetSpecifiedECCurveCore(specifiedParameters); + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + + private static ECCurve GetSpecifiedECCurveCore(SpecifiedECDomain specifiedParameters) + { + // sec1-v2 C.3: + // + // Versions 1, 2, and 3 are defined. + // 1 is just data, 2 and 3 mean that a seed is required (with different reasons for why, + // but they're human-reasons, not technical ones). + if (specifiedParameters.Version < 1 || specifiedParameters.Version > 3) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (specifiedParameters.Version > 1 && !specifiedParameters.Curve.Seed.HasValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + byte[] primeOrPoly; + bool prime; + + switch (specifiedParameters.FieldID.FieldType) + { + case Oids.EcPrimeField: + prime = true; + AsnReader primeReader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); + ReadOnlySpan primeValue = primeReader.ReadIntegerBytes().Span; + primeReader.ThrowIfNotEmpty(); + + if (primeValue[0] == 0) + { + primeValue = primeValue.Slice(1); + } + + if (primeValue.Length > (MaxFieldBitSize / 8)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + primeOrPoly = primeValue.ToArray(); + break; + case Oids.EcChar2Field: + prime = false; + AsnReader char2Reader = new AsnReader(specifiedParameters.FieldID.Parameters, AsnEncodingRules.BER); + AsnReader innerReader = char2Reader.ReadSequence(); + char2Reader.ThrowIfNotEmpty(); + + // Characteristic-two ::= SEQUENCE + // { + // m INTEGER, -- Field size + // basis CHARACTERISTIC-TWO.&id({BasisTypes}), + // parameters CHARACTERISTIC-TWO.&Type({BasisTypes}{@basis}) + // } + + if (!innerReader.TryReadInt32(out int m) || m > MaxFieldBitSize || m < 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + int k1; + int k2 = -1; + int k3 = -1; + + switch (innerReader.ReadObjectIdentifier()) + { + case Oids.EcChar2TrinomialBasis: + // Trinomial ::= INTEGER + if (!innerReader.TryReadInt32(out k1) || k1 >= m || k1 < 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + break; + case Oids.EcChar2PentanomialBasis: + // Pentanomial ::= SEQUENCE + // { + // k1 INTEGER, -- k1 > 0 + // k2 INTEGER, -- k2 > k1 + // k3 INTEGER -- k3 > k2 + // } + AsnReader pentanomialReader = innerReader.ReadSequence(); + + if (!pentanomialReader.TryReadInt32(out k1) || + !pentanomialReader.TryReadInt32(out k2) || + !pentanomialReader.TryReadInt32(out k3) || + k1 < 1 || + k2 <= k1 || + k3 <= k2 || + k3 >= m) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + pentanomialReader.ThrowIfNotEmpty(); + + break; + default: + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + innerReader.ThrowIfNotEmpty(); + + BitArray poly = new BitArray(m + 1); + poly.Set(m, true); + poly.Set(k1, true); + poly.Set(0, true); + + if (k2 > 0) + { + poly.Set(k2, true); + poly.Set(k3, true); + } + + primeOrPoly = new byte[(m + 7) / 8]; + poly.CopyTo(primeOrPoly, 0); + Array.Reverse(primeOrPoly); + break; + default: + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ECCurve curve; + + if (prime) + { + curve = new ECCurve + { + CurveType = ECCurve.ECCurveType.PrimeShortWeierstrass, + Prime = primeOrPoly, + }; + } + else + { + curve = new ECCurve + { + CurveType = ECCurve.ECCurveType.Characteristic2, + Polynomial = primeOrPoly, + }; + } + + curve.A = specifiedParameters.Curve.A.ToUnsignedIntegerBytes(primeOrPoly.Length); + curve.B = specifiedParameters.Curve.B.ToUnsignedIntegerBytes(primeOrPoly.Length); + curve.Order = specifiedParameters.Order.ToUnsignedIntegerBytes(primeOrPoly.Length); + + ReadOnlySpan baseSpan = specifiedParameters.Base.Span; + + // We only understand the uncompressed point encoding, but that's almost always what's used. + if (baseSpan[0] != 0x04 || baseSpan.Length != 2 * primeOrPoly.Length + 1) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + curve.G.X = baseSpan.Slice(1, primeOrPoly.Length).ToArray(); + curve.G.Y = baseSpan.Slice(1 + primeOrPoly.Length).ToArray(); + + if (specifiedParameters.Cofactor.HasValue) + { + curve.Cofactor = specifiedParameters.Cofactor.Value.ToUnsignedIntegerBytes(); + } + + return curve; + } + + internal static AsnWriter WriteSubjectPublicKeyInfo(ECParameters ecParameters) + { + ecParameters.Validate(); + + // Since the public key format for EC keys is not ASN.1, + // write the SPKI structure manually. + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + // SubjectPublicKeyInfo + writer.PushSequence(); + + // algorithm + WriteAlgorithmIdentifier(ecParameters, writer); + + // subjectPublicKey + WriteUncompressedPublicKey(ecParameters, writer); + + writer.PopSequence(); + return writer; + } + + private static AsnWriter WriteAlgorithmIdentifier(in ECParameters ecParameters) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + WriteAlgorithmIdentifier(ecParameters, writer); + return writer; + } + + private static void WriteAlgorithmIdentifier(in ECParameters ecParameters, AsnWriter writer) + { + writer.PushSequence(); + + writer.WriteObjectIdentifier(Oids.EcPublicKey); + WriteEcParameters(ecParameters, writer); + + writer.PopSequence(); + } + + internal static AsnWriter WritePkcs8PrivateKey(ECParameters ecParameters, AttributeAsn[]? attributes = null) + { + ecParameters.Validate(); + + if (ecParameters.D == null) + { + throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); + } + + // Don't need the domain parameters because they're contained in the algId. + AsnWriter ecPrivateKey = WriteEcPrivateKey(ecParameters, includeDomainParameters: false); + AsnWriter algorithmIdentifier = WriteAlgorithmIdentifier(ecParameters); + AsnWriter? attributeWriter = WritePrivateKeyInfoAttributes(attributes); + + return KeyFormatHelper.WritePkcs8(algorithmIdentifier, ecPrivateKey, attributeWriter); + } + + [return: NotNullIfNotNull(nameof(attributes))] + private static AsnWriter? WritePrivateKeyInfoAttributes(AttributeAsn[]? attributes) + { + if (attributes == null) + return null; + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + Asn1Tag tag = new Asn1Tag(TagClass.ContextSpecific, 0); + writer.PushSetOf(tag); + + for (int i = 0; i < attributes.Length; i++) + { + attributes[i].Encode(writer); + } + + writer.PopSetOf(tag); + return writer; + } + + private static void WriteEcParameters(ECParameters ecParameters, AsnWriter writer) + { + if (ecParameters.Curve.IsNamed) + { + Oid oid = ecParameters.Curve.Oid; + + // On Windows the FriendlyName is populated in places where the Value mightn't be. + if (string.IsNullOrEmpty(oid.Value)) + { + Debug.Assert(oid.FriendlyName != null); + oid = Oid.FromFriendlyName(oid.FriendlyName, OidGroup.All); + } + + writer.WriteObjectIdentifier(oid.Value!); + } + else if (ecParameters.Curve.IsExplicit) + { + Debug.Assert(ecParameters.Curve.IsPrime || ecParameters.Curve.IsCharacteristic2); + WriteSpecifiedECDomain(ecParameters, writer); + } + else + { + throw new CryptographicException( + SR.Format(SR.Cryptography_CurveNotSupported, ecParameters.Curve.CurveType.ToString())); + } + } + + private static void WriteSpecifiedECDomain(ECParameters ecParameters, AsnWriter writer) + { + int m; + int k1; + int k2; + int k3; + m = k1 = k2 = k3 = -1; + + if (ecParameters.Curve.IsCharacteristic2) + { + DetermineChar2Parameters(ecParameters, ref m, ref k1, ref k2, ref k3); + } + + // SpecifiedECDomain + writer.PushSequence(); + { + // version + // We don't know if the seed (if present) is verifiably random (2). + // We also don't know if the base point is verifiably random (3). + // So just be version 1. + writer.WriteInteger(1); + + // fieldId + writer.PushSequence(); + { + if (ecParameters.Curve.IsPrime) + { + writer.WriteObjectIdentifier(Oids.EcPrimeField); + writer.WriteIntegerUnsigned(ecParameters.Curve.Prime); + } + else + { + Debug.Assert(ecParameters.Curve.IsCharacteristic2); + + // id + writer.WriteObjectIdentifier(Oids.EcChar2Field); + + // Parameters (Characteristic-two) + writer.PushSequence(); + { + // m + writer.WriteInteger(m); + + if (k3 > 0) + { + writer.WriteObjectIdentifier(Oids.EcChar2PentanomialBasis); + + writer.PushSequence(); + { + writer.WriteInteger(k1); + writer.WriteInteger(k2); + writer.WriteInteger(k3); + + writer.PopSequence(); + } + } + else + { + Debug.Assert(k2 < 0); + Debug.Assert(k1 > 0); + + writer.WriteObjectIdentifier(Oids.EcChar2TrinomialBasis); + writer.WriteInteger(k1); + } + + writer.PopSequence(); + } + } + + writer.PopSequence(); + } + + // curve + WriteCurve(ecParameters.Curve, writer); + + // base + WriteUncompressedBasePoint(ecParameters, writer); + + // order + writer.WriteIntegerUnsigned(ecParameters.Curve.Order); + + // cofactor + if (ecParameters.Curve.Cofactor != null) + { + writer.WriteIntegerUnsigned(ecParameters.Curve.Cofactor); + } + + // hash is omitted. + + writer.PopSequence(); + } + } + + private static void DetermineChar2Parameters( + in ECParameters ecParameters, + ref int m, + ref int k1, + ref int k2, + ref int k3) + { + byte[] polynomial = ecParameters.Curve.Polynomial!; + int lastIndex = polynomial.Length - 1; + + // The most significant byte needs a set bit, and the least significant bit must be set. + if (polynomial[0] == 0 || (polynomial[lastIndex] & 1) != 1) + { + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + + for (int localBitIndex = 7; localBitIndex >= 0; localBitIndex--) + { + int test = 1 << localBitIndex; + + if ((polynomial[0] & test) == test) + { + m = checked(8 * lastIndex + localBitIndex); + } + } + + // Find the other set bits. Since we've already found m and 0, there is either + // one remaining (trinomial) or 3 (pentanomial). + for (int inverseIndex = 0; inverseIndex < polynomial.Length; inverseIndex++) + { + int forwardIndex = lastIndex - inverseIndex; + byte val = polynomial[forwardIndex]; + + for (int localBitIndex = 0; localBitIndex < 8; localBitIndex++) + { + int test = 1 << localBitIndex; + + if ((val & test) == test) + { + int bitIndex = 8 * inverseIndex + localBitIndex; + + if (bitIndex == 0) + { + // The bottom bit is always set, it's not considered a parameter. + } + else if (bitIndex == m) + { + break; + } + else if (k1 < 0) + { + k1 = bitIndex; + } + else if (k2 < 0) + { + k2 = bitIndex; + } + else if (k3 < 0) + { + k3 = bitIndex; + } + else + { + // More than pentanomial. + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + } + } + } + + if (k3 > 0) + { + // Pentanomial + } + else if (k2 > 0) + { + // There is no quatranomial + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + else if (k1 > 0) + { + // Trinomial + } + else + { + // No smaller bases exist + throw new CryptographicException(SR.Cryptography_InvalidECCharacteristic2Curve); + } + } + + private static void WriteCurve(in ECCurve curve, AsnWriter writer) + { + writer.PushSequence(); + WriteFieldElement(curve.A!, writer); + WriteFieldElement(curve.B!, writer); + + if (curve.Seed != null) + { + writer.WriteBitString(curve.Seed); + } + + writer.PopSequence(); + } + + private static void WriteFieldElement(byte[] fieldElement, AsnWriter writer) + { + int start = 0; + + while (start < fieldElement.Length - 1 && fieldElement[start] == 0) + { + start++; + } + + writer.WriteOctetString(fieldElement.AsSpan(start)); + } + + private static void WriteUncompressedBasePoint(in ECParameters ecParameters, AsnWriter writer) + { + int basePointLength = ecParameters.Curve.G.X!.Length * 2 + 1; + + // A NIST P-521 G will be at most 133 bytes (NIST 186-4 defines G.) + // 256 should be plenty for all but very atypical uses. + const int MaxStackAllocSize = 256; + Span basePointBytes = stackalloc byte[MaxStackAllocSize]; + byte[]? rented = null; + + if (basePointLength > MaxStackAllocSize) + { + basePointBytes = rented = CryptoPool.Rent(basePointLength); + } + + basePointBytes[0] = 0x04; + ecParameters.Curve.G.X.CopyTo(basePointBytes.Slice(1)); + ecParameters.Curve.G.Y.CopyTo(basePointBytes.Slice(1 + ecParameters.Curve.G.X.Length)); + + writer.WriteOctetString(basePointBytes.Slice(0, basePointLength)); + + if (rented is not null) + { + // G contains public EC parameters that are not sensitive. + CryptoPool.Return(rented, clearSize: 0); + } + } + + private static void WriteUncompressedPublicKey(in ECParameters ecParameters, AsnWriter writer) + { + int publicKeyLength = ecParameters.Q.X!.Length * 2 + 1; + + // A NIST P-521 Q will encode to 133 bytes: (521 + 7)/8 * 2 + 1. + // 256 should be plenty for all but very atypical uses. + const int MaxStackAllocSize = 256; + Span publicKeyBytes = stackalloc byte[MaxStackAllocSize]; + byte[]? rented = null; + + if (publicKeyLength > MaxStackAllocSize) + { + publicKeyBytes = rented = CryptoPool.Rent(publicKeyLength); + } + + publicKeyBytes[0] = 0x04; + ecParameters.Q.X.CopyTo(publicKeyBytes.Slice(1)); + ecParameters.Q.Y.CopyTo(publicKeyBytes.Slice(1 + ecParameters.Q.X!.Length)); + + writer.WriteBitString(publicKeyBytes.Slice(0, publicKeyLength)); + + if (rented is not null) + { + // Q contains public EC parameters that are not sensitive. + CryptoPool.Return(rented, clearSize: 0); + } + } + + internal static AsnWriter WriteECPrivateKey(in ECParameters ecParameters) + { + return WriteEcPrivateKey(ecParameters, includeDomainParameters: true); + } + + private static AsnWriter WriteEcPrivateKey(in ECParameters ecParameters, bool includeDomainParameters) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + // ECPrivateKey + writer.PushSequence(); + + // version 1 + writer.WriteInteger(1); + + // privateKey + writer.WriteOctetString(ecParameters.D); + + // domainParameters + if (includeDomainParameters) + { + Asn1Tag explicit0 = new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true); + writer.PushSequence(explicit0); + + WriteEcParameters(ecParameters, writer); + + writer.PopSequence(explicit0); + } + + // publicKey + if (ecParameters.Q.X != null) + { + Debug.Assert(ecParameters.Q.Y != null); + Asn1Tag explicit1 = new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true); + writer.PushSequence(explicit1); + + WriteUncompressedPublicKey(ecParameters, writer); + + writer.PopSequence(explicit1); + } + + writer.PopSequence(); + return writer; + } + } +} From 95f59e45cab34ccd677bb3b7fd62433cb5b9d29a Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Fri, 1 Aug 2025 00:41:48 -0700 Subject: [PATCH 4/6] Remove ECParameters from netfx again --- .../CompositeMLDsaManaged.ECDsa.cs | 37 +++++++++---------- .../Cryptography/CompositeMLDsaManaged.cs | 2 + .../Cryptography/EccKeyFormatHelper.cs | 9 ++--- .../CompositeMLDsaFactoryTests.cs | 13 ++----- .../CompositeMLDsa/CompositeMLDsaTestData.cs | 5 +++ .../CompositeMLDsaTestHelpers.cs | 2 + .../src/Microsoft.Bcl.Cryptography.csproj | 10 ----- .../src/System.Security.Cryptography.csproj | 15 +++----- .../Cryptography/ECCurve.ECCurveType.cs | 7 +--- .../Cryptography/ECCurve.NamedCurves.cs | 7 +--- .../System/Security/Cryptography/ECCurve.cs | 8 +--- .../Security/Cryptography/ECParameters.cs | 7 +--- .../System/Security/Cryptography/ECPoint.cs | 7 +--- .../Cryptography/EccKeyFormatHelper.cs | 18 +++++++-- 14 files changed, 57 insertions(+), 90 deletions(-) rename src/libraries/{Common => System.Security.Cryptography}/src/System/Security/Cryptography/ECCurve.ECCurveType.cs (87%) rename src/libraries/{Common => System.Security.Cryptography}/src/System/Security/Cryptography/ECCurve.NamedCurves.cs (98%) rename src/libraries/{Common => System.Security.Cryptography}/src/System/Security/Cryptography/ECCurve.cs (97%) rename src/libraries/{Common => System.Security.Cryptography}/src/System/Security/Cryptography/ECParameters.cs (96%) rename src/libraries/{Common => System.Security.Cryptography}/src/System/Security/Cryptography/ECPoint.cs (81%) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs index 0127ddabc9192b..9bb1690744669b 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs @@ -84,15 +84,13 @@ public static unsafe ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, R } } - ECParameters parameters = new ECParameters - { - Curve = algorithm.Curve, - }; + byte[]? x = null; + byte[]? y = null; // If public key is present, add it to the parameters. if (ecPrivateKey.PublicKey is ReadOnlyMemory publicKey) { - parameters.Q = EccKeyFormatHelper.GetECPointFromUncompressedPublicKey(publicKey.Span, algorithm.KeySizeInBytes); + EccKeyFormatHelper.GetECPointFromUncompressedPublicKey(publicKey.Span, algorithm.KeySizeInBytes, out x, out y); } byte[] d = new byte[ecPrivateKey.PrivateKey.Length]; @@ -100,11 +98,21 @@ public static unsafe ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, R using (PinAndClear.Track(d)) { ecPrivateKey.PrivateKey.CopyTo(d); - parameters.D = d; + +#if NET + ECParameters parameters = new ECParameters + { + Curve = algorithm.Curve, + Q = new ECPoint + { + X = x, + Y = y, + }, + D = d + }; parameters.Validate(); -#if NET return new ECDsaComponent(ECDsa.Create(parameters), algorithm); #else throw new PlatformNotSupportedException(); @@ -137,6 +145,7 @@ public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, Re throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); } +#if NET ECParameters parameters = new ECParameters() { Curve = ECCurve.CreateFromValue(algorithm.CurveOid), @@ -147,19 +156,7 @@ public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, Re } }; -#if NET - ECDsa? ecdsa = null; - - try - { - ecdsa = ECDsa.Create(parameters); - return new ECDsaComponent(ecdsa, algorithm); - } - catch (CryptographicException) - { - ecdsa?.Dispose(); - throw; - } + return new ECDsaComponent(ECDsa.Create(parameters), algorithm); #else throw new PlatformNotSupportedException(); #endif diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs index 0fab448bb21ce6..e5467e24d64494 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs @@ -730,6 +730,7 @@ private sealed class ECDsaAlgorithm(int keySizeInBits, string curveOid, HashAlgo internal int KeySizeInBytes => (KeySizeInBits + 7) / 8; +#if NET internal ECCurve Curve { get @@ -749,6 +750,7 @@ static ECCurve FailAndThrow(string oid) } } } +#endif } private sealed class EdDsaAlgorithm diff --git a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs index c881231a85e50a..350773bf98dcbc 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs @@ -5,7 +5,7 @@ namespace System.Security.Cryptography { internal static partial class EccKeyFormatHelper { - internal static ECPoint GetECPointFromUncompressedPublicKey(ReadOnlySpan publicKey, int fieldWidthInBytes) + internal static void GetECPointFromUncompressedPublicKey(ReadOnlySpan publicKey, int fieldWidthInBytes, out byte[] x, out byte[] y) { if (publicKey.Length == 0) { @@ -25,11 +25,8 @@ internal static ECPoint GetECPointFromUncompressedPublicKey(ReadOnlySpan p throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - return new ECPoint - { - X = publicKey.Slice(1, fieldWidthInBytes).ToArray(), - Y = publicKey.Slice(1 + fieldWidthInBytes).ToArray(), - }; + x = publicKey.Slice(1, fieldWidthInBytes).ToArray(); + y = publicKey.Slice(1 + fieldWidthInBytes).ToArray(); } } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs index d8d591eb96bb4e..2a80dd2525bd0e 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs @@ -186,14 +186,9 @@ public static void ImportBadPublicKey_TrailingData(CompositeMLDsaTestData.Compos } [Theory] - [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + [MemberData(nameof(CompositeMLDsaTestData.SupportedECDsaAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void ImportBadPublicKey_ECDsa_Uncompressed(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) { - if (CompositeMLDsaTestHelpers.ExecuteComponentFunc(vector.Algorithm, _ => true, ecdsa => false, _ => true)) - { - return; - } - byte[] key = vector.PublicKey.AsSpan().ToArray(); int formatIndex = CompositeMLDsaTestHelpers.MLDsaAlgorithms[vector.Algorithm].PublicKeySizeInBytes; @@ -323,13 +318,13 @@ public static void IsAlgorithmSupported_AgreesWithPlatform(CompositeMLDsaAlgorit { bool supported = CompositeMLDsaTestHelpers.ExecuteComponentFunc( algorithm, - _ => MLDsa.IsSupported, + rsa => MLDsa.IsSupported, #if NET ecdsa => ecdsa.IsSec && MLDsa.IsSupported, #else - _ => false, + ecdsa => false, #endif - _ => false); + eddsa => false); Assert.Equal( supported, diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs index e6bf7084e25a8b..65bb8c366609a9 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestData.cs @@ -46,6 +46,11 @@ internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, str public static IEnumerableSupportedAlgorithmIetfVectorsTestData => SupportedAlgorithmIetfVectors.Select(v => new object[] { v }); + public static IEnumerable SupportedECDsaAlgorithmIetfVectorsTestData => + SupportedAlgorithmIetfVectors + .Where(vector => CompositeMLDsa.IsAlgorithmSupported(vector.Algorithm) && CompositeMLDsaTestHelpers.IsECDsa(vector.Algorithm)) + .Select(v => new object[] { v }); + internal static CompositeMLDsaAlgorithm[] AllAlgorithms => field ??= [ CompositeMLDsaAlgorithm.MLDsa44WithRSA2048Pss, diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs index b98fc014a19a63..bc7dda2e2103a4 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.cs @@ -154,6 +154,8 @@ internal static T ExecuteComponentFunc( } } + internal static bool IsECDsa(CompositeMLDsaAlgorithm algorithm) => ExecuteComponentFunc(algorithm, rsa => false, ecdsa => true, eddsa => false); + internal static void AssertExportPublicKey(Action> callback) { callback(dsa => dsa.ExportCompositeMLDsaPublicKey()); diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index b67aa8daef64f3..9199288186c28d 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -370,16 +370,6 @@ Link="Common\System\Security\Cryptography\AesAEAD.cs" /> - - - - - diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 9a42cddaa5085c..bb7248e78862bb 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -396,16 +396,6 @@ Link="Common\System\Security\Cryptography\DSAKeyFormatHelper.cs" /> - - - - - + + + @@ -590,6 +583,8 @@ + + diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECCurve.ECCurveType.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.ECCurveType.cs similarity index 87% rename from src/libraries/Common/src/System/Security/Cryptography/ECCurve.ECCurveType.cs rename to src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.ECCurveType.cs index 32db002736239e..f935be1e6b29a6 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECCurve.ECCurveType.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.ECCurveType.cs @@ -5,12 +5,7 @@ namespace System.Security.Cryptography { -#if NETFRAMEWORK - internal -#else - public -#endif - partial struct ECCurve + public partial struct ECCurve { /// /// Represents the type of elliptic curve. diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECCurve.NamedCurves.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.NamedCurves.cs similarity index 98% rename from src/libraries/Common/src/System/Security/Cryptography/ECCurve.NamedCurves.cs rename to src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.NamedCurves.cs index 2aae6ebf7080d8..b5e99ff782ae84 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECCurve.NamedCurves.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.NamedCurves.cs @@ -5,12 +5,7 @@ namespace System.Security.Cryptography { -#if NETFRAMEWORK - internal -#else - public -#endif - partial struct ECCurve + public partial struct ECCurve { /// /// Factory class for creating named curves. diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECCurve.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.cs similarity index 97% rename from src/libraries/Common/src/System/Security/Cryptography/ECCurve.cs rename to src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.cs index f220560f666316..f8c66ee70f67c6 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECCurve.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECCurve.cs @@ -14,13 +14,7 @@ namespace System.Security.Cryptography /// which is either a prime curve or a characteristic-2 curve. /// [DebuggerDisplay("ECCurve = {Oid}")] -#if NETFRAMEWORK -#pragma warning disable CS0649 // Field 'ECCurve.G' is never assigned to, and will always have its default value - internal -#else - public -#endif - partial struct ECCurve + public partial struct ECCurve { /// /// Coefficient A. Applies only to Explicit curves. diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECParameters.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECParameters.cs similarity index 96% rename from src/libraries/Common/src/System/Security/Cryptography/ECParameters.cs rename to src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECParameters.cs index e1fe0e32f08b22..165f9877fcef84 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECParameters.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECParameters.cs @@ -6,12 +6,7 @@ namespace System.Security.Cryptography /// /// Represents the public and private key of the specified elliptic curve. /// -#if NETFRAMEWORK - internal -#else - public -#endif - struct ECParameters + public struct ECParameters { /// /// Public point. diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECPoint.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECPoint.cs similarity index 81% rename from src/libraries/Common/src/System/Security/Cryptography/ECPoint.cs rename to src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECPoint.cs index 73c60374f3b418..26af121e9eb46d 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECPoint.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/ECPoint.cs @@ -6,12 +6,7 @@ namespace System.Security.Cryptography /// /// Represents a point on an elliptic curve. /// -#if NETFRAMEWORK - internal -#else - public -#endif - struct ECPoint + public struct ECPoint { public byte[]? X; public byte[]? Y; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs index 019ff9b950e6d9..23a27cc33c3bd2 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs @@ -124,9 +124,13 @@ internal static void FromECPrivateKey( throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - ECPoint publicKey = key.PublicKey is not null - ? GetECPointFromUncompressedPublicKey(key.PublicKey.Value.Span, key.PrivateKey.Length) - : default(ECPoint); + byte[]? x = null; + byte[]? y = null; + + if (key.PublicKey is not null) + { + GetECPointFromUncompressedPublicKey(key.PublicKey.Value.Span, key.PrivateKey.Length, out x, out y); + } ECDomainParameters domainParameters; @@ -139,10 +143,16 @@ internal static void FromECPrivateKey( domainParameters = ECDomainParameters.Decode(algId.Parameters!.Value, AsnEncodingRules.DER); } + Debug.Assert((x == null) == (y == null)); + ret = new ECParameters { Curve = GetCurve(domainParameters), - Q = publicKey, + Q = new ECPoint + { + X = x, + Y = y, + }, D = key.PrivateKey.ToArray(), }; From 4a61dfed848cfc1c332985bb8dea24a967b5d674 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Fri, 1 Aug 2025 13:06:49 -0700 Subject: [PATCH 5/6] Export ECPrivateKey via ECParameters --- .../CompositeMLDsaManaged.ECDsa.cs | 92 ++++++++++++++++--- .../Cryptography/EccKeyFormatHelper.cs | 30 ++++++ .../src/Resources/Strings.resx | 18 ---- .../Cryptography/EccKeyFormatHelper.cs | 25 +---- 4 files changed, 112 insertions(+), 53 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs index 9bb1690744669b..d898dc7beae9e3 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs @@ -30,7 +30,9 @@ private ECDsaComponent(ECDsa ecdsa, ECDsaAlgorithm algorithm) _algorithm = algorithm; } - // OpenSSL supports the brainpool curves so this can be relaxed on a per-platform basis in the future if desired. + // While some of our OSes support the brainpool curves, not all do. + // Limit this implementation to the NIST curves until we have a better understanding + // of where native implementations of composite are aligning. public static bool IsAlgorithmSupported(ECDsaAlgorithm algorithm) => #if NET algorithm.CurveOid is Oids.secp256r1 or Oids.secp384r1 or Oids.secp521r1; @@ -148,7 +150,7 @@ public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, Re #if NET ECParameters parameters = new ECParameters() { - Curve = ECCurve.CreateFromValue(algorithm.CurveOid), + Curve = algorithm.Curve, Q = new ECPoint() { X = source.Slice(1, fieldWidth).ToArray(), @@ -165,10 +167,78 @@ public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, Re internal override bool TryExportPrivateKey(Span destination, out int bytesWritten) { #if NET - return _ecdsa.TryExportECPrivateKey(destination, out bytesWritten); + ECParameters ecParameters = _ecdsa.ExportParameters(includePrivateParameters: true); + + Debug.Assert(ecParameters.D != null); + + using (PinAndClear.Track(ecParameters.D)) + { + ecParameters.Validate(); + + if (ecParameters.D.Length != _algorithm.KeySizeInBytes) + { + Debug.Fail("Unexpected key size."); + throw new CryptographicException(); + } + + // The curve OID must match the composite ML-DSA algorithm. + if (!ecParameters.Curve.IsNamed || + (ecParameters.Curve.Oid.Value != _algorithm.Curve.Oid.Value && ecParameters.Curve.Oid.FriendlyName != _algorithm.Curve.Oid.FriendlyName)) + { + Debug.Fail("Unexpected curve OID."); + throw new CryptographicException(); + } + + return TryWriteKey(ecParameters.D, ecParameters.Q.X, ecParameters.Q.Y, _algorithm.CurveOid, destination, out bytesWritten); + } #else - throw new PlatformNotSupportedException(); + return TryWriteKey(d: [], x: [], y: [], curveOid: null, destination, out bytesWritten); #endif + + static bool TryWriteKey(byte[] d, byte[]? x, byte[]? y, string curveOid, Span destination, out int bytesWritten) + { +#if NET + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + try + { + // ECPrivateKey + using (writer.PushSequence()) + { + // version 1 + writer.WriteInteger(1); + + // privateKey + writer.WriteOctetString(d); + + // domainParameters + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true))) + { + writer.WriteObjectIdentifier(curveOid); + } + + // publicKey + if (x != null) + { + Debug.Assert(y != null); + + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true))) + { + EccKeyFormatHelper.WriteUncompressedPublicKey(x, y, writer); + } + } + } + + return writer.TryEncode(destination, out bytesWritten); + } + finally + { + writer.Reset(); + } +#else + throw new PlatformNotSupportedException(); +#endif + } } internal override bool TryExportPublicKey(Span destination, out int bytesWritten) @@ -184,21 +254,21 @@ internal override bool TryExportPublicKey(Span destination, out int bytesW return false; } - ECParameters parameters = _ecdsa.ExportParameters(includePrivateParameters: false); + ECParameters ecParameters = _ecdsa.ExportParameters(includePrivateParameters: false); + + ecParameters.Validate(); - if (parameters.Q.X is not byte[] x || - parameters.Q.Y is not byte[] y || - x.Length != fieldWidth || - y.Length != fieldWidth) + if (ecParameters.Q.X?.Length != fieldWidth) { + Debug.Fail("Unexpected key size."); throw new CryptographicException(); } // Uncompressed ECPoint format destination[0] = 0x04; - x.CopyTo(destination.Slice(1, fieldWidth)); - y.CopyTo(destination.Slice(1 + fieldWidth)); + ecParameters.Q.X.CopyTo(destination.Slice(1, fieldWidth)); + ecParameters.Q.Y.CopyTo(destination.Slice(1 + fieldWidth)); bytesWritten = 1 + 2 * fieldWidth; return true; diff --git a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs index 350773bf98dcbc..72df332a19fade 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/EccKeyFormatHelper.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Formats.Asn1; + namespace System.Security.Cryptography { internal static partial class EccKeyFormatHelper @@ -28,5 +30,33 @@ internal static void GetECPointFromUncompressedPublicKey(ReadOnlySpan publ x = publicKey.Slice(1, fieldWidthInBytes).ToArray(); y = publicKey.Slice(1 + fieldWidthInBytes).ToArray(); } + + internal static void WriteUncompressedPublicKey(byte[] x, byte[] y, AsnWriter writer) + { + int publicKeyLength = x.Length * 2 + 1; + + // A NIST P-521 Q will encode to 133 bytes: (521 + 7)/8 * 2 + 1. + // 256 should be plenty for all but very atypical uses. + const int MaxStackAllocSize = 256; + Span publicKeyBytes = stackalloc byte[MaxStackAllocSize]; + byte[]? rented = null; + + if (publicKeyLength > MaxStackAllocSize) + { + publicKeyBytes = rented = CryptoPool.Rent(publicKeyLength); + } + + publicKeyBytes[0] = 0x04; + x.CopyTo(publicKeyBytes.Slice(1)); + y.CopyTo(publicKeyBytes.Slice(1 + x.Length)); + + writer.WriteBitString(publicKeyBytes.Slice(0, publicKeyLength)); + + if (rented is not null) + { + // Q contains public EC parameters that are not sensitive. + CryptoPool.Return(rented, clearSize: 0); + } + } } } diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx index eef274248873fe..0cc70b1e6d7bce 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -159,9 +159,6 @@ Composite signature generation failed due to an error in one or both of the components. - - The specified curve '{0}' or its parameters are not valid for this platform. - ASN1 corrupted data. @@ -177,21 +174,6 @@ The size of the specified tag does not match the expected size of {0}. - - The specified key parameters are not valid. Q.X and Q.Y, or D, must be specified. Q.X, Q.Y must be the same length. If D is specified it must be the same length as Q.X and Q.Y if also specified for named curves or the same length as Order for explicit curves. - - - The specified Oid ({0}) is not valid. The Oid.FriendlyName or Oid.Value property must be set. - - - The specified Characteristic2 curve parameters are not valid. Polynomial, A, B, G.X, G.Y, and Order are required. A, B, G.X, G.Y must be the same length, and the same length as Q.X, Q.Y and D if those are specified. Cofactor is required. Seed and Hash are optional. Other parameters are not allowed. - - - The specified named curve parameters are not valid. Only the Oid parameter must be set. - - - The specified prime curve parameters are not valid. Prime, A, B, G.X, G.Y and Order are required and must be the same length, and the same length as Q.X, Q.Y and D if those are specified. Cofactor is required. Seed and Hash are optional. Other parameters are not allowed. - Specified key is not a valid size for this algorithm. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs index 23a27cc33c3bd2..daa8b514bca86d 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/EccKeyFormatHelper.cs @@ -771,30 +771,7 @@ private static void WriteUncompressedBasePoint(in ECParameters ecParameters, Asn private static void WriteUncompressedPublicKey(in ECParameters ecParameters, AsnWriter writer) { - int publicKeyLength = ecParameters.Q.X!.Length * 2 + 1; - - // A NIST P-521 Q will encode to 133 bytes: (521 + 7)/8 * 2 + 1. - // 256 should be plenty for all but very atypical uses. - const int MaxStackAllocSize = 256; - Span publicKeyBytes = stackalloc byte[MaxStackAllocSize]; - byte[]? rented = null; - - if (publicKeyLength > MaxStackAllocSize) - { - publicKeyBytes = rented = CryptoPool.Rent(publicKeyLength); - } - - publicKeyBytes[0] = 0x04; - ecParameters.Q.X.CopyTo(publicKeyBytes.Slice(1)); - ecParameters.Q.Y.CopyTo(publicKeyBytes.Slice(1 + ecParameters.Q.X!.Length)); - - writer.WriteBitString(publicKeyBytes.Slice(0, publicKeyLength)); - - if (rented is not null) - { - // Q contains public EC parameters that are not sensitive. - CryptoPool.Return(rented, clearSize: 0); - } + WriteUncompressedPublicKey(ecParameters.Q.X!, ecParameters.Q.Y!, writer); } internal static AsnWriter WriteECPrivateKey(in ECParameters ecParameters) From cb9fada82b96c63f268be2e0bebe67f986e25c36 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Sat, 2 Aug 2025 12:30:42 -0700 Subject: [PATCH 6/6] Add more tests for new code --- .../CompositeMLDsaManaged.ECDsa.cs | 105 ++++------ .../Cryptography/CompositeMLDsaManaged.cs | 4 +- .../CompositeMLDsaFactoryTests.cs | 183 ++++++++++++++++-- .../Microsoft.Bcl.Cryptography.Tests.csproj | 2 + 4 files changed, 213 insertions(+), 81 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs index d898dc7beae9e3..82111e0cf43d7f 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.ECDsa.cs @@ -6,6 +6,7 @@ using System.Formats.Asn1; using System.Runtime.InteropServices; using System.Security.Cryptography.Asn1; +using Internal.Cryptography; namespace System.Security.Cryptography { @@ -51,83 +52,65 @@ public static ECDsaComponent GenerateKey(ECDsaAlgorithm algorithm) public static unsafe ECDsaComponent ImportPrivateKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) { - try + Helpers.ThrowIfAsnInvalidLength(source); + + fixed (byte* ptr = &MemoryMarshal.GetReference(source)) { - AsnDecoder.ReadEncodedValue( - source, - AsnEncodingRules.BER, - out _, - out _, - out int firstValueLength); - - if (firstValueLength != source.Length) + using (MemoryManager manager = new PointerMemoryManager(ptr, source.Length)) { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } + ECPrivateKey ecPrivateKey = ECPrivateKey.Decode(manager.Memory, AsnEncodingRules.BER); - fixed (byte* ptr = &MemoryMarshal.GetReference(source)) - { - using (MemoryManager manager = new PointerMemoryManager(ptr, firstValueLength)) + if (ecPrivateKey.Version != 1) { - ECPrivateKey ecPrivateKey = ECPrivateKey.Decode(manager.Memory, AsnEncodingRules.BER); + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } - if (ecPrivateKey.Version != 1) + // If domain parameters are present, validate that they match the composite ML-DSA algorithm. + if (ecPrivateKey.Parameters is ECDomainParameters domainParameters) + { + if (domainParameters.Named is not string curveOid || curveOid != algorithm.CurveOid) { + // The curve specified must be named and match the required curve for the composite ML-DSA algorithm. throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } + } - // If domain parameters are present, validate that they match the composite ML-DSA algorithm. - if (ecPrivateKey.Parameters is ECDomainParameters domainParameters) - { - if (domainParameters.Named is not string curveOid || curveOid != algorithm.CurveOid) - { - // The curve specified must be named and match the required curve for the composite ML-DSA algorithm. - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - } - - byte[]? x = null; - byte[]? y = null; + byte[]? x = null; + byte[]? y = null; - // If public key is present, add it to the parameters. - if (ecPrivateKey.PublicKey is ReadOnlyMemory publicKey) - { - EccKeyFormatHelper.GetECPointFromUncompressedPublicKey(publicKey.Span, algorithm.KeySizeInBytes, out x, out y); - } + // If public key is present, add it to the parameters. + if (ecPrivateKey.PublicKey is ReadOnlyMemory publicKey) + { + EccKeyFormatHelper.GetECPointFromUncompressedPublicKey(publicKey.Span, algorithm.KeySizeInBytes, out x, out y); + } - byte[] d = new byte[ecPrivateKey.PrivateKey.Length]; + byte[] d = new byte[ecPrivateKey.PrivateKey.Length]; - using (PinAndClear.Track(d)) - { - ecPrivateKey.PrivateKey.CopyTo(d); + using (PinAndClear.Track(d)) + { + ecPrivateKey.PrivateKey.CopyTo(d); #if NET - ECParameters parameters = new ECParameters + ECParameters parameters = new ECParameters + { + Curve = algorithm.Curve, + Q = new ECPoint { - Curve = algorithm.Curve, - Q = new ECPoint - { - X = x, - Y = y, - }, - D = d - }; - - parameters.Validate(); - - return new ECDsaComponent(ECDsa.Create(parameters), algorithm); + X = x, + Y = y, + }, + D = d + }; + + parameters.Validate(); + + return new ECDsaComponent(ECDsa.Create(parameters), algorithm); #else - throw new PlatformNotSupportedException(); + throw new PlatformNotSupportedException(); #endif - } } } } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } public static unsafe ECDsaComponent ImportPublicKey(ECDsaAlgorithm algorithm, ReadOnlySpan source) @@ -192,12 +175,12 @@ internal override bool TryExportPrivateKey(Span destination, out int bytes return TryWriteKey(ecParameters.D, ecParameters.Q.X, ecParameters.Q.Y, _algorithm.CurveOid, destination, out bytesWritten); } #else - return TryWriteKey(d: [], x: [], y: [], curveOid: null, destination, out bytesWritten); + throw new PlatformNotSupportedException(); #endif +#if NET static bool TryWriteKey(byte[] d, byte[]? x, byte[]? y, string curveOid, Span destination, out int bytesWritten) { -#if NET AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); try @@ -235,10 +218,8 @@ static bool TryWriteKey(byte[] d, byte[]? x, byte[]? y, string curveOid, Span destination, out int bytesWritten) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs index e5467e24d64494..485f2acb3d6d83 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs @@ -740,12 +740,14 @@ internal ECCurve Curve Oids.secp256r1 => ECCurve.NamedCurves.nistP256, Oids.secp384r1 => ECCurve.NamedCurves.nistP384, Oids.secp521r1 => ECCurve.NamedCurves.nistP521, + "1.3.36.3.3.2.8.1.1.7" => ECCurve.NamedCurves.brainpoolP256r1, + "1.3.36.3.3.2.8.1.1.11" => ECCurve.NamedCurves.brainpoolP384r1, string oid => FailAndThrow(oid) }; static ECCurve FailAndThrow(string oid) { - Debug.Fail($"EC-DSA curve not supported ({oid})"); + Debug.Fail($"'{oid}' is not a valid ECDSA curve for Composite ML-DSA."); throw new CryptographicException(); } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs index 2a80dd2525bd0e..6c6e9c8565a54c 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaFactoryTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Formats.Asn1; using System.Linq; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -131,6 +132,151 @@ public static void ImportPrivateKey_UpperBound(CompositeMLDsaAlgorithm algorithm AssertImportBadPrivateKey(algorithm, new byte[bound.Value + 1]); } + [Fact] + public static void ImportBadPrivateKey_ECDsa_InvalidVersion() + { + CompositeMLDsaAlgorithm algorithm = CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256; + + // No version + AssertImportBadPrivateKey(algorithm, CreateKeyWithVersion(null)); + + // Unsupported version + AssertImportBadPrivateKey(algorithm, CreateKeyWithVersion(0)); + AssertImportBadPrivateKey(algorithm, CreateKeyWithVersion(2)); + + // Correct version, don't throw (unless platform does not support Composite ML-DSA) + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => import(), algorithm), + algorithm, + CreateKeyWithVersion(1)); + + static byte[] CreateKeyWithVersion(int? version) + { + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + return ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version, ecdsaKey.D, ecdsaKey.Curve.Oid.Value, ecdsaKey.Q)); + } + } + + [Fact] + public static void ImportBadPrivateKey_ECDsa_NoPrivateKey() + { + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + // no private key + byte[] compositeKey = ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version: 1, d: null, ecdsaKey.Curve.Oid.Value, point: ecdsaKey.Q)); + + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, compositeKey); + } + + [Fact] + public static void ImportBadPrivateKey_ECDsa_WrongCurve() + { + CompositeMLDsaAlgorithm algorithm = CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256; + + // Wrong curve OID + AssertImportBadPrivateKey( + algorithm, + CreateKeyWithCurveOid(ECCurve.NamedCurves.nistP521.Oid.Value)); + + AssertImportBadPrivateKey( + algorithm, + CreateKeyWithCurveOid(ECCurve.NamedCurves.brainpoolP256r1.Oid.Value)); + + // Domain parameters are optional, don't throw (unless platform does not support Composite ML-DSA) + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => import(), algorithm), + algorithm, + CreateKeyWithCurveOid(ECCurve.NamedCurves.nistP256.Oid.Value)); + + static byte[] CreateKeyWithCurveOid(string? oid) + { + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + return ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version: 1, ecdsaKey.D, oid, ecdsaKey.Q)); + } + } + + [Fact] + public static void ImportPrivateKey_ECDsa_NoPublicKey() + { + CompositeMLDsaAlgorithm algorithm = CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256; + ECParameters ecdsaKey = EccTestData.GetNistP256ReferenceKey(); + + // no public key + byte[] compositeKey = ComposeKeys( + MLDsaTestsData.IetfMLDsa65.PrivateSeed, + WriteECPrivateKey(version: 1, ecdsaKey.D, ecdsaKey.Curve.Oid.Value, point: null)); + + // Public key is optional, don't throw (unless platform does not support Composite ML-DSA) + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => import(), algorithm), + algorithm, + compositeKey); + } + + static byte[] ComposeKeys(byte[] mldsaKey, AsnWriter tradKey) + { + byte[] compositeKey = new byte[mldsaKey.Length + tradKey.GetEncodedLength()]; + mldsaKey.CopyTo(compositeKey, 0); + tradKey.Encode(compositeKey.AsSpan(mldsaKey.Length)); + return compositeKey; + } + + private static AsnWriter WriteECPrivateKey(int? version, byte[]? d, string? oid, ECPoint? point) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + // ECPrivateKey + using (writer.PushSequence()) + { + // version + if (version is int v) + { + writer.WriteInteger(v); + } + + // privateKey + if (d is not null) + { + writer.WriteOctetString(d); + } + + // domainParameters + if (oid is not null) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true))) + { + writer.WriteObjectIdentifier(oid); + } + } + + // publicKey + if (point is ECPoint q) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true))) + { + int publicKeyLength = 1 + q.X.Length + q.Y.Length; + byte[] publicKeyBytes = new byte[publicKeyLength]; + + publicKeyBytes[0] = 0x04; + q.X.CopyTo(publicKeyBytes.AsSpan(1)); + q.Y.CopyTo(publicKeyBytes.AsSpan(1 + q.X.Length)); + + writer.WriteBitString(publicKeyBytes); + } + } + } + + return writer; + } + private static void AssertImportBadPrivateKey(CompositeMLDsaAlgorithm algorithm, byte[] key) { CompositeMLDsaTestHelpers.AssertImportPrivateKey( @@ -285,24 +431,28 @@ private static void AssertImportBadPublicKey(CompositeMLDsaAlgorithm algorithm, [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void AlgorithmMatches_GenerateKey(CompositeMLDsaAlgorithm algorithm) { - AssertThrowIfNotSupported(() => - { - using CompositeMLDsa dsa = CompositeMLDsa.GenerateKey(algorithm); - Assert.Equal(algorithm, dsa.Algorithm); - }); + AssertThrowIfNotSupported( + () => + { + using CompositeMLDsa dsa = CompositeMLDsa.GenerateKey(algorithm); + Assert.Equal(algorithm, dsa.Algorithm); + }, + algorithm); } [Theory] [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void AlgorithmMatches_Import(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) { - CompositeMLDsaTestHelpers.AssertImportPublicKey(import => - AssertThrowIfNotSupported(() => - Assert.Equal(vector.Algorithm, import().Algorithm)), vector.Algorithm, vector.PublicKey); + CompositeMLDsaTestHelpers.AssertImportPublicKey( + import => AssertThrowIfNotSupported(() => Assert.Equal(vector.Algorithm, import().Algorithm), vector.Algorithm), + vector.Algorithm, + vector.PublicKey); - CompositeMLDsaTestHelpers.AssertImportPrivateKey(import => - AssertThrowIfNotSupported(() => - Assert.Equal(vector.Algorithm, import().Algorithm)), vector.Algorithm, vector.SecretKey); + CompositeMLDsaTestHelpers.AssertImportPrivateKey( + import => AssertThrowIfNotSupported(() => Assert.Equal(vector.Algorithm, import().Algorithm), vector.Algorithm), + vector.Algorithm, + vector.SecretKey); } [Fact] @@ -346,14 +496,11 @@ public static void IsSupported_InitializesCrypto() }, arg).Dispose(); } - /// - /// Asserts that on platforms that do not support Composite ML-DSA, the input test throws PlatformNotSupportedException. - /// If the test does pass, it implies that the test is validating code after the platform check. - /// - /// The test to run. - private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorithm? algorithm = null) + // Asserts the test throws PlatformNotSupportedException if Composite ML-DSA is supported; + // otherwise runs the test normally. + private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorithm algorithm) { - if (algorithm == null ? CompositeMLDsa.IsSupported : CompositeMLDsa.IsAlgorithmSupported(algorithm)) + if (CompositeMLDsa.IsAlgorithmSupported(algorithm)) { test(); } diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index 27b0a847e0a548..5abb2246869ee1 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -172,6 +172,8 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\CompositeMLDsa\CompositeMLDsaTestsBase.cs" /> +