From f8b88634de9acc165fa7ad069d5c3e8fcdbc9c1f Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Wed, 17 Sep 2025 23:16:31 -0700 Subject: [PATCH 1/5] SPKI and PKCS8 for Composite ML-DSA --- .../Security/Cryptography/CompositeMLDsa.cs | 14 +- .../Cryptography/CompositeMLDsaManaged.cs | 40 +- .../CompositeMLDsaContractTests.cs | 268 +++++++++++++ .../CompositeMLDsaFactoryTests.cs | 211 +++++++++- .../CompositeMLDsaImplementationTests.cs | 172 +++++++++ .../CompositeMLDsaMockImplementation.cs | 51 ++- .../CompositeMLDsaTestHelpers.cs | 360 ++++++++++++++++-- 7 files changed, 1072 insertions(+), 44 deletions(-) diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs index 6b7cd722e3ecd5..78eb7b5415376d 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsa.cs @@ -866,19 +866,13 @@ static void PrivateKeyReader( out CompositeMLDsa dsa) { CompositeMLDsaAlgorithm algorithm = GetAlgorithmIdentifier(in algorithmIdentifier); - AsnValueReader reader = new AsnValueReader(privateKeyContents.Span, AsnEncodingRules.BER); - if (!reader.TryReadPrimitiveOctetString(out ReadOnlySpan key) || reader.HasData) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - if (!algorithm.IsValidPrivateKeySize(key.Length)) + if (!algorithm.IsValidPrivateKeySize(privateKeyContents.Length)) { throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); } - dsa = CompositeMLDsaImplementation.ImportCompositeMLDsaPrivateKeyImpl(algorithm, key); + dsa = CompositeMLDsaImplementation.ImportCompositeMLDsaPrivateKeyImpl(algorithm, privateKeyContents.Span); } } @@ -1863,9 +1857,7 @@ private AsnWriter WriteSubjectPublicKeyToAsnWriter() ReadOnlySpan publicKey = buffer.AsSpan(0, written); - // TODO verify overhead - - // TODO: The ASN.1 overhead of a SubjectPublicKeyInfo encoding a public key is ___ bytes. + // The ASN.1 overhead of a SubjectPublicKeyInfo encoding a public key is around 24 bytes. // Round it off to 32. This checked operation should never throw because the inputs are not // user provided. int capacity = checked(32 + publicKey.Length); diff --git a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs index 5bb63453387841..f8fd72d7679c63 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/CompositeMLDsaManaged.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Formats.Asn1; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using Internal.Cryptography; @@ -367,8 +368,43 @@ protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan destination, out int bytesWritten) => - throw new PlatformNotSupportedException(); + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + AsnWriter? writer = null; + + try + { + using (CryptoPoolLease lease = CryptoPoolLease.Rent(Algorithm.MaxPrivateKeySizeInBytes)) + { + int privateKeySize = ExportCompositeMLDsaPrivateKeyCore(lease.Span); + + // Add some overhead for the ASN.1 structure. + int initialCapacity = 32 + privateKeySize; + + writer = new AsnWriter(AsnEncodingRules.DER, initialCapacity); + + using (writer.PushSequence()) + { + writer.WriteInteger(0); // Version + + using (writer.PushSequence()) + { + writer.WriteObjectIdentifier(Algorithm.Oid); + } + + writer.WriteOctetString(lease.Span.Slice(0, privateKeySize)); + } + + Debug.Assert(writer.GetEncodedLength() <= initialCapacity); + } + + return writer.TryEncode(destination, out bytesWritten); + } + finally + { + writer?.Reset(); + } + } protected override int ExportCompositeMLDsaPublicKeyCore(Span destination) { 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 5e1dd854f41660..587bff4bfc7cff 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 @@ -2,7 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Formats.Asn1; using System.Linq; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.SLHDsa.Tests; +using Test.Cryptography; using Xunit; using CompositeMLDsaTestVector = System.Security.Cryptography.Tests.CompositeMLDsaTestData.CompositeMLDsaTestVector; @@ -28,10 +32,26 @@ public static void NullArgumentValidation(CompositeMLDsaAlgorithm algorithm, boo dsa.Dispose(); } + PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 42); + AssertExtensions.Throws("data", () => dsa.SignData(null)); AssertExtensions.Throws("data", () => dsa.VerifyData(null, null)); AssertExtensions.Throws("signature", () => dsa.VerifyData(Array.Empty(), null)); + + AssertExtensions.Throws("password", () => dsa.ExportEncryptedPkcs8PrivateKey((string)null, pbeParameters)); + AssertExtensions.Throws("password", () => dsa.ExportEncryptedPkcs8PrivateKeyPem((string)null, pbeParameters)); + AssertExtensions.Throws("password", () => dsa.TryExportEncryptedPkcs8PrivateKey((string)null, pbeParameters, Span.Empty, out _)); + + AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(string.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKeyPem(string.Empty, null)); + AssertExtensions.Throws("pbeParameters", () => dsa.TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null, Span.Empty, out _)); + AssertExtensions.Throws("pbeParameters", () => dsa.TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null, Span.Empty, out _)); + AssertExtensions.Throws("pbeParameters", () => dsa.TryExportEncryptedPkcs8PrivateKey(string.Empty, null, Span.Empty, out _)); } [Fact] @@ -62,6 +82,37 @@ public static void ArgumentValidation(CompositeMLDsaAlgorithm algorithm, bool sh AssertExtensions.Throws("context", () => dsa.VerifyData(Array.Empty(), new byte[maxSignatureSize], new byte[256])); } + [Theory] + [MemberData(nameof(ArgumentValidationData))] + public static void ArgumentValidation_PbeParameters(CompositeMLDsaAlgorithm algorithm, bool shouldDispose) + { + using CompositeMLDsa dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + if (shouldDispose) + { + // Test that argument validation exceptions take precedence over ObjectDisposedException + dsa.Dispose(); + } + + CompositeMLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export => + { + // Unknown algorithm + AssertExtensions.Throws(() => + export(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.Unknown, HashAlgorithmName.SHA1, 42))); + + // TripleDes3KeyPkcs12 only works with SHA1 + AssertExtensions.Throws(() => + export(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA512, 42))); + }); + + CompositeMLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export => + { + // Bytes not allowed in TripleDes3KeyPkcs12 + AssertExtensions.Throws(() => + export(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 42))); + }, CompositeMLDsaTestHelpers.EncryptionPasswordType.Byte); + } + [Theory] [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void TryExportCompositeMLDsaPublicKey_LowerBound(CompositeMLDsaAlgorithm algorithm) @@ -926,6 +977,223 @@ public static void Dispose_CallsVirtual(CompositeMLDsaAlgorithm algorithm) CompositeMLDsaTestHelpers.VerifyDisposed(dsa); } + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ExportSubjectPublicKeyInfo_CallsExportPublicKey(CompositeMLDsaAlgorithm algorithm) + { + CompositeMLDsaTestHelpers.AssertExportSubjectPublicKeyInfo(export => + { + using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + dsa.ExportCompositeMLDsaPublicKeyCoreHook = dest => dest.Length; + dsa.AddLengthAssertion(); + dsa.AddFillDestination(1); + + byte[] exported = export(dsa); + AssertExtensions.GreaterThan(dsa.ExportCompositeMLDsaPublicKeyCoreCallCount, 0); + + SubjectPublicKeyInfoAsn exportedSpki = SubjectPublicKeyInfoAsn.Decode(exported, AsnEncodingRules.DER); + AssertExtensions.FilledWith(1, exportedSpki.SubjectPublicKey.Span); + Assert.Equal(CompositeMLDsaTestHelpers.AlgorithmToOid(algorithm), exportedSpki.Algorithm.Algorithm); + AssertExtensions.FalseExpression(exportedSpki.Algorithm.Parameters.HasValue); + }); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void TryExportPkcs8PrivateKey_DestinationTooSmall(CompositeMLDsaAlgorithm algorithm) + { + using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + // Early heuristic based bailout so no core methods are called + AssertExtensions.FalseExpression( + dsa.TryExportPkcs8PrivateKey(new byte[CompositeMLDsaTestHelpers.ExpectedPrivateKeySizeLowerBound(algorithm) - 1], out int bytesWritten)); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ExportPkcs8PrivateKey_DestinationInitialSize(CompositeMLDsaAlgorithm algorithm) + { + using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + dsa.TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + // The first call should at least be the size of the private key + destination.Fill(42); + AssertExtensions.GreaterThanOrEqualTo(destination.Length, CompositeMLDsaTestHelpers.ExpectedPrivateKeySizeLowerBound(algorithm)); + bytesWritten = destination.Length; + + // Before we return, update the next callback so subsequent calls fail the test + dsa.TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + Assert.Fail(); + bytesWritten = 0; + return true; + }; + + return true; + }; + + byte[] exported = dsa.ExportPkcs8PrivateKey(); + + Assert.Equal(1, dsa.TryExportPkcs8PrivateKeyCoreCallCount); + AssertExtensions.FilledWith(42, exported); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ExportPkcs8PrivateKey_Resizes(CompositeMLDsaAlgorithm algorithm) + { + using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + int originalSize = -1; + dsa.TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + // Return false to force a resize + bool ret = false; + originalSize = destination.Length; + bytesWritten = 0; + + // Before we return false, update the callback so the next call will succeed + dsa.TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + // New buffer must be larger than the original + bool ret = true; + AssertExtensions.GreaterThan(destination.Length, originalSize); + destination.Fill(42); + bytesWritten = destination.Length; + + // Before we return, update the next callback so subsequent calls fail the test + dsa.TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + Assert.Fail(); + bytesWritten = 0; + return true; + }; + + return ret; + }; + + return ret; + }; + + byte[] exported = dsa.ExportPkcs8PrivateKey(); + + Assert.Equal(2, dsa.TryExportPkcs8PrivateKeyCoreCallCount); + AssertExtensions.FilledWith(42, exported); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ExportPkcs8PrivateKey_IgnoreReturnValue(CompositeMLDsaAlgorithm algorithm) + { + using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + int[] valuesToWrite = [-1, 0, int.MaxValue]; + int index = 0; + + int finalDestinationSize = -1; + dsa.TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + // Go through all the values we want to test, and once we reach the last one, + // return true with a valid value + if (index >= valuesToWrite.Length) + { + finalDestinationSize = bytesWritten = 1; + return true; + } + + // This returned value should should be ignored. There's no way to check + // what happens with it, but at the very least we should expect no exceptions + // and the correct number of calls. + bytesWritten = valuesToWrite[index]; + index++; + return false; + }; + + int actualSize = dsa.ExportPkcs8PrivateKey().Length; + Assert.Equal(finalDestinationSize, actualSize); + Assert.Equal(valuesToWrite.Length + 1, dsa.TryExportPkcs8PrivateKeyCoreCallCount); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ExportPkcs8PrivateKey_HandleBadReturnValue(CompositeMLDsaAlgorithm algorithm) + { + using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + Func getBadReturnValue = (int destinationLength) => destinationLength + 1; + CompositeMLDsaMockImplementation.TryExportFunc hook = (Span destination, out int bytesWritten) => + { + bool ret = true; + + bytesWritten = getBadReturnValue(destination.Length); + + // Before we return, update the next callback so subsequent calls fail the test + dsa.TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + Assert.Fail(); + bytesWritten = 0; + return true; + }; + + return ret; + }; + + dsa.TryExportPkcs8PrivateKeyCoreHook = hook; + Assert.Throws(dsa.ExportPkcs8PrivateKey); + Assert.Equal(1, dsa.TryExportPkcs8PrivateKeyCoreCallCount); + + dsa.TryExportPkcs8PrivateKeyCoreHook = hook; + getBadReturnValue = (int destinationLength) => int.MaxValue; + Assert.Throws(dsa.ExportPkcs8PrivateKey); + Assert.Equal(2, dsa.TryExportPkcs8PrivateKeyCoreCallCount); + + dsa.TryExportPkcs8PrivateKeyCoreHook = hook; + getBadReturnValue = (int destinationLength) => -1; + Assert.Throws(dsa.ExportPkcs8PrivateKey); + Assert.Equal(3, dsa.TryExportPkcs8PrivateKeyCoreCallCount); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public static void ExportPkcs8PrivateKey_HandleBadReturnBuffer(CompositeMLDsaAlgorithm algorithm) + { + CompositeMLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(exportEncrypted => + { + using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm); + + // Create a bad encoding + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.WriteBitString("some string"u8); + byte[] validEncoding = writer.Encode(); + Memory badEncoding = validEncoding.AsMemory(0, validEncoding.Length - 1); // Chop off the last byte + + CompositeMLDsaMockImplementation.TryExportFunc hook = (Span destination, out int bytesWritten) => + { + bool ret = badEncoding.Span.TryCopyTo(destination); + bytesWritten = ret ? badEncoding.Length : 0; + return ret; + }; + + dsa.TryExportPkcs8PrivateKeyCoreHook = hook; + + // Exporting the key should work without any issues because there's no validation + AssertExtensions.SequenceEqual(badEncoding.Span, dsa.ExportPkcs8PrivateKey().AsSpan()); + + int numberOfCalls = dsa.TryExportPkcs8PrivateKeyCoreCallCount; + dsa.TryExportPkcs8PrivateKeyCoreCallCount = 0; + + // However, exporting the encrypted key should fail because it validates the PKCS#8 private key encoding first + AssertExtensions.Throws(() => + exportEncrypted(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA1, 1))); + + // Sanity check that the code to export the private key was called + Assert.Equal(numberOfCalls, dsa.TryExportPkcs8PrivateKeyCoreCallCount); + }); + } + private static void AssertExpectedFill(ReadOnlySpan buffer, ReadOnlySpan content, int offset, byte paddingElement) { // Ensure that the data was filled correctly 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 b07b0b30821e17..f52d787de2bc59 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 @@ -3,8 +3,10 @@ using System.Formats.Asn1; using System.Linq; +using System.Security.Cryptography.Asn1; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; +using Test.Cryptography; using Xunit; using Xunit.Sdk; @@ -24,6 +26,17 @@ public static void NullArgumentValidation() AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256, null)); AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportCompositeMLDsaPublicKey(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256, null)); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportPkcs8PrivateKey(null)); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportSubjectPublicKeyInfo(null)); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromPem(null)); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportEncryptedPkcs8PrivateKey("PLACEHOLDER", null)); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromEncryptedPem(null, "PLACEHOLDER")); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromEncryptedPem(null, "PLACEHOLDER"u8.ToArray())); + + AssertExtensions.Throws("password", static () => CompositeMLDsa.ImportEncryptedPkcs8PrivateKey((string)null, Array.Empty())); + AssertExtensions.Throws("password", static () => CompositeMLDsa.ImportFromEncryptedPem(string.Empty, (string)null)); + + AssertExtensions.Throws("passwordBytes", static () => CompositeMLDsa.ImportFromEncryptedPem(string.Empty, (byte[])null)); } [Theory] @@ -409,6 +422,189 @@ private static void AssertImportBadPublicKey(CompositeMLDsaAlgorithm algorithm, key); } + [Fact] + public static void ArgumentValidation_MalformedAsnEncoding() + { + // Generate a valid ASN.1 encoding + byte[] encodedBytes = CreateAsn1EncodedBytes(); + int actualEncodedLength = encodedBytes.Length; + + // Add a trailing byte so the length indicated in the encoding will be smaller than the actual data. + Array.Resize(ref encodedBytes, actualEncodedLength + 1); + AssertThrows(encodedBytes); + + // Remove the last byte so the length indicated in the encoding will be larger than the actual data. + Array.Resize(ref encodedBytes, actualEncodedLength - 1); + AssertThrows(encodedBytes); + + static void AssertThrows(byte[] encodedBytes) + { + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo( + import => Assert.Throws(() => import(encodedBytes)), + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(encodedBytes)))); + + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( + import => Assert.Throws(() => import(encodedBytes)), + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(encodedBytes)))); + + CompositeMLDsaTestHelpers.AssertImportEncryptedPkcs8PrivateKey( + import => Assert.Throws(() => import("PLACEHOLDER", encodedBytes)), + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import("PLACEHOLDER", encodedBytes)))); + } + } + + [Fact] + public static void ImportSpki_BerEncoding() + { + // Valid BER but invalid DER - uses indefinite length encoding + byte[] indefiniteLengthOctet = [0x04, 0x80, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00]; + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(import => + AssertThrowIfNotSupported(() => + Assert.Throws(() => import(indefiniteLengthOctet)))); + } + + [Fact] + public static void ImportPkcs8_WrongTypeInAsn() + { + // Create an incorrect ASN.1 structure to pass into the import methods. + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + AlgorithmIdentifierAsn algorithmIdentifier = new AlgorithmIdentifierAsn + { + Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384), + }; + algorithmIdentifier.Encode(writer); + byte[] wrongAsnType = writer.Encode(); + + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(wrongAsnType)))); + + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(wrongAsnType)))); + + CompositeMLDsaTestHelpers.AssertImportEncryptedPkcs8PrivateKey( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import("PLACEHOLDER", wrongAsnType)))); + } + + [Fact] + public static void ImportSubjectKeyPublicInfo_AlgorithmErrorsInAsn() + { +#if !NETFRAMEWORK // Does not support exporting RSA SPKI + if (!OperatingSystem.IsBrowser()) + { + // RSA key + using RSA rsa = RSA.Create(); + byte[] rsaSpkiBytes = rsa.ExportSubjectPublicKeyInfo(); + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(rsaSpkiBytes)))); + } +#endif + + // Create an invalid Composite ML-DSA SPKI with parameters + SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn + { + Algorithm = new AlgorithmIdentifierAsn + { + Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaTestData.AllIetfVectors[0].Algorithm), + Parameters = CompositeMLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid + }, + SubjectPublicKey = CompositeMLDsaTestData.AllIetfVectors[0].PublicKey, + }; + + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(spki.Encode())))); + + spki.Algorithm.Parameters = AsnUtils.DerNull; + + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(spki.Encode())))); + + // Sanity check + spki.Algorithm.Parameters = null; + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(import => AssertThrowIfNotSupported(() => import(spki.Encode()))); + } + + [Fact] + public static void ImportPkcs8PrivateKey_AlgorithmErrorsInAsn() + { +#if !NETFRAMEWORK // Does not support exporting RSA PKCS#8 private key + if (!OperatingSystem.IsBrowser()) + { + // RSA key isn't valid for ML-DSA + using RSA rsa = RSA.Create(); + byte[] rsaPkcs8Bytes = rsa.ExportPkcs8PrivateKey(); + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(rsaPkcs8Bytes)))); + } +#endif + + // Create an invalid Composite ML-DSA PKCS8 with parameters + PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn + { + PrivateKeyAlgorithm = new AlgorithmIdentifierAsn + { + Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaTestData.AllIetfVectors[0].Algorithm), + Parameters = CompositeMLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid + }, + PrivateKey = CompositeMLDsaTestData.AllIetfVectors[0].SecretKey, + }; + + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(pkcs8.Encode())))); + + pkcs8.PrivateKeyAlgorithm.Parameters = AsnUtils.DerNull; + + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( + import => AssertThrowIfNotSupported(() => Assert.Throws(() => import(pkcs8.Encode())))); + + // Sanity check + pkcs8.PrivateKeyAlgorithm.Parameters = null; + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey(import => AssertThrowIfNotSupported(() => import(pkcs8.Encode()))); + } + + [Fact] + public static void ImportFromPem_MalformedPem() + { + AssertThrows(WritePemRaw("UNKNOWN LABEL", [])); + AssertThrows(string.Empty); + AssertThrows(WritePemRaw("ENCRYPTED PRIVATE KEY", [])); + AssertThrows(WritePemRaw("PUBLIC KEY", []) + '\n' + WritePemRaw("PUBLIC KEY", [])); + AssertThrows(WritePemRaw("PRIVATE KEY", []) + '\n' + WritePemRaw("PUBLIC KEY", [])); + AssertThrows(WritePemRaw("PUBLIC KEY", []) + '\n' + WritePemRaw("PRIVATE KEY", [])); + AssertThrows(WritePemRaw("PRIVATE KEY", []) + '\n' + WritePemRaw("PRIVATE KEY", [])); + AssertThrows(WritePemRaw("PRIVATE KEY", "%")); + AssertThrows(WritePemRaw("PUBLIC KEY", "%")); + + static void AssertThrows(string pem) + { + AssertThrowIfNotSupported(() => + AssertExtensions.Throws("source", () => MLDsa.ImportFromPem(pem))); + AssertThrowIfNotSupported(() => + AssertExtensions.Throws("source", () => MLDsa.ImportFromPem(pem.AsSpan()))); + } + } + + [Fact] + public static void ImportFromEncryptedPem_MalformedPem() + { + AssertThrows(WritePemRaw("UNKNOWN LABEL", [])); + AssertThrows(WritePemRaw("CERTIFICATE", [])); + AssertThrows(string.Empty); + AssertThrows(WritePemRaw("ENCRYPTED PRIVATE KEY", []) + '\n' + WritePemRaw("ENCRYPTED PRIVATE KEY", [])); + AssertThrows(WritePemRaw("ENCRYPTED PRIVATE KEY", "%")); + + static void AssertThrows(string encryptedPem) + { + AssertThrowIfNotSupported(() => + AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"))); + AssertThrowIfNotSupported(() => + AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8))); + AssertThrowIfNotSupported(() => + AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem.AsSpan(), "PLACEHOLDER"))); + AssertThrowIfNotSupported(() => + AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8.ToArray()))); + } + } + [Theory] [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] public static void AlgorithmMatches_GenerateKey(CompositeMLDsaAlgorithm algorithm) @@ -477,9 +673,9 @@ public static void IsSupported_InitializesCrypto() // Asserts the test throws PlatformNotSupportedException if Composite ML-DSA is supported; // otherwise runs the test normally. - private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorithm algorithm) + private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorithm? algorithm = null) { - if (CompositeMLDsa.IsAlgorithmSupported(algorithm)) + if ((algorithm is null && CompositeMLDsa.IsSupported) || CompositeMLDsa.IsAlgorithmSupported(algorithm)) { test(); } @@ -499,5 +695,16 @@ private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorit } } } + + private static byte[] CreateAsn1EncodedBytes() + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + writer.WriteOctetString("some data"u8); + byte[] encodedBytes = writer.Encode(); + return encodedBytes; + } + + private static string WritePemRaw(string label, ReadOnlySpan data) => + $"-----BEGIN {label}-----\n{data.ToString()}\n-----END {label}-----"; } } 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 03ac1315b541ff..12885223eb1b26 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 @@ -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.Diagnostics; +using System.Security.Cryptography.SLHDsa.Tests; using Xunit; namespace System.Security.Cryptography.Tests @@ -39,6 +41,40 @@ private static void AssertCompositeMLDsaIsOnlyPublicAncestor(Func + CompositeMLDsaTestHelpers.AssertExportPrivateKey(export => + CompositeMLDsaTestHelpers.WithDispose(import(nonMinimalEncoding), mldsa => + AssertExtensions.SequenceEqual(CompositeMLDsaTestData.AllIetfVectors[0].SecretKey, export(mldsa))))); + } + #region Roundtrip by exporting then importing [Theory] @@ -97,6 +133,142 @@ public void RoundTrip_Export_Import_PrivateKey(CompositeMLDsaAlgorithm algorithm }); } + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Export_Import_Pkcs8PrivateKey(CompositeMLDsaAlgorithm algorithm) + { + // Generate new key + using CompositeMLDsa dsa = GenerateKey(algorithm); + byte[] privateKey = dsa.ExportCompositeMLDsaPrivateKey(); + byte[] publicKey = dsa.ExportCompositeMLDsaPublicKey(); + + CompositeMLDsaTestHelpers.AssertExportPkcs8PrivateKey(export => + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey(import => + { + // Roundtrip it using PKCS#8 + using CompositeMLDsa roundTrippedDsa = import(export(dsa)); + + // The keys should be the same + Assert.Equal(algorithm, roundTrippedDsa.Algorithm); + AssertExtensions.SequenceEqual(publicKey, roundTrippedDsa.ExportCompositeMLDsaPublicKey()); + AssertExtensions.SequenceEqual(privateKey, roundTrippedDsa.ExportCompositeMLDsaPrivateKey()); + })); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Export_Import_SPKI(CompositeMLDsaAlgorithm algorithm) + { + // Generate new key + using CompositeMLDsa dsa = GenerateKey(algorithm); + byte[] publicKey = dsa.ExportCompositeMLDsaPublicKey(); + byte[] privateKey = dsa.ExportCompositeMLDsaPrivateKey(); + + CompositeMLDsaTestHelpers.AssertExportPkcs8PrivateKey(export => + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey(import => + { + // Roundtrip it using SPKI + using CompositeMLDsa roundTrippedDsa = import(export(dsa)); + + // The keys should be the same + Assert.Equal(algorithm, roundTrippedDsa.Algorithm); + AssertExtensions.SequenceEqual(publicKey, roundTrippedDsa.ExportCompositeMLDsaPublicKey()); + AssertExtensions.SequenceEqual(privateKey, roundTrippedDsa.ExportCompositeMLDsaPrivateKey()); + })); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Export_Import_EncryptedPkcs8PrivateKey(CompositeMLDsaAlgorithm algorithm) + { + // Generate new key + using CompositeMLDsa dsa = GenerateKey(algorithm); + byte[] privateKey = dsa.ExportCompositeMLDsaPrivateKey(); + byte[] publicKey = dsa.ExportCompositeMLDsaPublicKey(); + + PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA1, 1); + + CompositeMLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export => + CompositeMLDsaTestHelpers.AssertImportEncryptedPkcs8PrivateKey(import => + { + // Roundtrip it using encrypted PKCS#8 + using CompositeMLDsa roundTrippedDsa = import("PLACEHOLDER", export(dsa, "PLACEHOLDER", pbeParameters)); + + // The keys should be the same + Assert.Equal(algorithm, roundTrippedDsa.Algorithm); + AssertExtensions.SequenceEqual(privateKey, roundTrippedDsa.ExportCompositeMLDsaPrivateKey()); + AssertExtensions.SequenceEqual(publicKey, roundTrippedDsa.ExportCompositeMLDsaPublicKey()); + })); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Export_Import_Pkcs8PrivateKeyPem(CompositeMLDsaAlgorithm algorithm) + { + // Generate new key + using CompositeMLDsa dsa = GenerateKey(algorithm); + byte[] privateKey = dsa.ExportCompositeMLDsaPrivateKey(); + byte[] publicKey = dsa.ExportCompositeMLDsaPublicKey(); + + CompositeMLDsaTestHelpers.AssertExportToPrivateKeyPem(export => + CompositeMLDsaTestHelpers.AssertImportFromPem(import => + { + // Roundtrip it using PEM + using CompositeMLDsa roundTrippedDsa = import(export(dsa)); + + // The keys should be the same + Assert.Equal(algorithm, roundTrippedDsa.Algorithm); + AssertExtensions.SequenceEqual(privateKey, roundTrippedDsa.ExportCompositeMLDsaPrivateKey()); + AssertExtensions.SequenceEqual(publicKey, roundTrippedDsa.ExportCompositeMLDsaPublicKey()); + })); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Export_Import_SPKIPem(CompositeMLDsaAlgorithm algorithm) + { + // Generate new key + using CompositeMLDsa dsa = GenerateKey(algorithm); + byte[] privateKey = dsa.ExportCompositeMLDsaPrivateKey(); + byte[] publicKey = dsa.ExportCompositeMLDsaPublicKey(); + + CompositeMLDsaTestHelpers.AssertExportToPublicKeyPem(export => + CompositeMLDsaTestHelpers.AssertImportFromPem(import => + { + // Roundtrip it using PEM + using CompositeMLDsa roundTrippedDsa = import(export(dsa)); + + // The keys should be the same + Assert.Equal(algorithm, roundTrippedDsa.Algorithm); + AssertExtensions.SequenceEqual(publicKey, roundTrippedDsa.ExportCompositeMLDsaPublicKey()); + Assert.Throws(() => roundTrippedDsa.ExportCompositeMLDsaPrivateKey()); + })); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Export_Import_EncryptedPkcs8PrivateKeyPem(CompositeMLDsaAlgorithm algorithm) + { + // Generate new key + using CompositeMLDsa dsa = GenerateKey(algorithm); + byte[] privateKey = dsa.ExportCompositeMLDsaPrivateKey(); + byte[] publicKey = dsa.ExportCompositeMLDsaPublicKey(); + + PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA1, 1); + + CompositeMLDsaTestHelpers.AssertExportToEncryptedPem(export => + CompositeMLDsaTestHelpers.AssertImportFromEncryptedPem(import => + { + // Roundtrip it using encrypted PKCS#8 + using CompositeMLDsa roundTrippedDsa = import(export(dsa, "PLACEHOLDER", pbeParameters), "PLACEHOLDER"); + + // The keys should be the same + Assert.Equal(algorithm, roundTrippedDsa.Algorithm); + AssertExtensions.SequenceEqual(privateKey, roundTrippedDsa.ExportCompositeMLDsaPrivateKey()); + AssertExtensions.SequenceEqual(publicKey, roundTrippedDsa.ExportCompositeMLDsaPublicKey()); + })); + } + #endregion Roundtrip by exporting then importing #region Roundtrip by importing then exporting diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaMockImplementation.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaMockImplementation.cs index b78e08e5b3ca4b..668afe8ff564dc 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaMockImplementation.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaMockImplementation.cs @@ -18,18 +18,21 @@ public CompositeMLDsaMockImplementation(CompositeMLDsaAlgorithm algorithm) internal delegate int SignDataFunc(ReadOnlySpan data, ReadOnlySpan context, Span destination); internal delegate bool VerifyDataFunc(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature); internal delegate int ExportFunc(Span destination); + internal delegate bool TryExportFunc(Span destination, out int written); internal delegate void DisposeAction(bool disposing); public int SignDataCoreCallCount = 0; public int VerifyDataCoreCallCount = 0; public int ExportCompositeMLDsaPublicKeyCoreCallCount = 0; public int ExportCompositeMLDsaPrivateKeyCoreCallCount = 0; + public int TryExportPkcs8PrivateKeyCoreCallCount = 0; public int DisposeCallCount = 0; public SignDataFunc SignDataCoreHook { get; set; } = (_, _, _) => { Assert.Fail(); return 0; }; public VerifyDataFunc VerifyDataCoreHook { get; set; } = (_, _, _) => { Assert.Fail(); return false; }; public ExportFunc ExportCompositeMLDsaPublicKeyCoreHook { get; set; } = _ => { Assert.Fail(); return 0; }; public ExportFunc ExportCompositeMLDsaPrivateKeyCoreHook { get; set; } = _ => { Assert.Fail(); return 0; }; + public TryExportFunc TryExportPkcs8PrivateKeyCoreHook { get; set; } = (destination, out bytesWritten) => { Assert.Fail(); bytesWritten = 0; return false; }; public DisposeAction DisposeHook { get; set; } = _ => { }; protected override int SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) @@ -64,7 +67,8 @@ protected override void Dispose(bool disposing) protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) { - throw new NotImplementedException(); + TryExportPkcs8PrivateKeyCoreCallCount++; + return TryExportPkcs8PrivateKeyCoreHook(destination, out bytesWritten); } public void AddLengthAssertion() @@ -93,9 +97,9 @@ public void AddLengthAssertion() ExportCompositeMLDsaPublicKeyCoreHook = (Span destination) => { int ret = oldExportCompositeMLDsaPublicKeyCoreHook(destination); - AssertExtensions.LessThanOrEqualTo( - CompositeMLDsaTestHelpers.MLDsaAlgorithms[Algorithm].PublicKeySizeInBytes, - destination.Length); + AssertExtensions.GreaterThanOrEqualTo( + destination.Length, + CompositeMLDsaTestHelpers.ExpectedPublicKeySizeLowerBound(Algorithm)); return ret; }; @@ -103,9 +107,19 @@ public void AddLengthAssertion() ExportCompositeMLDsaPrivateKeyCoreHook = (Span destination) => { int ret = oldExportCompositeMLDsaPrivateKeyCoreHook(destination); - AssertExtensions.LessThanOrEqualTo( - CompositeMLDsaTestHelpers.MLDsaAlgorithms[Algorithm].PrivateSeedSizeInBytes, - destination.Length); + AssertExtensions.GreaterThanOrEqualTo( + destination.Length, + CompositeMLDsaTestHelpers.ExpectedPrivateKeySizeLowerBound(Algorithm)); + return ret; + }; + + TryExportFunc oldTryExportPkcs8PrivateKeyCoreHook = TryExportPkcs8PrivateKeyCoreHook; + TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + bool ret = oldTryExportPkcs8PrivateKeyCoreHook(destination, out bytesWritten); + AssertExtensions.GreaterThanOrEqualTo( + destination.Length, + CompositeMLDsaTestHelpers.ExpectedPrivateKeySizeLowerBound(Algorithm)); return ret; }; } @@ -211,6 +225,14 @@ public void AddFillDestination(byte b) destination.Fill(b); return destination.Length; }; + + TryExportFunc oldTryExportPkcs8PrivateKeyCoreHook = TryExportPkcs8PrivateKeyCoreHook; + TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + bool ret = oldTryExportPkcs8PrivateKeyCoreHook(destination, out bytesWritten); + destination.Fill(b); + return ret; + }; } public void AddFillDestination(byte[] fillContents) @@ -253,6 +275,21 @@ public void AddFillDestination(byte[] fillContents) return 0; }; + + TryExportFunc oldTryExportPkcs8PrivateKeyCoreHook = TryExportPkcs8PrivateKeyCoreHook; + TryExportPkcs8PrivateKeyCoreHook = (Span destination, out int bytesWritten) => + { + bool ret = oldTryExportPkcs8PrivateKeyCoreHook(destination, out int localBytesWritten); + + if (fillContents.AsSpan().TryCopyTo(destination)) + { + bytesWritten = fillContents.Length; + return true; + } + + bytesWritten = 0; + return false; + }; } } } 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 8df9cb261f0503..265010748170ee 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 @@ -3,7 +3,10 @@ using System.Collections.Generic; using System.Formats.Asn1; +using System.Security.Cryptography.Asn1; using System.Security.Cryptography.Rsa.Tests; +using System.Text; +using Test.Cryptography; using Xunit; using Xunit.Sdk; @@ -11,6 +14,9 @@ namespace System.Security.Cryptography.Tests { internal static class CompositeMLDsaTestHelpers { + // DER encoding of ASN.1 BitString "foo" + internal static readonly ReadOnlyMemory s_derBitStringFoo = new byte[] { 0x03, 0x04, 0x00, 0x66, 0x6f, 0x6f }; + internal static readonly Dictionary MLDsaAlgorithms = new() { { CompositeMLDsaAlgorithm.MLDsa44WithRSA2048Pss, MLDsaAlgorithm.MLDsa44 }, @@ -35,36 +41,318 @@ internal static class CompositeMLDsaTestHelpers { CompositeMLDsaAlgorithm.MLDsa87WithECDsaP521, MLDsaAlgorithm.MLDsa87 }, }; - internal static void AssertImportPublicKey(Action> action, CompositeMLDsaAlgorithm algorithm, byte[] publicKey) + internal static void AssertImportPublicKey(Action> test, CompositeMLDsaAlgorithm algorithm, byte[] publicKey) => + AssertImportPublicKey(test, test, algorithm, publicKey); + + internal static void AssertImportPublicKey(Action> testDirectCall, Action> testEmbeddedCall, CompositeMLDsaAlgorithm algorithm, byte[] publicKey) { - action(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, publicKey)); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, publicKey)); if (publicKey?.Length == 0) { - action(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, Array.Empty().AsSpan())); - action(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, ReadOnlySpan.Empty)); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, Array.Empty().AsSpan())); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, ReadOnlySpan.Empty)); } else { - action(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, publicKey.AsSpan())); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPublicKey(algorithm, publicKey.AsSpan())); } + + SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn + { + Algorithm = new AlgorithmIdentifierAsn + { + Algorithm = AlgorithmToOid(algorithm), + Parameters = default(ReadOnlyMemory?), + }, + SubjectPublicKey = publicKey, + }; + + AssertImportSubjectKeyPublicInfo(import => testEmbeddedCall(() => import(spki.Encode()))); + } + + internal delegate CompositeMLDsa ImportSubjectKeyPublicInfoCallback(byte[] spki); + internal static void AssertImportSubjectKeyPublicInfo(Action test) => + AssertImportSubjectKeyPublicInfo(test, test); + + internal static void AssertImportSubjectKeyPublicInfo( + Action testDirectCall, + Action testEmbeddedCall) + { + testDirectCall(spki => CompositeMLDsa.ImportSubjectPublicKeyInfo(spki)); + testDirectCall(spki => CompositeMLDsa.ImportSubjectPublicKeyInfo(spki.AsSpan())); + + testEmbeddedCall(spki => CompositeMLDsa.ImportFromPem(PemEncoding.WriteString("PUBLIC KEY", spki))); + testEmbeddedCall(spki => CompositeMLDsa.ImportFromPem(PemEncoding.WriteString("PUBLIC KEY", spki).AsSpan())); } - internal static void AssertImportPrivateKey(Action> action, CompositeMLDsaAlgorithm algorithm, byte[] privateKey) + internal static void AssertImportPrivateKey(Action> test, CompositeMLDsaAlgorithm algorithm, byte[] privateKey) => + AssertImportPrivateKey(test, test, algorithm, privateKey); + + internal static void AssertImportPrivateKey(Action> testDirectCall, Action> testEmbeddedCall, CompositeMLDsaAlgorithm algorithm, byte[] privateKey) { - action(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, privateKey)); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, privateKey)); if (privateKey?.Length == 0) { - action(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, Array.Empty().AsSpan())); - action(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, ReadOnlySpan.Empty)); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, Array.Empty().AsSpan())); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, ReadOnlySpan.Empty)); } else { - action(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, privateKey.AsSpan())); + testDirectCall(() => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(algorithm, privateKey.AsSpan())); } + + PrivateKeyInfoAsn pkcs8 = new PrivateKeyInfoAsn + { + PrivateKeyAlgorithm = new AlgorithmIdentifierAsn + { + Algorithm = AlgorithmToOid(algorithm), + Parameters = default(ReadOnlyMemory?), + }, + PrivateKey = privateKey, + }; + + AssertImportPkcs8PrivateKey(import => testEmbeddedCall(() => import(pkcs8.Encode()))); + } + + internal delegate CompositeMLDsa ImportPkcs8PrivateKeyCallback(ReadOnlySpan pkcs8); + internal static void AssertImportPkcs8PrivateKey(Action callback) => + AssertImportPkcs8PrivateKey(callback, callback); + + internal static void AssertImportPkcs8PrivateKey( + Action testDirectCall, + Action testEmbeddedCall) + { + testDirectCall(pkcs8 => CompositeMLDsa.ImportPkcs8PrivateKey(pkcs8)); + testDirectCall(pkcs8 => CompositeMLDsa.ImportPkcs8PrivateKey(pkcs8.ToArray())); + + AssertImportFromPem(importPem => + { + testEmbeddedCall(pkcs8 => importPem(PemEncoding.WriteString("PRIVATE KEY", pkcs8))); + }); } + internal static void AssertImportFromPem(Action> callback) + { + callback(static (string pem) => CompositeMLDsa.ImportFromPem(pem)); + callback(static (string pem) => CompositeMLDsa.ImportFromPem(pem.AsSpan())); + } + + internal static void AssertImportEncryptedPkcs8PrivateKey( + Action test, + EncryptionPasswordType passwordTypeToTest = EncryptionPasswordType.All) => + AssertImportEncryptedPkcs8PrivateKey(test, test, passwordTypeToTest); + + internal delegate CompositeMLDsa ImportEncryptedPkcs8PrivateKeyCallback(string password, ReadOnlySpan pkcs8); + internal static void AssertImportEncryptedPkcs8PrivateKey( + Action testDirectCall, + Action testEmbeddedCall, + EncryptionPasswordType passwordTypeToTest = EncryptionPasswordType.All) + { + if ((passwordTypeToTest & EncryptionPasswordType.Char) != 0) + { + testDirectCall((password, pkcs8) => CompositeMLDsa.ImportEncryptedPkcs8PrivateKey(password, pkcs8.ToArray())); + testDirectCall((password, pkcs8) => CompositeMLDsa.ImportEncryptedPkcs8PrivateKey(password.AsSpan(), pkcs8)); + } + + if ((passwordTypeToTest & EncryptionPasswordType.Byte) != 0) + { + testDirectCall((password, pkcs8) => + CompositeMLDsa.ImportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(password), pkcs8.ToArray())); + } + + AssertImportFromEncryptedPem(importPem => + { + testEmbeddedCall((string password, ReadOnlySpan pkcs8) => + { + string pem = PemEncoding.WriteString("ENCRYPTED PRIVATE KEY", pkcs8); + return importPem(pem, password); + }); + }, passwordTypeToTest); + } + + internal delegate CompositeMLDsa ImportFromEncryptedPemCallback(string source, string password); + internal static void AssertImportFromEncryptedPem( + Action callback, + EncryptionPasswordType passwordTypeToTest = EncryptionPasswordType.All) + { + if ((passwordTypeToTest & EncryptionPasswordType.Char) != 0) + { + callback(static (string pem, string password) => CompositeMLDsa.ImportFromEncryptedPem(pem, password)); + callback(static (string pem, string password) => CompositeMLDsa.ImportFromEncryptedPem(pem.AsSpan(), password)); + } + + if ((passwordTypeToTest & EncryptionPasswordType.Byte) != 0) + { + callback(static (string pem, string password) => + CompositeMLDsa.ImportFromEncryptedPem(pem, Encoding.UTF8.GetBytes(password))); + callback(static (string pem, string password) => + CompositeMLDsa.ImportFromEncryptedPem(pem.AsSpan(), Encoding.UTF8.GetBytes(password))); + } + } + + internal static void AssertExportPublicKey(Action> callback) + { + callback(dsa => + { + // For simplicity, use a large enough size for all algorithms. + byte[] buffer = new byte[4096]; + + int size = dsa.ExportCompositeMLDsaPublicKey(buffer.AsSpan()); + Array.Resize(ref buffer, size); + + return buffer; + }); + + callback(dsa => dsa.ExportCompositeMLDsaPublicKey()); + callback(dsa => DoTryUntilDone(dsa.TryExportCompositeMLDsaPublicKey)); + + AssertExportSubjectPublicKeyInfo(exportSpki => + callback(dsa => + SubjectPublicKeyInfoAsn.Decode(exportSpki(dsa), AsnEncodingRules.DER).SubjectPublicKey.ToArray())); + } + + internal static void AssertExportPrivateKey(Action> callback) => + AssertExportPrivateKey(callback, callback); + + internal static void AssertExportPrivateKey(Action> directCallback, Action> indirectCallback) + { + directCallback(dsa => + { + // For simplicity, use a large enough size for all algorithms. + byte[] buffer = new byte[4096]; + + int size = dsa.ExportCompositeMLDsaPrivateKey(buffer.AsSpan()); + Array.Resize(ref buffer, size); + + return buffer; + }); + + directCallback(dsa => dsa.ExportCompositeMLDsaPrivateKey()); + directCallback(dsa => DoTryUntilDone(dsa.TryExportCompositeMLDsaPrivateKey)); + + AssertExportPkcs8PrivateKey(exportPkcs8 => + indirectCallback(dsa => + PrivateKeyInfoAsn.Decode( + exportPkcs8(dsa), AsnEncodingRules.DER).PrivateKey.ToArray())); + } + + internal static void AssertExportPkcs8PrivateKey(CompositeMLDsa dsa, Action callback) => + AssertExportPkcs8PrivateKey(export => callback(export(dsa))); + + internal static void AssertExportPkcs8PrivateKey(Action> callback) + { + callback(dsa => DoTryUntilDone(dsa.TryExportPkcs8PrivateKey)); + callback(dsa => dsa.ExportPkcs8PrivateKey()); + callback(dsa => DecodePem(dsa.ExportPkcs8PrivateKeyPem())); + + static byte[] DecodePem(string pem) + { + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal(Index.FromStart(0), fields.Location.Start); + Assert.Equal(Index.FromStart(pem.Length), fields.Location.End); + Assert.Equal("PRIVATE KEY", pem.AsSpan()[fields.Label].ToString()); + return Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString()); + } + } + + internal static void AssertExportSubjectPublicKeyInfo(CompositeMLDsa dsa, Action callback) => + AssertExportSubjectPublicKeyInfo(export => callback(export(dsa))); + + internal static void AssertExportSubjectPublicKeyInfo(Action> callback) + { + callback(dsa => DoTryUntilDone(dsa.TryExportSubjectPublicKeyInfo)); + callback(dsa => dsa.ExportSubjectPublicKeyInfo()); + callback(dsa => DecodePem(dsa.ExportSubjectPublicKeyInfoPem())); + + static byte[] DecodePem(string pem) + { + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal(Index.FromStart(0), fields.Location.Start); + Assert.Equal(Index.FromStart(pem.Length), fields.Location.End); + Assert.Equal("PUBLIC KEY", pem.AsSpan()[fields.Label].ToString()); + return Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString()); + } + } + + internal static void AssertEncryptedExportPkcs8PrivateKey( + CompositeMLDsa dsa, + string password, + PbeParameters pbeParameters, + Action callback) => + AssertEncryptedExportPkcs8PrivateKey(export => callback(export(dsa, password, pbeParameters))); + + internal delegate byte[] ExportEncryptedPkcs8PrivateKeyCallback(CompositeMLDsa dsa, string password, PbeParameters pbeParameters); + internal static void AssertEncryptedExportPkcs8PrivateKey( + Action callback, + EncryptionPasswordType passwordTypesToTest = EncryptionPasswordType.All) + { + if ((passwordTypesToTest & EncryptionPasswordType.Char) != 0) + { + callback((dsa, password, pbeParameters) => + DoTryUntilDone((Span destination, out int bytesWritten) => + dsa.TryExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters, destination, out bytesWritten))); + callback((dsa, password, pbeParameters) => + DoTryUntilDone((Span destination, out int bytesWritten) => + dsa.TryExportEncryptedPkcs8PrivateKey(password, pbeParameters, destination, out bytesWritten))); + + callback((dsa, password, pbeParameters) => dsa.ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters)); + callback((dsa, password, pbeParameters) => dsa.ExportEncryptedPkcs8PrivateKey(password, pbeParameters)); + + callback((dsa, password, pbeParameters) => DecodePem(dsa.ExportEncryptedPkcs8PrivateKeyPem(password.AsSpan(), pbeParameters))); + callback((dsa, password, pbeParameters) => DecodePem(dsa.ExportEncryptedPkcs8PrivateKeyPem(password, pbeParameters))); + } + + if ((passwordTypesToTest & EncryptionPasswordType.Byte) != 0) + { + callback((dsa, password, pbeParameters) => + DoTryUntilDone((Span destination, out int bytesWritten) => + dsa.TryExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters, destination, out bytesWritten))); + + callback((dsa, password, pbeParameters) => + dsa.ExportEncryptedPkcs8PrivateKey(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters)); + + callback((dsa, password, pbeParameters) => + DecodePem(dsa.ExportEncryptedPkcs8PrivateKeyPem(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters))); + } + + static byte[] DecodePem(string pem) + { + PemFields fields = PemEncoding.Find(pem.AsSpan()); + Assert.Equal(Index.FromStart(0), fields.Location.Start); + Assert.Equal(Index.FromStart(pem.Length), fields.Location.End); + Assert.Equal("ENCRYPTED PRIVATE KEY", pem.AsSpan()[fields.Label].ToString()); + return Convert.FromBase64String(pem.AsSpan()[fields.Base64Data].ToString()); + } + } + + internal delegate string ExportToPemCallback(CompositeMLDsa dsa, string password, PbeParameters pbeParameters); + internal static void AssertExportToEncryptedPem( + Action callback, + EncryptionPasswordType passwordTypesToTest = EncryptionPasswordType.All) + { + if ((passwordTypesToTest & EncryptionPasswordType.Char) != 0) + { + callback((dsa, password, pbeParameters) => + dsa.ExportEncryptedPkcs8PrivateKeyPem(password, pbeParameters)); + callback((dsa, password, pbeParameters) => + dsa.ExportEncryptedPkcs8PrivateKeyPem(password.AsSpan(), pbeParameters)); + } + + if ((passwordTypesToTest & EncryptionPasswordType.Byte) != 0) + { + callback((dsa, password, pbeParameters) => + dsa.ExportEncryptedPkcs8PrivateKeyPem(new ReadOnlySpan(Encoding.UTF8.GetBytes(password)), pbeParameters)); + } + } + + internal static void AssertExportToPrivateKeyPem(Action> callback) => + callback(dsa => dsa.ExportPkcs8PrivateKeyPem()); + + internal static void AssertExportToPublicKeyPem(Action> callback) => + callback(dsa => dsa.ExportSubjectPublicKeyInfoPem()); + internal class RsaAlgorithm(int keySizeInBits) { internal int KeySizeInBits { get; } = keySizeInBits; @@ -196,18 +484,6 @@ internal static int ExpectedPrivateKeySizeUpperBound(CompositeMLDsaAlgorithm alg internal static bool IsECDsa(CompositeMLDsaAlgorithm algorithm) => ExecuteComponentFunc(algorithm, rsa => false, ecdsa => true, eddsa => false); - internal static void AssertExportPublicKey(Action> callback) - { - callback(dsa => dsa.ExportCompositeMLDsaPublicKey()); - callback(dsa => DoTryUntilDone(dsa.TryExportCompositeMLDsaPublicKey)); - } - - internal static void AssertExportPrivateKey(Action> callback) - { - callback(dsa => dsa.ExportCompositeMLDsaPrivateKey()); - callback(dsa => DoTryUntilDone(dsa.TryExportCompositeMLDsaPrivateKey)); - } - internal static void WithDispose(T disposable, Action callback) where T : IDisposable { @@ -356,6 +632,33 @@ internal static void VerifyDisposed(CompositeMLDsa dsa) Assert.Throws(() => dsa.ExportCompositeMLDsaPublicKey()); } + internal static string? AlgorithmToOid(CompositeMLDsaAlgorithm algorithm) + { + return algorithm?.Name switch + { + "MLDSA44-RSA2048-PSS-SHA256" => "2.16.840.1.114027.80.9.1.0", + "MLDSA44-RSA2048-PKCS15-SHA256" => "2.16.840.1.114027.80.9.1.1", + "MLDSA44-Ed25519-SHA512" => "2.16.840.1.114027.80.9.1.2", + "MLDSA44-ECDSA-P256-SHA256" => "2.16.840.1.114027.80.9.1.3", + "MLDSA65-RSA3072-PSS-SHA512" => "2.16.840.1.114027.80.9.1.4", + "MLDSA65-RSA3072-PKCS15-SHA512" => "2.16.840.1.114027.80.9.1.5", + "MLDSA65-RSA4096-PSS-SHA512" => "2.16.840.1.114027.80.9.1.6", + "MLDSA65-RSA4096-PKCS15-SHA512" => "2.16.840.1.114027.80.9.1.7", + "MLDSA65-ECDSA-P256-SHA512" => "2.16.840.1.114027.80.9.1.8", + "MLDSA65-ECDSA-P384-SHA512" => "2.16.840.1.114027.80.9.1.9", + "MLDSA65-ECDSA-brainpoolP256r1-SHA512" => "2.16.840.1.114027.80.9.1.10", + "MLDSA65-Ed25519-SHA512" => "2.16.840.1.114027.80.9.1.11", + "MLDSA87-ECDSA-P384-SHA512" => "2.16.840.1.114027.80.9.1.12", + "MLDSA87-ECDSA-brainpoolP384r1-SHA512" => "2.16.840.1.114027.80.9.1.13", + "MLDSA87-Ed448-SHAKE256" => "2.16.840.1.114027.80.9.1.14", + "MLDSA87-RSA3072-PSS-SHA512" => "2.16.840.1.114027.80.9.1.15", + "MLDSA87-RSA4096-PSS-SHA512" => "2.16.840.1.114027.80.9.1.16", + "MLDSA87-ECDSA-P521-SHA512" => "2.16.840.1.114027.80.9.1.17", + + _ => throw new XunitException("Unknown algorithm."), + }; + } + private delegate bool TryExportFunc(Span destination, out int bytesWritten); private static byte[] DoTryUntilDone(TryExportFunc func) { @@ -369,5 +672,18 @@ private static byte[] DoTryUntilDone(TryExportFunc func) return buffer.AsSpan(0, written).ToArray(); } + + internal static EncryptionPasswordType GetValidPasswordTypes(PbeParameters pbeParameters) + => pbeParameters.EncryptionAlgorithm == PbeEncryptionAlgorithm.TripleDes3KeyPkcs12 + ? EncryptionPasswordType.Char + : EncryptionPasswordType.All; + + [Flags] + internal enum EncryptionPasswordType + { + Byte = 1, + Char = 2, + All = Char | Byte, + } } } From 1a50d86407099bd92cd5d588ac21893b298ba691 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Thu, 18 Sep 2025 01:17:58 -0700 Subject: [PATCH 2/5] MLDsa -> CompositeMLDsa --- .../CompositeMLDsa/CompositeMLDsaFactoryTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 f52d787de2bc59..f3f8c3b7204003 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 @@ -577,9 +577,9 @@ public static void ImportFromPem_MalformedPem() static void AssertThrows(string pem) { AssertThrowIfNotSupported(() => - AssertExtensions.Throws("source", () => MLDsa.ImportFromPem(pem))); + AssertExtensions.Throws("source", () => CompositeMLDsa.ImportFromPem(pem))); AssertThrowIfNotSupported(() => - AssertExtensions.Throws("source", () => MLDsa.ImportFromPem(pem.AsSpan()))); + AssertExtensions.Throws("source", () => CompositeMLDsa.ImportFromPem(pem.AsSpan()))); } } @@ -595,13 +595,13 @@ public static void ImportFromEncryptedPem_MalformedPem() static void AssertThrows(string encryptedPem) { AssertThrowIfNotSupported(() => - AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"))); + AssertExtensions.Throws("source", () => CompositeMLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"))); AssertThrowIfNotSupported(() => - AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8))); + AssertExtensions.Throws("source", () => CompositeMLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8))); AssertThrowIfNotSupported(() => - AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem.AsSpan(), "PLACEHOLDER"))); + AssertExtensions.Throws("source", () => CompositeMLDsa.ImportFromEncryptedPem(encryptedPem.AsSpan(), "PLACEHOLDER"))); AssertThrowIfNotSupported(() => - AssertExtensions.Throws("source", () => MLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8.ToArray()))); + AssertExtensions.Throws("source", () => CompositeMLDsa.ImportFromEncryptedPem(encryptedPem, "PLACEHOLDER"u8.ToArray()))); } } From 4efda715834c91bb5d9f6feeb40e8a78654da4ee Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Thu, 18 Sep 2025 09:57:57 -0700 Subject: [PATCH 3/5] fix test condition --- .../CompositeMLDsa/CompositeMLDsaFactoryTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f3f8c3b7204003..e69e3f59a2a2d4 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 @@ -675,7 +675,9 @@ public static void IsSupported_InitializesCrypto() // otherwise runs the test normally. private static void AssertThrowIfNotSupported(Action test, CompositeMLDsaAlgorithm? algorithm = null) { - if ((algorithm is null && CompositeMLDsa.IsSupported) || CompositeMLDsa.IsAlgorithmSupported(algorithm)) + bool isSupported = algorithm is null ? CompositeMLDsa.IsSupported : CompositeMLDsa.IsAlgorithmSupported(algorithm); + + if (isSupported) { test(); } From 5d220cfe18feb69db7e70b52d476ce5d2b0d926f Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Fri, 19 Sep 2025 01:04:05 -0700 Subject: [PATCH 4/5] PR feedback --- .../CompositeMLDsaContractTests.cs | 10 ++-- .../CompositeMLDsaFactoryTests.cs | 49 +++++++++-------- .../CompositeMLDsaImplementationTests.cs | 51 +++++++++--------- .../CompositeMLDsa/CompositeMLDsaTestData.cs | 21 +++++++- .../System/Security/Cryptography/AsnUtils.cs | 53 ++++++++++++++++++- 5 files changed, 125 insertions(+), 59 deletions(-) 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 587bff4bfc7cff..c3a96479066bd2 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 @@ -5,8 +5,6 @@ using System.Formats.Asn1; using System.Linq; using System.Security.Cryptography.Asn1; -using System.Security.Cryptography.SLHDsa.Tests; -using Test.Cryptography; using Xunit; using CompositeMLDsaTestVector = System.Security.Cryptography.Tests.CompositeMLDsaTestData.CompositeMLDsaTestVector; @@ -32,16 +30,14 @@ public static void NullArgumentValidation(CompositeMLDsaAlgorithm algorithm, boo dsa.Dispose(); } - PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 42); - AssertExtensions.Throws("data", () => dsa.SignData(null)); AssertExtensions.Throws("data", () => dsa.VerifyData(null, null)); AssertExtensions.Throws("signature", () => dsa.VerifyData(Array.Empty(), null)); - AssertExtensions.Throws("password", () => dsa.ExportEncryptedPkcs8PrivateKey((string)null, pbeParameters)); - AssertExtensions.Throws("password", () => dsa.ExportEncryptedPkcs8PrivateKeyPem((string)null, pbeParameters)); - AssertExtensions.Throws("password", () => dsa.TryExportEncryptedPkcs8PrivateKey((string)null, pbeParameters, Span.Empty, out _)); + AssertExtensions.Throws("password", () => dsa.ExportEncryptedPkcs8PrivateKey((string)null, null)); + AssertExtensions.Throws("password", () => dsa.ExportEncryptedPkcs8PrivateKeyPem((string)null, null)); + AssertExtensions.Throws("password", () => dsa.TryExportEncryptedPkcs8PrivateKey((string)null, null, Span.Empty, out _)); AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null)); AssertExtensions.Throws("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan.Empty, null)); 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 e69e3f59a2a2d4..348eb9e1de2b79 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 @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Formats.Asn1; -using System.Linq; using System.Security.Cryptography.Asn1; using Microsoft.DotNet.RemoteExecutor; -using Microsoft.DotNet.XUnitExtensions; using Test.Cryptography; using Xunit; using Xunit.Sdk; @@ -19,9 +17,9 @@ public static void NullArgumentValidation() { AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.GenerateKey(null)); AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.IsAlgorithmSupported(null)); - AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(null, Array.Empty())); + AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(null, null)); AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(null, ReadOnlySpan.Empty)); - AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.ImportCompositeMLDsaPublicKey(null, Array.Empty())); + AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.ImportCompositeMLDsaPublicKey(null, null)); AssertExtensions.Throws("algorithm", static () => CompositeMLDsa.ImportCompositeMLDsaPublicKey(null, ReadOnlySpan.Empty)); AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportCompositeMLDsaPrivateKey(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256, null)); @@ -30,10 +28,10 @@ public static void NullArgumentValidation() AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportSubjectPublicKeyInfo(null)); AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromPem(null)); AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportEncryptedPkcs8PrivateKey("PLACEHOLDER", null)); - AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromEncryptedPem(null, "PLACEHOLDER")); - AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromEncryptedPem(null, "PLACEHOLDER"u8.ToArray())); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromEncryptedPem(null, (string)null)); + AssertExtensions.Throws("source", static () => CompositeMLDsa.ImportFromEncryptedPem(null, (byte[])null)); - AssertExtensions.Throws("password", static () => CompositeMLDsa.ImportEncryptedPkcs8PrivateKey((string)null, Array.Empty())); + AssertExtensions.Throws("password", static () => CompositeMLDsa.ImportEncryptedPkcs8PrivateKey((string)null, null)); AssertExtensions.Throws("password", static () => CompositeMLDsa.ImportFromEncryptedPem(string.Empty, (string)null)); AssertExtensions.Throws("passwordBytes", static () => CompositeMLDsa.ImportFromEncryptedPem(string.Empty, (byte[])null)); @@ -59,7 +57,7 @@ public static void ImportBadPrivateKey_ShortMLDsaSeed(CompositeMLDsaAlgorithm al public static void ImportBadPrivateKey_OnlyMLDsaSeed(CompositeMLDsaAlgorithm algorithm) { MLDsaKeyInfo mldsaVector = CompositeMLDsaTestData.GetMLDsaIetfTestVector(algorithm); - AssertImportBadPrivateKey(algorithm, mldsaVector.PrivateSeed.ToArray()); + AssertImportBadPrivateKey(algorithm, mldsaVector.PrivateSeed); } [Theory] @@ -89,14 +87,14 @@ public static void ImportBadPrivateKey_Rsa_WrongAlgorithm() { // Get vector for MLDsa65WithRSA3072Pss CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); // But use MLDsa65WithRSA4096Pss AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.SecretKey); // And flip differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.SecretKey); } @@ -106,14 +104,14 @@ public static void ImportBadPrivateKey_ECDsa_WrongAlgorithm() { // Get vector for MLDsa65WithECDsaP256 CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); // But use MLDsa65WithECDsaP384 AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.SecretKey); // And flip differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); AssertImportBadPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.SecretKey); } @@ -311,7 +309,7 @@ public static void ImportBadPublicKey_ShortMLDsaKey(CompositeMLDsaAlgorithm algo public static void ImportBadPublicKey_OnlyMLDsaKey(CompositeMLDsaAlgorithm algorithm) { MLDsaKeyInfo mldsaVector = CompositeMLDsaTestData.GetMLDsaIetfTestVector(algorithm); - AssertImportBadPublicKey(algorithm, mldsaVector.PublicKey.ToArray()); + AssertImportBadPublicKey(algorithm, mldsaVector.PublicKey); } [Theory] @@ -365,14 +363,14 @@ public static void ImportBadPublicKey_Rsa_WrongAlgorithm() { // Get vector for MLDsa65WithRSA3072Pss CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss); // But use MLDsa65WithRSA4096Pss AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss, differentTradKey.PublicKey); // And flip differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithRSA4096Pss); AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, differentTradKey.PublicKey); } @@ -382,14 +380,14 @@ public static void ImportBadPublicKey_ECDsa_WrongAlgorithm() { // Get vector for MLDsa65WithECDsaP256 CompositeMLDsaTestData.CompositeMLDsaTestVector differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256); // But use MLDsa65WithECDsaP384 AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384, differentTradKey.PublicKey); // And flip differentTradKey = - CompositeMLDsaTestData.AllIetfVectors.Single(vector => vector.Algorithm == CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); + CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384); AssertImportBadPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, differentTradKey.PublicKey); } @@ -456,15 +454,16 @@ static void AssertThrows(byte[] encodedBytes) [Fact] public static void ImportSpki_BerEncoding() { - // Valid BER but invalid DER - uses indefinite length encoding - byte[] indefiniteLengthOctet = [0x04, 0x80, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00]; + byte[] spki = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).Spki; + byte[] berSpki = AsnUtils.ConvertDerToNonDerBer(spki); + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(import => AssertThrowIfNotSupported(() => - Assert.Throws(() => import(indefiniteLengthOctet)))); + Assert.Throws(() => import(berSpki)))); } [Fact] - public static void ImportPkcs8_WrongTypeInAsn() + public static void Import_WrongAsnType() { // Create an incorrect ASN.1 structure to pass into the import methods. AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); @@ -504,10 +503,10 @@ public static void ImportSubjectKeyPublicInfo_AlgorithmErrorsInAsn() { Algorithm = new AlgorithmIdentifierAsn { - Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaTestData.AllIetfVectors[0].Algorithm), + Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384), Parameters = CompositeMLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid }, - SubjectPublicKey = CompositeMLDsaTestData.AllIetfVectors[0].PublicKey, + SubjectPublicKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).PublicKey, }; CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo( @@ -542,10 +541,10 @@ public static void ImportPkcs8PrivateKey_AlgorithmErrorsInAsn() { PrivateKeyAlgorithm = new AlgorithmIdentifierAsn { - Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaTestData.AllIetfVectors[0].Algorithm), + Algorithm = CompositeMLDsaTestHelpers.AlgorithmToOid(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384), Parameters = CompositeMLDsaTestHelpers.s_derBitStringFoo, // <-- Invalid }, - PrivateKey = CompositeMLDsaTestData.AllIetfVectors[0].SecretKey, + PrivateKey = CompositeMLDsaTestData.GetIetfTestVector(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP384).SecretKey, }; CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey( 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 12885223eb1b26..49ff300e57eb34 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 @@ -1,8 +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.Diagnostics; -using System.Security.Cryptography.SLHDsa.Tests; +using Test.Cryptography; using Xunit; namespace System.Security.Cryptography.Tests @@ -41,38 +40,20 @@ private static void AssertCompositeMLDsaIsOnlyPublicAncestor(Func CompositeMLDsaTestHelpers.AssertExportPrivateKey(export => CompositeMLDsaTestHelpers.WithDispose(import(nonMinimalEncoding), mldsa => - AssertExtensions.SequenceEqual(CompositeMLDsaTestData.AllIetfVectors[0].SecretKey, export(mldsa))))); + AssertExtensions.SequenceEqual(vector.SecretKey, export(mldsa))))); } #region Roundtrip by exporting then importing @@ -297,6 +278,26 @@ public void RoundTrip_Import_Export_PrivateKey(CompositeMLDsaTestData.CompositeM info.SecretKey); } + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Import_Export_SpkiPublicKey(CompositeMLDsaTestData.CompositeMLDsaTestVector info) + { + CompositeMLDsaTestHelpers.AssertImportSubjectKeyPublicInfo(import => + CompositeMLDsaTestHelpers.AssertExportSubjectPublicKeyInfo(export => + CompositeMLDsaTestHelpers.WithDispose(import(info.Spki), dsa => + AssertExtensions.SequenceEqual(info.Spki, export(dsa))))); + } + + [Theory] + [MemberData(nameof(CompositeMLDsaTestData.SupportedAlgorithmIetfVectorsTestData), MemberType = typeof(CompositeMLDsaTestData))] + public void RoundTrip_Import_Export_Pkcs8PrivateKey(CompositeMLDsaTestData.CompositeMLDsaTestVector info) + { + CompositeMLDsaTestHelpers.AssertImportPkcs8PrivateKey(import => + CompositeMLDsaTestHelpers.AssertExportPrivateKey(export => + CompositeMLDsaTestHelpers.WithDispose(import(info.Pkcs8), dsa => + CompositeMLDsaTestHelpers.AssertPrivateKeyEquals(info.Algorithm, info.SecretKey, export(dsa))))); + } + #endregion Roundtrip by importing then exporting protected override CompositeMLDsa GenerateKey(CompositeMLDsaAlgorithm algorithm) => 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 65bb8c366609a9..d2ba00adbb7ea9 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 @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Formats.Asn1; using System.Linq; using Xunit.Sdk; @@ -20,6 +21,8 @@ public class CompositeMLDsaTestVector internal byte[] Pkcs8 { get; } internal byte[] Signature { get; } + internal byte[] Spki { get; } + internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, string pk, string x5c, string sk, string sk_pkcs8, string m, string s) { Id = tcId; @@ -30,6 +33,19 @@ internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, str Pkcs8 = Convert.FromBase64String(sk_pkcs8); Message = Convert.FromBase64String(m); Signature = Convert.FromBase64String(s); + + AsnReader reader = new AsnReader(Certificate, AsnEncodingRules.DER); + AsnReader certificate = reader.ReadSequence(); + AsnReader tbsCertificate = certificate.ReadSequence(); + + tbsCertificate.ReadEncodedValue(); // Version + tbsCertificate.ReadEncodedValue(); // SerialNumber + tbsCertificate.ReadEncodedValue(); // Signature + tbsCertificate.ReadEncodedValue(); // Issuer + tbsCertificate.ReadEncodedValue(); // Validity + tbsCertificate.ReadEncodedValue(); // Subject + + Spki = tbsCertificate.ReadEncodedValue().ToArray(); } public override string ToString() => Id; @@ -43,7 +59,7 @@ internal CompositeMLDsaTestVector(string tcId, CompositeMLDsaAlgorithm algo, str internal static CompositeMLDsaTestVector[] SupportedAlgorithmIetfVectors => field ??= AllIetfVectors.Where(v => CompositeMLDsa.IsAlgorithmSupported(v.Algorithm)).ToArray(); - public static IEnumerableSupportedAlgorithmIetfVectorsTestData => + public static IEnumerable SupportedAlgorithmIetfVectorsTestData => SupportedAlgorithmIetfVectors.Select(v => new object[] { v }); public static IEnumerable SupportedECDsaAlgorithmIetfVectorsTestData => @@ -100,5 +116,8 @@ internal static MLDsaKeyInfo GetMLDsaIetfTestVector(CompositeMLDsaAlgorithm algo throw new XunitException($"Algorithm '{algorithm.Name}' doesn't have ML-DSA component."); } } + + internal static CompositeMLDsaTestVector GetIetfTestVector(CompositeMLDsaAlgorithm algorithm) => + AllIetfVectors.Single(v => v.Algorithm == algorithm); } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs b/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs index 44830f0858f25e..25fcf5a36a6298 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.Formats.Asn1; using System.Security.Cryptography; using System.Security.Cryptography.Asn1; @@ -72,6 +73,56 @@ static HashAlgorithmName GetHashAlgorithmFromPbkdf2Params(Pbkdf2Params pbkdf2Par string other => throw new XunitException($"Unknown hash algorithm OID '{other}'."), }; } - } + } + + internal static byte[] ConvertDerToNonDerBer(byte[] derBytes) + { + // Convert a valid DER encoding to BER by making the length octets of the first value non-minimal. + byte[] berBytes = new byte[derBytes.Length + 1]; + + int index = 0; + + // Skip to the last byte for a high tag number. + if ((derBytes[index] & 0b11111) == 0b11111) + { + index++; + + while (derBytes[index] >= 0x80) + { + index++; + } + } + + // Copy the tag + derBytes.AsSpan(0, index + 1).CopyTo(berBytes); + + // Advance to the length + index++; + + // Make the length one byte longer + if (derBytes[index] < 0x80) + { + // Short form, so just make it long form by adding the length length + berBytes[index] = 0x80 | 1; + + derBytes.AsSpan(index).CopyTo(berBytes.AsSpan(index + 1)); + } + else + { + // Long form, so increase the length length by one and add a 0x00 byte + byte lengthLength = derBytes[index]; + lengthLength++; + + // X.690 section 8.1.3.5c says: the value 11111111_2 shall not be used + Assert.NotEqual(0b11111111, lengthLength); + + berBytes[index] = lengthLength; + berBytes[index + 1] = 0x00; + + derBytes.AsSpan(index + 1).CopyTo(berBytes.AsSpan(index + 2)); + } + + return berBytes; + } } } From 6c9ae1a4a59d781750262944194a451ef2b6444c Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Fri, 19 Sep 2025 10:56:45 -0700 Subject: [PATCH 5/5] Update src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs --- .../Common/tests/System/Security/Cryptography/AsnUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs b/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs index 25fcf5a36a6298..680d27d7782b87 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AsnUtils.cs @@ -113,7 +113,7 @@ internal static byte[] ConvertDerToNonDerBer(byte[] derBytes) byte lengthLength = derBytes[index]; lengthLength++; - // X.690 section 8.1.3.5c says: the value 11111111_2 shall not be used + // X.690 section 8.1.3.5c says: the value 0b11111111 shall not be used Assert.NotEqual(0b11111111, lengthLength); berBytes[index] = lengthLength;