File: System\Security\Cryptography\X509Certificates\X509CertificateKeyAccessors.cs
Web Access
Project: src\src\libraries\Microsoft.Bcl.Cryptography\src\Microsoft.Bcl.Cryptography.csproj (Microsoft.Bcl.Cryptography)
// 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;
 
#if !NET10_0_OR_GREATER
using System.Formats.Asn1;
using System.Security.Cryptography.Asn1;
#endif
 
#if !NET10_0_OR_GREATER && !NETSTANDARD
using System.Diagnostics;
using Internal.Cryptography;
#endif
 
namespace System.Security.Cryptography.X509Certificates
{
    /// <summary>
    /// Helper methods to access keys on <see cref="X509Certificate2"/>.
    /// </summary>
    public static class X509CertificateKeyAccessors
    {
        /// <summary>
        ///   Gets the <see cref="MLKem"/> public key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the public key.
        /// </param>
        /// <returns>
        ///   The public key, or <see langword="null"/> if this certificate does not have an ML-KEM public key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   The certificate has an ML-KEM public key, but the platform does not support ML-KEM.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   The public key was invalid, or otherwise could not be imported.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static MLKem? GetMLKemPublicKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetMLKemPublicKey();
#else
            if (MLKemAlgorithm.FromOid(certificate.GetKeyAlgorithm()) is null)
            {
                return null;
            }
 
            ArraySegment<byte> encoded = GetCertificateSubjectPublicKeyInfo(certificate);
 
            try
            {
                return MLKem.ImportSubjectPublicKeyInfo(encoded);
            }
            finally
            {
                // SubjectPublicKeyInfo does not need to clear since it's public
                CryptoPool.Return(encoded, clearSize: 0);
            }
#endif
        }
 
        /// <summary>
        ///   Gets the <see cref="MLKem"/> private key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the private key.
        /// </param>
        /// <returns>
        ///   The private key, or <see langword="null"/> if this certificate does not have an ML-KEM private key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Retrieving an ML-KEM private key from a certificate is not supported on this platform.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   An error occurred accessing the private key.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static MLKem? GetMLKemPrivateKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetMLKemPrivateKey();
#else
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLKem)));
#endif
        }
 
        /// <summary>
        ///   Combines a private key with a certificate containing the associated public key into a
        ///   new instance that can access the private key.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the public key.
        /// </param>
        /// <param name="privateKey">
        ///   The ML-KEM private key that corresponds to the ML-KEM public key in this certificate.
        /// </param>
        /// <returns>
        ///   A new certificate with the <see cref="X509Certificate2.HasPrivateKey" /> property set to <see langword="true"/>.
        ///   The current certificate isn't modified.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> or <paramref name="privateKey"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   The specified private key doesn't match the public key for this certificate.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   The certificate already has an associated private key.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Combining a certificate and an ML-KEM private key is not supported on this platform.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certificate, MLKem privateKey)
        {
            ArgumentNullException.ThrowIfNull(certificate);
            ArgumentNullException.ThrowIfNull(privateKey);
 
#if NET10_0_OR_GREATER
            return certificate.CopyWithPrivateKey(privateKey);
#else
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLKem)));
#endif
        }
 
        /// <summary>
        ///   Gets the <see cref="MLDsa"/> public key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the public key.
        /// </param>
        /// <returns>
        ///   The public key, or <see langword="null"/> if this certificate does not have an ML-DSA public key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   The certificate has an ML-DSA public key, but the platform does not support ML-DSA.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   The public key was invalid, or otherwise could not be imported.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static MLDsa? GetMLDsaPublicKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetMLDsaPublicKey();
#else
            if (MLDsaAlgorithm.GetMLDsaAlgorithmFromOid(certificate.GetKeyAlgorithm()) is null)
            {
                return null;
            }
 
            ArraySegment<byte> encoded = GetCertificateSubjectPublicKeyInfo(certificate);
 
            try
            {
                return MLDsa.ImportSubjectPublicKeyInfo(encoded);
            }
            finally
            {
                // SubjectPublicKeyInfo does not need to clear since it's public
                CryptoPool.Return(encoded, clearSize: 0);
            }
#endif
        }
 
        /// <summary>
        ///   Gets the <see cref="MLDsa"/> private key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the private key.
        /// </param>
        /// <returns>
        ///   The private key, or <see langword="null"/> if this certificate does not have an ML-DSA private key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///  <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Retrieving an ML-DSA private key from a certificate is not supported on this platform.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   An error occurred accessing the private key.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static MLDsa? GetMLDsaPrivateKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetMLDsaPrivateKey();
#elif NETSTANDARD
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa)));
#else
            if (!Helpers.IsOSPlatformWindows)
                throw new PlatformNotSupportedException();
 
            return CertificateHelpers.GetPrivateKey<MLDsa>(
                certificate,
                _ =>
                {
                    Debug.Fail("CryptoApi does not support ML-DSA.");
                    throw new PlatformNotSupportedException();
                },
                cngKey => new MLDsaCng(cngKey, transferOwnership: true));
