From b6b9e663ab704ffb916980a1c68b48db2f0caead Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Tue, 30 Jun 2026 11:42:38 -0700 Subject: [PATCH 1/3] Add CompositeMLDsa CNG (NCrypt) --- .../Security/Cryptography/CompositeMLDsa.cs | 10 +- .../Cryptography/CompositeMLDsaCng.Windows.cs | 189 +++++++++++++++++- .../Cryptography/CompositeMLDsaCng.cs | 20 +- .../CompositeMLDsaCngTests.Windows.cs | 176 ++++++++++++++++ .../CompositeMLDsa/CompositeMLDsaCngTests.cs | 24 +++ .../CompositeMLDsaContractTests.cs | 32 +-- .../CompositeMLDsa/CompositeMLDsaTestData.cs | 51 +++-- .../CompositeMLDsaTestHelpers.Cng.cs | 95 +++++++++ .../CompositeMLDsaTestHelpers.cs | 2 +- .../CompositeMLDsa/CompositeMLDsaTestsBase.cs | 2 +- .../src/Resources/Strings.resx | 5 +- .../Cryptography/CngIdentifierExtensions.cs | 8 + .../Microsoft.Bcl.Cryptography.Tests.csproj | 6 + .../ref/System.Security.Cryptography.cs | 4 + .../src/Resources/Strings.resx | 5 +- .../Security/Cryptography/Cng.NotSupported.cs | 3 + .../Security/Cryptography/CngAlgorithm.cs | 12 ++ .../Cryptography/CngAlgorithmGroup.cs | 11 + .../System.Security.Cryptography.Tests.csproj | 6 + 19 files changed, 605 insertions(+), 56 deletions(-) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.Windows.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs index 1d67ed324aaaa2..2da20f8ec2f96f 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs @@ -25,7 +25,7 @@ public abstract class CompositeMLDsa : IDisposable #pragma warning restore SA1001 #endif { - private static readonly string[] s_knownOids = + private protected static readonly string[] KnownOids = [ Oids.MLDsa44WithRSA2048PssPreHashSha256, Oids.MLDsa44WithRSA2048Pkcs15PreHashSha256, @@ -666,7 +666,7 @@ public static CompositeMLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan sourc Helpers.ThrowIfAsnInvalidLength(source); ThrowIfNotSupported(); - KeyFormatHelper.ReadSubjectPublicKeyInfo(s_knownOids, source, SubjectPublicKeyReader, out int read, out CompositeMLDsa dsa); + KeyFormatHelper.ReadSubjectPublicKeyInfo(KnownOids, source, SubjectPublicKeyReader, out int read, out CompositeMLDsa dsa); Debug.Assert(read == source.Length); return dsa; @@ -861,7 +861,7 @@ public static CompositeMLDsa ImportPkcs8PrivateKey(ReadOnlySpan source) Helpers.ThrowIfAsnInvalidLength(source); ThrowIfNotSupported(); - KeyFormatHelper.ReadPkcs8(s_knownOids, source, PrivateKeyReader, out int read, out CompositeMLDsa dsa); + KeyFormatHelper.ReadPkcs8(KnownOids, source, PrivateKeyReader, out int read, out CompositeMLDsa dsa); Debug.Assert(read == source.Length); return dsa; @@ -1977,7 +1977,7 @@ private static CompositeMLDsaAlgorithm GetAlgorithmIdentifier(ref readonly Value return algorithm; } - private static void ThrowIfNotSupported() + private protected static void ThrowIfNotSupported() { if (!IsSupported) { @@ -1993,6 +1993,6 @@ private static void ThrowIfNotSupported(CompositeMLDsaAlgorithm algorithm) } } - private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, typeof(CompositeMLDsa)); + private protected void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, typeof(CompositeMLDsa)); } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs index 4b1165677d5c63..9219e682fbdc50 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs @@ -1,31 +1,198 @@ // 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; +using System.Formats.Asn1; +using System.Runtime.Versioning; +using System.Security.Cryptography.Asn1; +using Microsoft.Win32.SafeHandles; +using static Interop.BCrypt; + namespace System.Security.Cryptography { public sealed partial class CompositeMLDsaCng : CompositeMLDsa { - public partial CngKey GetKey() => - throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + internal const string NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_44_ECDSA_P256_SHA256 = PqcBlobHelpers.BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_44_ECDSA_P256_SHA256; + internal const string NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P256_SHA512 = PqcBlobHelpers.BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P256_SHA512; + internal const string NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P384_SHA512 = PqcBlobHelpers.BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P384_SHA512; + internal const string NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_87_ECDSA_P384_SHA512 = PqcBlobHelpers.BCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_87_ECDSA_P384_SHA512; + + [SupportedOSPlatform("windows")] + private static partial CompositeMLDsaAlgorithm AlgorithmFromHandle(CngKey key, out CngKey duplicateKey) + { + ArgumentNullException.ThrowIfNull(key); + ThrowIfNotSupported(); + + if (key.AlgorithmGroup != CngAlgorithmGroup.CompositeMLDsa) + { + throw new ArgumentException(SR.Cryptography_ArgCompositeMLDsaRequiresCompositeMLDsaKey, nameof(key)); + } + + CompositeMLDsaAlgorithm algorithm = AlgorithmFromHandleImpl(key); + +#if SYSTEM_SECURITY_CRYPTOGRAPHY + duplicateKey = CngHelpers.Duplicate(key.HandleNoDuplicate, key.IsEphemeral); +#else + duplicateKey = key.Duplicate(); +#endif + + return algorithm; + } + + private static CompositeMLDsaAlgorithm AlgorithmFromHandleImpl(CngKey key) + { + string? parameterSet = +#if SYSTEM_SECURITY_CRYPTOGRAPHY + key.HandleNoDuplicate.GetPropertyAsString(KeyPropertyName.ParameterSetName, CngPropertyOptions.None); +#else + key.GetPropertyAsString(KeyPropertyName.ParameterSetName, CngPropertyOptions.None); +#endif + + return parameterSet switch + { + NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_44_ECDSA_P256_SHA256 => CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256, + NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P256_SHA512 => CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, + NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_65_ECDSA_P384_SHA512 => CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, + NCRYPT_COMPOSITE_MLDSA_PARAMETER_SET_87_ECDSA_P384_SHA512 => CompositeMLDsaAlgorithm.MLDsa87WithECDsaP384, + _ => throw DebugFailAndGetException(parameterSet), + }; + + static Exception DebugFailAndGetException(string? parameterSet) + { + Debug.Fail($"Unexpected parameter set {parameterSet}"); + throw new CryptographicException(); + } + } + + public partial CngKey GetKey() + { + ThrowIfDisposed(); + +#if SYSTEM_SECURITY_CRYPTOGRAPHY + return CngHelpers.Duplicate(_key.HandleNoDuplicate, _key.IsEphemeral); +#else +#pragma warning disable CA1416 // only supported on: 'windows' + return _key.Duplicate(); +#pragma warning restore CA1416 // only supported on: 'windows' +#endif + } /// - protected override int SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) => - throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + protected override int SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) + { + int bytesWritten = 0; + + using (SafeNCryptKeyHandle duplicatedHandle = _key.Handle) + { + fixed (void* pContext = context) + { + BCRYPT_PQDSA_PADDING_INFO paddingInfo = default; + paddingInfo.pbCtx = (IntPtr)pContext; + paddingInfo.cbCtx = context.Length; + + bool result = duplicatedHandle.TrySignHash( + data, + destination, + Interop.NCrypt.AsymmetricPaddingMode.NCRYPT_PAD_PQDSA_FLAG, + &paddingInfo, + out bytesWritten); + + if (!result) + { + Debug.Fail("Buffer too small but caller should have already validated the buffer size."); + throw new CryptographicException(); + } + } + } + + return bytesWritten; + } /// - protected override int ExportCompositeMLDsaPrivateKeyCore(Span destination) => - throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) + { + using (SafeNCryptKeyHandle duplicatedHandle = _key.Handle) + { + fixed (void* pContext = context) + { + BCRYPT_PQDSA_PADDING_INFO paddingInfo = default; + paddingInfo.pbCtx = (IntPtr)pContext; + paddingInfo.cbCtx = context.Length; + + return duplicatedHandle.VerifyHash( + data, + signature, + Interop.NCrypt.AsymmetricPaddingMode.NCRYPT_PAD_PQDSA_FLAG, + &paddingInfo); + } + } + } /// protected override int ExportCompositeMLDsaPublicKeyCore(Span destination) => - throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + ExportKey(CngKeyBlobFormat.PQDsaPublicBlob, Algorithm.MaxPublicKeySizeInBytes, destination); /// - protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) => - throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + protected override int ExportCompositeMLDsaPrivateKeyCore(Span destination) => + CngPkcs8.AllowsOnlyEncryptedExport(_key) + ? throw new CryptographicException(SR.Cryptography_KeyNotExtractable) + : ExportKey(CngKeyBlobFormat.PQDsaPrivateBlob, Algorithm.MaxPrivateKeySizeInBytes, destination); + + /// + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(_key); + + // Windows NCrypt does not yet support PKCS#8 export for Composite ML-DSA. + if (encryptedOnlyExport) + { + throw new CryptographicException(SR.Cryptography_KeyNotExtractable); + } + + return TryExportPkcs8FromExportedPrivateKey(destination, out bytesWritten); + } /// - protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => - throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa))); + protected override void Dispose(bool disposing) + { + if (disposing) + { + _key.Dispose(); + _key = null!; + } + + base.Dispose(disposing); + } + + private int ExportKey(CngKeyBlobFormat blobFormat, int maxKeySize, Span destination) + { + byte[] blob = _key.Export(blobFormat); + + using (PinAndClear.Track(blob)) + { + ReadOnlySpan keyBytes = PqcBlobHelpers.DecodeCompositeMLDsaBlob(blob, out ReadOnlySpan parameterSet, out string blobType); + + if (!PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(Algorithm, out string? expectedParameterSet)) + { + Debug.Fail($"Unknown algorithm {Algorithm.Name}."); + throw new CryptographicException(); + } + + if (blobType != blobFormat.Format || + keyBytes.Length > maxKeySize || + !parameterSet.SequenceEqual(expectedParameterSet)) + { + Debug.Fail( + $"{nameof(blobType)}: {blobType}, " + + $"{nameof(parameterSet)}: {parameterSet.ToString()}, " + + $"{nameof(keyBytes)}.Length: {keyBytes.Length} / {destination.Length}"); + + throw new CryptographicException(); + } + + keyBytes.CopyTo(destination); + return keyBytes.Length; + } + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.cs index 246109e6c8b2d6..ca87e022bc249e 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.Versioning; +using Internal.Cryptography; namespace System.Security.Cryptography { @@ -20,6 +21,8 @@ namespace System.Security.Cryptography [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] public sealed partial class CompositeMLDsaCng : CompositeMLDsa { + private CngKey _key; + /// /// Initializes a new instance of the class by using the specified . /// @@ -37,13 +40,22 @@ public sealed partial class CompositeMLDsaCng : CompositeMLDsa /// [SupportedOSPlatform("windows")] public CompositeMLDsaCng(CngKey key) - : base(AlgorithmFromHandle(key)) + : base(AlgorithmFromHandleWithPlatformCheck(key, out CngKey duplicateKey)) { - throw new PlatformNotSupportedException(); + _key = duplicateKey; + } + + private static CompositeMLDsaAlgorithm AlgorithmFromHandleWithPlatformCheck(CngKey key, out CngKey duplicateKey) + { + if (!Helpers.IsOSPlatformWindows) + { + throw new PlatformNotSupportedException(); + } + + return AlgorithmFromHandle(key, out duplicateKey); } - private static CompositeMLDsaAlgorithm AlgorithmFromHandle(CngKey key) => - throw new PlatformNotSupportedException(); + private static partial CompositeMLDsaAlgorithm AlgorithmFromHandle(CngKey key, out CngKey duplicateKey); /// /// Gets a new representing the key used by the current instance. diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.Windows.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.Windows.cs new file mode 100644 index 00000000000000..28e51c8515d7bf --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.Windows.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Sdk; +using static System.Security.Cryptography.Tests.CompositeMLDsaTestData; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(CompositeMLDsa), nameof(CompositeMLDsa.IsSupported))] + [PlatformSpecific(TestPlatforms.Windows)] + public sealed class CompositeMLDsaCngTests_AllowPlaintextExport : CompositeMLDsaTestsBase + { + protected override CompositeMLDsa GenerateKey(CompositeMLDsaAlgorithm algorithm) => + CompositeMLDsaTestHelpers.GenerateKey(algorithm, CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport); + + protected override CompositeMLDsa ImportPrivateKey(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) => + CompositeMLDsaTestHelpers.ImportPrivateKey(algorithm, source, CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport); + + protected override CompositeMLDsa ImportPublicKey(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) => + CompositeMLDsaTestHelpers.ImportPublicKey(algorithm, source); + } + + // Windows Insider builds don't support PKCS#8 export so we can't implement encrypted exports + [ActiveIssue("https://github.com/dotnet/runtime/issues/117000")] + [ConditionalClass(typeof(CompositeMLDsa), nameof(CompositeMLDsa.IsSupported))] + [PlatformSpecific(TestPlatforms.Windows)] + public sealed class CompositeMLDsaCngTests_AllowExport : CompositeMLDsaTestsBase + { + protected override CompositeMLDsa GenerateKey(CompositeMLDsaAlgorithm algorithm) => + CompositeMLDsaTestHelpers.GenerateKey(algorithm, CngExportPolicies.AllowExport); + + protected override CompositeMLDsa ImportPrivateKey(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) => + CompositeMLDsaTestHelpers.ImportPrivateKey(algorithm, source, CngExportPolicies.AllowExport); + + protected override CompositeMLDsa ImportPublicKey(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) => + CompositeMLDsaTestHelpers.ImportPublicKey(algorithm, source); + } + + [ConditionalClass(typeof(CompositeMLDsa), nameof(CompositeMLDsa.IsSupported))] + [PlatformSpecific(TestPlatforms.Windows)] + public sealed class CompositeMLDsaCngTests + { + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void ImportPrivateKey_NoExportFlag(CompositeMLDsaTestVector info) + { + using (CompositeMLDsa dsa = CompositeMLDsaTestHelpers.ImportPrivateKey(info.Algorithm, info.SecretKey, CngExportPolicies.None)) + { + CompositeMLDsaTestHelpers.AssertExportPublicKey( + export => AssertExtensions.SequenceEqual(info.PublicKey, export(dsa))); + + CompositeMLDsaTestHelpers.AssertExportPrivateKey( + export => Assert.Throws(() => export(dsa)), + export => CompositeMLDsaTestHelpers.AssertThrowsCryptographicExceptionWithHResult(() => export(dsa))); + + byte[] signature = new byte[info.Algorithm.MaxSignatureSizeInBytes]; + int written = dsa.SignData("test"u8, signature); + AssertExtensions.TrueExpression(dsa.VerifyData("test"u8, signature.AsSpan(0, written))); + } + } + + [ConditionalFact(typeof(CompositeMLDsa), nameof(CompositeMLDsa.IsSupported))] + public void ImportPrivateKey_Persisted() + { + CompositeMLDsaTestVector testVector = GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256); + + CngKey key = PqcBlobHelpers.EncodeCompositeMLDsaBlob( + PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256, out var parameterSet) + ? parameterSet + : throw new XunitException("Unsupported algorithm."), + testVector.SecretKey, + Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB, + blob => + { + CngProperty dsaBlob = new CngProperty( + Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB, + blob.ToArray(), + CngPropertyOptions.None); + + CngKeyCreationParameters creationParams = new(); + creationParams.Parameters.Add(dsaBlob); + creationParams.ExportPolicy = CngExportPolicies.AllowPlaintextExport; + creationParams.KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey; + + CngKey key = CngKey.Create(CngAlgorithm.CompositeMLDsa, $"CompositeMLDsaCngTests_{nameof(ImportPrivateKey_Persisted)}", creationParams); + return key; + }); + + try + { + using (CompositeMLDsa dsa = new CompositeMLDsaCng(key)) + { + CompositeMLDsaTestHelpers.AssertExportPublicKey(export => + AssertExtensions.SequenceEqual(testVector.PublicKey, export(dsa))); + + CompositeMLDsaTestHelpers.AssertExportPrivateKey(export => + AssertExtensions.SequenceEqual(testVector.SecretKey, export(dsa))); + + byte[] signature = new byte[CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256.MaxSignatureSizeInBytes]; + int written = dsa.SignData("test"u8, signature); + AssertExtensions.TrueExpression(dsa.VerifyData("test"u8, signature.AsSpan(0, written))); + } + } + finally + { + key.Delete(); + } + } + + [Fact] + public void CompositeMLDsaCng_WrongAlgorithm() + { + using RSACng rsa = new RSACng(); + using CngKey key = rsa.Key; + Assert.Throws("key", () => new CompositeMLDsaCng(key)); + } + + [Theory] + [InlineData(default(string))] + [InlineData($"CompositeMLDsaCngTests_{nameof(CompositeMLDsaCng_DuplicateHandle)}")] + public void CompositeMLDsaCng_DuplicateHandle(string? name) + { + CngProperty parameterSet = CompositeMLDsaTestHelpers.GetCngProperty(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256); + CngKeyCreationParameters creationParams = new(); + creationParams.Parameters.Add(parameterSet); + + CngKey key = CngKey.Create(CngAlgorithm.CompositeMLDsa, name, creationParams); + + try + { + IEnumerable generateFive = Enumerable.Range(0, 5).Select(_ => new CompositeMLDsaCng(key)); + List disposables = new List(10); + disposables.AddRange(generateFive); + CompositeMLDsaCng dsa = new CompositeMLDsaCng(key); + disposables.AddRange(generateFive); + + foreach (CompositeMLDsaCng disposable in disposables) + { + disposable.Dispose(); + } + + byte[] signature = new byte[CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256.MaxSignatureSizeInBytes]; + int written = dsa.SignData("test"u8, signature); + AssertExtensions.TrueExpression(dsa.VerifyData("test"u8, signature.AsSpan(0, written))); + } + finally + { + key.Delete(); + } + } + + [Fact] + public static void CompositeMLDsaCng_GetKey() + { + CngProperty parameterSet = CompositeMLDsaTestHelpers.GetCngProperty(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256); + CngKeyCreationParameters creationParams = new(); + creationParams.Parameters.Add(parameterSet); + + using (CngKey key = CngKey.Create(CngAlgorithm.CompositeMLDsa, keyName: null, creationParams)) + using (CompositeMLDsaCng dsaKey = new(key)) + using (CngKey getKey1 = dsaKey.GetKey()) + { + using (CngKey getKey2 = dsaKey.GetKey()) + { + Assert.NotSame(key, getKey1); + Assert.NotSame(getKey1, getKey2); + } + + Assert.Equal(key.Algorithm, getKey1.Algorithm); // Assert.NoThrow on getKey1.Algorithm + } + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs new file mode 100644 index 00000000000000..cf84cd355c1665 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public sealed class CompositeMLDsaCngTests_AllPlatforms + { + [Fact] + public void CompositeMLDsaCng_Ctor_ArgValidation() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + AssertExtensions.Throws("key", static () => new CompositeMLDsaCng(null)); + } + else + { + Assert.Throws(() => new CompositeMLDsaCng(null)); + } + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs index 6b523c4c51c6ab..191df10e968562 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs @@ -809,26 +809,30 @@ public static void TryExportCompositeMLDsaPrivateKey_CallsCore(CompositeMLDsaAlg public static void TrySignData_CallsCore(CompositeMLDsaTestVector vector) { using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(vector.Algorithm); + + byte[] message = vector.Message; + byte[] expectedSignature = vector.Signature; + dsa.SignDataCoreHook = (_, _, _) => { return -1; }; - dsa.AddFillDestination(vector.Signature); - dsa.AddDataBufferIsSameAssertion(vector.Message); + dsa.AddFillDestination(expectedSignature); + dsa.AddDataBufferIsSameAssertion(message); dsa.AddContextBufferIsSameAssertion(Array.Empty()); - byte[] exported = dsa.SignData(vector.Message, Array.Empty()); + byte[] exported = dsa.SignData(message, Array.Empty()); AssertExtensions.LessThan(0, dsa.SignDataCoreCallCount); - AssertExtensions.SequenceEqual(exported, vector.Signature); + AssertExtensions.SequenceEqual(exported, expectedSignature); - byte[] signature = CreatePaddedFilledArray(vector.Signature.Length, 42); + byte[] signature = CreatePaddedFilledArray(expectedSignature.Length, 42); // Extra bytes in destination buffer should not be touched Memory destination = signature.AsMemory(PaddingSize, vector.Algorithm.MaxSignatureSizeInBytes); dsa.AddDestinationBufferIsSameAssertion(destination); dsa.SignDataCoreCallCount = 0; - int bytesWritten = dsa.SignData(vector.Message, destination.Span, Array.Empty()); - Assert.Equal(vector.Signature.Length, bytesWritten); + int bytesWritten = dsa.SignData(message, destination.Span, Array.Empty()); + Assert.Equal(expectedSignature.Length, bytesWritten); Assert.Equal(1, dsa.SignDataCoreCallCount); - AssertExpectedFill(signature, vector.Signature, PaddingSize, 42); + AssertExpectedFill(signature, expectedSignature, PaddingSize, 42); } [Theory] @@ -836,15 +840,19 @@ public static void TrySignData_CallsCore(CompositeMLDsaTestVector vector) public static void VerifyData_CallsCore(CompositeMLDsaTestVector vector) { using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(vector.Algorithm); + + byte[] message = vector.Message; + byte[] expectedSignature = vector.Signature; + dsa.VerifyDataCoreHook = (_, _, _) => true; - dsa.AddDataBufferIsSameAssertion(vector.Message); - dsa.AddSignatureBufferIsSameAssertion(vector.Signature); + dsa.AddDataBufferIsSameAssertion(message); + dsa.AddSignatureBufferIsSameAssertion(expectedSignature); dsa.AddContextBufferIsSameAssertion(Array.Empty()); - AssertExtensions.TrueExpression(dsa.VerifyData(vector.Message, vector.Signature, Array.Empty())); + AssertExtensions.TrueExpression(dsa.VerifyData(message, expectedSignature, Array.Empty())); AssertExtensions.Equal(1, dsa.VerifyDataCoreCallCount); - AssertExtensions.TrueExpression(dsa.VerifyData(vector.Message, vector.Signature, Array.Empty())); + AssertExtensions.TrueExpression(dsa.VerifyData(message, expectedSignature, Array.Empty())); AssertExtensions.Equal(2, dsa.VerifyDataCoreCallCount); } 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 6e41a1db8a9848..70b101d79fe84e 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 @@ -14,31 +14,42 @@ public class CompositeMLDsaTestVector { internal string Id { get; } internal CompositeMLDsaAlgorithm Algorithm { get; } - internal byte[] Message { get; } - internal byte[] Context { get; } - internal byte[] PublicKey { get; } - internal byte[] Certificate { get; } - internal byte[] SecretKey { get; } - internal byte[] Pkcs8 { get; } - internal byte[] Signature { get; } - internal byte[] SignatureWithContext { get; } - internal byte[] Spki { get; } + private readonly byte[] _publicKey; + private readonly byte[] _certificate; + private readonly byte[] _secretKey; + private readonly byte[] _pkcs8; + private readonly byte[] _message; + private readonly byte[] _context; + private readonly byte[] _signature; + private readonly byte[] _signatureWithContext; + private byte[] _spki; + + internal byte[] Message => _message.AsSpan().ToArray(); + internal byte[] Context => _context.AsSpan().ToArray(); + internal byte[] PublicKey => _publicKey.AsSpan().ToArray(); + internal byte[] Certificate => _certificate.AsSpan().ToArray(); + internal byte[] SecretKey => _secretKey.AsSpan().ToArray(); + internal byte[] Pkcs8 => _pkcs8.AsSpan().ToArray(); + internal byte[] Signature => _signature.AsSpan().ToArray(); + internal byte[] SignatureWithContext => _signatureWithContext.AsSpan().ToArray(); + + internal byte[] Spki => _spki.AsSpan().ToArray(); internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, string pk, string x5c, string sk, string sk_pkcs8, string m, string ctx, string s, string sWithContext) { Id = tcId; Algorithm = algo; - PublicKey = Convert.FromBase64String(pk); - Certificate = Convert.FromBase64String(x5c); - SecretKey = Convert.FromBase64String(sk); - Pkcs8 = Convert.FromBase64String(sk_pkcs8); - Message = Convert.FromBase64String(m); - Context = Convert.FromBase64String(ctx); - Signature = Convert.FromBase64String(s); - SignatureWithContext = Convert.FromBase64String(sWithContext); - - AsnReader reader = new AsnReader(Certificate, AsnEncodingRules.DER); + _publicKey = Convert.FromBase64String(pk); + _certificate = Convert.FromBase64String(x5c); + _secretKey = Convert.FromBase64String(sk); + _pkcs8 = Convert.FromBase64String(sk_pkcs8); + _message = Convert.FromBase64String(m); + _context = Convert.FromBase64String(ctx); + _signature = Convert.FromBase64String(s); + _signatureWithContext = Convert.FromBase64String(sWithContext); + + AsnReader reader = new AsnReader(_certificate, AsnEncodingRules.DER); AsnReader certificate = reader.ReadSequence(); AsnReader tbsCertificate = certificate.ReadSequence(); @@ -49,7 +60,7 @@ internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, str tbsCertificate.ReadEncodedValue(); // Validity tbsCertificate.ReadEncodedValue(); // Subject - Spki = tbsCertificate.ReadEncodedValue().ToArray(); + _spki = tbsCertificate.ReadEncodedValue().ToArray(); } public override string ToString() => Id; diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs new file mode 100644 index 00000000000000..9cca05a283dd3d --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Xunit; +using Xunit.Sdk; + +namespace System.Security.Cryptography.Tests +{ + internal static partial class CompositeMLDsaTestHelpers + { + private const int NTE_NOT_SUPPORTED = unchecked((int)0x80090029); + + internal static CompositeMLDsaCng ImportPublicKey(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) + { + CngKey key = PqcBlobHelpers.EncodeCompositeMLDsaBlob( + PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(algorithm, out var parameterSet) + ? parameterSet + : throw new XunitException($"Unsupported algorithm: {algorithm.Name}."), + source, + Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PUBLIC_BLOB, + blob => CngKey.Import(blob.ToArray(), CngKeyBlobFormat.PQDsaPublicBlob)); + + return new CompositeMLDsaCng(key); + } + + internal static CompositeMLDsaCng GenerateKey(CompositeMLDsaAlgorithm algorithm, CngExportPolicies exportPolicies) + { + CngProperty parameterSet = GetCngProperty(algorithm); + + CngKeyCreationParameters creationParams = new(); + creationParams.Parameters.Add(parameterSet); + creationParams.ExportPolicy = exportPolicies; + + CngKey key = CngKey.Create(CngAlgorithm.CompositeMLDsa, keyName: null, creationParams); + return new CompositeMLDsaCng(key); + } + + internal static CompositeMLDsaCng ImportPrivateKey(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source, CngExportPolicies exportPolicies) + { + CngKey key = PqcBlobHelpers.EncodeCompositeMLDsaBlob( + PqcBlobHelpers.TryGetCompositeMLDsaParameterSet(algorithm, out var parameterSet) + ? parameterSet + : throw new XunitException($"Unsupported algorithm: {algorithm.Name}."), + source, + Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB, + blob => + { + CngProperty dsaBlob = new CngProperty( + Interop.BCrypt.KeyBlobType.BCRYPT_PQDSA_PRIVATE_BLOB, + blob.ToArray(), + CngPropertyOptions.None); + + CngKeyCreationParameters creationParams = new(); + creationParams.Parameters.Add(dsaBlob); + creationParams.ExportPolicy = exportPolicies; + + CngKey key = CngKey.Create(CngAlgorithm.CompositeMLDsa, keyName: null, creationParams); + return key; + }); + + return new CompositeMLDsaCng(key); + } + + internal static CngProperty GetCngProperty(CompositeMLDsaAlgorithm algorithm) + { + string parameterSetValue = algorithm.Name switch + { + "MLDSA44-ECDSA-P256-SHA256" => "44-ECDSA-P256-SHA256", + "MLDSA65-ECDSA-P256-SHA512" => "65-ECDSA-P256-SHA512", + "MLDSA65-ECDSA-P384-SHA512" => "65-ECDSA-P384-SHA512", + "MLDSA87-ECDSA-P384-SHA512" => "87-ECDSA-P384-SHA512", + _ => throw new XunitException("Unknown algorithm."), + }; + + byte[] byteValue = new byte[(parameterSetValue.Length + 1) * 2]; // Null terminator + int written = Encoding.Unicode.GetBytes(parameterSetValue, 0, parameterSetValue.Length, byteValue, 0); + Assert.Equal(byteValue.Length - 2, written); + + return new CngProperty( + "ParameterSetName", + byteValue, + CngPropertyOptions.None); + } + + // CryptographicException can only have both HRESULT and Message set starting in .NET Core 3.0+. + // To work around this, the product code throws an exception derived from CryptographicException + // that has both set. This assert checks for that instead. + internal static void AssertThrowsCryptographicExceptionWithHResult(Action export) + { + CryptographicException ce = Assert.ThrowsAny(export); + Assert.Equal(NTE_NOT_SUPPORTED, ce.HResult); + } + } +} 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 3ccadf5a25a89e..8bae800f435ffa 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 @@ -12,7 +12,7 @@ namespace System.Security.Cryptography.Tests { - internal static class CompositeMLDsaTestHelpers + internal static partial class CompositeMLDsaTestHelpers { // DER encoding of ASN.1 BitString "foo" internal static readonly ReadOnlyMemory s_derBitStringFoo = new byte[] { 0x03, 0x04, 0x00, 0x66, 0x6f, 0x6f }; 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 f413b6089160e4..e1eacd52e00105 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 @@ -35,7 +35,7 @@ public void GenerateSignVerifyWithPublicKey(CompositeMLDsaAlgorithm algorithm) { ExerciseSuccessfulVerify(publicKey, data, signature, []); - Assert.Throws(() => publicKey.SignData(data)); + Assert.ThrowsAny(() => publicKey.SignData(data)); } } diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx index 065d4467e23e9e..def6ca66049365 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -1,4 +1,4 @@ - + @@ -141,6 +141,9 @@ Algorithm '{0}' is not supported on this platform. + + Keys used with the CompositeMLDsaCng algorithm must have an algorithm group of CompositeMLDsa. + Keys used with the MLDsaCng algorithm must have an algorithm group of MLDsa. diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngIdentifierExtensions.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngIdentifierExtensions.cs index d67bba43329d25..ad4da157fd24d4 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngIdentifierExtensions.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/CngIdentifierExtensions.cs @@ -7,6 +7,7 @@ internal static class CngAlgorithmExtensions { private static CngAlgorithm? _mlDsaCngAlgorithm; private static CngAlgorithm? _mlKemCngAlgorithm; + private static CngAlgorithm? _compositeMldsaCngAlgorithm; extension(CngAlgorithm) { @@ -15,6 +16,9 @@ internal static class CngAlgorithmExtensions internal static CngAlgorithm MLKem => _mlKemCngAlgorithm ??= new CngAlgorithm("ML-KEM"); // BCRYPT_MLKEM_ALGORITHM + + internal static CngAlgorithm CompositeMLDsa => + _compositeMldsaCngAlgorithm ??= new CngAlgorithm("Composite-ML-DSA"); // BCRYPT_COMPOSITE_MLDSA_ALGORITHM } } @@ -22,6 +26,7 @@ internal static class CngAlgorithmGroupExtensions { private static CngAlgorithmGroup? _mlDsaCngAlgorithmGroup; private static CngAlgorithmGroup? _mlKemCngAlgorithmGroup; + private static CngAlgorithmGroup? _compositeMLDsaCngAlgorithmGroup; extension(CngAlgorithmGroup) { @@ -30,6 +35,9 @@ internal static class CngAlgorithmGroupExtensions internal static CngAlgorithmGroup MLKem => _mlKemCngAlgorithmGroup ??= new CngAlgorithmGroup("MLKEM"); // NCRYPT_MLKEM_ALGORITHM_GROUP + + internal static CngAlgorithmGroup CompositeMLDsa => + _compositeMLDsaCngAlgorithmGroup ??= new CngAlgorithmGroup("CompositeMLDSA"); // NCRYPT_COMPOSITE_MLDSA_ALGORITHM_GROUP } } 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 616f3521063d97..d64eb9c68ab3c4 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 @@ -161,6 +161,10 @@ Link="CommonTest\System\Security\Cryptography\SP800108HmacCounterKdfTests.Helpers.cs" /> + + + + @@ -243,6 +243,9 @@ The Android X.509 certificate chain could not be initialized. + + Keys used with the CompositeMLDsaCng algorithm must have an algorithm group of CompositeMLDsa. + Keys used with the DSACng algorithm must have an algorithm group of DSA. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs index 7b74e87c4b143a..1ba7d0d1f1ef23 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Cng.NotSupported.cs @@ -469,6 +469,9 @@ protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out public sealed partial class CompositeMLDsaCng : CompositeMLDsa { + private static partial CompositeMLDsaAlgorithm AlgorithmFromHandle(CngKey key, out CngKey duplicateKey) => + throw new PlatformNotSupportedException(); + public partial CngKey GetKey() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_CryptographyCng); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithm.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithm.cs index a837b0d5960afc..4f5bcaad197e67 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithm.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithm.cs @@ -228,6 +228,18 @@ public static CngAlgorithm Sha512 [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] public static CngAlgorithm SlhDsa => field ??= new CngAlgorithm("SLH-DSA"); // BCRYPT_SLHDSA_ALGORITHM + /// + /// Gets a new object that specifies the Composite Module-Lattice-Based + /// Digital Signature Algorithm (Composite ML-DSA). + /// + /// + /// A new object that specifies the Composite Module-Lattice-Based + /// Digital Signature Algorithm (Composite ML-DSA). + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public static CngAlgorithm CompositeMLDsa => + field ??= new CngAlgorithm("Composite-ML-DSA"); // BCRYPT_COMPOSITE_MLDSA_ALGORITHM + private static CngAlgorithm? s_ecdh; private static CngAlgorithm? s_ecdhp256; private static CngAlgorithm? s_ecdhp384; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs index 1709ba5e7d257b..39a0c2a08fd010 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs @@ -157,6 +157,17 @@ public static CngAlgorithmGroup Rsa public static CngAlgorithmGroup SlhDsa => field ??= new CngAlgorithmGroup("SLHDSA"); // NCRYPT_SLHDSA_ALGORITHM_GROUP + /// + /// Gets a object that specifies the Composite Module-Lattice-Based Digital Signature + /// Algorithm (Composite ML-DSA) family of algorithms. + /// + /// + /// An object that specifies the Composite ML-DSA family of algorithms. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public static CngAlgorithmGroup CompositeMLDsa => + field ??= new CngAlgorithmGroup("CompositeMLDSA"); // NCRYPT_COMPOSITE_MLDSA_ALGORITHM_GROUP + private static CngAlgorithmGroup? s_dh; private static CngAlgorithmGroup? s_dsa; private static CngAlgorithmGroup? s_ecdh; diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index c69819fbeaf6d1..f7baf6bbfa073f 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -278,6 +278,8 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\AES\AesFactory.cs" /> + + + Date: Tue, 7 Jul 2026 06:56:39 +0000 Subject: [PATCH 2/3] Address some feedback --- .../Cryptography/CompositeMLDsaCng.Windows.cs | 46 +++++++++---------- .../CompositeMLDsaCngTests.Windows.cs | 33 +++++++------ .../CompositeMLDsa/CompositeMLDsaCngTests.cs | 4 +- .../CompositeMLDsaContractTests.cs | 10 ++-- .../CompositeMLDsaFactoryTests.cs | 40 ++++++++-------- .../CompositeMLDsaImplementationTests.cs | 20 ++++---- .../CompositeMLDsa/CompositeMLDsaTestData.cs | 20 ++++---- .../CompositeMLDsa/CompositeMLDsaTestsBase.cs | 20 ++++---- 8 files changed, 98 insertions(+), 95 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs index 9219e682fbdc50..4dd6582b00cf2c 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs @@ -6,7 +6,6 @@ using System.Runtime.Versioning; using System.Security.Cryptography.Asn1; using Microsoft.Win32.SafeHandles; -using static Interop.BCrypt; namespace System.Security.Cryptography { @@ -30,12 +29,7 @@ private static partial CompositeMLDsaAlgorithm AlgorithmFromHandle(CngKey key, o CompositeMLDsaAlgorithm algorithm = AlgorithmFromHandleImpl(key); -#if SYSTEM_SECURITY_CRYPTOGRAPHY - duplicateKey = CngHelpers.Duplicate(key.HandleNoDuplicate, key.IsEphemeral); -#else duplicateKey = key.Duplicate(); -#endif - return algorithm; } @@ -68,13 +62,7 @@ public partial CngKey GetKey() { ThrowIfDisposed(); -#if SYSTEM_SECURITY_CRYPTOGRAPHY - return CngHelpers.Duplicate(_key.HandleNoDuplicate, _key.IsEphemeral); -#else -#pragma warning disable CA1416 // only supported on: 'windows' return _key.Duplicate(); -#pragma warning restore CA1416 // only supported on: 'windows' -#endif } /// @@ -86,16 +74,21 @@ protected override int SignDataCore(ReadOnlySpan data, ReadOnlySpan { fixed (void* pContext = context) { - BCRYPT_PQDSA_PADDING_INFO paddingInfo = default; + Interop.BCrypt.BCRYPT_PQDSA_PADDING_INFO paddingInfo = default; paddingInfo.pbCtx = (IntPtr)pContext; paddingInfo.cbCtx = context.Length; - bool result = duplicatedHandle.TrySignHash( - data, - destination, - Interop.NCrypt.AsymmetricPaddingMode.NCRYPT_PAD_PQDSA_FLAG, - &paddingInfo, - out bytesWritten); + bool result = false; + + unsafe + { + result = duplicatedHandle.TrySignHash( + data, + destination, + Interop.NCrypt.AsymmetricPaddingMode.NCRYPT_PAD_PQDSA_FLAG, + &paddingInfo, + out bytesWritten); + } if (!result) { @@ -115,15 +108,18 @@ protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan generateFive = Enumerable.Range(0, 5).Select(_ => new CompositeMLDsaCng(key)); List disposables = new List(10); disposables.AddRange(generateFive); - CompositeMLDsaCng dsa = new CompositeMLDsaCng(key); - disposables.AddRange(generateFive); - foreach (CompositeMLDsaCng disposable in disposables) + using (CompositeMLDsaCng dsa = new CompositeMLDsaCng(key)) { - disposable.Dispose(); - } + disposables.AddRange(generateFive); - byte[] signature = new byte[CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256.MaxSignatureSizeInBytes]; - int written = dsa.SignData("test"u8, signature); - AssertExtensions.TrueExpression(dsa.VerifyData("test"u8, signature.AsSpan(0, written))); + foreach (CompositeMLDsaCng disposable in disposables) + { + disposable.Dispose(); + } + + byte[] signature = new byte[CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256.MaxSignatureSizeInBytes]; + int written = dsa.SignData("test"u8, signature); + AssertExtensions.TrueExpression(dsa.VerifyData("test"u8, signature.AsSpan(0, written))); + } } finally { diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs index cf84cd355c1665..2248f6a3c2f53f 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaCngTests.cs @@ -6,10 +6,10 @@ namespace System.Security.Cryptography.Tests { - public sealed class CompositeMLDsaCngTests_AllPlatforms + public static class CompositeMLDsaCngTests_AllPlatforms { [Fact] - public void CompositeMLDsaCng_Ctor_ArgValidation() + public static void CompositeMLDsaCng_Ctor_ArgValidation() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs index 191df10e968562..12760049ab7cd4 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs @@ -705,7 +705,7 @@ public static void SignData_BufferSize(CompositeMLDsaTestVector vector) return testSignatureSize; }; - byte[] signature = dsa.SignData(vector.Message); + byte[] signature = dsa.SignData(vector.Message.ToArray()); Assert.Equal(testSignatureSize, signature.Length); signature = new byte[vector.Algorithm.MaxSignatureSizeInBytes]; @@ -810,8 +810,8 @@ public static void TrySignData_CallsCore(CompositeMLDsaTestVector vector) { using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(vector.Algorithm); - byte[] message = vector.Message; - byte[] expectedSignature = vector.Signature; + byte[] message = vector.Message.ToArray(); + byte[] expectedSignature = vector.Signature.ToArray(); dsa.SignDataCoreHook = (_, _, _) => { return -1; }; dsa.AddFillDestination(expectedSignature); @@ -841,8 +841,8 @@ public static void VerifyData_CallsCore(CompositeMLDsaTestVector vector) { using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(vector.Algorithm); - byte[] message = vector.Message; - byte[] expectedSignature = vector.Signature; + byte[] message = vector.Message.ToArray(); + byte[] expectedSignature = vector.Signature.ToArray(); dsa.VerifyDataCoreHook = (_, _, _) => true; dsa.AddDataBufferIsSameAssertion(message); 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 57611fb2cb878e..324a6323a1961c 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 @@ -82,8 +82,8 @@ public static void ImportBadPrivateKey_ShortTradKey(CompositeMLDsaAlgorithm algo [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); + byte[] key = new byte[vector.SecretKey.Length + 1]; + vector.SecretKey.CopyTo(key); AssertImportBadPrivateKey(vector.Algorithm, key); } @@ -96,13 +96,13 @@ public static void ImportBadPrivateKey_Rsa_WrongAlgorithm() CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); // But use MLDsa65WithRSA4096Pss - AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.SecretKey); + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.SecretKey.ToArray()); // And flip differentTradKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); - AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.SecretKey); + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.SecretKey.ToArray()); } [Fact] @@ -113,13 +113,13 @@ public static void ImportBadPrivateKey_ECDsa_WrongAlgorithm() CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); // But use MLDsa65WithECDsaP384 - AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.SecretKey); + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.SecretKey.ToArray()); // And flip differentTradKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); - AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.SecretKey); + AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.SecretKey.ToArray()); } [Theory] @@ -383,8 +383,8 @@ public static void ImportBadPublicKey_ShortTradKey(CompositeMLDsaAlgorithm algor [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); + byte[] key = new byte[vector.PublicKey.Length + 1]; + vector.PublicKey.CopyTo(key); AssertImportBadPublicKey(vector.Algorithm, key); } @@ -394,7 +394,7 @@ public static void ImportBadPublicKey_TrailingData(CompositeMLDsaTestData.Compos [MemberData(nameof(CompositeMLDsaTestData.SupportedECDsaAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void ImportBadPublicKey_ECDsa_Uncompressed(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) { - byte[] key = vector.PublicKey.AsSpan().ToArray(); + byte[] key = vector.PublicKey.ToArray(); int formatIndex = CompositeMLDsaTestHelpers.MLDsaAlgorithms[vector.Algorithm].PublicKeySizeInBytes; // Uncompressed @@ -421,13 +421,13 @@ public static void ImportBadPublicKey_Rsa_WrongAlgorithm() CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); // But use MLDsa65WithRSA4096Pss - AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.PublicKey); + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.PublicKey.ToArray()); // And flip differentTradKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); - AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.PublicKey); + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.PublicKey.ToArray()); } [Fact] @@ -438,13 +438,13 @@ public static void ImportBadPublicKey_ECDsa_WrongAlgorithm() CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); // But use MLDsa65WithECDsaP384 - AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.PublicKey); + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.PublicKey.ToArray()); // And flip differentTradKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); - AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.PublicKey); + AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.PublicKey.ToArray()); } [Theory] @@ -509,7 +509,7 @@ static void AssertThrows(byte[] encodedBytes) [Fact] public static void ImportSpki_BerEncoding() { - byte[] spki = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).Spki; + byte[] spki = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).Spki.ToArray(); byte[] berSpki = AsnUtils.ConvertDerToNonDerBer(spki); CompositeMLDsaTestHelpers.AssertImportSubjectPublicKeyInfo(import => @@ -561,7 +561,7 @@ public static void ImportSubjectPublicKeyInfo_AlgorithmErrorsInAsn() Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384), Parameters = CompositeMLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid }, - SubjectPublicKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).PublicKey, + SubjectPublicKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).PublicKey.ToArray(), }; CompositeMLDsaTestHelpers.AssertImportSubjectPublicKeyInfo( @@ -588,7 +588,7 @@ public static void ImportSubjectPublicKeyInfo_SupportedButHasUnsupportedAlgorith Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa87WithEd448), Parameters = null, }, - SubjectPublicKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa87WithEd448).PublicKey, + SubjectPublicKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa87WithEd448).PublicKey.ToArray(), }; CompositeMLDsaTestHelpers.AssertImportSubjectPublicKeyInfo( @@ -617,7 +617,7 @@ public static void ImportPkcs8PrivateKey_AlgorithmErrorsInAsn() Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384), Parameters = CompositeMLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid }, - PrivateKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).SecretKey, + PrivateKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).SecretKey.ToArray(), }; CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( @@ -643,7 +643,7 @@ public static void ImportPkcs8PrivateKey_SupportedButHasUnsupportedAlgorithm() Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa87WithEd448), Parameters = null, }, - PrivateKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa87WithEd448).SecretKey, + PrivateKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa87WithEd448).SecretKey.ToArray(), }; CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( @@ -715,12 +715,12 @@ public static void AlgorithmMatches_Import(CompositeMLDsaTestData.CompositeMLDsa CompositeMLDsaTestHelpers.AssertImportPublicKey( import => AssertThrowIfNotSupported(() => Assert.Equal(vector.Algorithm, import().Algorithm), vector.Algorithm), vector.Algorithm, - vector.PublicKey); + vector.PublicKey.ToArray()); CompositeMLDsaTestHelpers.AssertImportPrivateKey( import => AssertThrowIfNotSupported(() => Assert.Equal(vector.Algorithm, import().Algorithm), vector.Algorithm), vector.Algorithm, - vector.SecretKey); + vector.SecretKey.ToArray()); } [Fact] diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaImplementationTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaImplementationTests.cs index 2d7c9c7bba839e..1c69b05bfea453 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaImplementationTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaImplementationTests.cs @@ -21,10 +21,10 @@ public static void CompositeMLDsaIsOnlyPublicAncestor_GenerateKey(CompositeMLDsa public static void CompositeMLDsaIsOnlyPublicAncestor_Import(CompositeMLDsaTestData.CompositeMLDsaTestVector info) { CompositeMLDsaTestHelpers.AssertImportPublicKey( - AssertCompositeMLDsaIsOnlyPublicAncestor, info.Algorithm, info.PublicKey); + AssertCompositeMLDsaIsOnlyPublicAncestor, info.Algorithm, info.PublicKey.ToArray()); CompositeMLDsaTestHelpers.AssertImportPrivateKey( - AssertCompositeMLDsaIsOnlyPublicAncestor, info.Algorithm, info.SecretKey); + AssertCompositeMLDsaIsOnlyPublicAncestor, info.Algorithm, info.SecretKey.ToArray()); } private static void AssertCompositeMLDsaIsOnlyPublicAncestor(Func createKey) @@ -46,7 +46,7 @@ public static void ImportPkcs8_BerEncoding() CompositeMLDsaTestData.CompositeMLDsaTestVector vector = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); // Pkcs8 is DER encoded, so create a BER encoding from it by making it use a non-minimal length encoding. - byte[] key = vector.Pkcs8; + byte[] key = vector.Pkcs8.ToArray(); byte[] nonMinimalEncoding = AsnUtils.ConvertDerToNonDerBer(key); @@ -262,7 +262,7 @@ public void RoundTrip_Import_Export_PublicKey(CompositeMLDsaTestData.CompositeML CompositeMLDsaTestHelpers.WithDispose(import(), dsa => CompositeMLDsaTestHelpers.AssertPublicKeyEquals(info.Algorithm, info.PublicKey, export(dsa)))), info.Algorithm, - info.PublicKey); + info.PublicKey.ToArray()); } [Theory] @@ -274,27 +274,31 @@ public void RoundTrip_Import_Export_PrivateKey(CompositeMLDsaTestData.CompositeM CompositeMLDsaTestHelpers.WithDispose(import(), dsa => CompositeMLDsaTestHelpers.AssertPrivateKeyEquals(info.Algorithm, info.SecretKey, export(dsa)))), info.Algorithm, - info.SecretKey); + info.SecretKey.ToArray()); } [Theory] [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public void RoundTrip_Import_Export_SpkiPublicKey(CompositeMLDsaTestData.CompositeMLDsaTestVector info) { + byte[] spki = info.Spki.ToArray(); + CompositeMLDsaTestHelpers.AssertImportSubjectPublicKeyInfo(import => CompositeMLDsaTestHelpers.AssertExportSubjectPublicKeyInfo(export => - CompositeMLDsaTestHelpers.WithDispose(import(info.Spki), dsa => - AssertExtensions.SequenceEqual(info.Spki, export(dsa))))); + CompositeMLDsaTestHelpers.WithDispose(import(spki), dsa => + AssertExtensions.SequenceEqual(spki, export(dsa))))); } [Theory] [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public void RoundTrip_Import_Export_Pkcs8PrivateKey(CompositeMLDsaTestData.CompositeMLDsaTestVector info) { + byte[] secretKey = info.SecretKey.ToArray(); + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey(import => CompositeMLDsaTestHelpers.AssertExportPrivateKey(export => CompositeMLDsaTestHelpers.WithDispose(import(info.Pkcs8), dsa => - CompositeMLDsaTestHelpers.AssertPrivateKeyEquals(info.Algorithm, info.SecretKey, export(dsa))))); + CompositeMLDsaTestHelpers.AssertPrivateKeyEquals(info.Algorithm, secretKey, export(dsa))))); } #endregion Roundtrip by importing then exporting 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 70b101d79fe84e..0c2b797e4069b1 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 @@ -25,16 +25,16 @@ public class CompositeMLDsaTestVector private readonly byte[] _signatureWithContext; private byte[] _spki; - internal byte[] Message => _message.AsSpan().ToArray(); - internal byte[] Context => _context.AsSpan().ToArray(); - internal byte[] PublicKey => _publicKey.AsSpan().ToArray(); - internal byte[] Certificate => _certificate.AsSpan().ToArray(); - internal byte[] SecretKey => _secretKey.AsSpan().ToArray(); - internal byte[] Pkcs8 => _pkcs8.AsSpan().ToArray(); - internal byte[] Signature => _signature.AsSpan().ToArray(); - internal byte[] SignatureWithContext => _signatureWithContext.AsSpan().ToArray(); - - internal byte[] Spki => _spki.AsSpan().ToArray(); + internal ReadOnlySpan Message => _message; + internal ReadOnlySpan Context => _context; + internal ReadOnlySpan PublicKey => _publicKey; + internal ReadOnlySpan Certificate => _certificate; + internal ReadOnlySpan SecretKey => _secretKey; + internal ReadOnlySpan Pkcs8 => _pkcs8; + internal ReadOnlySpan Signature => _signature; + internal ReadOnlySpan SignatureWithContext => _signatureWithContext; + + internal ReadOnlySpan Spki => _spki; internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, string pk, string x5c, string sk, string sk_pkcs8, string m, string ctx, string s, string sWithContext) { 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 e1eacd52e00105..426422f036cf2f 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 @@ -130,9 +130,9 @@ from vector in CompositeMLDsaTestData.SupportedAlgorithmIetfVectors [MemberData(nameof(SupportedIetfVectorsWithContextFlagTestData))] public void ImportExportVerify(CompositeMLDsaTestData.CompositeMLDsaTestVector vector, bool useContext) { - byte[] message = vector.Message; - byte[] context = useContext ? vector.Context : Array.Empty(); - byte[] expectedSignature = useContext ? vector.SignatureWithContext : vector.Signature; + byte[] message = vector.Message.ToArray(); + byte[] context = useContext ? vector.Context.ToArray() : Array.Empty(); + byte[] expectedSignature = (useContext ? vector.SignatureWithContext : vector.Signature).ToArray(); using (CompositeMLDsa privateKey = ImportPrivateKey(vector.Algorithm, vector.SecretKey)) { @@ -163,9 +163,9 @@ public void ImportExportVerify(CompositeMLDsaTestData.CompositeMLDsaTestVector v [MemberData(nameof(SupportedIetfVectorsWithContextFlagTestData))] public void ImportSignVerify(CompositeMLDsaTestData.CompositeMLDsaTestVector vector, bool useContext) { - byte[] message = vector.Message; - byte[] context = useContext ? vector.Context : Array.Empty(); - byte[] expectedSignature = useContext ? vector.SignatureWithContext : vector.Signature; + byte[] message = vector.Message.ToArray(); + byte[] context = useContext ? vector.Context.ToArray() : Array.Empty(); + byte[] expectedSignature = (useContext ? vector.SignatureWithContext : vector.Signature).ToArray(); byte[] signature; @@ -296,8 +296,8 @@ public void TryExportPublicKey_BufferTooSmall(CompositeMLDsaTestData.CompositeML [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public void ImportPrivateKey_TrailingData(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) { - byte[] secretKeyWithTrailingData = vector.SecretKey; - Array.Resize(ref secretKeyWithTrailingData, vector.SecretKey.Length + 1); + byte[] secretKeyWithTrailingData = new byte[vector.SecretKey.Length + 1]; + vector.SecretKey.CopyTo(secretKeyWithTrailingData); Assert.Throws(() => ImportPrivateKey(vector.Algorithm, secretKeyWithTrailingData)); } @@ -305,8 +305,8 @@ public void ImportPrivateKey_TrailingData(CompositeMLDsaTestData.CompositeMLDsaT [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] public void ImportPublicKey_TrailingData(CompositeMLDsaTestData.CompositeMLDsaTestVector vector) { - byte[] publicKeyWithTrailingData = vector.PublicKey; - Array.Resize(ref publicKeyWithTrailingData, vector.PublicKey.Length + 1); + byte[] publicKeyWithTrailingData = new byte[vector.PublicKey.Length + 1]; + vector.PublicKey.CopyTo(publicKeyWithTrailingData); Assert.Throws(() => ImportPublicKey(vector.Algorithm, publicKeyWithTrailingData)); } From 26707b498fe4a937c84006465ec4c59dd0dd59a3 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Tue, 7 Jul 2026 01:08:32 -0700 Subject: [PATCH 3/3] remaining feedback --- .../Cryptography/CompositeMLDsaCng.Windows.cs | 65 +++++++++++++++++-- .../CompositeMLDsaTestHelpers.Cng.cs | 7 ++ .../ref/System.Security.Cryptography.cs | 4 -- .../Security/Cryptography/CngAlgorithm.cs | 2 +- .../Cryptography/CngAlgorithmGroup.cs | 2 +- 5 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs index 4dd6582b00cf2c..aef91bca9ae361 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaCng.Windows.cs @@ -129,22 +129,61 @@ protected override int ExportCompositeMLDsaPublicKeyCore(Span destination) ExportKey(CngKeyBlobFormat.PQDsaPublicBlob, Algorithm.MaxPublicKeySizeInBytes, destination); /// - protected override int ExportCompositeMLDsaPrivateKeyCore(Span destination) => - CngPkcs8.AllowsOnlyEncryptedExport(_key) - ? throw new CryptographicException(SR.Cryptography_KeyNotExtractable) - : ExportKey(CngKeyBlobFormat.PQDsaPrivateBlob, Algorithm.MaxPrivateKeySizeInBytes, destination); + protected override int ExportCompositeMLDsaPrivateKeyCore(Span destination) + { + if (CngPkcs8.AllowsOnlyEncryptedExport(_key)) + { + ArraySegment pkcs8 = GetRentedPkcs8ForEncryptedOnlyExport(); + + try + { + ReadOnlySpan privateKey = KeyFormatHelper.ReadPkcs8([Algorithm.Oid], pkcs8.AsSpan(), out _); + + if (!privateKey.TryCopyTo(destination)) + { + Debug.Fail($"Private key size too large for buffer: {privateKey.Length} / {destination.Length}"); + throw new CryptographicException(); + } + + return privateKey.Length; + } + finally + { + CryptoPool.Return(pkcs8); + } + } + + return ExportKey(CngKeyBlobFormat.PQDsaPrivateBlob, Algorithm.MaxPrivateKeySizeInBytes, destination); + } /// protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) { bool encryptedOnlyExport = CngPkcs8.AllowsOnlyEncryptedExport(_key); - // Windows NCrypt does not yet support PKCS#8 export for Composite ML-DSA. if (encryptedOnlyExport) { - throw new CryptographicException(SR.Cryptography_KeyNotExtractable); + ArraySegment pkcs8 = GetRentedPkcs8ForEncryptedOnlyExport(); + + try + { + if (destination.Length < pkcs8.Count) + { + bytesWritten = 0; + return false; + } + + bytesWritten = pkcs8.Count; + pkcs8.AsSpan().CopyTo(destination); + return true; + } + finally + { + CryptoPool.Return(pkcs8); + } } + // Windows NCrypt does not yet support PKCS#8 export for Composite ML-DSA, so build it from the private key. return TryExportPkcs8FromExportedPrivateKey(destination, out bytesWritten); } @@ -190,5 +229,19 @@ private int ExportKey(CngKeyBlobFormat blobFormat, int maxKeySize, Span de return keyBytes.Length; } } + + private ArraySegment GetRentedPkcs8ForEncryptedOnlyExport() + { + const string TemporaryExportPassword = "DotnetExportPhrase"; + byte[] exported = _key.ExportPkcs8KeyBlob(TemporaryExportPassword, 1); + + using (PinAndClear.Track(exported)) + { + return KeyFormatHelper.DecryptPkcs8( + TemporaryExportPassword, + exported, + out _); + } + } } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs index 9cca05a283dd3d..2a1692ef052113 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaTestHelpers.Cng.cs @@ -9,6 +9,13 @@ namespace System.Security.Cryptography.Tests { internal static partial class CompositeMLDsaTestHelpers { + private static readonly CngAlgorithm s_compositeMLDsa = new CngAlgorithm("Composite-ML-DSA"); + + extension (CngAlgorithm) + { + internal static CngAlgorithm CompositeMLDsa => s_compositeMLDsa; + } + private const int NTE_NOT_SUPPORTED = unchecked((int)0x80090029); internal static CompositeMLDsaCng ImportPublicKey(CompositeMLDsaAlgorithm algorithm, ReadOnlySpan source) diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 6cf2e086576d60..c0ed4128c3b29a 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -326,8 +326,6 @@ public sealed partial class CngAlgorithm : System.IEquatable [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] - public static CngAlgorithm CompositeMLDsa => + internal static CngAlgorithm CompositeMLDsa => field ??= new CngAlgorithm("Composite-ML-DSA"); // BCRYPT_COMPOSITE_MLDSA_ALGORITHM private static CngAlgorithm? s_ecdh; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs index 39a0c2a08fd010..519ddfc8dc3ade 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CngAlgorithmGroup.cs @@ -165,7 +165,7 @@ public static CngAlgorithmGroup Rsa /// An object that specifies the Composite ML-DSA family of algorithms. /// [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] - public static CngAlgorithmGroup CompositeMLDsa => + internal static CngAlgorithmGroup CompositeMLDsa => field ??= new CngAlgorithmGroup("CompositeMLDSA"); // NCRYPT_COMPOSITE_MLDSA_ALGORITHM_GROUP private static CngAlgorithmGroup? s_dh;