From 3116350c222d35f25659a3013738636bd220a260 Mon Sep 17 00:00:00 2001 From: Pranav Senthilnathan Date: Sat, 19 Sep 2026 16:38:02 -0700 Subject: [PATCH] Add Composite ML-KEM certificate key accessors (#134259) Add cert accessor stubs like we did for Composite ML-DSA. I've omitted PublicKey changes but am going to mark the Composite ML-KEM API as implemented anyway (we can add it in the future if we need it): Fixes #129633 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e308cd1-67b1-4bd6-a16d-2cada6f92de8 --- .../X509CertificateKeyAccessors.cs | 148 ++++++++++++++++++ .../ref/System.Security.Cryptography.cs | 6 + .../X509Certificates/X509Certificate2.cs | 102 ++++++++++++ 3 files changed, 256 insertions(+) diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateKeyAccessors.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateKeyAccessors.cs index f660dbd297516f..8bfa3c92a1ca1d 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateKeyAccessors.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateKeyAccessors.cs @@ -537,6 +537,154 @@ public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certific #endif } +#if NET11_0_OR_GREATER || NETFRAMEWORK + /// + /// Gets the public key from this certificate. + /// + /// + /// The X.509 certificate that contains the public key. + /// + /// + /// The public key, or if this certificate does not have a Composite ML-KEM public key. + /// + /// + /// is . + /// + /// + /// The certificate has a Composite ML-KEM public key, but the platform does not support Composite ML-KEM. + /// + /// + /// The public key was invalid, or otherwise could not be imported. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public static CompositeMLKem? GetCompositeMLKemPublicKey(this X509Certificate2 certificate) + { + ArgumentNullException.ThrowIfNull(certificate); + +#if NET11_0_OR_GREATER + return certificate.GetCompositeMLKemPublicKey(); +#else + if (CompositeMLKemAlgorithm.GetAlgorithmFromOid(certificate.GetKeyAlgorithm()) is null) + { + return null; + } + + ArraySegment encoded = GetCertificateSubjectPublicKeyInfo(certificate); + + try + { + return CompositeMLKem.ImportSubjectPublicKeyInfo(encoded); + } + finally + { + // SubjectPublicKeyInfo does not need to clear since it's public + CryptoPool.Return(encoded, clearSize: 0); + } +#endif + } + + /// + /// Gets the private key from this certificate. + /// + /// + /// The X.509 certificate that contains the private key. + /// + /// + /// The private key, or if this certificate does not have a Composite ML-KEM private key. + /// + /// + /// is . + /// + /// + /// Retrieving a Composite ML-KEM private key from a certificate is not supported on this platform. + /// + /// + /// An error occurred accessing the private key. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public static CompositeMLKem? GetCompositeMLKemPrivateKey(this X509Certificate2 certificate) + { + ArgumentNullException.ThrowIfNull(certificate); + +#if NET11_0_OR_GREATER + return certificate.GetCompositeMLKemPrivateKey(); +#else + if (CompositeMLKemAlgorithm.GetAlgorithmFromOid(certificate.GetKeyAlgorithm()) is null) + { + return null; + } + + throw new PlatformNotSupportedException(); +#endif + } + + /// + /// Combines a private key with a certificate containing the associated public key into a + /// new instance that can access the private key. + /// + /// + /// The X.509 certificate that contains the public key. + /// + /// + /// The Composite ML-KEM private key that corresponds to the Composite ML-KEM public key in this certificate. + /// + /// + /// A new certificate with the property set to . + /// The current certificate isn't modified. + /// + /// + /// or is . + /// + /// + /// The specified private key doesn't match the public key for this certificate. + /// + /// + /// The certificate already has an associated private key. + /// + /// + /// Combining a certificate and a Composite ML-KEM private key is not supported on this platform. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certificate, CompositeMLKem privateKey) + { + ArgumentNullException.ThrowIfNull(certificate); + ArgumentNullException.ThrowIfNull(privateKey); + +#if NET11_0_OR_GREATER + return certificate.CopyWithPrivateKey(privateKey); +#else + if (!Helpers.IsOSPlatformWindows) + throw new PlatformNotSupportedException(); + + if (certificate.HasPrivateKey) + throw new InvalidOperationException(SR.Cryptography_Cert_AlreadyHasPrivateKey); + + using (CompositeMLKem? publicKey = GetCompositeMLKemPublicKey(certificate)) + { + 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 = publicKey.ExportEncapsulationKey(); + byte[] pk2 = privateKey.ExportEncapsulationKey(); + + if (!pk1.SequenceEqual(pk2)) + { + throw new ArgumentException(SR.Cryptography_PrivateKey_DoesNotMatch, nameof(privateKey)); + } + } + + throw new PlatformNotSupportedException(); +#endif + } +#endif + #if !NET private static ArraySegment GetCertificateSubjectPublicKeyInfo(X509Certificate2 certificate) { 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 f3f65c90b89398..a0de85693058aa 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -4099,6 +4099,8 @@ public X509Certificate2(string fileName, string? password, System.Security.Crypt public int Version { get { throw null; } } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.CompositeMLDsa privateKey) { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.CompositeMLKem privateKey) { throw null; } public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.ECDiffieHellman privateKey) { throw null; } public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.MLDsa privateKey) { throw null; } public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.MLKem privateKey) { throw null; } @@ -4125,6 +4127,10 @@ public X509Certificate2(string fileName, string? password, System.Security.Crypt public System.Security.Cryptography.CompositeMLDsa? GetCompositeMLDsaPrivateKey() { throw null; } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public System.Security.Cryptography.CompositeMLDsa? GetCompositeMLDsaPublicKey() { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public System.Security.Cryptography.CompositeMLKem? GetCompositeMLKemPrivateKey() { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] + public System.Security.Cryptography.CompositeMLKem? GetCompositeMLKemPublicKey() { throw null; } public System.Security.Cryptography.ECDiffieHellman? GetECDiffieHellmanPrivateKey() { throw null; } public System.Security.Cryptography.ECDiffieHellman? GetECDiffieHellmanPublicKey() { throw null; } public System.Security.Cryptography.MLDsa? GetMLDsaPrivateKey() { throw null; } 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 74a408ce2461b5..0c4a6d54110c96 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 @@ -1182,6 +1182,108 @@ public X509Certificate2 CopyWithPrivateKey(CompositeMLDsa privateKey) throw new PlatformNotSupportedException(); } + /// + /// Gets the public key from this certificate. + /// + /// + /// The public key, or if this certificate does not have a Composite ML-KEM public key. + /// + /// + /// The certificate has a Composite ML-KEM public key, but the platform does not support Composite ML-KEM. + /// + /// + /// The public key was invalid, or otherwise could not be imported. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public CompositeMLKem? GetCompositeMLKemPublicKey() + { + if (CompositeMLKemAlgorithm.GetAlgorithmFromOid(GetKeyAlgorithm()) is null) + { + return null; + } + + Debug.Assert(!OperatingSystem.IsBrowser()); + return PublicKey.EncodeSubjectPublicKeyInfo().Encode(CompositeMLKem.ImportSubjectPublicKeyInfo); + } + + /// + /// Gets the private key from this certificate. + /// + /// + /// The private key, or if this certificate does not have a Composite ML-KEM private key. + /// + /// + /// Retrieving a Composite ML-KEM private key from a certificate is not supported on this platform. + /// + /// + /// An error occurred accessing the private key. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public CompositeMLKem? GetCompositeMLKemPrivateKey() + { + if (CompositeMLKemAlgorithm.GetAlgorithmFromOid(GetKeyAlgorithm()) is null) + { + return null; + } + + throw new PlatformNotSupportedException(); + } + + /// + /// Combines a private key with a certificate containing the associated public key into a + /// new instance that can access the private key. + /// + /// + /// The Composite ML-KEM private key that corresponds to the Composite ML-KEM 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. + /// + /// + /// Combining a certificate and a Composite ML-KEM private key is not supported on this platform. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)] + public X509Certificate2 CopyWithPrivateKey(CompositeMLKem privateKey) + { + ArgumentNullException.ThrowIfNull(privateKey); + + if (HasPrivateKey) + throw new InvalidOperationException(SR.Cryptography_Cert_AlreadyHasPrivateKey); + + using (CompositeMLKem? publicKey = GetCompositeMLKemPublicKey()) + { + 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 = publicKey.ExportEncapsulationKey(); + byte[] pk2 = privateKey.ExportEncapsulationKey(); + + if (!pk1.SequenceEqual(pk2)) + { + throw new ArgumentException(SR.Cryptography_PrivateKey_DoesNotMatch, nameof(privateKey)); + } + } + + throw new PlatformNotSupportedException(); + } + /// /// Creates a new X509 certificate from the file contents of an RFC 7468 PEM-encoded /// certificate and private key.