#endif
        }
 
        /// <summary>
        ///   Combines a private key with a certificate containing the associated public key into a
        ///   new instance that can access the private key.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the public key.
        /// </param>
        /// <param name="privateKey">
        ///   The ML-DSA private key that corresponds to the ML-DSA public key in this certificate.
        /// </param>
        /// <returns>
        ///   A new certificate with the <see cref="X509Certificate2.HasPrivateKey" /> property set to <see langword="true"/>.
        ///   The current certificate isn't modified.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> or <paramref name="privateKey"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   The specified private key doesn't match the public key for this certificate.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   The certificate already has an associated private key.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Combining a certificate and an ML-DSA private key is not supported on this platform.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certificate, MLDsa privateKey)
        {
            ArgumentNullException.ThrowIfNull(certificate);
            ArgumentNullException.ThrowIfNull(privateKey);
 
#if NET10_0_OR_GREATER
            return certificate.CopyWithPrivateKey(privateKey);
#elif NETSTANDARD
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa)));
#else
            if (!Helpers.IsOSPlatformWindows)
                throw new PlatformNotSupportedException();
 
            if (certificate.HasPrivateKey)
                throw new InvalidOperationException(SR.Cryptography_Cert_AlreadyHasPrivateKey);
 
            using (MLDsa? publicKey = GetMLDsaPublicKey(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));
                }
 
                using (CryptoPoolLease pk1 = CryptoPoolLease.Rent(publicKey.Algorithm.PublicKeySizeInBytes, skipClear: true))
                using (CryptoPoolLease pk2 = CryptoPoolLease.Rent(publicKey.Algorithm.PublicKeySizeInBytes, skipClear: true))
                {
                    publicKey.ExportMLDsaPublicKey(pk1.Span);
                    privateKey.ExportMLDsaPublicKey(pk2.Span);
 
                    if (!pk1.Span.SequenceEqual(pk2.Span))
                    {
                        throw new ArgumentException(SR.Cryptography_PrivateKey_DoesNotMatch, nameof(privateKey));
                    }
                }
            }
 
            return CertificateHelpers.CopyWithPrivateKey(certificate, privateKey);
#endif
        }
 
        /// <summary>
        ///   Gets the <see cref="SlhDsa"/> public key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X509 certificate that contains the public key.
        /// </param>
        /// <returns>
        ///   The public key, or <see langword="null"/> if this certificate does not have an SLH-DSA public key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   The certificate has an SLH-DSA public key, but the platform does not support SLH-DSA.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   The public key was invalid, or otherwise could not be imported.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static SlhDsa? GetSlhDsaPublicKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetSlhDsaPublicKey();
#else
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(SlhDsa)));
#endif
        }
 
        /// <summary>
        ///   Gets the <see cref="SlhDsa"/> private key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X509 certificate that contains the private key.
        /// </param>
        /// <returns>
        ///   The private key, or <see langword="null"/> if this certificate does not have an SLH-DSA private key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Retrieving an SLH-DSA private key from a certificate is not supported on this platform.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   An error occurred accessing the private key.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static SlhDsa? GetSlhDsaPrivateKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetSlhDsaPrivateKey();
#else
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(SlhDsa)));
#endif
        }
 
        /// <summary>
        ///   Combines a private key with a certificate containing the associated public key into a
        ///   new instance that can access the private key.
        /// </summary>
        /// <param name="certificate">
        ///   The X509 certificate that contains the public key.
        /// </param>
        /// <param name="privateKey">
        ///   The SLH-DSA private key that corresponds to the SLH-DSA public key in this certificate.
        /// </param>
        /// <returns>
        ///   A new certificate with the <see cref="X509Certificate2.HasPrivateKey" /> property set to <see langword="true"/>.
        ///   The current certificate isn't modified.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> or <paramref name="privateKey"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   The specified private key doesn't match the public key for this certificate.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   The certificate already has an associated private key.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Combining a certificate and an SLH-DSA private key is not supported on this platform.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certificate, SlhDsa privateKey)
        {
            ArgumentNullException.ThrowIfNull(certificate);
            ArgumentNullException.ThrowIfNull(privateKey);
 
#if NET10_0_OR_GREATER
            return certificate.CopyWithPrivateKey(privateKey);
#else
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(SlhDsa)));
#endif
        }
 
        /// <summary>
        ///   Gets the <see cref="CompositeMLDsa"/> public key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the public key.
        /// </param>
        /// <returns>
        ///   The public key, or <see langword="null"/> if this certificate does not have a Composite ML-DSA public key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   The certificate has a Composite ML-DSA public key, but the platform does not support Composite ML-DSA.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   The public key was invalid, or otherwise could not be imported.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static CompositeMLDsa? GetCompositeMLDsaPublicKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetCompositeMLDsaPublicKey();
