From a6fb0b8c088472d6f2afeac969c2a8af39336517 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 7 Apr 2025 15:42:35 -0700 Subject: [PATCH 1/2] Make CertificateRequest et al work with ML-DSA --- .../System/Security/Cryptography/Helpers.cs | 14 +- .../src/System/Security/Cryptography/MLDsa.cs | 14 +- .../Security/Cryptography/MLDsaAlgorithm.cs | 11 +- .../MLDsa/MLDsaTestImplementation.cs | 73 ++++ .../X509Certificates/CertificateAuthority.cs | 309 ++++++++++++--- .../ref/System.Security.Cryptography.cs | 15 + .../ref/System.Security.Cryptography.csproj | 2 +- .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 1 + .../X509Certificates/AndroidCertificatePal.cs | 12 + .../AppleCertificatePal.Keys.iOS.cs | 6 + .../AppleCertificatePal.Keys.macOS.cs | 6 + .../X509Certificates/AppleCertificatePal.cs | 6 + .../CertificatePal.Windows.PrivateKey.cs | 12 + .../CertificateRequest.Load.cs | 24 +- .../X509Certificates/CertificateRequest.cs | 124 +++++- .../CertificateRevocationListBuilder.Build.cs | 54 ++- .../X509Certificates/ICertificatePal.cs | 2 + .../MLDsaX509SignatureGenerator.cs | 61 +++ .../OpenSslX509CertificateReader.cs | 44 +- .../X509Certificates/PublicKey.cs | 10 + .../X509Certificates/X509Certificate2.cs | 111 ++++++ .../X509SignatureGenerator.cs | 10 + .../System.Security.Cryptography.Tests.csproj | 2 + .../CertificateRequestApiTests.cs | 143 +++---- .../CertificateRequestChainTests.cs | 85 ++-- .../CertificateRequestLoadTests.cs | 176 ++++++++ .../CertificateCreation/CrlBuilderTests.cs | 375 ++++++++++++++---- .../PrivateKeyAssociationTests.cs | 272 +++++++++++++ 29 files changed, 1688 insertions(+), 289 deletions(-) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/MLDsaX509SignatureGenerator.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs b/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs index c66145a58b4f8b..f2690272c27aaf 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs @@ -116,14 +116,16 @@ internal static int HashOidToByteLength(string hashOid) internal static CryptographicException CreateAlgorithmUnknownException(AsnWriter encodedId) { #if NET10_0_OR_GREATER - return encodedId.Encode(static encoded => - new CryptographicException( - SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, Convert.ToHexString(encoded)))); + return encodedId.Encode(static encoded => CreateAlgorithmUnknownException(Convert.ToHexString(encoded))); #else - return new CryptographicException( - SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, - HexConverter.ToString(encodedId.Encode(), HexConverter.Casing.Upper))); + return CreateAlgorithmUnknownException(HexConverter.ToString(encodedId.Encode(), HexConverter.Casing.Upper)); #endif } + + internal static CryptographicException CreateAlgorithmUnknownException(string algorithmId) + { + throw new CryptographicException( + SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, algorithmId)); + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs index a800b8cc53650d..6909aee7755277 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsa.cs @@ -753,7 +753,12 @@ public static MLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan source) AsnValueReader reader = new AsnValueReader(source, AsnEncodingRules.DER); SubjectPublicKeyInfoAsn.Decode(ref reader, manager.Memory, out SubjectPublicKeyInfoAsn spki); - MLDsaAlgorithm algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(spki.Algorithm.Algorithm); + MLDsaAlgorithm? algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(spki.Algorithm.Algorithm); + + if (algorithm is null) + { + throw Helpers.CreateAlgorithmUnknownException(spki.Algorithm.Algorithm); + } if (spki.Algorithm.Parameters.HasValue) { @@ -803,7 +808,12 @@ public static MLDsa ImportPkcs8PrivateKey(ReadOnlySpan source) AsnValueReader reader = new AsnValueReader(source, AsnEncodingRules.DER); PrivateKeyInfoAsn.Decode(ref reader, manager.Memory, out PrivateKeyInfoAsn pki); - MLDsaAlgorithm algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(pki.PrivateKeyAlgorithm.Algorithm); + MLDsaAlgorithm? algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(pki.PrivateKeyAlgorithm.Algorithm); + + if (algorithm is null) + { + throw Helpers.CreateAlgorithmUnknownException(pki.PrivateKeyAlgorithm.Algorithm); + } if (pki.PrivateKeyAlgorithm.Parameters.HasValue) { diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLDsaAlgorithm.cs b/src/libraries/Common/src/System/Security/Cryptography/MLDsaAlgorithm.cs index de5cd727ba12b2..ed1c7585cba0f3 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/MLDsaAlgorithm.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLDsaAlgorithm.cs @@ -107,22 +107,15 @@ private MLDsaAlgorithm(string name, int secretKeySizeInBytes, int publicKeySizeI /// public static MLDsaAlgorithm MLDsa87 { get; } = new MLDsaAlgorithm("ML-DSA-87", 4896, 2592, 4627, Oids.MLDsa87); - internal static MLDsaAlgorithm GetMLDsaAlgorithmFromOid(string oid) + internal static MLDsaAlgorithm? GetMLDsaAlgorithmFromOid(string? oid) { return oid switch { Oids.MLDsa44 => MLDsa44, Oids.MLDsa65 => MLDsa65, Oids.MLDsa87 => MLDsa87, - _ => ThrowAlgorithmUnknown(oid), + _ => null, }; } - - [DoesNotReturn] - private static MLDsaAlgorithm ThrowAlgorithmUnknown(string algorithmId) - { - throw new CryptographicException( - SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, algorithmId)); - } } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs new file mode 100644 index 00000000000000..b6427cc4ef0b2b --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/MLDsa/MLDsaTestImplementation.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + internal sealed class MLDsaTestImplementation : MLDsa + { + internal delegate void ExportAction(Span destination); + internal delegate void SignAction(ReadOnlySpan data, ReadOnlySpan context, Span destination); + internal delegate bool VerifyFunc(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature); + + internal ExportAction ExportMLDsaPrivateSeedHook { get; set; } + internal ExportAction ExportMLDsaPublicKeyHook { get; set; } + internal ExportAction ExportMLDsaSecretKeyHook { get; set; } + internal SignAction SignDataHook { get; set; } + internal VerifyFunc VerifyDataHook { get; set; } + internal Action DisposeHook { get; set; } = _ => { }; + + private MLDsaTestImplementation(MLDsaAlgorithm algorithm) : base(algorithm) + { + } + + protected override void Dispose(bool disposing) => DisposeHook(disposing); + + protected override void ExportMLDsaPrivateSeedCore(Span destination) => ExportMLDsaPrivateSeedHook(destination); + protected override void ExportMLDsaPublicKeyCore(Span destination) => ExportMLDsaPublicKeyHook(destination); + protected override void ExportMLDsaSecretKeyCore(Span destination) => ExportMLDsaSecretKeyHook(destination); + + protected override void SignDataCore(ReadOnlySpan data, ReadOnlySpan context, Span destination) => + SignDataHook(data, context, destination); + + protected override bool VerifyDataCore(ReadOnlySpan data, ReadOnlySpan context, ReadOnlySpan signature) => + VerifyDataHook(data, context, signature); + + internal static MLDsaTestImplementation CreateOverriddenCoreMethodsFail(MLDsaAlgorithm algorithm) + { + return new MLDsaTestImplementation(algorithm) + { + ExportMLDsaPrivateSeedHook = _ => Assert.Fail(), + ExportMLDsaPublicKeyHook = _ => Assert.Fail(), + ExportMLDsaSecretKeyHook = _ => Assert.Fail(), + SignDataHook = (_, _, _) => Assert.Fail(), + VerifyDataHook = (_, _, _) => { Assert.Fail(); return false; }, + }; + } + + internal static MLDsaTestImplementation CreateNoOp(MLDsaAlgorithm algorithm) + { + return new MLDsaTestImplementation(algorithm) + { + ExportMLDsaPrivateSeedHook = d => d.Clear(), + ExportMLDsaPublicKeyHook = d => d.Clear(), + ExportMLDsaSecretKeyHook = d => d.Clear(), + SignDataHook = (data, context, destination) => destination.Clear(), + VerifyDataHook = (data, context, signature) => signature.IndexOfAnyExcept((byte)0) == -1, + }; + } + + internal static MLDsaTestImplementation Wrap(MLDsa other) + { + return new MLDsaTestImplementation(other.Algorithm) + { + ExportMLDsaPrivateSeedHook = d => other.ExportMLDsaPrivateSeed(d), + ExportMLDsaPublicKeyHook = d => other.ExportMLDsaPublicKey(d), + ExportMLDsaSecretKeyHook = d => other.ExportMLDsaSecretKey(d), + SignDataHook = (data, context, destination) => other.SignData(data, destination, context), + VerifyDataHook = (data, context, signature) => other.VerifyData(data, signature, context), + }; + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index 38b9ff44c09230..84e6fbb3f73a64 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Formats.Asn1; using System.Linq; +using System.Runtime.CompilerServices; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests.Common @@ -148,7 +149,7 @@ internal void Revoke(X509Certificate2 certificate, DateTimeOffset revocationTime internal X509Certificate2 CreateSubordinateCA( string subject, - RSA publicKey, + PublicKey publicKey, int? depthLimit = null) { return CreateCertificate( @@ -164,7 +165,7 @@ internal X509Certificate2 CreateSubordinateCA( s_caKeyUsage }); } - internal X509Certificate2 CreateEndEntity(string subject, RSA publicKey, X509ExtensionCollection extensions) + internal X509Certificate2 CreateEndEntity(string subject, PublicKey publicKey, X509ExtensionCollection extensions) { return CreateCertificate( subject, @@ -173,7 +174,14 @@ internal X509Certificate2 CreateEndEntity(string subject, RSA publicKey, X509Ext extensions); } - internal X509Certificate2 CreateOcspSigner(string subject, RSA publicKey) + internal X509Certificate2 CreateOcspSigner(string subject, RSA rsa) + { + return CreateOcspSigner( + subject, + X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1).PublicKey); + } + + internal X509Certificate2 CreateOcspSigner(string subject, PublicKey publicKey) { return CreateCertificate( subject, @@ -207,7 +215,7 @@ private void RebuildRootWithRevocation(X509Extension cdpExtension, X509Extension throw new InvalidOperationException(); } - var req = new CertificateRequest(subjectName, _cert.PublicKey, HashAlgorithmName.SHA256); + var req = new CertificateRequest(subjectName, _cert.PublicKey, HashAlgorithmIfNeeded(_cert.GetKeyAlgorithm())); foreach (X509Extension ext in _cert.Extensions) { @@ -222,21 +230,21 @@ private void RebuildRootWithRevocation(X509Extension cdpExtension, X509Extension X509Certificate2 dispose = _cert; using (dispose) - using (RSA rsa = _cert.GetRSAPrivateKey()) + using (KeyHolder key = new KeyHolder(_cert)) using (X509Certificate2 tmp = req.Create( subjectName, - X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1), + key.GetGenerator(), new DateTimeOffset(_cert.NotBefore), new DateTimeOffset(_cert.NotAfter), serial)) { - _cert = tmp.CopyWithPrivateKey(rsa); + _cert = key.OntoCertificate(tmp); } } private X509Certificate2 CreateCertificate( string subject, - RSA publicKey, + PublicKey publicKey, TimeSpan nestingBuffer, X509ExtensionCollection extensions, bool ocspResponder = false) @@ -257,9 +265,9 @@ private X509Certificate2 CreateCertificate( } CertificateRequest request = new CertificateRequest( - subject, + new X500DistinguishedName(subject), publicKey, - HashAlgorithmName.SHA256, + HashAlgorithmIfNeeded(_cert.GetKeyAlgorithm()), RSASignaturePadding.Pkcs1); foreach (X509Extension extension in extensions) @@ -282,11 +290,15 @@ private X509Certificate2 CreateCertificate( byte[] serial = new byte[sizeof(long)]; RandomNumberGenerator.Fill(serial); - return request.Create( - _cert, - _cert.NotBefore.Add(nestingBuffer), - _cert.NotAfter.Subtract(nestingBuffer), - serial); + using (KeyHolder key = new KeyHolder(_cert)) + { + return request.Create( + _cert.SubjectName, + key.GetGenerator(), + _cert.NotBefore.Add(nestingBuffer), + _cert.NotAfter.Subtract(nestingBuffer), + serial); + } } internal byte[] GetCertData() @@ -337,14 +349,14 @@ internal byte[] GetCrl() nextUpdate = newExpiry; } - using (RSA key = _cert.GetRSAPrivateKey()) + using (KeyHolder key = new KeyHolder(_cert)) { crl = builder.Build( CorruptRevocationIssuerName ? s_nonParticipatingName : _cert.SubjectName, - X509SignatureGenerator.CreateForRSA(key, RSASignaturePadding.Pkcs1), + key.GetGenerator(), _crlNumber, nextUpdate, - HashAlgorithmName.SHA256, + HashAlgorithmIfNeeded(key.ToPublicKey().Oid.Value), _akidExtension, thisUpdate); } @@ -366,16 +378,10 @@ private byte[] BuildCrlManually( DateTimeOffset newExpiry, X509AuthorityKeyIdentifierExtension akidExtension) { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - using (writer.PushSequence()) - { - writer.WriteObjectIdentifier("1.2.840.113549.1.1.11"); - writer.WriteNull(); - } + using KeyHolder key = new KeyHolder(_cert); + byte[] signatureAlgId = key.GetSignatureAlgorithmIdentifier(); - byte[] signatureAlgId = writer.Encode(); - writer.Reset(); + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); // TBSCertList using (writer.PushSequence()) @@ -473,17 +479,11 @@ private byte[] BuildCrlManually( byte[] tbsCertList = writer.Encode(); writer.Reset(); - byte[] signature; + byte[] signature = key.Sign(tbsCertList); - using (RSA key = _cert.GetRSAPrivateKey()) + if (CorruptRevocationSignature) { - signature = - key.SignData(tbsCertList, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - if (CorruptRevocationSignature) - { - signature[5] ^= 0xFF; - } + signature[5] ^= 0xFF; } // CertificateList @@ -568,7 +568,7 @@ singleExtensions [1] EXPLICIT Extensions OPTIONAL } { writer.PushSequence(s_context1); - // Fracational seconds "MUST NOT" be used here. Android and macOS 13+ enforce this and + // Fractional seconds "MUST NOT" be used here. Android and macOS 13+ enforce this and // reject GeneralizedTime's with fractional seconds, so omit them. // RFC 6960: 4.2.2.1: // The format for GeneralizedTime is as specified in Section 4.1.2.5.2 of [RFC5280]. @@ -630,18 +630,11 @@ certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL } { writer.WriteEncodedValue(tbsResponseData); - using (writer.PushSequence()) + using (KeyHolder key = new KeyHolder(responder)) { - writer.WriteObjectIdentifier("1.2.840.113549.1.1.11"); - writer.WriteNull(); - } + writer.WriteEncodedValue(key.GetSignatureAlgorithmIdentifier()); - using (RSA rsa = responder.GetRSAPrivateKey()) - { - byte[] signature = rsa.SignData( - tbsResponseData, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); + byte[] signature = key.Sign(tbsResponseData); if (CorruptRevocationSignature) { @@ -794,6 +787,7 @@ private enum CertStatus Revoked, } + [OverloadResolutionPriority(-1)] internal static void BuildPrivatePki( PkiOptions pkiOptions, out RevocationResponder responder, @@ -807,6 +801,35 @@ internal static void BuildPrivatePki( string subjectName = null, int keySize = DefaultKeySize, X509ExtensionCollection extensions = null) + { + BuildPrivatePki( + pkiOptions, + out responder, + out rootAuthority, + out intermediateAuthorities, + out endEntityCert, + intermediateAuthorityCount, + testName, + registerAuthorities, + pkiOptionsInSubject, + subjectName, + KeyFactory.RSASize(keySize), + extensions); + } + + internal static void BuildPrivatePki( + PkiOptions pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority[] intermediateAuthorities, + out X509Certificate2 endEntityCert, + int intermediateAuthorityCount, + string testName = null, + bool registerAuthorities = true, + bool pkiOptionsInSubject = false, + string subjectName = null, + KeyFactory keyFactory = null, + X509ExtensionCollection extensions = null) { bool rootDistributionViaHttp = !pkiOptions.HasFlag(PkiOptions.NoRootCertDistributionUri); bool issuerRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaCrl); @@ -823,14 +846,19 @@ internal static void BuildPrivatePki( // default to client extensions ??= new X509ExtensionCollection() { s_eeConstraints, s_eeKeyUsage, s_tlsClientEku }; - using (RSA rootKey = RSA.Create(keySize)) - using (RSA eeKey = RSA.Create(keySize)) + if (keyFactory is null) { - var rootReq = new CertificateRequest( - BuildSubject("A Revocation Test Root", testName, pkiOptions, pkiOptionsInSubject), - rootKey, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); + // Should we add variance here? Sometimes default to ML-DSA, sometimes EC-DSA, sometimes RSA? + // Fully randomized (after an IsSupported check) feels too chaotic. + + keyFactory = KeyFactory.RSA; + } + + using (KeyHolder rootKey = KeyHolder.CreateKey(keyFactory)) + using (KeyHolder eeKey = KeyHolder.CreateKey(keyFactory)) + { + CertificateRequest rootReq = rootKey.CreateRequest( + BuildSubject("A Revocation Test Root", testName, pkiOptions, pkiOptionsInSubject)); X509BasicConstraintsExtension caConstraints = new X509BasicConstraintsExtension(true, false, 0, true); @@ -862,7 +890,7 @@ internal static void BuildPrivatePki( for (int intermediateIndex = 0; intermediateIndex < intermediateAuthorityCount; intermediateIndex++) { - using RSA intermediateKey = RSA.Create(keySize); + using KeyHolder intermediateKey = KeyHolder.CreateKey(keyFactory); // Don't dispose this, it's being transferred to the CertificateAuthority X509Certificate2 intermedCert; @@ -870,8 +898,8 @@ internal static void BuildPrivatePki( { X509Certificate2 intermedPub = issuingAuthority.CreateSubordinateCA( BuildSubject($"A Revocation Test CA {intermediateIndex}", testName, pkiOptions, pkiOptionsInSubject), - intermediateKey); - intermedCert = intermedPub.CopyWithPrivateKey(intermediateKey); + intermediateKey.ToPublicKey()); + intermedCert = intermediateKey.OntoCertificate(intermedPub); intermedPub.Dispose(); } @@ -894,11 +922,11 @@ internal static void BuildPrivatePki( endEntityCert = issuingAuthority.CreateEndEntity( BuildSubject(subjectName ?? "A Revocation Test Cert", testName, pkiOptions, pkiOptionsInSubject), - eeKey, + eeKey.ToPublicKey(), extensions); X509Certificate2 tmp = endEntityCert; - endEntityCert = endEntityCert.CopyWithPrivateKey(eeKey); + endEntityCert = eeKey.OntoCertificate(endEntityCert); tmp.Dispose(); } @@ -913,6 +941,7 @@ internal static void BuildPrivatePki( } } + [OverloadResolutionPriority(-1)] internal static void BuildPrivatePki( PkiOptions pkiOptions, out RevocationResponder responder, @@ -926,7 +955,6 @@ internal static void BuildPrivatePki( int keySize = DefaultKeySize, X509ExtensionCollection extensions = null) { - BuildPrivatePki( pkiOptions, out responder, @@ -944,6 +972,36 @@ internal static void BuildPrivatePki( intermediateAuthority = intermediateAuthorities.Single(); } + internal static void BuildPrivatePki( + PkiOptions pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + string testName = null, + bool registerAuthorities = true, + bool pkiOptionsInSubject = false, + string subjectName = null, + KeyFactory keyFactory = null, + X509ExtensionCollection extensions = null) + { + BuildPrivatePki( + pkiOptions, + out responder, + out rootAuthority, + out CertificateAuthority[] intermediateAuthorities, + out endEntityCert, + intermediateAuthorityCount: 1, + testName: testName, + registerAuthorities: registerAuthorities, + pkiOptionsInSubject: pkiOptionsInSubject, + subjectName: subjectName, + keyFactory: keyFactory, + extensions: extensions); + + intermediateAuthority = intermediateAuthorities.Single(); + } + private static string BuildSubject( string cn, string testName, @@ -955,5 +1013,136 @@ private static string BuildSubject( return $"CN=\"{cn}\"" + testNamePart + pkiOptionsPart; } + + private static HashAlgorithmName HashAlgorithmIfNeeded(string publicKeyOid) + { + const string Rsa = "1.2.840.113549.1.1.1"; + const string RsaPss = "1.2.840.113549.1.1.10"; + const string EcPublicKey = "1.2.840.10045.2.1"; + const string Dsa = "1.2.840.10040.4.1"; + + return publicKeyOid switch + { + Rsa or RsaPss or EcPublicKey or Dsa => HashAlgorithmName.SHA256, + _ => default, + }; + } + + internal void Diag() + { + Console.WriteLine(_cert.ExportCertificatePem()); + + using (ECDsa ecdsa = _cert.GetECDsaPrivateKey()) + { + Console.WriteLine(ecdsa.ExportPkcs8PrivateKeyPem()); + } + } + + internal sealed class KeyFactory + { + internal static KeyFactory RSA { get; } = + new(() => Cryptography.RSA.Create(DefaultKeySize)); + + internal static KeyFactory ECDsa { get; } = + new(() => Cryptography.ECDsa.Create(ECCurve.NamedCurves.nistP384)); + + internal static KeyFactory MLDsa { get; } = + new(() => Cryptography.MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)); + + private Func _factory; + + private KeyFactory(Func factory) + { + _factory = factory; + } + + internal IDisposable CreateKey() + { + return _factory(); + } + + internal static KeyFactory RSASize(int keySize) + { + return new KeyFactory(() => Cryptography.RSA.Create(keySize)); + } + } + + private sealed class KeyHolder : IDisposable + { + private readonly IDisposable _key; + private X509SignatureGenerator _generator; + + internal KeyHolder(IDisposable key) + { + _key = key; + } + + internal KeyHolder(X509Certificate2 cert) + { + // We're always in the context of signing something, so EC-DH does not apply. + _key = + cert.GetRSAPrivateKey() ?? + cert.GetECDsaPrivateKey() ?? + cert.GetMLDsaPrivateKey() ?? + (IDisposable)cert.GetDSAPrivateKey() ?? + throw new NotSupportedException(); + } + + public void Dispose() + { + _key?.Dispose(); + } + + internal static KeyHolder CreateKey(KeyFactory factory) + { + return new KeyHolder(factory.CreateKey()); + } + + internal CertificateRequest CreateRequest(string subject) + { + return _key switch + { + RSA rsa => new CertificateRequest(subject, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), + ECDsa ecdsa => new CertificateRequest(subject, ecdsa, HashAlgorithmName.SHA256), + MLDsa mldsa => new CertificateRequest(subject, mldsa), + _ => throw new NotSupportedException(), + }; + } + + internal X509SignatureGenerator GetGenerator() + { + return _generator ??= _key switch + { + RSA rsa => X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1), + ECDsa ecdsa => X509SignatureGenerator.CreateForECDsa(ecdsa), + MLDsa mldsa => X509SignatureGenerator.CreateForMLDsa(mldsa), + _ => throw new NotSupportedException(), + }; + } + + internal PublicKey ToPublicKey() + { + return GetGenerator().PublicKey; + } + + internal X509Certificate2 OntoCertificate(X509Certificate2 cert) + { + return CertificateCreation.CertificateRequestChainTests.CloneWithPrivateKey(cert, _key); + } + + internal byte[] Sign(byte[] data) + { + X509SignatureGenerator generator = GetGenerator(); + return generator.SignData(data, HashAlgorithmIfNeeded(generator.PublicKey.Oid.Value)); + } + + internal byte[] GetSignatureAlgorithmIdentifier() + { + X509SignatureGenerator generator = GetGenerator(); + + return generator.GetSignatureAlgorithmIdentifier( + HashAlgorithmIfNeeded(generator.PublicKey.Oid.Value)); + } + } } } diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 0930925517942f..2d3742e17650f4 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3050,10 +3050,14 @@ namespace System.Security.Cryptography.X509Certificates public sealed partial class CertificateRequest { public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.MLDsa key) { } public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding) { } public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } public CertificateRequest(System.Security.Cryptography.X509Certificates.X500DistinguishedName subjectName, System.Security.Cryptography.X509Certificates.PublicKey publicKey, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding? rsaSignaturePadding = null) { } public CertificateRequest(string subjectName, System.Security.Cryptography.ECDsa key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public CertificateRequest(string subjectName, System.Security.Cryptography.MLDsa key) { } public CertificateRequest(string subjectName, System.Security.Cryptography.RSA key, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, System.Security.Cryptography.RSASignaturePadding padding) { } public System.Collections.ObjectModel.Collection CertificateExtensions { get { throw null; } } public System.Security.Cryptography.HashAlgorithmName HashAlgorithm { get { throw null; } } @@ -3173,6 +3177,9 @@ public PublicKey(System.Security.Cryptography.Oid oid, System.Security.Cryptogra public System.Security.Cryptography.ECDsa? GetECDsaPublicKey() { throw null; } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + public System.Security.Cryptography.MLDsa? GetMLDsaPublicKey() { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public System.Security.Cryptography.MLKem? GetMLKemPublicKey() { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public System.Security.Cryptography.RSA? GetRSAPublicKey() { throw null; } @@ -3485,6 +3492,8 @@ public X509Certificate2(string fileName, string? password, System.Security.Crypt public string Thumbprint { get { throw null; } } public int Version { get { throw null; } } public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.ECDiffieHellman privateKey) { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.MLDsa privateKey) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromEncryptedPem(System.ReadOnlySpan certPem, System.ReadOnlySpan keyPem, System.ReadOnlySpan password) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] @@ -3504,6 +3513,10 @@ public X509Certificate2(string fileName, string? password, System.Security.Crypt public static System.Security.Cryptography.X509Certificates.X509ContentType GetCertContentType(string fileName) { throw null; } public System.Security.Cryptography.ECDiffieHellman? GetECDiffieHellmanPrivateKey() { throw null; } public System.Security.Cryptography.ECDiffieHellman? GetECDiffieHellmanPublicKey() { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public System.Security.Cryptography.MLDsa? GetMLDsaPrivateKey() { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public System.Security.Cryptography.MLDsa? GetMLDsaPublicKey() { throw null; } public string GetNameInfo(System.Security.Cryptography.X509Certificates.X509NameType nameType, bool forIssuer) { throw null; } [System.ObsoleteAttribute("X509Certificate and X509Certificate2 are immutable. Use X509CertificateLoader to create a new certificate.", DiagnosticId="SYSLIB0026", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public override void Import(byte[] rawData) { } @@ -3893,6 +3906,8 @@ protected X509SignatureGenerator() { } public System.Security.Cryptography.X509Certificates.PublicKey PublicKey { get { throw null; } } protected abstract System.Security.Cryptography.X509Certificates.PublicKey BuildPublicKey(); public static System.Security.Cryptography.X509Certificates.X509SignatureGenerator CreateForECDsa(System.Security.Cryptography.ECDsa key) { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public static System.Security.Cryptography.X509Certificates.X509SignatureGenerator CreateForMLDsa(System.Security.Cryptography.MLDsa key) { throw null; } public static System.Security.Cryptography.X509Certificates.X509SignatureGenerator CreateForRSA(System.Security.Cryptography.RSA key, System.Security.Cryptography.RSASignaturePadding signaturePadding) { throw null; } public abstract byte[] GetSignatureAlgorithmIdentifier(System.Security.Cryptography.HashAlgorithmName hashAlgorithm); public abstract byte[] SignData(byte[] data, System.Security.Cryptography.HashAlgorithmName hashAlgorithm); diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.csproj index 0ac11594442a03..e31ebee917ae6b 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.csproj @@ -1,7 +1,7 @@ $(NetCoreAppCurrent) - $(NoWarn);SYSLIB0026 + $(NoWarn);SYSLIB0026;SYSLIB5006 $(NoWarn);CS0809 diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 71eecc5c53b883..649988e1d25852 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -291,6 +291,9 @@ This method cannot be used since no signing key was provided via a constructor, use an overload accepting an X509SignatureGenerator instead. + + The intended signature algorithm requires a HashAlgorithmName, but one was not provided when building the CertificatRequest object. + The requested notAfter value ({0}) is later than issuerCertificate.NotAfter ({1}). diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index dceaa60c3eb3ea..ea154b09677923 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -673,6 +673,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs index 2eeb342907239a..5901274228a63c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs @@ -447,6 +447,12 @@ internal SafeKeyHandle? PrivateKeyHandle return new ECDiffieHellmanImplementation.ECDiffieHellmanAndroid(ecKey); } + public MLDsa? GetMLDsaPrivateKey() + { + // MLDsa is not supported on Android + return null; + } + public ICertificatePal CopyWithPrivateKey(DSA privateKey) { DSAImplementation.DSAAndroid? typedKey = privateKey as DSAImplementation.DSAAndroid; @@ -498,6 +504,12 @@ public ICertificatePal CopyWithPrivateKey(ECDiffieHellman privateKey) } } + public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) + { + throw new PlatformNotSupportedException( + SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa))); + } + public ICertificatePal CopyWithPrivateKey(RSA privateKey) { RSAImplementation.RSAAndroid? typedKey = privateKey as RSAImplementation.RSAAndroid; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs index e5903066a14bfd..078e6d2f7a392f 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs @@ -28,6 +28,12 @@ public ICertificatePal CopyWithPrivateKey(ECDiffieHellman privateKey) return ImportPkcs12(this, privateKey); } + public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) + { + throw new PlatformNotSupportedException( + SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa))); + } + public ICertificatePal CopyWithPrivateKey(RSA privateKey) { return ImportPkcs12(this, privateKey); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.macOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.macOS.cs index 228e70098870db..c57e6194574e3c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.macOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.macOS.cs @@ -122,6 +122,12 @@ public ICertificatePal CopyWithPrivateKey(ECDiffieHellman privateKey) } } + public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) + { + throw new PlatformNotSupportedException( + SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa))); + } + public ICertificatePal CopyWithPrivateKey(RSA privateKey) { var typedKey = privateKey as RSAImplementation.RSASecurityTransforms; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs index cf6fc63583e450..366c37a0598919 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs @@ -364,6 +364,12 @@ public byte[] SubjectPublicKeyInfo return new ECDiffieHellmanImplementation.ECDiffieHellmanSecurityTransforms(publicKey, privateKey); } + public MLDsa? GetMLDsaPrivateKey() + { + // MLDsa is not supported on Apple platforms. + return null; + } + public string GetNameInfo(X509NameType nameType, bool forIssuer) { EnsureCertData(); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.PrivateKey.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.PrivateKey.cs index dc3458ad1bedc0..dcb526751abe78 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.PrivateKey.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.PrivateKey.cs @@ -80,6 +80,12 @@ public bool HasPrivateKey ); } + public MLDsa? GetMLDsaPrivateKey() + { + // MLDsa is not supported on Windows. + return null; + } + public ICertificatePal CopyWithPrivateKey(DSA dsa) { DSACng? dsaCng = dsa as DSACng; @@ -168,6 +174,12 @@ public ICertificatePal CopyWithPrivateKey(ECDiffieHellman ecdh) } } + public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) + { + throw new PlatformNotSupportedException( + SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa))); + } + public ICertificatePal CopyWithPrivateKey(RSA rsa) { RSACng? rsaCng = rsa as RSACng; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.Load.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.Load.cs index 49b75d94d05f1b..5893a8ee51bd75 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.Load.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.Load.cs @@ -36,7 +36,9 @@ public static CertificateRequest LoadSigningRequestPem( CertificateRequestLoadOptions options = CertificateRequestLoadOptions.Default, RSASignaturePadding? signerSignaturePadding = null) { - ArgumentException.ThrowIfNullOrEmpty(signerHashAlgorithm.Name, nameof(signerHashAlgorithm)); + // Since ML-DSA (and others) don't require a hash algorithm, but we don't + // know what signature algorithm is being used until the call to Create, + // we can't check here. if ((options & ~AllOptions) != 0) { @@ -118,7 +120,9 @@ private static unsafe CertificateRequest LoadSigningRequest( CertificateRequestLoadOptions options, RSASignaturePadding? signerSignaturePadding) { - ArgumentException.ThrowIfNullOrEmpty(signerHashAlgorithm.Name, nameof(signerHashAlgorithm)); + // Since ML-DSA (and others) don't require a hash algorithm, but we don't + // know what signature algorithm is being used until the call to Create, + // we can't check here. if ((options & ~AllOptions) != 0) { @@ -294,6 +298,7 @@ private static bool VerifyX509Signature( { RSA? rsa = publicKey.GetRSAPublicKey(); ECDsa? ecdsa = publicKey.GetECDsaPublicKey(); + MLDsa? mldsa = publicKey.GetMLDsaPublicKey(); try { @@ -338,6 +343,11 @@ private static bool VerifyX509Signature( case Oids.ECDsaWithSha1: hashAlg = HashAlgorithmName.SHA1; break; + case Oids.MLDsa44: + case Oids.MLDsa65: + case Oids.MLDsa87: + hashAlg = default; + break; default: throw new NotSupportedException( SR.Format(SR.Cryptography_UnknownKeyAlgorithm, algorithmIdentifier.Algorithm)); @@ -371,6 +381,15 @@ private static bool VerifyX509Signature( } return ecdsa.VerifyData(toBeSigned, signature, hashAlg, DSASignatureFormat.Rfc3279DerSequence); + case Oids.MLDsa44: + case Oids.MLDsa65: + case Oids.MLDsa87: + if (mldsa is null) + { + return false; + } + + return mldsa.VerifyData(toBeSigned, signature); default: Debug.Fail( $"Algorithm ID {algorithmIdentifier.Algorithm} was in the first switch, but not the second"); @@ -389,6 +408,7 @@ private static bool VerifyX509Signature( { rsa?.Dispose(); ecdsa?.Dispose(); + mldsa?.Dispose(); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index 16c7c608e2a01b..6413406a50382a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Formats.Asn1; using System.Runtime.Versioning; using System.Security.Cryptography.Asn1; @@ -20,7 +21,7 @@ namespace System.Security.Cryptography.X509Certificates [UnsupportedOSPlatform("browser")] public sealed partial class CertificateRequest { - private readonly AsymmetricAlgorithm? _key; + private readonly object? _key; private readonly X509SignatureGenerator? _generator; private readonly RSASignaturePadding? _rsaPadding; @@ -179,7 +180,57 @@ public CertificateRequest( } /// - /// Create a CertificateRequest for the specified subject name, encoded public key, and hash algorithm. + /// Create a CertificateRequest for the specified subject name and ML-DSA key. + /// + /// + /// The parsed representation of the subject name for the certificate or certificate request. + /// + /// + /// An ML-DSA key whose public key material will be included in the certificate or certificate request. + /// This key will be used as a private key if is called. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public CertificateRequest( + string subjectName, + MLDsa key) + { + ArgumentNullException.ThrowIfNull(subjectName); + ArgumentNullException.ThrowIfNull(key); + + SubjectName = new X500DistinguishedName(subjectName); + + _key = key; + _generator = X509SignatureGenerator.CreateForMLDsa(key); + PublicKey = _generator.PublicKey; + } + + /// + /// Create a CertificateRequest for the specified subject name and ML-DSA key. + /// + /// + /// The parsed representation of the subject name for the certificate or certificate request. + /// + /// + /// An ML-DSA key whose public key material will be included in the certificate or certificate request. + /// This key will be used as a private key if is called. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public CertificateRequest( + X500DistinguishedName subjectName, + MLDsa key) + { + ArgumentNullException.ThrowIfNull(subjectName); + ArgumentNullException.ThrowIfNull(key); + + SubjectName = subjectName; + + _key = key; + _generator = X509SignatureGenerator.CreateForMLDsa(key); + PublicKey = _generator.PublicKey; + } + + /// + /// Create a CertificateRequest for the specified subject name, encoded public key, and hash algorithm. /// /// /// The parsed representation of the subject name for the certificate or certificate request. @@ -194,7 +245,10 @@ public CertificateRequest(X500DistinguishedName subjectName, PublicKey publicKey { ArgumentNullException.ThrowIfNull(subjectName); ArgumentNullException.ThrowIfNull(publicKey); - ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + + // Since ML-DSA (and others) don't require a hash algorithm, but we don't + // know what signature algorithm is being used until the call to Create, + // we can't check here. SubjectName = subjectName; PublicKey = publicKey; @@ -225,7 +279,10 @@ public CertificateRequest( { ArgumentNullException.ThrowIfNull(subjectName); ArgumentNullException.ThrowIfNull(publicKey); - ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + + // Since ML-DSA (and others) don't require a hash algorithm, but we don't + // know what signature algorithm is being used until the call to Create, + // we can't check here. SubjectName = subjectName; PublicKey = publicKey; @@ -320,6 +377,11 @@ public byte[] CreateSigningRequest() /// /// This object was created with a constructor which did not accept a signing key. /// + /// - or - + /// + /// The signature generator requires a non-default value for , + /// but this object was created without one being provided. + /// /// /// /// A cryptographic error occurs while creating the signing request. @@ -334,6 +396,12 @@ public byte[] CreateSigningRequest(X509SignatureGenerator signatureGenerator) { ArgumentNullException.ThrowIfNull(signatureGenerator); + if (string.IsNullOrEmpty(HashAlgorithm.Name) && + CertificateRevocationListBuilder.HashAlgorithmRequired(signatureGenerator.PublicKey.Oid.Value)) + { + throw new InvalidOperationException(SR.Cryptography_CertReq_NoHashAlgorithmProvided); + } + X501Attribute[] attributes = Array.Empty(); bool hasExtensions = CertificateExtensions.Count > 0; @@ -456,6 +524,11 @@ public string CreateSigningRequestPem() /// /// This object was created with a constructor which did not accept a signing key. /// + /// - or - + /// + /// The signature generator requires a non-default value for , + /// but this object was created without one being provided. + /// /// /// /// A cryptographic error occurs while creating the signing request. @@ -525,6 +598,13 @@ public X509Certificate2 CreateSelfSigned(DateTimeOffset notBefore, DateTimeOffse { return certificate.CopyWithPrivateKey(ecdsa); } + + MLDsa? mldsa = _key as MLDsa; + + if (mldsa is not null) + { + return certificate.CopyWithPrivateKey(mldsa); + } } Debug.Fail($"Key was of no known type: {_key?.GetType().FullName ?? "null"}"); @@ -568,8 +648,15 @@ public X509Certificate2 CreateSelfSigned(DateTimeOffset notBefore, DateTimeOffse /// has a different key algorithm than the requested certificate. /// /// - /// is an RSA certificate and this object was created without - /// specifying an value in the constructor. + /// + /// is an RSA certificate and this object was created via a constructor + /// which does not accept a value. + /// + /// - or - + /// + /// uses a public key algorithm which requires a non-default value + /// for , but this object was created without one being provided. + /// /// public X509Certificate2 Create( X509Certificate2 issuerCertificate, @@ -619,8 +706,15 @@ public X509Certificate2 Create( /// has a different key algorithm than the requested certificate. /// /// - /// is an RSA certificate and this object was created via a constructor - /// which does not accept a value. + /// + /// is an RSA certificate and this object was created via a constructor + /// which does not accept a value. + /// + /// - or - + /// + /// uses a public key algorithm which requires a non-default value + /// for , but this object was created without one being provided. + /// /// public X509Certificate2 Create( X509Certificate2 issuerCertificate, @@ -759,6 +853,10 @@ public X509Certificate2 Create( /// /// is null or has length 0. /// Any error occurs during the signing operation. + /// + /// The signature generator requires a non-default value for , + /// but this object was created without one being provided. + /// public X509Certificate2 Create( X500DistinguishedName issuerName, X509SignatureGenerator generator, @@ -800,6 +898,10 @@ public X509Certificate2 Create( /// /// has length 0. /// Any error occurs during the signing operation. + /// + /// The signature generator requires a non-default value for , + /// but this object was created without one being provided. + /// public X509Certificate2 Create( X500DistinguishedName issuerName, X509SignatureGenerator generator, @@ -815,6 +917,12 @@ public X509Certificate2 Create( if (serialNumber.Length < 1) throw new ArgumentException(SR.Arg_EmptyOrNullArray, nameof(serialNumber)); + if (string.IsNullOrEmpty(HashAlgorithm.Name) && + CertificateRevocationListBuilder.HashAlgorithmRequired(generator.PublicKey.Oid.Value)) + { + throw new InvalidOperationException(SR.Cryptography_CertReq_NoHashAlgorithmProvided); + } + byte[] signatureAlgorithm = generator.GetSignatureAlgorithmIdentifier(HashAlgorithm); AlgorithmIdentifierAsn signatureAlgorithmAsn; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs index ae2d7b1a097ea3..ddb33ba06b6859 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRevocationListBuilder.Build.cs @@ -92,7 +92,8 @@ public sealed partial class CertificateRevocationListBuilder /// - or - /// /// has the empty string as the value of - /// . + /// and + /// uses a public key algorithm that requires a hash to be specified. /// /// - or - /// @@ -140,7 +141,8 @@ private byte[] Build( if (nextUpdate <= thisUpdate) throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); - ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + if (HashAlgorithmRequired(issuerCertificate)) + ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); // Check the Basic Constraints and Key Usage extensions to help identify inappropriate certificates. // Note that this is not a security check. The system library backing X509Chain will use these same criteria @@ -173,7 +175,7 @@ private byte[] Build( nameof(issuerCertificate)); } - AsymmetricAlgorithm? key = null; + IDisposable? key = null; string keyAlgorithm = issuerCertificate.GetKeyAlgorithm(); X509SignatureGenerator generator; @@ -196,6 +198,13 @@ private byte[] Build( key = ecdsa; generator = X509SignatureGenerator.CreateForECDsa(ecdsa!); break; + case Oids.MLDsa44: + case Oids.MLDsa65: + case Oids.MLDsa87: + MLDsa? mldsa = issuerCertificate.GetMLDsaPrivateKey(); + key = mldsa; + generator = X509SignatureGenerator.CreateForMLDsa(mldsa!); + break; default: throw new ArgumentException( SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm), @@ -280,7 +289,8 @@ private byte[] Build( /// - or - /// /// has the empty string as the value of - /// . + /// and + /// uses a public key algorithm that requires a hash to be specified. /// /// /// @@ -324,7 +334,9 @@ private byte[] Build( if (nextUpdate <= thisUpdate) throw new ArgumentException(SR.Cryptography_CRLBuilder_DatesReversed); - ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + if (HashAlgorithmRequired(generator.PublicKey.Oid.Value)) + ArgumentException.ThrowIfNullOrEmpty(hashAlgorithm.Name, nameof(hashAlgorithm)); + ArgumentNullException.ThrowIfNull(authorityKeyIdentifier); byte[] signatureAlgId = generator.GetSignatureAlgorithmIdentifier(hashAlgorithm); @@ -437,5 +449,37 @@ private byte[] Build( byte[] crl = writer.Encode(); return crl; } + + private static bool HashAlgorithmRequired(X509Certificate2 certificate) => + HashAlgorithmRequired(certificate.GetKeyAlgorithm()); + + internal static bool HashAlgorithmRequired(string? keyAlgorithm) + { + // This list could either be written as "ML-DSA and friends return false", + // or "RSA and friends return true". + // + // The consequences of returning true is that the hashAlgorithm parameter + // gets pre-validated to not be null or empty, which means false positives + // impact new ML-DSA-like algorithms. + // + // The consequences of returning false is that the hashAlgorithm parameter + // is not pre-validated. That just means that in a false negative the user + // gets probably the same exception, but from a different callstack. + // + // False positives or negatives are not possible with the simple Build that takes + // only an X509Certificate2, as we control the destiny there entirely, it's only + // for the power user scenario of the X509SignatureGenerator that this is a concern. + // + // Since the false-positive is worse than the false-negative, the list is written + // as explicit-true, implicit-false. + return keyAlgorithm switch + { + Oids.Rsa or + Oids.RsaPss or + Oids.EcPublicKey or + Oids.Dsa => true, + _ => false, + }; + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePal.cs index 931e5b17618eca..6828be2404ae42 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePal.cs @@ -31,12 +31,14 @@ internal interface ICertificatePal : ICertificatePalCore DSA? GetDSAPrivateKey(); ECDsa? GetECDsaPrivateKey(); ECDiffieHellman? GetECDiffieHellmanPrivateKey(); + MLDsa? GetMLDsaPrivateKey(); string GetNameInfo(X509NameType nameType, bool forIssuer); void AppendPrivateKeyInfo(StringBuilder sb); ICertificatePal CopyWithPrivateKey(DSA privateKey); ICertificatePal CopyWithPrivateKey(ECDsa privateKey); ICertificatePal CopyWithPrivateKey(RSA privateKey); ICertificatePal CopyWithPrivateKey(ECDiffieHellman privateKey); + ICertificatePal CopyWithPrivateKey(MLDsa privateKey); PolicyData GetPolicyData(); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/MLDsaX509SignatureGenerator.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/MLDsaX509SignatureGenerator.cs new file mode 100644 index 00000000000000..a7c2498c90ad57 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/MLDsaX509SignatureGenerator.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Formats.Asn1; +using Internal.Cryptography; + +namespace System.Security.Cryptography.X509Certificates +{ + internal sealed class MLDsaX509SignatureGenerator : X509SignatureGenerator + { + private readonly MLDsa _key; + + internal MLDsaX509SignatureGenerator(MLDsa key) + { + Debug.Assert(key != null); + + _key = key; + } + + public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) + { + // Ignore the hashAlgorithm parameter. + // This generator only supports ML-DSA "Pure" signatures, but the overall design of + // CertificateRequest makes it easy for a hashAlgorithm value to get here. + + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + writer.PushSequence(); + writer.WriteObjectIdentifier(_key.Algorithm.Oid); + writer.PopSequence(); + return writer.Encode(); + } + + public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) + { + ArgumentNullException.ThrowIfNull(data); + + // Ignore the hashAlgorithm parameter. + // This generator only supports ML-DSA "Pure" signatures, but the overall design of + // CertificateRequest makes it easy for a hashAlgorithm value to get here. + + byte[] signature = new byte[_key.Algorithm.SignatureSizeInBytes]; + int written = _key.SignData(data, signature); + Debug.Assert(written == signature.Length); + return signature; + } + + protected override PublicKey BuildPublicKey() + { + Oid oid = new Oid(_key.Algorithm.Oid, null); + byte[] pkBytes = new byte[_key.Algorithm.PublicKeySizeInBytes]; + int written = _key.ExportMLDsaPublicKey(pkBytes); + Debug.Assert(written == pkBytes.Length); + + return new PublicKey( + oid, + null, + new AsnEncodedData(oid, pkBytes, skipCopy: true)); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs index 5935531c02267b..99ba8e9b9f389d 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs @@ -17,6 +17,7 @@ internal sealed class OpenSslX509CertificateReader : ICertificatePal private SafeX509Handle _cert; private SafeEvpPKeyHandle? _privateKey; + private MLDsa? _mldsaPrivateKey; private X500DistinguishedName? _subjectName; private X500DistinguishedName? _issuerName; private string? _subject; @@ -249,7 +250,7 @@ internal OpenSslX509CertificateReader(SafeX509Handle handle) public bool HasPrivateKey { - get { return _privateKey != null; } + get { return _privateKey != null || _mldsaPrivateKey != null; } } public IntPtr Handle @@ -608,6 +609,16 @@ public ECDiffieHellman GetECDiffieHellmanPublicKey() return new ECDiffieHellmanOpenSsl(_privateKey); } + public MLDsa? GetMLDsaPrivateKey() + { + if (_mldsaPrivateKey is null) + { + return null; + } + + return DuplicatePrivateKey(_mldsaPrivateKey); + } + private OpenSslX509CertificateReader CopyWithPrivateKey(SafeEvpPKeyHandle privateKey) { // This could be X509Duplicate for a full clone, but since OpenSSL certificates @@ -677,6 +688,37 @@ public ICertificatePal CopyWithPrivateKey(ECDiffieHellman privateKey) } } + public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) + { + SafeX509Handle certHandle = Interop.Crypto.X509UpRef(_cert); + OpenSslX509CertificateReader duplicate = new OpenSslX509CertificateReader(certHandle); + duplicate._mldsaPrivateKey = DuplicatePrivateKey(privateKey); + + return duplicate; + } + + private static MLDsa DuplicatePrivateKey(MLDsa key) + { + MLDsaAlgorithm alg = key.Algorithm; + byte[] rented = CryptoPool.Rent(alg.SecretKeySizeInBytes); + int written = 0; + + try + { + written = key.ExportMLDsaPrivateSeed(rented); + return MLDsa.ImportMLDsaPrivateSeed(alg, new ReadOnlySpan(rented, 0, written)); + } + catch (CryptographicException) + { + written = key.ExportMLDsaSecretKey(rented); + return MLDsa.ImportMLDsaSecretKey(alg, new ReadOnlySpan(rented, 0, written)); + } + finally + { + CryptoPool.Return(rented, written); + } + } + public ICertificatePal CopyWithPrivateKey(RSA privateKey) { RSAOpenSsl? typedKey = privateKey as RSAOpenSsl; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs index cc813be300e830..e9b272e183ad07 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/PublicKey.cs @@ -314,6 +314,16 @@ public static PublicKey CreateFromSubjectPublicKeyInfo(ReadOnlySpan source return MLKem.ImportSubjectPublicKeyInfo(ExportSubjectPublicKeyInfo()); } + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + [UnsupportedOSPlatform("browser")] + public MLDsa? GetMLDsaPublicKey() + { + if (MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(_oid.Value) is null) + return null; + + return MLDsa.ImportSubjectPublicKeyInfo(ExportSubjectPublicKeyInfo()); + } + internal AsnWriter EncodeSubjectPublicKeyInfo() { SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs index 46999ed5d7b367..ae3b72bae4f91c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Formats.Asn1; using System.IO; using System.Net; @@ -778,6 +779,116 @@ public X509Certificate2 CopyWithPrivateKey(ECDiffieHellman privateKey) return new X509Certificate2(pal); } + /// + /// Get the public key from this certificate. + /// + /// + /// The public key, or if this certificate does not have an ML-DSA public key. + /// + /// + /// The certificate has an ML-DSA public key, but the platform does not support ML-DSA. + /// + /// + /// The public key was invalid, or otherwise could not be imported. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public MLDsa? GetMLDsaPublicKey() + { + MLDsaAlgorithm? algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(GetKeyAlgorithm()); + + if (algorithm is null) + { + return null; + } + + byte[] publicKey = Pal.PublicKeyValue; + + if (publicKey.Length != algorithm.PublicKeySizeInBytes) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return MLDsa.ImportMLDsaPublicKey(algorithm, Pal.PublicKeyValue); + } + + /// + /// Get the private key from this certificate. + /// + /// + /// The private key, or if this certificate does not have an ML-DSA private key. + /// + /// + /// An error occurred accessing the private key. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public MLDsa? GetMLDsaPrivateKey() + { + MLDsaAlgorithm? algorithm = MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(GetKeyAlgorithm()); + + if (algorithm is null) + { + return null; + } + + return Pal.GetMLDsaPrivateKey(); + } + + /// + /// Combines a private key with a certificate containing the associated public key into a + /// new instance that can access the private key. + /// + /// + /// The ML-DSA private key that corresponds to the ML-DSA public key in this certificate. + /// + /// + /// A new certificate with the property set to . + /// The current certificate isn't modified. + /// + /// + /// is . + /// + /// + /// The specified private key doesn't match the public key for this certificate. + /// + /// + /// The certificate already has an associated private key. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public X509Certificate2 CopyWithPrivateKey(MLDsa privateKey) + { + ArgumentNullException.ThrowIfNull(privateKey); + + if (HasPrivateKey) + throw new InvalidOperationException(SR.Cryptography_Cert_AlreadyHasPrivateKey); + + using (MLDsa? publicKey = GetMLDsaPublicKey()) + { + if (publicKey is null) + { + throw new ArgumentException(SR.Cryptography_PrivateKey_WrongAlgorithm); + } + + if (publicKey.Algorithm != privateKey.Algorithm) + { + throw new ArgumentException(SR.Cryptography_PrivateKey_DoesNotMatch, nameof(privateKey)); + } + + byte[] pk1 = new byte[publicKey.Algorithm.PublicKeySizeInBytes]; + byte[] pk2 = new byte[pk1.Length]; + + int w1 = publicKey.ExportMLDsaPublicKey(pk1); + int w2 = privateKey.ExportMLDsaPublicKey(pk2); + + if (w1 != w2 || !pk1.AsSpan().SequenceEqual(pk2)) + { + throw new ArgumentException(SR.Cryptography_PrivateKey_DoesNotMatch, nameof(privateKey)); + } + } + + ICertificatePal pal = Pal.CopyWithPrivateKey(privateKey); + return new X509Certificate2(pal); + } + /// /// Creates a new X509 certificate from the file contents of an RFC 7468 PEM-encoded /// certificate and private key. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SignatureGenerator.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SignatureGenerator.cs index e493ab5fc7e17f..9a4c727269b4a0 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SignatureGenerator.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509SignatureGenerator.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.CodeAnalysis; + namespace System.Security.Cryptography.X509Certificates { public abstract class X509SignatureGenerator @@ -32,5 +34,13 @@ public static X509SignatureGenerator CreateForRSA(RSA key, RSASignaturePadding s throw new ArgumentException(SR.Cryptography_InvalidPaddingMode); } + + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public static X509SignatureGenerator CreateForMLDsa(MLDsa key) + { + ArgumentNullException.ThrowIfNull(key); + + return new MLDsaX509SignatureGenerator(key); + } } } diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index cdc96a787447de..4e27f400f8b485 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -284,6 +284,8 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\ECDsa\ECDsaTestsBase.cs" /> + ( + "subjectName", + () => new CertificateRequest(subjectName, key)); + + subjectName = ""; + + AssertExtensions.Throws( + "key", + () => new CertificateRequest(subjectName, key)); + } + + [Fact] + public static void CtorValidation_MLDSA_X500DN() + { + X500DistinguishedName subjectName = null; + MLDsa key = null; + + AssertExtensions.Throws( + "subjectName", + () => new CertificateRequest(subjectName, key)); + + subjectName = new X500DistinguishedName(""); + + AssertExtensions.Throws( + "key", + () => new CertificateRequest(subjectName, key)); + } + + [Fact] + public static void MLDSA_DoesNotSetHashAlgorithm() + { + using (MLDsa key = MLDsaTestImplementation.CreateNoOp(MLDsaAlgorithm.MLDsa65)) + { + CertificateRequest req = new CertificateRequest("CN=Test", key); + Assert.Null(req.HashAlgorithm.Name); + } + } + [Fact] public static void CtorValidation_RSA_string() { @@ -233,14 +278,36 @@ public static void CtorValidation_PublicKey_X500DN() X509SignatureGenerator generator = X509SignatureGenerator.CreateForECDsa(ecdsa); publicKey = generator.PublicKey; } + } - AssertExtensions.Throws( - "hashAlgorithm", - () => new CertificateRequest(subjectName, publicKey, default(HashAlgorithmName))); + [Fact] + public static void PublicKeyCtor_HashAlgorithm_LateVerification() + { + X500DistinguishedName name = new X500DistinguishedName("CN=Test"); + + using (ECDsa ecdsa = ECDsa.Create(EccTestData.Secp384r1Data.KeyParameters)) + { + X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(ecdsa); + + CertificateRequest req = new CertificateRequest(name, gen.PublicKey, default); + Assert.Null(req.HashAlgorithm.Name); + DateTimeOffset now = DateTimeOffset.UtcNow; + + Assert.Throws( + () => req.Create(name, gen, now.AddMinutes(-1), now.AddMinutes(1), new byte[] { 1, 2, 3 })); + } - AssertExtensions.Throws( - "hashAlgorithm", - () => new CertificateRequest(subjectName, publicKey, new HashAlgorithmName(""))); + using (RSA rsa = RSA.Create(TestData.RsaBigExponentParams)) + { + X509SignatureGenerator gen = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); + + CertificateRequest req = new CertificateRequest(name, gen.PublicKey, default); + Assert.Null(req.HashAlgorithm.Name); + DateTimeOffset now = DateTimeOffset.UtcNow; + + Assert.Throws( + () => req.Create(name, gen, now.AddMinutes(-1), now.AddMinutes(1), new byte[] { 1, 2, 3 })); + } } [Fact] @@ -416,70 +483,6 @@ public static void LoadNullPemString() () => CertificateRequest.LoadSigningRequestPem((string)null, HashAlgorithmName.SHA256)); } - [Fact] - public static void LoadWithDefaultHashAlgorithm() - { - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequest(Array.Empty(), default(HashAlgorithmName))); - - { - int consumed = -1; - - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequest( - ReadOnlySpan.Empty, - default(HashAlgorithmName), - out consumed)); - - Assert.Equal(-1, consumed); - } - - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequestPem(string.Empty, default(HashAlgorithmName))); - - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequestPem( - ReadOnlySpan.Empty, - default(HashAlgorithmName))); - } - - [Fact] - public static void LoadWithEmptyHashAlgorithm() - { - HashAlgorithmName hashAlgorithm = new HashAlgorithmName(""); - - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequest(Array.Empty(), hashAlgorithm)); - - { - int consumed = -1; - - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequest( - ReadOnlySpan.Empty, - hashAlgorithm, - out consumed)); - - Assert.Equal(-1, consumed); - } - - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequestPem(string.Empty, hashAlgorithm)); - - Assert.Throws( - "signerHashAlgorithm", - () => CertificateRequest.LoadSigningRequestPem( - ReadOnlySpan.Empty, - hashAlgorithm)); - } - [Theory] [InlineData(-1)] [InlineData(4)] diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs index 008402ee37b4e2..59f890284f8e2c 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs @@ -29,6 +29,28 @@ public static void CreateChain_ECC() } } + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] + public static void CreateChain_MLDSA() + { + using (MLDsa rootKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87)) + using (MLDsa intermed1Key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)) + using (MLDsa intermed2Key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)) + using (MLDsa leafKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44)) + { + byte[] pubKey = new byte[leafKey.Algorithm.PublicKeySizeInBytes]; + leafKey.ExportMLDsaPublicKey(pubKey); + + using (MLDsa leafPubKey = MLDsa.ImportMLDsaPublicKey(leafKey.Algorithm, pubKey)) + { + CreateAndTestChain( + rootKey, + intermed1Key, + intermed2Key, + leafPubKey); + } + } + } + [Fact] public static void CreateChain_RSA() { @@ -203,7 +225,7 @@ public static void ChainCertRequirements(bool useIntermed, bool? isCA, X509KeyUs private static CertificateRequest OpenCertRequest( string dn, - AsymmetricAlgorithm key, + object key, HashAlgorithmName hashAlgorithm) { X500DistinguishedName x500dn = new X500DistinguishedName(dn); @@ -211,30 +233,27 @@ private static CertificateRequest OpenCertRequest( RSA rsa => new CertificateRequest(x500dn, rsa, hashAlgorithm, RSASignaturePadding.Pkcs1), ECDsa ecdsa => new CertificateRequest(x500dn, ecdsa, hashAlgorithm), ECDiffieHellman ecdh => new CertificateRequest(x500dn, new PublicKey(ecdh), hashAlgorithm), + MLDsa mldsa => new CertificateRequest(x500dn, mldsa), _ => throw new InvalidOperationException( $"Had no handler for key of type {key?.GetType().FullName ?? "null"}") }; } - private static X509SignatureGenerator OpenGenerator(AsymmetricAlgorithm key) + private static X509SignatureGenerator OpenGenerator(object key) { - RSA rsa = key as RSA; - - if (rsa != null) - return X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); - - ECDsa ecdsa = key as ECDsa; - - if (ecdsa != null) - return X509SignatureGenerator.CreateForECDsa(ecdsa); - - throw new InvalidOperationException( - $"Had no handler for key of type {key?.GetType().FullName ?? "null"}"); + return key switch + { + RSA rsa => X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1), + ECDsa ecdsa => X509SignatureGenerator.CreateForECDsa(ecdsa), + MLDsa mldsa => X509SignatureGenerator.CreateForMLDsa(mldsa), + _ => throw new InvalidOperationException( + $"Had no handler for key of type {key?.GetType().FullName ?? "null"}") + }; } private static CertificateRequest CreateChainRequest( string dn, - AsymmetricAlgorithm key, + object key, HashAlgorithmName hashAlgorithm, bool isCa, int? pathLen) @@ -323,32 +342,24 @@ private static void DisposeChainCerts(X509Chain chain) } } - private static X509Certificate2 CloneWithPrivateKey(X509Certificate2 cert, AsymmetricAlgorithm key) + internal static X509Certificate2 CloneWithPrivateKey(X509Certificate2 cert, object key) { - RSA rsa = key as RSA; - - if (rsa != null) - return cert.CopyWithPrivateKey(rsa); - - ECDsa ecdsa = key as ECDsa; - - if (ecdsa != null) - return cert.CopyWithPrivateKey(ecdsa); - - DSA dsa = key as DSA; - - if (dsa != null) - return cert.CopyWithPrivateKey(dsa); - - throw new InvalidOperationException( - $"Had no handler for key of type {key?.GetType().FullName ?? "null"}"); + return key switch + { + RSA rsa => cert.CopyWithPrivateKey(rsa), + ECDsa ecdsa => cert.CopyWithPrivateKey(ecdsa), + MLDsa mldsa => cert.CopyWithPrivateKey(mldsa), + DSA dsa => cert.CopyWithPrivateKey(dsa), + _ => throw new InvalidOperationException( + $"Had no handler for key of type {key?.GetType().FullName ?? "null"}") + }; } private static void CreateAndTestChain( - AsymmetricAlgorithm rootPrivKey, - AsymmetricAlgorithm intermed1PrivKey, - AsymmetricAlgorithm intermed2PrivKey, - AsymmetricAlgorithm leafPubKey) + object rootPrivKey, + object intermed1PrivKey, + object intermed2PrivKey, + object leafPubKey) { const string RootDN = "CN=Experimental Root Certificate"; const string Intermed1DN = "CN=First Intermediate Certificate, O=Experimental"; diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestLoadTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestLoadTests.cs index b29429f9e80a9f..f96046145e6b4b 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestLoadTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestLoadTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Formats.Asn1; using System.Net; using Test.Cryptography; using Xunit; @@ -718,6 +719,181 @@ public static void LoadRequestWithAttributeValues() Assert.Equal("0C053132333435", attr.RawData.ByteArrayToHex()); } + [Fact] + public static void Load_NoHashAlgorithm_LateVerification() + { + CertificateRequest req = CertificateRequest.LoadSigningRequestPem( + TestData.BigExponentPkcs10Pem, + default, + CertificateRequestLoadOptions.SkipSignatureValidation); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMonths(-1); + DateTimeOffset notAfter = now.AddMonths(1); + + using (RSA key = RSA.Create(TestData.RsaBigExponentParams)) + { + X509SignatureGenerator generator = X509SignatureGenerator.CreateForRSA(key, RSASignaturePadding.Pkcs1); + + InvalidOperationException ex = Assert.Throws( + () => req.Create( + req.SubjectName, + generator, + notBefore, + notAfter, + new byte[] { 3, 1, 0, 1, 3, 3, 3 })); + + Assert.Contains("HashAlgorithm", ex.Message); + + ex = Assert.Throws( + () => req.CreateSigningRequest(generator)); + + Assert.Contains("HashAlgorithm", ex.Message); + + ex = Assert.Throws( + () => req.CreateSigningRequestPem(generator)); + + Assert.Contains("HashAlgorithm", ex.Message); + } + } + + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] + public static void Load_NoHashAlgorithm_OKForMLDsa() + { + CertificateRequest req = CertificateRequest.LoadSigningRequestPem( + TestData.BigExponentPkcs10Pem, + default, + CertificateRequestLoadOptions.SkipSignatureValidation); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMonths(-1); + DateTimeOffset notAfter = now.AddMonths(1); + + using (MLDsa key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44)) + { + // Assert.NoThrow + using X509Certificate2 cert = req.Create( + req.SubjectName, + X509SignatureGenerator.CreateForMLDsa(key), + notBefore, + notAfter, + new byte[] { 3, 1, 0, 1, 3, 3, 3 }); + + Assert.Equal("2.16.840.1.101.3.4.3.17", cert.SignatureAlgorithm.Value); + } + } + + [Fact] + public static void LoadCreate_MatchesCreate_RSAPkcs1() + { + using (RSA key = RSA.Create(2048)) + { + LoadCreate_MatchesCreate( + new CertificateRequest( + "CN=Roundtrip, O=RSA, OU=PKCS1", + key, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1), + X509SignatureGenerator.CreateForRSA(key, RSASignaturePadding.Pkcs1), + deterministicSignature: true); + } + } + + [Fact] + public static void LoadCreate_MatchesCreate_RSAPss() + { + using (RSA key = RSA.Create(2048)) + { + LoadCreate_MatchesCreate( + new CertificateRequest( + "CN=Roundtrip, O=RSA, OU=PSS", + key, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pss), + X509SignatureGenerator.CreateForRSA(key, RSASignaturePadding.Pss), + deterministicSignature: false); + } + } + + [Fact] + public static void LoadCreate_MatchesCreate_ECDsa() + { + using (ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP384)) + { + LoadCreate_MatchesCreate( + new CertificateRequest( + "CN=Roundtrip, O=EC-DSA", + key, + HashAlgorithmName.SHA256), + X509SignatureGenerator.CreateForECDsa(key), + deterministicSignature: false); + } + } + + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] + public static void LoadCreate_MatchesCreate_MLDsa() + { + using (MLDsa key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)) + { + LoadCreate_MatchesCreate( + new CertificateRequest("CN=Roundtrip, O=ML-DSA", key), + X509SignatureGenerator.CreateForMLDsa(key), + deterministicSignature: false); + } + } + + private static void LoadCreate_MatchesCreate( + CertificateRequest request, + X509SignatureGenerator generator, + bool deterministicSignature) + { + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset notBefore = now.AddMonths(-1); + DateTimeOffset notAfter = now.AddMonths(1); + byte[] serial = new byte[] { 0x02, 0x04, 0x06, 0x08, 0x07, 0x05, 0x03, 0x01 }; + + request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); + request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("0.0.1", null) }, false)); + + byte[] pkcs10 = request.CreateSigningRequest(generator); + CertificateRequest loaded = CertificateRequest.LoadSigningRequest( + pkcs10, + HashAlgorithmName.SHA256, + CertificateRequestLoadOptions.UnsafeLoadCertificateExtensions); + + using (X509Certificate2 one = request.Create(request.SubjectName, generator, notBefore, notAfter, serial)) + using (X509Certificate2 two = loaded.Create(request.SubjectName, generator, notBefore, notAfter, serial)) + { + if (deterministicSignature) + { + AssertExtensions.SequenceEqual(one.RawDataMemory.Span, two.RawDataMemory.Span); + } + else + { + // tbsCertificate and signatureAlgorithm should match, signature should not. + // + // Certificate ::= SEQUENCE { + // tbsCertificate TBSCertificate, + // signatureAlgorithm AlgorithmIdentifier, + // signature BIT STRING } + + AsnValueReader readerOne = new AsnValueReader(one.RawDataMemory.Span, AsnEncodingRules.DER); + AsnValueReader readerTwo = new AsnValueReader(two.RawDataMemory.Span, AsnEncodingRules.DER); + + AsnValueReader certOne = readerOne.ReadSequence(); + AsnValueReader certTwo = readerTwo.ReadSequence(); + readerOne.ThrowIfNotEmpty(); + readerTwo.ThrowIfNotEmpty(); + + AssertExtensions.SequenceEqual(certOne.ReadEncodedValue(), certTwo.ReadEncodedValue()); + AssertExtensions.SequenceEqual(certOne.ReadEncodedValue(), certTwo.ReadEncodedValue()); + AssertExtensions.SequenceNotEqual(certOne.ReadEncodedValue(), certTwo.ReadEncodedValue()); + certOne.ThrowIfNotEmpty(); + certTwo.ThrowIfNotEmpty(); + } + } + } + private static void VerifyBigExponentRequest( CertificateRequest req, CertificateRequestLoadOptions options) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CrlBuilderTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CrlBuilderTests.cs index 563502efbda656..cf8c7974e84ab6 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CrlBuilderTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CrlBuilderTests.cs @@ -16,6 +16,35 @@ public static class CrlBuilderTests { private const string CertParam = "issuerCertificate"; + public enum CertKind + { + ECDsa, + MLDsa, + RsaPkcs1, + RsaPss, + } + + public static IEnumerable SupportedCertKinds() + { + yield return new object[] { CertKind.ECDsa }; + + if (MLDsa.IsSupported) + { + yield return new object[] { CertKind.MLDsa }; + } + + yield return new object[] { CertKind.RsaPkcs1 }; + yield return new object[] { CertKind.RsaPss }; + } + + public static IEnumerable NoHashAlgorithmCertKinds() + { + if (MLDsa.IsSupported) + { + yield return new object[] { CertKind.MLDsa }; + } + } + [Fact] public static void AddEntryArgumentValidation() { @@ -153,63 +182,136 @@ public static void BuildWithNextUpdateBeforeThisUpdate() }); } - [Fact] - public static void BuildWithNoHashAlgorithm() + [Theory] + [MemberData(nameof(SupportedCertKinds))] + public static void BuildWithNoHashAlgorithm(CertKind certKind) { BuildCertificateAndRun( + certKind, new X509Extension[] { X509BasicConstraintsExtension.CreateForCertificateAuthority(), }, - static (cert, now) => + static (certKind, cert, now) => { HashAlgorithmName hashAlg = default; CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); - Assert.Throws( - "hashAlgorithm", - () => builder.Build(cert, 0, now.AddMinutes(5), hashAlg, null, now)); + Action certBuild = () => builder.Build(cert, 0, now.AddMinutes(5), hashAlg, null, now); - using (ECDsa key = cert.GetECDsaPrivateKey()) + if (RequiresHashAlgorithm(certKind)) { - X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(key); - X500DistinguishedName dn = cert.SubjectName; + Assert.Throws("hashAlgorithm", certBuild); + } + else + { + // Assert.NoThrow + certBuild(); + } + + X509SignatureGenerator gen = GetSignatureGenerator(certKind, cert, out IDisposable key); - Assert.Throws( - "hashAlgorithm", - () => builder.Build(dn, gen, 0, now.AddMinutes(5), hashAlg, null, now)); + using (key) + { + X500DistinguishedName dn = cert.SubjectName; + X509AuthorityKeyIdentifierExtension akid = + X509AuthorityKeyIdentifierExtension.CreateFromCertificate(cert, true, false); + + Action genBuild = () => builder.Build(dn, gen, 0, now.AddMinutes(5), hashAlg, akid, now); + + if (RequiresHashAlgorithm(certKind)) + { + Assert.Throws("hashAlgorithm", genBuild); + } + else + { + // Assert.NoThrow + genBuild(); + } } }); } - [Fact] - public static void BuildWithEmptyHashAlgorithm() + [Theory] + [MemberData(nameof(SupportedCertKinds))] + public static void BuildWithEmptyHashAlgorithm(CertKind certKind) { BuildCertificateAndRun( + certKind, new X509Extension[] { X509BasicConstraintsExtension.CreateForCertificateAuthority(), }, - static (cert, now) => + static (certKind, cert, now) => { HashAlgorithmName hashAlg = new HashAlgorithmName(""); CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); - ArgumentException e = Assert.Throws( - "hashAlgorithm", - () => builder.Build(cert, 0, now.AddMinutes(5), hashAlg, null, now)); - Assert.Contains("empty", e.Message); + Action certAction = () => builder.Build(cert, 0, now.AddMinutes(5), hashAlg, null, now); - using (ECDsa key = cert.GetECDsaPrivateKey()) + if (RequiresHashAlgorithm(certKind)) + { + ArgumentException e = Assert.Throws("hashAlgorithm", certAction); + + Assert.Contains("empty", e.Message); + } + else + { + // Assert.NoThrow + certAction(); + } + + X509SignatureGenerator gen = GetSignatureGenerator(certKind, cert, out IDisposable key); + + using (key) { - X509SignatureGenerator gen = X509SignatureGenerator.CreateForECDsa(key); X500DistinguishedName dn = cert.SubjectName; + X509AuthorityKeyIdentifierExtension akid = + X509AuthorityKeyIdentifierExtension.CreateFromCertificate(cert, true, false); + + Action genAction = () => builder.Build(dn, gen, 0, now.AddMinutes(5), hashAlg, akid, now); + + if (RequiresHashAlgorithm(certKind)) + { + Assert.Throws("hashAlgorithm", genAction); + } + else + { + // Assert.NoThrow + genAction(); + } + } + }); + } - e = Assert.Throws( - "hashAlgorithm", - () => builder.Build(dn, gen, 0, now.AddMinutes(5), hashAlg, null, now)); + [Theory] + [MemberData(nameof(NoHashAlgorithmCertKinds))] + public static void BuildPqcWithHashAlgorithm(CertKind certKind) + { + BuildCertificateAndRun( + certKind, + new X509Extension[] + { + X509BasicConstraintsExtension.CreateForCertificateAuthority(), + }, + static (certKind, cert, now) => + { + HashAlgorithmName hashAlg = new HashAlgorithmName(""); + CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); - Assert.Contains("empty", e.Message); + // Assert.NoThrow + builder.Build(cert, 0, now.AddMinutes(5), HashAlgorithmName.SHA256); + + X509SignatureGenerator gen = GetSignatureGenerator(certKind, cert, out IDisposable key); + + using (key) + { + X500DistinguishedName dn = cert.SubjectName; + X509AuthorityKeyIdentifierExtension akid = + X509AuthorityKeyIdentifierExtension.CreateFromCertificate(cert, true, false); + + // Assert.NoThrow + builder.Build(dn, gen, 0, now.AddMinutes(5), HashAlgorithmName.SHA256, akid); } }); } @@ -349,7 +451,7 @@ public static void BuildWithGeneratorArgumentValidation() } [Fact] - public static void BuildEmpty() + public static void BuildEmptyRsaPkcs1() { BuildRsaCertificateAndRun( new X509Extension[] @@ -371,7 +473,8 @@ public static void BuildEmpty() // In fact, because RSASSA-PKCS1 is a deterministic algorithm, we can check it for a fixed output. AssertExtensions.SequenceEqual(BuildEmptyExpectedCrl, built); - }); + }, + callerName: "BuildEmpty"); } [Theory] @@ -421,20 +524,24 @@ public static void BuildEmptyRsaPss(string hashName) }); } - [Fact] - public static void BuildEmptyEcdsa() + [Theory] + [MemberData(nameof(SupportedCertKinds))] + public static void BuildEmpty(CertKind certKind) { BuildCertificateAndRun( + certKind, new X509Extension[] { X509BasicConstraintsExtension.CreateForCertificateAuthority(), }, - (cert, now) => + (certKind, cert, now) => { CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); DateTimeOffset nextUpdate = now.AddHours(1); - byte[] crl = builder.Build(cert, 2, nextUpdate, HashAlgorithmName.SHA256); + HashAlgorithmName hashAlg = RequiresHashAlgorithm(certKind) ? HashAlgorithmName.SHA256 : default; + + byte[] crl = builder.Build(cert, 2, nextUpdate, hashAlg, GetRsaPadding(certKind)); AsnReader reader = new AsnReader(crl, AsnEncodingRules.DER); reader = reader.ReadSequence(); @@ -444,16 +551,7 @@ public static void BuildEmptyEcdsa() byte[] signature = reader.ReadBitString(out _); reader.ThrowIfNotEmpty(); - using (ECDsa pubKey = cert.GetECDsaPublicKey()) - { - Assert.True( - pubKey.VerifyData( - tbs.Span, - signature, - HashAlgorithmName.SHA256, - DSASignatureFormat.Rfc3279DerSequence), - "Certificate public key verifies CRL"); - } + VerifySignature(certKind, cert, tbs.Span, signature, hashAlg); VerifyCrlFields( crl, @@ -465,26 +563,30 @@ public static void BuildEmptyEcdsa() }); } - [Fact] - public static void BuildEmptyEcdsa_NoSubjectKeyIdentifier() + [Theory] + [MemberData(nameof(SupportedCertKinds))] + public static void BuildEmpty_NoSubjectKeyIdentifier(CertKind certKind) { BuildCertificateAndRun( + certKind, new X509Extension[] { X509BasicConstraintsExtension.CreateForCertificateAuthority(), }, - (cert, now) => + (certKind, cert, now) => { CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder(); DateTimeOffset nextUpdate = now.AddHours(1); DateTimeOffset thisUpdate = now; + HashAlgorithmName hashAlg = RequiresHashAlgorithm(certKind) ? HashAlgorithmName.SHA256 : default; byte[] crl = builder.Build( cert, 2, nextUpdate, - HashAlgorithmName.SHA256, - thisUpdate: thisUpdate); + hashAlg, + GetRsaPadding(certKind), + thisUpdate); AsnReader reader = new AsnReader(crl, AsnEncodingRules.DER); reader = reader.ReadSequence(); @@ -494,16 +596,7 @@ public static void BuildEmptyEcdsa_NoSubjectKeyIdentifier() byte[] signature = reader.ReadBitString(out _); reader.ThrowIfNotEmpty(); - using (ECDsa pubKey = cert.GetECDsaPublicKey()) - { - Assert.True( - pubKey.VerifyData( - tbs.Span, - signature, - HashAlgorithmName.SHA256, - DSASignatureFormat.Rfc3279DerSequence), - "Certificate public key verifies CRL"); - } + VerifySignature(certKind, cert, tbs.Span, signature, hashAlg); VerifyCrlFields( crl, @@ -1430,17 +1523,40 @@ public static void LoadAndResignPublicCrl() } private static void BuildCertificateAndRun( + CertKind certKind, IEnumerable extensions, - Action action, + Action action, bool addSubjectKeyIdentifier = true, [CallerMemberName] string callerName = null) { - using (ECDsa key = ECDsa.Create()) + string subjectName = $"CN=\"{callerName}\""; + CertificateRequest req; + IDisposable key = null; + + try { - CertificateRequest req = new CertificateRequest( - $"CN=\"{callerName}\"", - key, - HashAlgorithmName.SHA384); + if (certKind == CertKind.ECDsa) + { + ECDsa ecdsa = ECDsa.Create(); + key = ecdsa; + req = new CertificateRequest(subjectName, ecdsa, HashAlgorithmName.SHA384); + } + else if (certKind == CertKind.RsaPkcs1 || certKind == CertKind.RsaPss) + { + RSA rsa = RSA.Create(TestData.RsaBigExponentParams); + key = rsa; + req = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA384, GetRsaPadding(certKind)); + } + else if (certKind == CertKind.MLDsa) + { + MLDsa mldsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44); + key = mldsa; + req = new CertificateRequest(subjectName, mldsa); + } + else + { + throw new NotSupportedException($"Unsupported CertKind: {certKind}"); + } if (addSubjectKeyIdentifier) { @@ -1456,42 +1572,41 @@ private static void BuildCertificateAndRun( using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMonths(-1), now.AddMonths(1))) { - action(cert, now); + action(certKind, cert, now); } } + finally + { + key?.Dispose(); + } } - private static void BuildRsaCertificateAndRun( + private static void BuildCertificateAndRun( IEnumerable extensions, Action action, bool addSubjectKeyIdentifier = true, [CallerMemberName] string callerName = null) { - using (RSA key = RSA.Create(TestData.RsaBigExponentParams)) - { - CertificateRequest req = new CertificateRequest( - $"CN=\"{callerName}\"", - key, - HashAlgorithmName.SHA384, - RSASignaturePadding.Pkcs1); - - if (addSubjectKeyIdentifier) - { - req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); - } - - foreach (X509Extension ext in extensions) - { - req.CertificateExtensions.Add(ext); - } - - DateTimeOffset now = DateTimeOffset.UtcNow; + BuildCertificateAndRun( + CertKind.ECDsa, + extensions, + (certKind, cert, now) => action(cert, now), + addSubjectKeyIdentifier, + callerName); + } - using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMonths(-1), now.AddMonths(1))) - { - action(cert, now); - } - } + private static void BuildRsaCertificateAndRun( + IEnumerable extensions, + Action action, + bool addSubjectKeyIdentifier = true, + [CallerMemberName] string callerName = null) + { + BuildCertificateAndRun( + CertKind.RsaPkcs1, + extensions, + (certKind, cert, now) => action(cert, now), + addSubjectKeyIdentifier, + callerName); } private static void VerifyCrlFields( @@ -1579,6 +1694,90 @@ private static DateTimeOffset ReadX509Time(AsnReader reader) return reader.ReadGeneralizedTime(); } + private static X509SignatureGenerator GetSignatureGenerator( + CertKind certKind, + X509Certificate2 cert, + out IDisposable key) + { + if (certKind == CertKind.RsaPkcs1 || certKind == CertKind.RsaPss) + { + RSA rsa = cert.GetRSAPrivateKey(); + key = rsa; + return X509SignatureGenerator.CreateForRSA(rsa, GetRsaPadding(certKind)); + } + else if (certKind == CertKind.ECDsa) + { + ECDsa ecdsa = cert.GetECDsaPrivateKey(); + key = ecdsa; + return X509SignatureGenerator.CreateForECDsa(ecdsa); + } + else if (certKind == CertKind.MLDsa) + { + MLDsa mldsa = cert.GetMLDsaPrivateKey(); + key = mldsa; + return X509SignatureGenerator.CreateForMLDsa(mldsa); + } + else + { + throw new NotSupportedException($"Unsupported CertKind: {certKind}"); + } + } + + private static void VerifySignature( + CertKind certKind, + X509Certificate2 cert, + ReadOnlySpan data, + ReadOnlySpan signature, + HashAlgorithmName hashAlgorithm) + { + bool signatureValid; + + if (certKind == CertKind.RsaPkcs1 || certKind == CertKind.RsaPss) + { + using RSA rsa = cert.GetRSAPublicKey(); + signatureValid = rsa.VerifyData(data, signature, hashAlgorithm, GetRsaPadding(certKind)); + } + else if (certKind == CertKind.ECDsa) + { + using ECDsa ecdsa = cert.GetECDsaPublicKey(); + signatureValid = ecdsa.VerifyData(data, signature, hashAlgorithm, DSASignatureFormat.Rfc3279DerSequence); + } + else if (certKind == CertKind.MLDsa) + { + using MLDsa mldsa = cert.GetMLDsaPublicKey(); + signatureValid = mldsa.VerifyData(data, signature); + } + else + { + throw new NotSupportedException($"Unsupported CertKind: {certKind}"); + } + + if (!signatureValid) + { + Assert.Fail($"{certKind} signature validation failed when it should have succeeded."); + } + } + + private static bool RequiresHashAlgorithm(CertKind certKind) + { + return certKind switch + { + CertKind.ECDsa or CertKind.RsaPkcs1 or CertKind.RsaPss => true, + CertKind.MLDsa => false, + _ => throw new NotSupportedException(certKind.ToString()) + }; + } + + private static RSASignaturePadding GetRsaPadding(CertKind certKind) + { + return certKind switch + { + CertKind.RsaPkcs1 => RSASignaturePadding.Pkcs1, + CertKind.RsaPss => RSASignaturePadding.Pss, + _ => null, + }; + } + private static ReadOnlySpan BuildEmptyExpectedCrl => new byte[] { 0x30, 0x82, 0x01, 0x8E, 0x30, 0x78, 0x02, 0x01, 0x01, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs index 79dca40a0a8c6d..b3c53d54016ded 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using Test.Cryptography; using Xunit; @@ -550,5 +551,276 @@ public static void ThirdPartyProvider_ECDsa() Assert.True(ecdsaOther.VerifyData(data, signature, hashAlgorithm)); } } + + [Fact] + public static void CheckCopyWithPrivateKey_RSA() + { + using (X509Certificate2 withKey = X509CertificateLoader.LoadPkcs12(TestData.PfxData, TestData.PfxDataPassword)) + using (X509Certificate2 pubOnly = X509CertificateLoader.LoadCertificate(withKey.RawDataMemory.Span)) + using (RSA privKey = withKey.GetRSAPrivateKey()) + using (X509Certificate2 wrongAlg = X509Certificate2.CreateFromPem(TestData.EcDhCertificate)) + { + CheckCopyWithPrivateKey( + pubOnly, + wrongAlg, + privKey, + [ + () => RSA.Create(2048), + () => RSA.Create(4096) + ], + RSACertificateExtensions.CopyWithPrivateKey, + RSACertificateExtensions.GetRSAPublicKey, + RSACertificateExtensions.GetRSAPrivateKey, + (priv, pub) => + { + byte[] data = new byte[RandomNumberGenerator.GetInt32(97)]; + RandomNumberGenerator.Fill(data); + + byte[] signature = priv.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + Assert.True(pub.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); + }); + } + } + + [Fact] + public static void CheckCopyWithPrivateKey_DSA() + { + using (X509Certificate2 withKey = X509CertificateLoader.LoadPkcs12(TestData.Dsa1024Pfx, TestData.Dsa1024PfxPassword)) + using (X509Certificate2 pubOnly = X509CertificateLoader.LoadCertificate(withKey.RawDataMemory.Span)) + using (DSA privKey = withKey.GetDSAPrivateKey()) + using (X509Certificate2 wrongAlg = X509Certificate2.CreateFromPem(TestData.EcDhCertificate)) + { + CheckCopyWithPrivateKey( + pubOnly, + wrongAlg, + privKey, + [ + () => + { + DSA dsa = DSA.Create(); + dsa.ImportParameters(TestData.GetDSA1024Params()); + return dsa; + }, + () => + { + DSA dsa = DSA.Create(); + + if (Dsa.Tests.DSASignVerify.SupportsFips186_3) + { + dsa.ImportParameters(Dsa.Tests.DSATestData.GetDSA2048Params()); + } + else + { + dsa.ImportParameters(TestData.GetDSA1024Params()); + } + + return dsa; + } + ], + DSACertificateExtensions.CopyWithPrivateKey, + DSACertificateExtensions.GetDSAPublicKey, + DSACertificateExtensions.GetDSAPrivateKey, + (priv, pub) => + { + byte[] data = new byte[RandomNumberGenerator.GetInt32(97)]; + RandomNumberGenerator.Fill(data); + + byte[] signature = priv.SignData(data, HashAlgorithmName.SHA1); + Assert.True(pub.VerifyData(data, signature, HashAlgorithmName.SHA1)); + }); + } + } + + [Fact] + public static void CheckCopyWithPrivateKey_ECDSA() + { + // A plain "ecPublicKey" cert can be either ECDSA or ECDH, but EcDhCertificate has a KeyUsage that + // says it is not suitable for being ECDSA. + // that stop them from being interchangeable, making them a much better test case than (e.g.) RSA + using (X509Certificate2 pubOnly = X509Certificate2.CreateFromPem(TestData.ECDsaCertificate)) + using (ECDsa privKey = ECDsa.Create()) + using (X509Certificate2 wrongAlg = X509Certificate2.CreateFromPem(TestData.EcDhCertificate)) + { + privKey.ImportFromPem(TestData.ECDsaECPrivateKey); + + CheckCopyWithPrivateKey( + pubOnly, + wrongAlg, + privKey, + [ + () => ECDsa.Create(ECCurve.NamedCurves.nistP256), + () => ECDsa.Create(ECCurve.NamedCurves.nistP384), + () => ECDsa.Create(ECCurve.NamedCurves.nistP521), + ], + ECDsaCertificateExtensions.CopyWithPrivateKey, + ECDsaCertificateExtensions.GetECDsaPublicKey, + ECDsaCertificateExtensions.GetECDsaPrivateKey, + (priv, pub) => + { + byte[] data = new byte[RandomNumberGenerator.GetInt32(97)]; + RandomNumberGenerator.Fill(data); + + byte[] signature = priv.SignData(data, HashAlgorithmName.SHA256); + Assert.True(pub.VerifyData(data, signature, HashAlgorithmName.SHA256)); + }); + } + } + + [Fact] + public static void CheckCopyWithPrivateKey_ECDH() + { + // The ECDH methods don't reject certs that lack the KeyAgreement KU, so test EC-DH vs RSA. + using (X509Certificate2 pubOnly = X509Certificate2.CreateFromPem(TestData.EcDhCertificate)) + using (ECDiffieHellman privKey = ECDiffieHellman.Create()) + using (X509Certificate2 wrongAlg = X509CertificateLoader.LoadCertificate(TestData.CertWithEnhancedKeyUsage)) + { + privKey.ImportFromPem(TestData.EcDhPkcs8Key); + + CheckCopyWithPrivateKey( + pubOnly, + wrongAlg, + privKey, + [ + () => ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256), + () => ECDiffieHellman.Create(ECCurve.NamedCurves.nistP384), + () => ECDiffieHellman.Create(ECCurve.NamedCurves.nistP521), + ], + (cert, ecdh) => cert.CopyWithPrivateKey(ecdh), + cert => cert.GetECDiffieHellmanPublicKey(), + cert => cert.GetECDiffieHellmanPrivateKey(), + (priv, pub) => + { + ECParameters ecParams = pub.ExportParameters(false); + + using (ECDiffieHellman other = ECDiffieHellman.Create(ecParams.Curve)) + using (ECDiffieHellmanPublicKey otherPub = other.PublicKey) + using (ECDiffieHellmanPublicKey usPub = pub.PublicKey) + { + byte[] otherToUs = other.DeriveKeyFromHash(usPub, HashAlgorithmName.SHA256); + byte[] usToOther = priv.DeriveKeyFromHash(otherPub, HashAlgorithmName.SHA256); + + AssertExtensions.SequenceEqual(otherToUs, usToOther); + } + }); + } + } + + [ConditionalFact(typeof(MLDsa), nameof(MLDsa.IsSupported))] + public static void CheckCopyWithPrivateKey_MLDSA() + { + using (MLDsa privKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65)) + { + CertificateRequest req = new CertificateRequest($"CN={nameof(CheckCopyWithPrivateKey_MLDSA)}", privKey); + DateTimeOffset now = DateTimeOffset.UtcNow; + + X509Certificate2 pubOnly = req.Create( + req.SubjectName, + X509SignatureGenerator.CreateForMLDsa(privKey), + now.AddMinutes(-10), + now.AddMinutes(10), + new byte[] { 2, 4, 6, 8, 9, 7, 5, 3, 1 }); + + using (pubOnly) + using (X509Certificate2 wrongAlg = X509CertificateLoader.LoadCertificate(TestData.CertWithEnhancedKeyUsage)) + { + CheckCopyWithPrivateKey( + pubOnly, + wrongAlg, + privKey, + [ + () => MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44), + () => MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65), + () => MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87), + ], + (cert, key) => cert.CopyWithPrivateKey(key), + cert => cert.GetMLDsaPublicKey(), + cert => cert.GetMLDsaPrivateKey(), + (priv, pub) => + { + byte[] data = new byte[RandomNumberGenerator.GetInt32(97)]; + RandomNumberGenerator.Fill(data); + + byte[] signature = new byte[pub.Algorithm.SignatureSizeInBytes]; + int written = priv.SignData(data, signature); + Assert.Equal(signature.Length, written); + Assert.True(pub.VerifyData(data, signature)); + }); + } + } + } + + private static void CheckCopyWithPrivateKey( + X509Certificate2 cert, + X509Certificate2 wrongAlgorithmCert, + TKey correctPrivateKey, + IEnumerable> incorrectKeys, + Func copyWithPrivateKey, + Func getPublicKey, + Func getPrivateKey, + Action keyProver) + where TKey : class, IDisposable + { + Exception e = Assert.Throws( + null, + () => copyWithPrivateKey(wrongAlgorithmCert, correctPrivateKey)); + + Assert.Contains("algorithm", e.Message); + + List generatedKeys = new(); + + foreach (Func func in incorrectKeys) + { + TKey incorrectKey = func(); + generatedKeys.Add(incorrectKey); + + e = Assert.Throws( + "privateKey", + () => copyWithPrivateKey(cert, incorrectKey)); + + Assert.Contains("does not match", e.Message); + Assert.DoesNotContain("algorithm", e.Message); + } + + using (X509Certificate2 withKey = copyWithPrivateKey(cert, correctPrivateKey)) + { + e = Assert.Throws(() => copyWithPrivateKey(withKey, correctPrivateKey)); + + Assert.Contains("already has", e.Message); + + foreach (TKey incorrectKey in generatedKeys) + { + e = Assert.Throws(() => copyWithPrivateKey(withKey, incorrectKey)); + + Assert.Contains("already has", e.Message); + } + + using (TKey pub = getPublicKey(withKey)) + using (TKey pub2 = getPublicKey(withKey)) + using (TKey pubOnly = getPublicKey(cert)) + using (TKey priv = getPrivateKey(withKey)) + using (TKey priv2 = getPrivateKey(withKey)) + { + Assert.NotSame(pub, pub2); + Assert.NotSame(pub, pubOnly); + Assert.NotSame(pub2, pubOnly); + Assert.NotSame(priv, priv2); + + keyProver(priv, pub2); + keyProver(priv2, pub); + keyProver(priv, pubOnly); + + priv.Dispose(); + pub2.Dispose(); + + keyProver(priv2, pub); + keyProver(priv2, pubOnly); + } + } + + foreach (TKey incorrectKey in generatedKeys) + { + incorrectKey.Dispose(); + } + } } } From 124c3281c091dd6ab92bbe68c140b2c469c1d880 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Tue, 8 Apr 2025 09:22:34 -0700 Subject: [PATCH 2/2] Make CertificateAuthority build in other projects, too --- .../X509Certificates/CertificateAuthority.cs | 20 ++++++++++++------- .../CertificateRequestChainTests.cs | 12 ++--------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index 84e6fbb3f73a64..e414f2ec0f4cfb 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -7,6 +7,9 @@ using System.Runtime.CompilerServices; using Xunit; +// PQC types are used throughout, but only when the caller requests them. +#pragma warning disable SYSLIB5006 + namespace System.Security.Cryptography.X509Certificates.Tests.Common { // This class represents only a portion of what is required to be a proper Certificate Authority. @@ -1028,14 +1031,17 @@ private static HashAlgorithmName HashAlgorithmIfNeeded(string publicKeyOid) }; } - internal void Diag() + internal static X509Certificate2 CloneWithPrivateKey(X509Certificate2 cert, object key) { - Console.WriteLine(_cert.ExportCertificatePem()); - - using (ECDsa ecdsa = _cert.GetECDsaPrivateKey()) + return key switch { - Console.WriteLine(ecdsa.ExportPkcs8PrivateKeyPem()); - } + RSA rsa => cert.CopyWithPrivateKey(rsa), + ECDsa ecdsa => cert.CopyWithPrivateKey(ecdsa), + MLDsa mldsa => cert.CopyWithPrivateKey(mldsa), + DSA dsa => cert.CopyWithPrivateKey(dsa), + _ => throw new InvalidOperationException( + $"Had no handler for key of type {key?.GetType().FullName ?? "null"}") + }; } internal sealed class KeyFactory @@ -1127,7 +1133,7 @@ internal PublicKey ToPublicKey() internal X509Certificate2 OntoCertificate(X509Certificate2 cert) { - return CertificateCreation.CertificateRequestChainTests.CloneWithPrivateKey(cert, _key); + return CloneWithPrivateKey(cert, _key); } internal byte[] Sign(byte[] data) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs index 59f890284f8e2c..7f65777898dbda 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/CertificateRequestChainTests.cs @@ -342,17 +342,9 @@ private static void DisposeChainCerts(X509Chain chain) } } - internal static X509Certificate2 CloneWithPrivateKey(X509Certificate2 cert, object key) + private static X509Certificate2 CloneWithPrivateKey(X509Certificate2 cert, object key) { - return key switch - { - RSA rsa => cert.CopyWithPrivateKey(rsa), - ECDsa ecdsa => cert.CopyWithPrivateKey(ecdsa), - MLDsa mldsa => cert.CopyWithPrivateKey(mldsa), - DSA dsa => cert.CopyWithPrivateKey(dsa), - _ => throw new InvalidOperationException( - $"Had no handler for key of type {key?.GetType().FullName ?? "null"}") - }; + return Common.CertificateAuthority.CloneWithPrivateKey(cert, key); } private static void CreateAndTestChain(