From 911a9f1463e1856bae8992f3c63809861eb93d11 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 15 Apr 2025 15:20:09 -0400 Subject: [PATCH 1/4] Work in progress for ML-KEM private keys --- .../MLKemImplementation.NotSupported.cs | 10 +-- .../Cryptography/MLKemImplementation.cs | 45 +++++++++++ .../ref/System.Security.Cryptography.cs | 4 + .../src/System.Security.Cryptography.csproj | 10 +++ .../MLKemImplementation.OpenSsl.cs | 12 +-- .../X509Certificates/AndroidCertificatePal.cs | 11 +++ .../X509Certificates/AppleCertificatePal.cs | 11 +++ .../CertificatePal.Windows.PrivateKey.cs | 11 +++ .../X509Certificates/ICertificatePal.cs | 2 + .../OpenSslX509CertificateReader.cs | 26 +++++++ .../X509Certificates/X509Certificate2.cs | 75 +++++++++++++++++++ .../PrivateKeyAssociationTests.cs | 29 +++++++ 12 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.cs diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.NotSupported.cs b/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.NotSupported.cs index db5b67f8b7d2f8..b2f444bf8da27a 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.NotSupported.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.NotSupported.cs @@ -5,7 +5,7 @@ namespace System.Security.Cryptography { - internal sealed class MLKemImplementation : MLKem + internal sealed partial class MLKemImplementation : MLKem { internal static new bool IsSupported => false; @@ -15,14 +15,14 @@ private MLKemImplementation(MLKemAlgorithm algorithm) : base(algorithm) throw new PlatformNotSupportedException(); } - internal static MLKem GenerateKeyImpl(MLKemAlgorithm algorithm) + internal static MLKemImplementation GenerateKeyImpl(MLKemAlgorithm algorithm) { _ = algorithm; Debug.Fail("Caller should have checked platform availability."); throw new PlatformNotSupportedException(); } - internal static MLKem ImportPrivateSeedImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) + internal static MLKemImplementation ImportPrivateSeedImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) { _ = algorithm; _ = source; @@ -30,7 +30,7 @@ internal static MLKem ImportPrivateSeedImpl(MLKemAlgorithm algorithm, ReadOnlySp throw new PlatformNotSupportedException(); } - internal static MLKem ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) + internal static MLKemImplementation ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) { _ = algorithm; _ = source; @@ -38,7 +38,7 @@ internal static MLKem ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadO throw new PlatformNotSupportedException(); } - internal static MLKem ImportEncapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) + internal static MLKemImplementation ImportEncapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) { _ = algorithm; _ = source; diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.cs b/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.cs new file mode 100644 index 00000000000000..3c3d5af5151aea --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.cs @@ -0,0 +1,45 @@ +// 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; + +namespace System.Security.Cryptography +{ + internal sealed partial class MLKemImplementation : MLKem + { + /// + /// Duplicates an ML-KEM private key by export/import. + /// Only intended to be used when the key type is unknown. + /// + internal static MLKemImplementation DuplicatePrivateKey(MLKem key) + { + // The implementation type and any platform types (e.g. MLKemOpenSsl) + // should inherently know how to clone themselves without the crudeness + // of export/import. + Debug.Assert(key is not (MLKemImplementation or MLKemOpenSsl)); + + MLKemAlgorithm alg = key.Algorithm; + byte[] rented = CryptoPool.Rent(alg.DecapsulationKeySizeInBytes); + int size = 0; + + try + { + size = alg.PrivateSeedSizeInBytes; + Span buffer = rented.AsSpan(0, size); + key.ExportPrivateSeed(buffer); + return ImportPrivateSeedImpl(alg, buffer); + } + catch (CryptographicException) + { + size = alg.DecapsulationKeySizeInBytes; + Span buffer = rented.AsSpan(0, size); + key.ExportDecapsulationKey(buffer); + return ImportDecapsulationKeyImpl(alg, buffer); + } + finally + { + CryptoPool.Return(rented, size); + } + } + } +} 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 4b85a927b186b1..852aabfc111c5c 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3520,6 +3520,8 @@ public X509Certificate2(string fileName, string? password, System.Security.Crypt 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.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey(System.Security.Cryptography.MLKem 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")] @@ -3544,6 +3546,8 @@ public X509Certificate2(string fileName, string? password, System.Security.Crypt [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] public System.Security.Cryptography.MLDsa? GetMLDsaPublicKey() { throw null; } [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] + public System.Security.Cryptography.MLKem? GetMLKemPrivateKey() { throw null; } + [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")] public System.Security.Cryptography.MLKem? GetMLKemPublicKey() { 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}")] 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 5df79ca79ecd78..5875585f0b4b8d 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -799,6 +799,8 @@ Link="Common\System\Security\Cryptography\SP800108HmacCounterKdfImplementationManaged.cs" /> + + + + + source) + internal static MLKemImplementation ImportPrivateSeedImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) { Debug.Assert(IsSupported); Debug.Assert(source.Length == algorithm.PrivateSeedSizeInBytes); @@ -44,7 +44,7 @@ internal static MLKem ImportPrivateSeedImpl(MLKemAlgorithm algorithm, ReadOnlySp return new MLKemImplementation(algorithm, key, hasSeed: true, hasDecapsulationKey: true); } - internal static MLKem ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) + internal static MLKemImplementation ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) { Debug.Assert(IsSupported); Debug.Assert(source.Length == algorithm.DecapsulationKeySizeInBytes); @@ -53,7 +53,7 @@ internal static MLKem ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadO return new MLKemImplementation(algorithm, key, hasSeed: false, hasDecapsulationKey: true); } - internal static MLKem ImportEncapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) + internal static MLKemImplementation ImportEncapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan source) { Debug.Assert(IsSupported); Debug.Assert(source.Length == algorithm.EncapsulationKeySizeInBytes); @@ -72,6 +72,8 @@ protected override void Dispose(bool disposing) } } + internal SafeEvpPKeyHandle DuplicateHandle() => _key.DuplicateHandle(); + protected override void DecapsulateCore(ReadOnlySpan ciphertext, Span sharedSecret) { Interop.Crypto.EvpKemDecapsulate(_key, ciphertext, sharedSecret); 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 5901274228a63c..d072e5071926b7 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 @@ -453,6 +453,12 @@ internal SafeKeyHandle? PrivateKeyHandle return null; } + public MLKem? GetMLKemPrivateKey() + { + // MLKem is not supported on Android + return null; + } + public ICertificatePal CopyWithPrivateKey(DSA privateKey) { DSAImplementation.DSAAndroid? typedKey = privateKey as DSAImplementation.DSAAndroid; @@ -510,6 +516,11 @@ public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa))); } + public ICertificatePal CopyWithPrivateKey(MLKem privateKey) + { + throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLKem))); + } + 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.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs index 366c37a0598919..7ccfd2386f4b1b 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 @@ -370,6 +370,17 @@ public byte[] SubjectPublicKeyInfo return null; } + public MLKem? GetMLKemPrivateKey() + { + // MLKem is not supported on Apple platforms. + return null; + } + + public ICertificatePal CopyWithPrivateKey(MLKem privateKey) + { + throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLKem))); + } + 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 dcb526751abe78..257227bea5aaf2 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 @@ -86,6 +86,12 @@ public bool HasPrivateKey return null; } + public MLKem? GetMLKemPrivateKey() + { + // MLKem is not supported on Windows. + return null; + } + public ICertificatePal CopyWithPrivateKey(DSA dsa) { DSACng? dsaCng = dsa as DSACng; @@ -180,6 +186,11 @@ public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLDsa))); } + public ICertificatePal CopyWithPrivateKey(MLKem privateKey) + { + throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(MLKem))); + } + public ICertificatePal CopyWithPrivateKey(RSA rsa) { RSACng? rsaCng = rsa as RSACng; 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 6828be2404ae42..1231bcb4836628 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 @@ -32,6 +32,7 @@ internal interface ICertificatePal : ICertificatePalCore ECDsa? GetECDsaPrivateKey(); ECDiffieHellman? GetECDiffieHellmanPrivateKey(); MLDsa? GetMLDsaPrivateKey(); + MLKem? GetMLKemPrivateKey(); string GetNameInfo(X509NameType nameType, bool forIssuer); void AppendPrivateKeyInfo(StringBuilder sb); ICertificatePal CopyWithPrivateKey(DSA privateKey); @@ -39,6 +40,7 @@ internal interface ICertificatePal : ICertificatePalCore ICertificatePal CopyWithPrivateKey(RSA privateKey); ICertificatePal CopyWithPrivateKey(ECDiffieHellman privateKey); ICertificatePal CopyWithPrivateKey(MLDsa privateKey); + ICertificatePal CopyWithPrivateKey(MLKem privateKey); PolicyData GetPolicyData(); } } 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 16e97576c8a1c7..2cab09b0670b44 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 @@ -618,6 +618,16 @@ public ECDiffieHellman GetECDiffieHellmanPublicKey() return new MLDsaOpenSsl(_privateKey); } + public MLKem? GetMLKemPrivateKey() + { + if (_privateKey is null || _privateKey.IsInvalid) + { + return null; + } + + return new MLKemOpenSsl(_privateKey); + } + private OpenSslX509CertificateReader CopyWithPrivateKey(SafeEvpPKeyHandle privateKey) { // This could be X509Duplicate for a full clone, but since OpenSSL certificates @@ -705,6 +715,22 @@ public ICertificatePal CopyWithPrivateKey(MLDsa privateKey) } } + public ICertificatePal CopyWithPrivateKey(MLKem privateKey) + { + switch (privateKey) + { + case MLKemOpenSsl implOpenSsl: + return CopyWithPrivateKey(implOpenSsl.DuplicateKeyHandle()); + case MLKemImplementation impl: + return CopyWithPrivateKey(impl.DuplicateHandle()); + default: + using (MLKemImplementation clone = MLKemImplementation.DuplicatePrivateKey(privateKey)) + { + return CopyWithPrivateKey(clone.DuplicateHandle()); + } + } + } + public ICertificatePal CopyWithPrivateKey(RSA privateKey) { RSAOpenSsl? typedKey = privateKey as RSAOpenSsl; 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 c9050f64066e06..6942deaa76b6af 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 @@ -803,6 +803,81 @@ public X509Certificate2 CopyWithPrivateKey(ECDiffieHellman privateKey) return publicKey; } + /// + /// Gets the private key from this certificate. + /// + /// + /// The private key, or if this certificate does not have an ML-KEM private key. + /// + /// + /// An error occurred accessing the private key. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public MLKem? GetMLKemPrivateKey() + { + MLKemAlgorithm? algorithm = MLKemAlgorithm.FromOid(GetKeyAlgorithm()); + + if (algorithm is null) + { + return null; + } + + return Pal.GetMLKemPrivateKey(); + } + + /// + /// Combines a private key with a certificate containing the associated public key into a + /// new instance that can access the private key. + /// + /// + /// The ML-KEM private key that corresponds to the 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. + /// + [Experimental(Experimentals.PostQuantumCryptographyDiagId)] + public X509Certificate2 CopyWithPrivateKey(MLKem privateKey) + { + ArgumentNullException.ThrowIfNull(privateKey); + + if (HasPrivateKey) + throw new InvalidOperationException(SR.Cryptography_Cert_AlreadyHasPrivateKey); + + using (MLKem? publicKey = GetMLKemPublicKey()) + { + 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.AsSpan().SequenceEqual(pk2)) + { + throw new ArgumentException(SR.Cryptography_PrivateKey_DoesNotMatch, nameof(privateKey)); + } + } + + ICertificatePal pal = Pal.CopyWithPrivateKey(privateKey); + return new X509Certificate2(pal); + } + /// /// Gets the public key from this certificate. /// 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 00423d7e5f09b8..52853e07199e7f 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Security.Cryptography.Tests; using Test.Cryptography; using Xunit; @@ -750,6 +751,34 @@ public static void CheckCopyWithPrivateKey_MLDSA() } } + [ConditionalFact(typeof(MLKem), nameof(MLKem.IsSupported))] + public static void CheckCopyWithPrivateKey_MLKem() + { + using (X509Certificate2 pubOnly = X509Certificate2.CreateFromPem(MLKemTestData.IetfMlKem512CertificatePem)) + using (MLKem privKey = MLKem.ImportPkcs8PrivateKey(MLKemTestData.IetfMlKem512PrivateKeySeed)) + using (X509Certificate2 wrongAlg = X509CertificateLoader.LoadCertificate(TestData.CertWithEnhancedKeyUsage)) + { + CheckCopyWithPrivateKey( + pubOnly, + wrongAlg, + privKey, + [ + () => MLKem.GenerateKey(MLKemAlgorithm.MLKem512), + () => MLKem.GenerateKey(MLKemAlgorithm.MLKem768), + () => MLKem.GenerateKey(MLKemAlgorithm.MLKem1024), + ], + (cert, key) => cert.CopyWithPrivateKey(key), + cert => cert.GetMLKemPublicKey(), + cert => cert.GetMLKemPrivateKey(), + (priv, pub) => + { + byte[] ciphertext = pub.Encapsulate(out byte[] pubSharedSecret); + byte[] privSharedSecret = priv.Decapsulate(ciphertext); + AssertExtensions.SequenceEqual(pubSharedSecret, privSharedSecret); + }); + } + } + private static void CheckCopyWithPrivateKey( X509Certificate2 cert, X509Certificate2 wrongAlgorithmCert, From 6b1c8142f26994fb207f8c73aedefdd967f91fd7 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Tue, 15 Apr 2025 22:26:10 -0400 Subject: [PATCH 2/4] Teach the PEM importer about MLKem --- .../X509Certificates/X509Certificate2.cs | 96 ++++++++++++++----- .../tests/X509Certificates/CertTests.cs | 15 +++ .../PrivateKeyAssociationTests.cs | 2 +- 3 files changed, 86 insertions(+), 27 deletions(-) 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 6942deaa76b6af..d67935833010f7 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 @@ -1162,19 +1162,33 @@ public static X509Certificate2 CreateFromPem(ReadOnlySpan certPem, ReadOnl return keyAlgorithm switch { - Oids.Rsa => ExtractKeyFromPem(keyPem, s_RsaPublicKeyPrivateKeyLabels, RSA.Create, certificate.CopyWithPrivateKey), - Oids.Dsa when Helpers.IsDSASupported => ExtractKeyFromPem(keyPem, s_DsaPublicKeyPrivateKeyLabels, DSA.Create, certificate.CopyWithPrivateKey), + Oids.Rsa => ExtractKeyFromPem( + keyPem, + s_RsaPublicKeyPrivateKeyLabels, + static keyPem => CreateAndImport(keyPem, RSA.Create), + certificate.CopyWithPrivateKey), + Oids.Dsa when Helpers.IsDSASupported => ExtractKeyFromPem( + keyPem, + s_DsaPublicKeyPrivateKeyLabels, + static keyPem => CreateAndImport(keyPem, DSA.Create), + certificate.CopyWithPrivateKey), Oids.EcPublicKey when IsECDsa(certificate) => ExtractKeyFromPem( keyPem, s_EcPublicKeyPrivateKeyLabels, - ECDsa.Create, + static keyPem => CreateAndImport(keyPem, ECDsa.Create), certificate.CopyWithPrivateKey), Oids.EcPublicKey when IsECDiffieHellman(certificate) => ExtractKeyFromPem( keyPem, s_EcPublicKeyPrivateKeyLabels, - ECDiffieHellman.Create, + static keyPem => CreateAndImport(keyPem, ECDiffieHellman.Create), + certificate.CopyWithPrivateKey), + Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024 => + ExtractKeyFromPem( + keyPem, + [PemLabels.Pkcs8PrivateKey], + MLKem.ImportFromPem, certificate.CopyWithPrivateKey), _ => throw new CryptographicException(SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm)), }; @@ -1233,19 +1247,35 @@ public static X509Certificate2 CreateFromEncryptedPem(ReadOnlySpan certPem return keyAlgorithm switch { - Oids.Rsa => ExtractKeyFromEncryptedPem(keyPem, password, RSA.Create, certificate.CopyWithPrivateKey), - Oids.Dsa when Helpers.IsDSASupported => ExtractKeyFromEncryptedPem(keyPem, password, DSA.Create, certificate.CopyWithPrivateKey), + Oids.Rsa => + ExtractKeyFromEncryptedPem( + keyPem, + password, + static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, RSA.Create), + certificate.CopyWithPrivateKey), + Oids.Dsa when Helpers.IsDSASupported => + ExtractKeyFromEncryptedPem( + keyPem, + password, + static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, DSA.Create), + certificate.CopyWithPrivateKey), Oids.EcPublicKey when IsECDsa(certificate) => ExtractKeyFromEncryptedPem( keyPem, password, - ECDsa.Create, + static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, ECDsa.Create), certificate.CopyWithPrivateKey), Oids.EcPublicKey when IsECDiffieHellman(certificate) => ExtractKeyFromEncryptedPem( keyPem, password, - ECDiffieHellman.Create, + static (keyPem, password) => CreateAndImportEncrypted(keyPem, password, ECDiffieHellman.Create), + certificate.CopyWithPrivateKey), + Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024 => + ExtractKeyFromEncryptedPem( + keyPem, + password, + MLKem.ImportFromEncryptedPem, certificate.CopyWithPrivateKey), _ => throw new CryptographicException(SR.Format(SR.Cryptography_UnknownKeyAlgorithm, keyAlgorithm)), }; @@ -1638,11 +1668,28 @@ public bool MatchesHostname(string hostname, bool allowWildcards = true, bool al return false; } + private static TAlg CreateAndImport(ReadOnlySpan keyPem, Func factory) where TAlg : AsymmetricAlgorithm + { + TAlg alg = factory(); + alg.ImportFromPem(keyPem); + return alg; + } + + private static TAlg CreateAndImportEncrypted( + ReadOnlySpan keyPem, + ReadOnlySpan password, + Func factory) where TAlg : AsymmetricAlgorithm + { + TAlg alg = factory(); + alg.ImportFromEncryptedPem(keyPem, password); + return alg; + } + private static X509Certificate2 ExtractKeyFromPem( ReadOnlySpan keyPem, - string[] labels, - Func factory, - Func import) where TAlg : AsymmetricAlgorithm + ReadOnlySpan labels, + Func, TAlg> factory, + Func import) where TAlg : IDisposable { foreach ((ReadOnlySpan contents, PemFields fields) in PemEnumerator.Utf16(keyPem)) { @@ -1652,10 +1699,8 @@ private static X509Certificate2 ExtractKeyFromPem( { if (label.SequenceEqual(eligibleLabel)) { - using (TAlg key = factory()) + using (TAlg key = factory(contents[fields.Location])) { - key.ImportFromPem(contents[fields.Location]); - try { return import(key); @@ -1675,8 +1720,8 @@ private static X509Certificate2 ExtractKeyFromPem( private static X509Certificate2 ExtractKeyFromEncryptedPem( ReadOnlySpan keyPem, ReadOnlySpan password, - Func factory, - Func import) where TAlg : AsymmetricAlgorithm + Func, ReadOnlySpan, TAlg> factory, + Func import) where TAlg : IDisposable { foreach ((ReadOnlySpan contents, PemFields fields) in PemEnumerator.Utf16(keyPem)) { @@ -1684,18 +1729,17 @@ private static X509Certificate2 ExtractKeyFromEncryptedPem( if (label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey)) { - TAlg key = factory(); - key.ImportFromEncryptedPem(contents[fields.Location], password); - - try + using (TAlg key = factory(contents[fields.Location], password)) { - return import(key); - } - catch (ArgumentException ae) - { - throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae); + try + { + return import(key); + } + catch (ArgumentException ae) + { + throw new CryptographicException(SR.Cryptography_X509_NoOrMismatchedPemKey, ae); + } } - } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertTests.cs index 64ded87aa4e041..88c2777cc9e40e 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using System.Security.Cryptography.Tests; using System.Security.Cryptography.Dsa.Tests; using System.Security.Cryptography.X509Certificates.Tests.CertificateCreation; using System.Threading; @@ -129,6 +130,20 @@ public static void PrivateKey_FromCertificate_CanExportPrivate_ECDiffieHellman() } } + [ConditionalFact(typeof(MLKem), nameof(MLKem.IsSupported))] + public static void PrivateKey_FromCertificate_CanExportPrivate_MLKem() + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(MLKemTestData.IetfMlKem512CertificatePem)) + using (MLKem key = MLKem.ImportPkcs8PrivateKey(MLKemTestData.IetfMlKem512PrivateKeySeed)) + using (X509Certificate2 certWithKey = cert.CopyWithPrivateKey(key)) + using (MLKem certKey = certWithKey.GetMLKemPrivateKey()) + { + Assert.NotNull(certKey); + byte[] expectedKey = MLKemTestData.IetfMlKem512PrivateKeyDecapsulationKey; + AssertExtensions.SequenceEqual(expectedKey, certKey.ExportDecapsulationKey()); + } + } + [Fact] public static void PublicPrivateKey_IndependentLifetimes_ECDsa() { 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 52853e07199e7f..158c6cc3239cab 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs @@ -772,7 +772,7 @@ public static void CheckCopyWithPrivateKey_MLKem() cert => cert.GetMLKemPrivateKey(), (priv, pub) => { - byte[] ciphertext = pub.Encapsulate(out byte[] pubSharedSecret); + pub.Encapsulate(out byte[] ciphertext, out byte[] pubSharedSecret); byte[] privSharedSecret = priv.Decapsulate(ciphertext); AssertExtensions.SequenceEqual(pubSharedSecret, privSharedSecret); }); From 7424e1e42d80794b1cfe6f74f544c78d85d31e40 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 16 Apr 2025 12:03:34 -0400 Subject: [PATCH 3/4] Add tests for PEM loading --- .../Security/Cryptography/MLKemTestData.cs | 72 +++++++ .../X509Certificate2PemTests.cs | 184 ++++++++++++++++-- 2 files changed, 241 insertions(+), 15 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemTestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemTestData.cs index 6bca66e08adbb8..a03f8f7996996a 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemTestData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemTestData.cs @@ -54,12 +54,20 @@ public static class MLKemTestData MFQCAQAwCwYJYIZIAWUDBAQBBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8="); + internal static string IetfMlKem512PrivateKeySeedPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem512PrivateKeySeed); + internal static byte[] IetfMlKem512EncryptedPrivateKeySeed => field ??= Convert.FromBase64String(@" MIGyMFYGCSqGSIb3DQEFDTBJMDEGCSqGSIb3DQEFDDAkBBBu4zqgXqt7HTK6mTmr 5B/aAgIIADAMBggqhkiG9w0CCQUAMBQGCCqGSIb3DQMHBAioOjwRcwdjBwRYSGy/ LN0wpvceGrPIQr/FTvN2wRvoozbkYMC1Tzs4phJh8lbMgdLgbTA0mCK16lBWgjdi /vxAu7Wn/wmKjFTqvST9vKxgu8sotadxpERtJaecmAaHqMjFtA=="); + internal static string IetfMlKem512EncryptedPrivateKeySeedPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem512EncryptedPrivateKeySeed); + internal static byte[] IetfMlKem512PrivateKeyExpandedKey => field ??= Convert.FromBase64String(@" MIIGeAIBADALBglghkgBZQMEBAEEggZkBIIGYHBVT9Q2NE8nhbGzsbrBhLZnkAMz bCbxWn3oeMSCXGvgPzxKSA91t0hqrTHToAUYYj/SB6tSjdYnIUlYNa4AYsNnt0px @@ -97,6 +105,10 @@ public static class MLKemTestData uBw7xZoGWhttY7JsgvEB/2SAY7N24rtsW3RV9lWlDC/q2t4VDvoODm82WuogISIj JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw=="); + internal static string IetfMlKem512PrivateKeyExpandedKeyPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem512PrivateKeyExpandedKey); + internal static byte[] IetfMlKem512EncryptedPrivateKeyExpandedKey => field ??= Convert.FromBase64String(@" MIIG3DBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQlj5FxGXOP5cuSHuH VZ+GkAICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQI7I35SG76s0YEggaA @@ -136,6 +148,10 @@ public static class MLKemTestData 9bO6Iz/eChNTAJkI0gAyZmqkScYOiBxORGaclfQFGLznOD2umXKrv0Mb4pqXiVP8 L6AcpfWf8A/oue1gG6wJpQeFrQJ6z+yWa/G6C/lJazw="); + internal static string IetfMlKem512EncryptedPrivateKeyExpandedKeyPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem512EncryptedPrivateKeyExpandedKey); + internal static byte[] IetfMlKem512PrivateKeyBoth => field ??= Convert.FromBase64String(@" MIIGvgIBADALBglghkgBZQMEBAEEggaqMIIGpgRAAAECAwQFBgcICQoLDA0ODxAR EhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+PwSC @@ -175,6 +191,10 @@ public static class MLKemTestData VfZVpQwv6treFQ76Dg5vNlrqICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9 Pj8="); + internal static string IetfMlKem512PrivateKeyBothPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem512PrivateKeyBoth); + internal static byte[] IetfMlKem512EncryptedPrivateKeyBoth => field ??= Convert.FromBase64String(@" MIIHJDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQ5zTKk8w8fC1UNK4+ tIDqMAICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQINW2WksGdFJ0EggbI @@ -216,6 +236,10 @@ public static class MLKemTestData d/TwrYq/C1f/xaKue2pvMrjj909cxDZVq7X9E9s9aBR8m1FzUPoNkfoGIVZANitT 1ZBGWJKA1Fw="); + internal static string IetfMlKem512EncryptedPrivateKeyBothPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem512EncryptedPrivateKeyBoth); + internal static byte[] IetfMlKem512PrivateKeyDecapsulationKey => field ??= ( "70554fd436344f2785b1b3b1bac184b6679003336c26f15a7de878c4825c6be03f3c4a480f75b7486aad31d3a00518623fd2" + "07ab528dd62721495835ae0062c367b74a71baf10aad0e8a2902076be31348beb15ccc0957cdebb4aff226756bbc601b6568" + @@ -362,12 +386,20 @@ public static class MLKemTestData MFQCAQAwCwYJYIZIAWUDBAQCBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8="); + internal static string IetfMlKem768PrivateKeySeedPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem768PrivateKeySeed); + internal static byte[] IetfMlKem768EncryptedPrivateKeySeed => field ??= Convert.FromBase64String(@" MIGyMFYGCSqGSIb3DQEFDTBJMDEGCSqGSIb3DQEFDDAkBBDVvN7dPv1xeTQ5V4S4 lNYAAgIIADAMBggqhkiG9w0CCQUAMBQGCCqGSIb3DQMHBAhxYX16f/Or8ARY98/3 tAF57U+XfDsiweIKGW37VcOMgrJr4jl8Tn6E1MC9sNiSKXd5Ge93Oscm46wIYOG/ ltLe5Ba3maubTj7Sj1UHsFIRE0NGcpha09u2JH8iHIBR4tvBtg=="); + internal static string IetfMlKem768EncryptedPrivateKeySeedPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem768EncryptedPrivateKeySeed); + internal static byte[] IetfMlKem768PrivateKeyExpandedKey => field ??= Convert.FromBase64String(@" MIIJeAIBADALBglghkgBZQMEBAIEgglkBIIJYCfSp38zdW9hII7xE6voJZWHPUq8 cw5bXWeVKb9qTOtjg0JyMahhL0FVBRWsulLkjq2LlCgzu+aGXRPRSnnSxcPgfwoF @@ -421,6 +453,10 @@ public static class MLKemTestData 4ssy2ovDQvpN6gV4ok4W2Pj5ODqVt3BQ9Nn9L1cz7sHWPvPCPr+ZGBc2aacgISIj JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw=="); + internal static string IetfMlKem768PrivateKeyExpandedKeyPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem768PrivateKeyExpandedKey); + internal static byte[] IetfMlKem768EncryptedPrivateKeyExpandedKey => field ??= Convert.FromBase64String(@" MIIJ3DBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQdV5wgVIICzzniNpD y7WD9gICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIj7uC5kmav+kEggmA @@ -476,6 +512,10 @@ public static class MLKemTestData X3qZc/K8q1BBn9dqcJRIKr/dZ7Mq1U6sa5zg+sDIZvLoS/weutBuPRHP9AofQWpS F1JkgTbf0PrGVr3jgdaXCY/7vfsB6+utgcs1F7KfKZA="); + internal static string IetfMlKem768EncryptedPrivateKeyExpandedKeyPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem768EncryptedPrivateKeyExpandedKey); + internal static byte[] IetfMlKem768PrivateKeyBoth => field ??= Convert.FromBase64String(@" MIIJvgIBADALBglghkgBZQMEBAIEggmqMIIJpgRAAAECAwQFBgcICQoLDA0ODxAR EhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+PwSC @@ -531,6 +571,10 @@ public static class MLKemTestData /S9XM+7B1j7zwj6/mRgXNmmnICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9 Pj8="); + internal static string IetfMlKem768PrivateKeyBothPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem768PrivateKeyBoth); + internal static byte[] IetfMlKem768EncryptedPrivateKeyBoth => field ??= Convert.FromBase64String(@" MIIKJDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQcdUu8kW63IlZ7x2z ACye4gICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQICqHaOOkCVBQEggnI @@ -588,6 +632,10 @@ public static class MLKemTestData uTR1HHzBYXNcscJfaQZJcS/hbHBaCvKgEvhUYTmXbSgaD1+fNq3gbthRZhNUOfiR RDd5KC8EEzk="); + internal static string IetfMlKem768EncryptedPrivateKeyBothPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem768EncryptedPrivateKeyBoth); + internal static byte[] IetfMlKem768PrivateKeyDecapsulationKey => field ??= ( "27d2a77f33756f61208ef113abe82595873d4abc730e5b5d679529bf6a4ceb6383427231a8612f41550515acba52e48ead8b" + "942833bbe6865d13d14a79d2c5c3e07f0a056d8de7aadfcaba058c493c80b37cab8c562753bb3ba6b6ec8297f885eaa7540d" + @@ -783,12 +831,20 @@ public static class MLKemTestData MFQCAQAwCwYJYIZIAWUDBAQDBEKAQAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZ GhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8="); + internal static string IetfMlKem1024PrivateKeySeedPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem1024PrivateKeySeed); + internal static byte[] IetfMlKem1024EncryptedPrivateKeySeed => field ??= Convert.FromBase64String(@" MIGyMFYGCSqGSIb3DQEFDTBJMDEGCSqGSIb3DQEFDDAkBBArGFO1mU77a3ys0aR0 +mWBAgIIADAMBggqhkiG9w0CCQUAMBQGCCqGSIb3DQMHBAh48Gqhu7YOpwRYPR66 W02NrqRok/CagC9uo/viGlLLC5CUl4Y9cE3ZCEwfDxFufNeALt2Kusg+gJLMSq16 g6YgQHQJeKZusLSnwzxOutuyKKgbGuIWxFBmtDZrXDjCO913Ow=="); + internal static string IetfMlKem1024EncryptedPrivateKeySeedPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem1024EncryptedPrivateKeySeed); + internal static byte[] IetfMlKem1024PrivateKeyExpandedKey => field ??= Convert.FromBase64String(@" MIIMeAIBADALBglghkgBZQMEBAMEggxkBIIMYPd7f2sVxz/izFRrZ/t3TKGbQs1G Pqn7uYTKR3p3tscQh8vwUavkc2qQcsbocMgxHFWWP1AKPHsbjypYVY9JxiUntsWU @@ -858,6 +914,10 @@ public static class MLKemTestData 7VjcYod2uYOILhF1YTSeXBMafhFqBGOGHX0YZjxWJ8OMcUfdqt/Uis16RTUgISIj JCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw=="); + internal static string IetfMlKem1024PrivateKeyExpandedKeyPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem1024PrivateKeyExpandedKey); + internal static byte[] IetfMlKem1024EncryptedPrivateKeyExpandedKey => field ??= Convert.FromBase64String(@" MIIM3DBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQE/G+HHo48gCgwImJ HbfEggICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIlT+E3yFzlnkEggyA @@ -929,6 +989,10 @@ public static class MLKemTestData hi5UqCzJNmdxMEtwwyVHXuQBnNUlgl2/c4XAFIUwnQ11SM7UFPDwkDYzj529XwqA 00ExhHl+b5Un8kb2eyOSe9UgG+cAMgA+m892u4ZKOSE="); + internal static string IetfMlKem1024EncryptedPrivateKeyExpandedKeyPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem1024EncryptedPrivateKeyExpandedKey); + internal static byte[] IetfMlKem1024PrivateKeyBoth => field ??= Convert.FromBase64String(@" MIIMvgIBADALBglghkgBZQMEBAMEggyqMIIMpgRAAAECAwQFBgcICQoLDA0ODxAR EhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+PwSC @@ -1000,6 +1064,10 @@ public static class MLKemTestData GGY8VifDjHFH3arf1IrNekU1ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9 Pj8="); + internal static string IetfMlKem1024PrivateKeyBothPem => field ??= PemEncoding.WriteString( + "PRIVATE KEY", + IetfMlKem1024PrivateKeyBoth); + internal static byte[] IetfMlKem1024EncryptedPrivateKeyBoth => field ??= Convert.FromBase64String(@" MIINJDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQVR0rwDXJnxYGA7N9 /eveiQICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIzvch3uhQ1pEEggzI @@ -1073,6 +1141,10 @@ public static class MLKemTestData 9xpeir1cJ7dnmi2BncLvSCQDgnPUfs4awqmONkcqE4VtYzi10s588zWtXZcH3ar7 FIgRVDi1lQg="); + internal static string IetfMlKem1024EncryptedPrivateKeyBothPem => field ??= PemEncoding.WriteString( + "ENCRYPTED PRIVATE KEY", + IetfMlKem1024EncryptedPrivateKeyBoth); + internal static byte[] IetfMlKem1024PrivateKeyDecapsulationKey => field ??= ( "f77b7f6b15c73fe2cc546b67fb774ca19b42cd463ea9fbb984ca477a77b6c71087cbf051abe4736a9072c6e870c8311c5596" + "3f500a3c7b1b8f2a58558f49c62527b6c594b5e7acb3bcf597273a5743517d151208bd4aa61e75ba67b0bd594a994919627a" + diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs index 7411bf5b250cb5..bf786fc7dc2b1c 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/X509Certificate2PemTests.cs @@ -1,7 +1,9 @@ // 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.Runtime.InteropServices; +using System.Security.Cryptography.Tests; using Test.Cryptography; using Xunit; @@ -266,6 +268,133 @@ public static void CreateFromPem_ECDH_Pkcs8_Success() } } + [ConditionalFact(typeof(MLKem), nameof(MLKem.IsSupported))] + public static void CreateFromPem_MLKem_Pkcs8_Success() + { + (string CertificatePem, string PrivateKeyPem, string Thumbprint)[] cases = + [ + ( + MLKemTestData.IetfMlKem512CertificatePem, + MLKemTestData.IetfMlKem512PrivateKeySeedPem, + "877316CB8B7E5C389E99B1094DE4F60E3BBFDA12D2E4ACB8C014D84CFC009E6F" + ), + ( + MLKemTestData.IetfMlKem512CertificatePem, + MLKemTestData.IetfMlKem512PrivateKeyExpandedKeyPem, + "877316CB8B7E5C389E99B1094DE4F60E3BBFDA12D2E4ACB8C014D84CFC009E6F" + ), + ( + MLKemTestData.IetfMlKem512CertificatePem, + MLKemTestData.IetfMlKem512PrivateKeyBothPem, + "877316CB8B7E5C389E99B1094DE4F60E3BBFDA12D2E4ACB8C014D84CFC009E6F" + ), + ( + MLKemTestData.IetfMlKem768CertificatePem, + MLKemTestData.IetfMlKem768PrivateKeySeedPem, + "0E9DFBEDB039156B568D8F59953DD4DA6B81E30EC8E071A775DF6BC2E77B2DCE" + ), + ( + MLKemTestData.IetfMlKem768CertificatePem, + MLKemTestData.IetfMlKem768PrivateKeyExpandedKeyPem, + "0E9DFBEDB039156B568D8F59953DD4DA6B81E30EC8E071A775DF6BC2E77B2DCE" + ), + ( + MLKemTestData.IetfMlKem768CertificatePem, + MLKemTestData.IetfMlKem768PrivateKeyBothPem, + "0E9DFBEDB039156B568D8F59953DD4DA6B81E30EC8E071A775DF6BC2E77B2DCE" + ), + ( + MLKemTestData.IetfMlKem1024CertificatePem, + MLKemTestData.IetfMlKem1024PrivateKeySeedPem, + "32819AD8477F3620619B1E97744AA26AA9617760A8694E1BD8D17DE8B49A8E8A" + ), + ( + MLKemTestData.IetfMlKem1024CertificatePem, + MLKemTestData.IetfMlKem1024PrivateKeyExpandedKeyPem, + "32819AD8477F3620619B1E97744AA26AA9617760A8694E1BD8D17DE8B49A8E8A" + ), + ( + MLKemTestData.IetfMlKem1024CertificatePem, + MLKemTestData.IetfMlKem1024PrivateKeyBothPem, + "32819AD8477F3620619B1E97744AA26AA9617760A8694E1BD8D17DE8B49A8E8A" + ), + ]; + + foreach((string CertificatePem, string PrivateKeyPem, string Thumbprint) in cases) + { + using (X509Certificate2 cert = X509Certificate2.CreateFromPem(CertificatePem, PrivateKeyPem)) + { + Assert.Equal(Thumbprint, cert.GetCertHashString(HashAlgorithmName.SHA256)); + AssertKeysMatch(PrivateKeyPem, cert.GetMLKemPrivateKey); + } + } + } + + [ConditionalFact(typeof(MLKem), nameof(MLKem.IsSupported))] + public static void CreateFromEncryptedPem_MLKem_Pkcs8_Success() + { + (string CertificatePem, string EncryptedPrivateKeyPem, string Thumbprint)[] cases = + [ + ( + MLKemTestData.IetfMlKem512CertificatePem, + MLKemTestData.IetfMlKem512EncryptedPrivateKeySeedPem, + "877316CB8B7E5C389E99B1094DE4F60E3BBFDA12D2E4ACB8C014D84CFC009E6F" + ), + ( + MLKemTestData.IetfMlKem512CertificatePem, + MLKemTestData.IetfMlKem512EncryptedPrivateKeyExpandedKeyPem, + "877316CB8B7E5C389E99B1094DE4F60E3BBFDA12D2E4ACB8C014D84CFC009E6F" + ), + ( + MLKemTestData.IetfMlKem512CertificatePem, + MLKemTestData.IetfMlKem512EncryptedPrivateKeyBothPem, + "877316CB8B7E5C389E99B1094DE4F60E3BBFDA12D2E4ACB8C014D84CFC009E6F" + ), + ( + MLKemTestData.IetfMlKem768CertificatePem, + MLKemTestData.IetfMlKem768EncryptedPrivateKeySeedPem, + "0E9DFBEDB039156B568D8F59953DD4DA6B81E30EC8E071A775DF6BC2E77B2DCE" + ), + ( + MLKemTestData.IetfMlKem768CertificatePem, + MLKemTestData.IetfMlKem768EncryptedPrivateKeyExpandedKeyPem, + "0E9DFBEDB039156B568D8F59953DD4DA6B81E30EC8E071A775DF6BC2E77B2DCE" + ), + ( + MLKemTestData.IetfMlKem768CertificatePem, + MLKemTestData.IetfMlKem768EncryptedPrivateKeyBothPem, + "0E9DFBEDB039156B568D8F59953DD4DA6B81E30EC8E071A775DF6BC2E77B2DCE" + ), + ( + MLKemTestData.IetfMlKem1024CertificatePem, + MLKemTestData.IetfMlKem1024EncryptedPrivateKeySeedPem, + "32819AD8477F3620619B1E97744AA26AA9617760A8694E1BD8D17DE8B49A8E8A" + ), + ( + MLKemTestData.IetfMlKem1024CertificatePem, + MLKemTestData.IetfMlKem1024EncryptedPrivateKeyExpandedKeyPem, + "32819AD8477F3620619B1E97744AA26AA9617760A8694E1BD8D17DE8B49A8E8A" + ), + ( + MLKemTestData.IetfMlKem1024CertificatePem, + MLKemTestData.IetfMlKem1024EncryptedPrivateKeyBothPem, + "32819AD8477F3620619B1E97744AA26AA9617760A8694E1BD8D17DE8B49A8E8A" + ), + ]; + + foreach((string CertificatePem, string PrivateKeyPem, string Thumbprint) in cases) + { + using (X509Certificate2 cert = X509Certificate2.CreateFromEncryptedPem( + CertificatePem, + PrivateKeyPem, + MLKemTestData.EncryptedPrivateKeyPassword)) + { + Assert.Equal(Thumbprint, cert.GetCertHashString(HashAlgorithmName.SHA256)); + AssertKeysMatch(PrivateKeyPem, cert.GetMLKemPrivateKey, MLKemTestData.EncryptedPrivateKeyPassword); + } + } + } + [Fact] [SkipOnPlatform(PlatformSupport.MobileAppleCrypto, "DSA is not available")] public static void CreateFromPem_Dsa_Pkcs8_Success() @@ -449,32 +578,48 @@ public static void CreateFromPem_PublicOnly_CryptographicException_CertIsPkcs7() X509Certificate2.CreateFromPem(certContents)); } - private static void AssertKeysMatch(string keyPem, Func keyLoader, string password = null) where T : AsymmetricAlgorithm + private static void AssertKeysMatch(string keyPem, Func keyLoader, string password = null) where T : IDisposable { - AsymmetricAlgorithm key = keyLoader(); + IDisposable key = keyLoader(); Assert.NotNull(key); - AsymmetricAlgorithm alg = key switch - { - RSA => RSA.Create(), - DSA => DSA.Create(), - ECDsa => ECDsa.Create(), - ECDiffieHellman => ECDiffieHellman.Create(), - _ => null - }; + IDisposable alg; - using (key) - using (alg) + if (key is AsymmetricAlgorithm) { + AsymmetricAlgorithm asymmetricAlg = key switch + { + RSA => RSA.Create(), + DSA => DSA.Create(), + ECDsa => ECDsa.Create(), + ECDiffieHellman => ECDiffieHellman.Create(), + _ => null, + }; + if (password is null) { - alg.ImportFromPem(keyPem); + asymmetricAlg.ImportFromPem(keyPem); } else { - alg.ImportFromEncryptedPem(keyPem, password); + asymmetricAlg.ImportFromEncryptedPem(keyPem, password); } - byte[] data = alg.ExportPkcs8PrivateKey(); + alg = asymmetricAlg; + } + else if (key is MLKem) + { + alg = password is null ? MLKem.ImportFromPem(keyPem) : MLKem.ImportFromEncryptedPem(keyPem, password); + } + else + { + Assert.Fail($"Unhandled key type {key.GetType()}."); + throw new UnreachableException(); + } + + using (key) + using (alg) + { + byte[] data = RandomNumberGenerator.GetBytes(32); switch ((alg, key)) { @@ -505,6 +650,15 @@ private static void AssertKeysMatch(string keyPem, Func keyLoader, string Assert.Equal(key1, key2); } break; + case (MLKem kem, MLKem kemPem): + kem.Encapsulate(out byte[] ciphertext, out byte[] sharedSecret1); + byte[] sharedSecret2 = kemPem.Decapsulate(ciphertext); + AssertExtensions.SequenceEqual(sharedSecret1, sharedSecret2); + + kemPem.Encapsulate(out ciphertext, out sharedSecret1); + sharedSecret2 = kem.Decapsulate(ciphertext); + AssertExtensions.SequenceEqual(sharedSecret1, sharedSecret2); + break; default: throw new CryptographicException("Unknown key algorithm"); } From ca1f9c725f64871498509427769aa231db0c8cf0 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 16 Apr 2025 12:34:34 -0400 Subject: [PATCH 4/4] Add test for other MLKems --- .../PrivateKeyAssociationTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) 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 158c6cc3239cab..e736896cacd6f5 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CertificateCreation/PrivateKeyAssociationTests.cs @@ -779,6 +779,71 @@ public static void CheckCopyWithPrivateKey_MLKem() } } + [ConditionalFact(typeof(MLKem), nameof(MLKem.IsSupported))] + public static void CheckCopyWithPrivateKey_MLKem_OtherMLKem_Seed() + { + using (X509Certificate2 pubOnly = X509Certificate2.CreateFromPem(MLKemTestData.IetfMlKem512CertificatePem)) + using (MLKemContract contract = new(MLKemAlgorithm.MLKem512)) + { + contract.OnExportPrivateSeedCore = (Span source) => + { + MLKemTestData.IncrementalSeed.CopyTo(source); + }; + + contract.OnExportEncapsulationKeyCore = (Span source) => + { + PublicKey publicKey = PublicKey.CreateFromSubjectPublicKeyInfo(MLKemTestData.IetfMlKem512Spki, out _); + publicKey.EncodedKeyValue.RawData.AsSpan().CopyTo(source); + }; + + using (X509Certificate2 cert = pubOnly.CopyWithPrivateKey(contract)) + { + AssertExtensions.TrueExpression(cert.HasPrivateKey); + + using (MLKem kem = cert.GetMLKemPrivateKey()) + { + AssertExtensions.SequenceEqual(MLKemTestData.IncrementalSeed, kem.ExportPrivateSeed()); + } + } + } + } + + [ConditionalFact(typeof(MLKem), nameof(MLKem.IsSupported))] + public static void CheckCopyWithPrivateKey_MLKem_OtherMLKem_DecapsulationKey() + { + using (X509Certificate2 pubOnly = X509Certificate2.CreateFromPem(MLKemTestData.IetfMlKem512CertificatePem)) + using (MLKemContract contract = new(MLKemAlgorithm.MLKem512)) + { + contract.OnExportPrivateSeedCore = (Span source) => + { + throw new CryptographicException("Should signal to try decaps key"); + }; + + contract.OnExportDecapsulationKeyCore = (Span source) => + { + MLKemTestData.IetfMlKem512PrivateKeyDecapsulationKey.AsSpan().CopyTo(source); + }; + + contract.OnExportEncapsulationKeyCore = (Span source) => + { + PublicKey publicKey = PublicKey.CreateFromSubjectPublicKeyInfo(MLKemTestData.IetfMlKem512Spki, out _); + publicKey.EncodedKeyValue.RawData.AsSpan().CopyTo(source); + }; + + using (X509Certificate2 cert = pubOnly.CopyWithPrivateKey(contract)) + { + AssertExtensions.TrueExpression(cert.HasPrivateKey); + + using (MLKem kem = cert.GetMLKemPrivateKey()) + { + AssertExtensions.SequenceEqual( + MLKemTestData.IetfMlKem512PrivateKeyDecapsulationKey, + kem.ExportDecapsulationKey()); + } + } + } + } + private static void CheckCopyWithPrivateKey( X509Certificate2 cert, X509Certificate2 wrongAlgorithmCert,