#else
            if (CompositeMLDsaAlgorithm.GetAlgorithmFromOid(certificate.GetKeyAlgorithm()) is null)
            {
                return null;
            }
 
            ArraySegment<byte> encoded = GetCertificateSubjectPublicKeyInfo(certificate);
 
            try
            {
                return CompositeMLDsa.ImportSubjectPublicKeyInfo(encoded);
            }
            finally
            {
                // SubjectPublicKeyInfo does not need to clear since it's public
                CryptoPool.Return(encoded, clearSize: 0);
            }
#endif
        }
 
        /// <summary>
        ///   Gets the <see cref="CompositeMLDsa"/> private key from this certificate.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the private key.
        /// </param>
        /// <returns>
        ///   The private key, or <see langword="null"/> if this certificate does not have a Composite ML-DSA private key.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///  <paramref name="certificate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Retrieving a Composite ML-DSA private key from a certificate is not supported on this platform.
        /// </exception>
        /// <exception cref="CryptographicException">
        ///   An error occurred accessing the private key.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static CompositeMLDsa? GetCompositeMLDsaPrivateKey(this X509Certificate2 certificate)
        {
            ArgumentNullException.ThrowIfNull(certificate);
 
#if NET10_0_OR_GREATER
            return certificate.GetCompositeMLDsaPrivateKey();
#else
            if (CompositeMLDsaAlgorithm.GetAlgorithmFromOid(certificate.GetKeyAlgorithm()) is null)
            {
                return null;
            }
 
            throw new PlatformNotSupportedException();
#endif
        }
 
        /// <summary>
        ///   Combines a private key with a certificate containing the associated public key into a
        ///   new instance that can access the private key.
        /// </summary>
        /// <param name="certificate">
        ///   The X.509 certificate that contains the public key.
        /// </param>
        /// <param name="privateKey">
        ///   The Composite ML-DSA private key that corresponds to the Composite ML-DSA public key in this certificate.
        /// </param>
        /// <returns>
        ///   A new certificate with the <see cref="X509Certificate2.HasPrivateKey" /> property set to <see langword="true"/>.
        ///   The current certificate isn't modified.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="certificate"/> or <paramref name="privateKey"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   The specified private key doesn't match the public key for this certificate.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   The certificate already has an associated private key.
        /// </exception>
        /// <exception cref="PlatformNotSupportedException">
        ///   Combining a certificate and a Composite ML-DSA private key is not supported on this platform.
        /// </exception>
        [Experimental(Experimentals.PostQuantumCryptographyDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
        public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 certificate, CompositeMLDsa privateKey)
        {
            ArgumentNullException.ThrowIfNull(certificate);
            ArgumentNullException.ThrowIfNull(privateKey);
 
#if NET10_0_OR_GREATER
            return certificate.CopyWithPrivateKey(privateKey);
#elif NETSTANDARD
            throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(CompositeMLDsa)));
#else
            if (!Helpers.IsOSPlatformWindows)
                throw new PlatformNotSupportedException();
 
            if (certificate.HasPrivateKey)
                throw new InvalidOperationException(SR.Cryptography_Cert_AlreadyHasPrivateKey);
 
            using (CompositeMLDsa? publicKey = GetCompositeMLDsaPublicKey(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.ExportCompositeMLDsaPublicKey();
                byte[] pk2 = privateKey.ExportCompositeMLDsaPublicKey();
 
                if (!pk1.SequenceEqual(pk2))
                {
                    throw new ArgumentException(SR.Cryptography_PrivateKey_DoesNotMatch, nameof(privateKey));
                }
            }
 
            throw new PlatformNotSupportedException();
#endif
        }
 
#if !NET10_0_OR_GREATER
        private static ArraySegment<byte> GetCertificateSubjectPublicKeyInfo(X509Certificate2 certificate)
        {
            // We construct the SubjectPublicKeyInfo from the certificate as-is, parameters and all. Consumers
            // decide if the parameters are good or not.
            SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn
            {
                Algorithm = new AlgorithmIdentifierAsn
                {
                    Algorithm = certificate.GetKeyAlgorithm(),
 
                    // .NET Framework uses "empty" to indicate no value, not null, so normalize empty to null since
                    // the Asn types expect the parameters to be an ASN.1 ANY.
                    Parameters = certificate.GetKeyAlgorithmParameters() switch
                    {
                        null or { Length: 0 } => default(ReadOnlyMemory<byte>?),
                        byte[] array => array,
                    },
                },
                SubjectPublicKey = certificate.GetPublicKey(),
            };
 
            AsnWriter writer = new(AsnEncodingRules.DER);
            spki.Encode(writer);
 
            byte[] rented = CryptoPool.Rent(writer.GetEncodedLength());
            int written = writer.Encode(rented);
            return new ArraySegment<byte>(rented, offset: 0, count: written);
        }
#endif
    }
}