From e9bb837ace382c39329720cc1797b2b7b3e36513 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Mon, 27 Jul 2026 10:57:40 -0700 Subject: [PATCH 1/3] Enable ML-DSA CMS tests on .NET Framework Windows now supports ML-DSA, so drop the Windows-gating SupportsDraft10Pkcs8 helper and run the ML-DSA SignedCms/SignerInfo tests wherever MLDsa.IsSupported. Document the .NET vs .NET Framework quirks the tests exercise: - .NET Framework CMS is backed by Windows CAPI, which does not recognize SHAKE digest algorithms and throws CRYPT_E_UNKNOWN_ALGO on sign, whereas managed .NET builds the CMS itself and succeeds. Tests assert each behavior in-place. - Verifying a detached ML-DSA signature without supplying the content does not throw on .NET Framework the way it does on .NET (detachedNoContentThrows). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/SignedCms/SignedCmsTests.cs | 59 +++++++++++++------ .../SignedCms/SignedCmsTests.netcoreapp.cs | 10 ++-- .../tests/SignedCms/SignerInfoTests.cs | 38 +++++++----- 3 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs index 286201907b912d..bfd31418496c1b 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs @@ -16,9 +16,6 @@ namespace System.Security.Cryptography.Pkcs.Tests [ActiveIssue("https://github.com/dotnet/runtime/issues/126697", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsNativeAot))] public static partial class SignedCmsTests { - // TODO: Windows does not support draft 10 PKCS#8 format yet. Remove this and use MLDsa.IsSupported when it does. - public static bool SupportsDraft10Pkcs8 => MLDsa.IsSupported && !PlatformDetection.IsWindows; - [Fact] public static void DefaultStateBehavior() { @@ -668,25 +665,38 @@ public static void AddFirstSigner_SlhDsa(SubjectIdentifierType identifierType, b } select new object[] { sit, detached, data.hashAlgorithm, data.algorithm }; - [ConditionalTheory(typeof(SignedCmsTests), nameof(SupportsDraft10Pkcs8))] + [ConditionalTheory(typeof(MLDsa), nameof(MLDsa.IsSupported))] [MemberData(nameof(AddFirstSignerMLDsaTestData))] public static void AddFirstSigner_MLDsa(SubjectIdentifierType identifierType, bool detached, string digestOid, MLDsaAlgorithm algorithm) { + void SignWithMLDsa(SignedCms cms) + { + using (X509Certificate2 signerCert = Certificates.MLDsaIetf[algorithm].TryGetCertificateWithPrivateKey()) + { + CmsSigner signer = new CmsSigner(identifierType, signerCert); + signer.IncludeOption = X509IncludeOption.EndCertOnly; + signer.DigestAlgorithm = new Oid(digestOid, digestOid); + cms.ComputeSignature(signer); + } + } + + if (PlatformDetection.IsNetFramework && (digestOid == Oids.Shake128 || digestOid == Oids.Shake256)) + { + // .NET Framework's CMS is backed by Windows CAPI, which does not recognize SHAKE + // digest algorithms and fails signing with CRYPT_E_UNKNOWN_ALGO. .NET builds the + // CMS in managed code and succeeds. + ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 }); + SignedCms cms = new SignedCms(contentInfo, detached); + Assert.Throws(() => SignWithMLDsa(cms)); + return; + } + byte[]? signature = null; AssertAddFirstSigner( identifierType, detached, - cms => - { - using (X509Certificate2 signerCert = Certificates.MLDsaIetf[algorithm].TryGetCertificateWithPrivateKey()) - { - CmsSigner signer = new CmsSigner(identifierType, signerCert); - signer.IncludeOption = X509IncludeOption.EndCertOnly; - signer.DigestAlgorithm = new Oid(digestOid, digestOid); - cms.ComputeSignature(signer); - } - }, + SignWithMLDsa, firstSigner => { // Store signature for comparison after roundtrip. @@ -700,7 +710,10 @@ public static void AddFirstSigner_MLDsa(SubjectIdentifierType identifierType, bo { byte[] sig2 = roundtrippedFirstSigner.GetSignature(); Assert.Equal(signature, sig2); - }); + }, + // On .NET Framework, verifying a detached ML-DSA signature without supplying the + // content does not throw the way it does on .NET (and for RSA/ECDSA on both). + detachedNoContentThrows: !PlatformDetection.IsNetFramework); } private static void AssertAddFirstSigner( @@ -708,7 +721,8 @@ private static void AssertAddFirstSigner( bool detached, Action signCms, Action assertFirstSigner, - Action assertRoundtrippedFirstSigner) + Action assertRoundtrippedFirstSigner, + bool detachedNoContentThrows = true) { ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 }); SignedCms cms = new SignedCms(contentInfo, detached); @@ -750,7 +764,16 @@ private static void AssertAddFirstSigner( if (detached) { - Assert.Throws(() => cms.CheckSignature(true)); + if (detachedNoContentThrows) + { + Assert.Throws(() => cms.CheckSignature(true)); + } + else + { + // Assert.NoThrow + cms.CheckSignature(true); + } + cms = new SignedCms(contentInfo, detached); cms.Decode(encoded); } @@ -1802,7 +1825,7 @@ private static void CheckNoSignature(byte[] encoded, bool badOid=false) } } - [ConditionalFact(typeof(SignedCmsTests), nameof(SupportsDraft10Pkcs8))] + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] public static void ComputeSignature_MLDsa_DefaultDigest() { #if !NETFRAMEWORK diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.netcoreapp.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.netcoreapp.cs index 9af0b1c62f1e78..5bd8b0e358ee2d 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.netcoreapp.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.netcoreapp.cs @@ -102,7 +102,7 @@ public static void SignCmsUsingExplicitECDsaP521Key() } } - [ConditionalFact(typeof(SignedCmsTests), nameof(SupportsDraft10Pkcs8))] + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] public static void SignCmsUsingExplicitMLDsaKey() { using (X509Certificate2 cert = Certificates.MLDsaIetf[MLDsaAlgorithm.MLDsa65].TryGetCertificateWithPrivateKey()) @@ -158,7 +158,7 @@ public static void CounterSignCmsUsingExplicitECDsaKeyForFirstSignerAndRSAForCou } } - [ConditionalFact(typeof(SignedCmsTests), nameof(SupportsDraft10Pkcs8))] + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] public static void CounterSignCmsUsingExplicitECDsaKeyForFirstSignerAndMLDsaForCounterSignature() { using (X509Certificate2 cert = Certificates.ECDsaP256Win.TryGetCertificateWithPrivateKey()) @@ -170,7 +170,7 @@ public static void CounterSignCmsUsingExplicitECDsaKeyForFirstSignerAndMLDsaForC } } - [ConditionalFact(typeof(SignedCmsTests), nameof(SupportsDraft10Pkcs8))] + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] public static void CounterSignCmsUsingExplicitMLDsaKeyForFirstSignerAndRSAForCounterSignature() { using (X509Certificate2 cert = Certificates.MLDsaIetf[MLDsaAlgorithm.MLDsa65].TryGetCertificateWithPrivateKey()) @@ -808,7 +808,7 @@ public static void CreateSignature_Ecdsa_ThrowsWithRsaSignaturePadding() } } - [ConditionalFact(typeof(SignedCmsTests), nameof(SupportsDraft10Pkcs8))] + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] public static void CreateSignature_MLDsa_ThrowsWithRsaSignaturePadding() { ContentInfo content = new ContentInfo(new byte[] { 1, 2, 3 }); @@ -1016,7 +1016,7 @@ public static void ComputeSignature_Ecdsa_Sha3_Roundtrip(string hashAlgorithm) } } - [ConditionalTheory(typeof(SignedCmsTests), nameof(SupportsDraft10Pkcs8))] + [ConditionalTheory(typeof(MLDsa), nameof(MLDsa.IsSupported))] [InlineData(Oids.Sha3_256)] [InlineData(Oids.Sha3_384)] [InlineData(Oids.Sha3_512)] diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs index e24d1aab7679a0..e61c1a865a2ff8 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs @@ -814,27 +814,37 @@ public static void AddCounterSigner_SlhDsa(SubjectIdentifierType identifierType, } select new object[] { sit, data.hashAlgorithm, data.algorithm }; - // TODO: Windows does not support draft 10 PKCS#8 format yet. Remove this and use MLDsa.IsSupported when it does. - private static bool SupportsDraft10Pkcs8 => MLDsa.IsSupported && !PlatformDetection.IsWindows; - - public static bool MLDsaAndRsaSha1SignaturesSupported => SignatureSupport.SupportsRsaSha1Signatures && SupportsDraft10Pkcs8; + public static bool MLDsaAndRsaSha1SignaturesSupported => SignatureSupport.SupportsRsaSha1Signatures && MLDsa.IsSupported; [ConditionalTheory(typeof(SignerInfoTests), nameof(MLDsaAndRsaSha1SignaturesSupported))] [MemberData(nameof(AddCounterSignerMLDsaTestData))] public static void AddCounterSigner_MLDsa(SubjectIdentifierType identifierType, string digestOid, MLDsaAlgorithm algorithm) { + void CounterSignWithMLDsa(SignerInfo signer) + { + using (X509Certificate2 signerCert = Certificates.MLDsaIetf[algorithm].TryGetCertificateWithPrivateKey()) + { + CmsSigner counterSigner = new CmsSigner(identifierType, signerCert); + counterSigner.IncludeOption = X509IncludeOption.EndCertOnly; + counterSigner.DigestAlgorithm = new Oid(digestOid, digestOid); + signer.ComputeCounterSignature(counterSigner); + } + } + + if (PlatformDetection.IsNetFramework && (digestOid == Oids.Shake128 || digestOid == Oids.Shake256)) + { + // .NET Framework's CMS is backed by Windows CAPI, which does not recognize SHAKE + // digest algorithms and fails signing with CRYPT_E_UNKNOWN_ALGO. .NET builds the + // CMS in managed code and succeeds. + SignedCms cms = new SignedCms(); + cms.Decode(SignedDocuments.RsaPkcs1OneSignerIssuerAndSerialNumber); + Assert.Throws(() => CounterSignWithMLDsa(cms.SignerInfos[0])); + return; + } + AssertAddCounterSigner( identifierType, - signer => - { - using (X509Certificate2 signerCert = Certificates.MLDsaIetf[algorithm].TryGetCertificateWithPrivateKey()) - { - CmsSigner counterSigner = new CmsSigner(identifierType, signerCert); - counterSigner.IncludeOption = X509IncludeOption.EndCertOnly; - counterSigner.DigestAlgorithm = new Oid(digestOid, digestOid); - signer.ComputeCounterSignature(counterSigner); - } - }, + CounterSignWithMLDsa, (cms, counterSigner) => { byte[] signature = counterSigner.GetSignature(); From 89cc1a2de72280ad37b798b73f3bc80da292c6cc Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Mon, 27 Jul 2026 17:28:35 -0700 Subject: [PATCH 2/3] Correct netfx detached CMS verification test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ab780736-e998-4327-b006-ee49fcf9ac12 --- .../tests/SignedCms/SignedCmsTests.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs index bfd31418496c1b..2cb14009e40fe7 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs @@ -710,10 +710,7 @@ void SignWithMLDsa(SignedCms cms) { byte[] sig2 = roundtrippedFirstSigner.GetSignature(); Assert.Equal(signature, sig2); - }, - // On .NET Framework, verifying a detached ML-DSA signature without supplying the - // content does not throw the way it does on .NET (and for RSA/ECDSA on both). - detachedNoContentThrows: !PlatformDetection.IsNetFramework); + }); } private static void AssertAddFirstSigner( @@ -721,8 +718,7 @@ private static void AssertAddFirstSigner( bool detached, Action signCms, Action assertFirstSigner, - Action assertRoundtrippedFirstSigner, - bool detachedNoContentThrows = true) + Action assertRoundtrippedFirstSigner) { ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 }); SignedCms cms = new SignedCms(contentInfo, detached); @@ -764,16 +760,7 @@ private static void AssertAddFirstSigner( if (detached) { - if (detachedNoContentThrows) - { - Assert.Throws(() => cms.CheckSignature(true)); - } - else - { - // Assert.NoThrow - cms.CheckSignature(true); - } - + Assert.Throws(() => cms.CheckSignature(true)); cms = new SignedCms(contentInfo, detached); cms.Decode(encoded); } From 76260d3063e33c2fea9188035b4b34ff68ea2866 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Mon, 27 Jul 2026 18:45:03 -0700 Subject: [PATCH 3/3] Validate netfx CMS SHAKE error codes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ab780736-e998-4327-b006-ee49fcf9ac12 --- .../tests/SignedCms/SignedCmsTests.cs | 5 ++++- .../tests/SignedCms/SignerInfoTests.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs index 2cb14009e40fe7..c6b0983affae80 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs @@ -682,12 +682,15 @@ void SignWithMLDsa(SignedCms cms) if (PlatformDetection.IsNetFramework && (digestOid == Oids.Shake128 || digestOid == Oids.Shake256)) { + const int CryptEUnknownAlgorithm = unchecked((int)0x80091002); + // .NET Framework's CMS is backed by Windows CAPI, which does not recognize SHAKE // digest algorithms and fails signing with CRYPT_E_UNKNOWN_ALGO. .NET builds the // CMS in managed code and succeeds. ContentInfo contentInfo = new ContentInfo(new byte[] { 9, 8, 7, 6, 5 }); SignedCms cms = new SignedCms(contentInfo, detached); - Assert.Throws(() => SignWithMLDsa(cms)); + CryptographicException exception = Assert.Throws(() => SignWithMLDsa(cms)); + Assert.Equal(CryptEUnknownAlgorithm, exception.HResult); return; } diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs index e61c1a865a2ff8..466b258795ea15 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignerInfoTests.cs @@ -833,12 +833,15 @@ void CounterSignWithMLDsa(SignerInfo signer) if (PlatformDetection.IsNetFramework && (digestOid == Oids.Shake128 || digestOid == Oids.Shake256)) { + const int CryptEUnknownAlgorithm = unchecked((int)0x80091002); + // .NET Framework's CMS is backed by Windows CAPI, which does not recognize SHAKE // digest algorithms and fails signing with CRYPT_E_UNKNOWN_ALGO. .NET builds the // CMS in managed code and succeeds. SignedCms cms = new SignedCms(); cms.Decode(SignedDocuments.RsaPkcs1OneSignerIssuerAndSerialNumber); - Assert.Throws(() => CounterSignWithMLDsa(cms.SignerInfos[0])); + CryptographicException exception = Assert.Throws(() => CounterSignWithMLDsa(cms.SignerInfos[0])); + Assert.Equal(CryptEUnknownAlgorithm, exception.HResult); return; }