Repository: dotnet/runtime
Pull request #114266: ML-KEM: PKCS#8 Exports
Author: login: vcsjones
Labels: name: area-System.Security
name: new-api-needs-documentation
Created: 2025-04-04T16:00:37Z | Merged: 2025-04-04T23:14:43Z

This adds PKCS#8 exports to `MLKem` and `MLKemOpenSsl`.

Contributes to #113508 


Diff:
diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kem.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kem.cs
index 39d1597bc6190a..7fec2cd26ea838 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kem.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kem.cs
@@ -34,7 +34,9 @@ private static partial int CryptoNative_EvpKemDecapsulate(
         [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpKemGetPalId")]
         private static partial int CryptoNative_EvpKemGetPalId(
             SafeEvpPKeyHandle kem,
-            out PalKemAlgorithmId kemId);
+            out PalKemAlgorithmId kemId,
+            out int hasSeed,
+            out int hasDecapsulationKey);
 
         [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpKemGeneratePkey", StringMarshalling = StringMarshalling.Utf8)]
         private static partial SafeEvpPKeyHandle CryptoNative_EvpKemGeneratePkey(
@@ -103,23 +105,31 @@ internal static SafeEvpPKeyHandle EvpKemGeneratePkey(string kemName, ReadOnlySpa
             return handle;
         }
 
-        internal static PalKemAlgorithmId EvpKemGetKemIdentifier(SafeEvpPKeyHandle key)
+        internal static PalKemAlgorithmId EvpKemGetKemIdentifier(
+            SafeEvpPKeyHandle key,
+            out bool hasSeed,
+            out bool hasDecapsulationKey)
         {
             const int Success = 1;
+            const int Yes = 1;
             const int Fail = 0;
-            int result = CryptoNative_EvpKemGetPalId(key, out PalKemAlgorithmId kemId);
-
-            return result switch
-            {
-                Success => kemId,
-                Fail => throw CreateOpenSslCryptographicException(),
-                int other => throw FailThrow(other),
-            };
+            int result = CryptoNative_EvpKemGetPalId(
+                key,
+                out PalKemAlgorithmId kemId,
+                out int pKeyHasSeed,
+                out int pKeyHasDecapsulationKey);
 
-            static Exception FailThrow(int result)
+            switch (result)
             {
-                Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_EvpKemGetPalId)}.");
-                return new CryptographicException();
+                case Success:
+                    hasSeed = pKeyHasSeed == Yes;
+                    hasDecapsulationKey = pKeyHasDecapsulationKey == Yes;
+                    return kemId;
+                case Fail:
+                    throw CreateOpenSslCryptographicException();
+                default:
+                    Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_EvpKemGetPalId)}.");
+                    throw new CryptographicException();
             }
         }
 
diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs b/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs
index a10bf42040b589..9d8e35812bfefe 100644
--- a/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs
+++ b/src/libraries/Common/src/System/Security/Cryptography/MLKem.cs
@@ -778,6 +778,115 @@ public byte[] ExportSubjectPublicKeyInfo()
             return ExportSubjectPublicKeyInfoCore().Encode();
         }
 
+        /// <summary>
+        ///   Attempts to export the current key in the PKCS#8 PrivateKeyInfo format
+        ///   into the provided buffer.
+        /// </summary>
+        /// <param name="destination">
+        ///   The buffer to receive the PKCS#8 PrivateKeyInfo value.
+        /// </param>
+        /// <param name="bytesWritten">
+        ///   When this method returns, contains the number of bytes written to the <paramref name="destination"/> buffer.
+        ///   This parameter is treated as uninitialized.
+        /// </param>
+        /// <returns>
+        ///   <see langword="true" /> if <paramref name="destination"/> was large enough to hold the result;
+        ///   otherwise, <see langword="false" />.
+        /// </returns>
+        /// <exception cref="ObjectDisposedException">
+        ///   This instance has been disposed.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   An error occurred while exporting the key.
+        /// </exception>
+        public bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten)
+        {
+            ThrowIfDisposed();
+
+            // An ML-KEM-512 "seed" export with no attributes is 86 bytes. A buffer smaller than that cannot hold a
+            // PKCS#8 encoded key. If we happen to get a buffer smaller than that, it won't export.
+            const int MinimumPossiblePkcs8MLKemKey = 86;
+
+            if (destination.Length < MinimumPossiblePkcs8MLKemKey)
+            {
+                bytesWritten = 0;
+                return false;
+            }
+
+            return TryExportPkcs8PrivateKeyCore(destination, out bytesWritten);
+        }
+
+        /// <summary>
+        ///   Export the current key in the PKCS#8 PrivateKeyInfo format.
+        /// </summary>
+        /// <returns>
+        ///   A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
+        /// </returns>
+        /// <exception cref="ObjectDisposedException">
+        ///   This instance has been disposed.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   An error occurred while exporting the key.
+        /// </exception>
+        public byte[] ExportPkcs8PrivateKey()
+        {
+            ThrowIfDisposed();
+
+            // A PKCS#8 ML-KEM-1024 ExpandedKey has an ASN.1 overhead of 28 bytes, assuming no attributes.
+            // Make it an even 32 and that should give a good starting point for a buffer size.
+            // Decapsulation keys are always larger than the seed, so if we end up with a seed export it should
+            // fit in the initial buffer.
+            int size = Algorithm.DecapsulationKeySizeInBytes + 32;
+            byte[] buffer = ArrayPool<byte>.Shared.Rent(size); // Released to callers, do not use CryptoPool.
+            int written;
+
+            while (!TryExportPkcs8PrivateKeyCore(buffer, out written))
+            {
+                ClearAndReturnToPool(buffer, written);
+                size = checked(size * 2);
+                buffer = ArrayPool<byte>.Shared.Rent(size);
+            }
+
+            if (written > buffer.Length)
+            {
+                // We got a nonsense value written back. Clear the buffer, but don't put it back in the pool.
+                CryptographicOperations.ZeroMemory(buffer);
+                throw new CryptographicException();
+            }
+
+            byte[] result = buffer.AsSpan(0, written).ToArray();
+            ClearAndReturnToPool(buffer, written);
+            return result;
+
+            static void ClearAndReturnToPool(byte[] buffer, int clearSize)
+            {
+                CryptographicOperations.ZeroMemory(buffer.AsSpan(0, clearSize));
+                ArrayPool<byte>.Shared.Return(buffer);
+            }
+        }
+
+        /// <summary>
+        ///   When overridden in a derived class, attempts to export the current key in the PKCS#8 PrivateKeyInfo format
+        ///   into the provided buffer.
+        /// </summary>
+        /// <param name="destination">
+        ///   The buffer to receive the PKCS#8 PrivateKeyInfo value.
+        /// </param>
+        /// <param name="bytesWritten">
+        ///   When this method returns, contains the number of bytes written to the <paramref name="destination"/> buffer.
+        /// </param>
+        /// <returns>
+        ///   <see langword="true" /> if <paramref name="destination"/> was large enough to hold the result;
+        ///   otherwise, <see langword="false" />.
+        /// </returns>
+        /// <exception cref="ObjectDisposedException">
+        ///   This instance has been disposed.
+        /// </exception>
+        /// <exception cref="CryptographicException">
+        ///   An error occurred while exporting the key.
+        /// </exception>
+        protected abstract bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten);
+
         /// <summary>
         ///  Imports an ML-KEM encapsulation key from an X.509 SubjectPublicKeyInfo structure.
         /// </summary>
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 2aa6517bc2d169..db5b67f8b7d2f8 100644
--- a/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.NotSupported.cs
+++ b/src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.NotSupported.cs
@@ -75,5 +75,11 @@ protected override void ExportEncapsulationKeyCore(Span<byte> destination)
             Debug.Fail("Caller should have checked platform availability.");
             throw new PlatformNotSupportedException();
         }
+
+        protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten)
+        {
+            Debug.Fail("Caller should have checked platform availability.");
+            throw new PlatformNotSupportedException();
+        }
     }
 }
diff --git a/src/libraries/Common/src/System/Security/Cryptography/MLKemPkcs8.cs b/src/libraries/Common/src/System/Security/Cryptography/MLKemPkcs8.cs
new file mode 100644
index 00000000000000..6a3fef4b221b90
--- /dev/null
+++ b/src/libraries/Common/src/System/Security/Cryptography/MLKemPkcs8.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 System.Formats.Asn1;
+using System.Security.Cryptography.Asn1;
+
+namespace System.Security.Cryptography
+{
+    internal static class MLKemPkcs8
+    {
+        internal static bool TryExportPkcs8PrivateKey(
+            MLKem kem,
+            bool hasSeed,
+            bool hasDecapsulationKey,
+            Span<byte> destination,
+            out int bytesWritten)
+        {
+            AlgorithmIdentifierAsn algorithmIdentifier = new()
+            {
+                Algorithm = kem.Algorithm.Oid,
+                Parameters = default(ReadOnlyMemory<byte>?),
+            };
+
+            MLKemPrivateKeyAsn privateKeyAsn = default;
+            byte[]? rented = null;
+            int written = 0;
+
+            try
+            {
+                if (hasSeed)
+                {
+                    int seedSize = kem.Algorithm.PrivateSeedSizeInBytes;
+                    rented = CryptoPool.Rent(seedSize);
+                    Memory<byte> buffer = rented.AsMemory(0, seedSize);
+                    kem.ExportPrivateSeed(buffer.Span);
+                    written = buffer.Length;
+                    privateKeyAsn.Seed = buffer;
+                }
+                else if (hasDecapsulationKey)
+                {
+                    int decapsulationKeySize = kem.Algorithm.DecapsulationKeySizeInBytes;
+                    rented = CryptoPool.Rent(decapsulationKeySize);
+                    Memory<byte> buffer = rented.AsMemory(0, decapsulationKeySize);
+                    kem.ExportDecapsulationKey(buffer.Span);
+                    written = buffer.Length;
+                    privateKeyAsn.ExpandedKey = buffer;
+                }
+                else
+                {
+                    throw new CryptographicException(SR.Cryptography_NotValidPrivateKey);
+                }
+
+                AsnWriter algorithmWriter = new(AsnEncodingRules.DER);
+                algorithmIdentifier.Encode(algorithmWriter);
+                AsnWriter privateKeyWriter = new(AsnEncodingRules.DER);
+                privateKeyAsn.Encode(privateKeyWriter);
+                AsnWriter pkcs8Writer = KeyFormatHelper.WritePkcs8(algorithmWriter, privateKeyWriter);
+
+                bool result = pkcs8Writer.TryEncode(destination, out bytesWritten);
+                privateKeyWriter.Reset();
+                pkcs8Writer.Reset();
+                return result;
+            }
+            finally
+            {
+                if (rented is not null)
+                {
+                    CryptoPool.Return(rented, written);
+                }
+            }
+        }
+    }
+}
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs
index a3709ea60b5d85..6f4007bf85b8fc 100644
--- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs
+++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs
@@ -459,6 +459,65 @@ public void Encapsulate_Overlaps_WhenTrimmed_Works()
             AssertExtensions.SequenceEqual(sharedSecret.Slice(0, sharedSecretWritten), decapsulated);
         }
 
+        [Fact]
+        public void TryExportPkcs8PrivateKey_Seed_Roundtrip()
+        {
+            using MLKem kem = ImportPrivateSeed(MLKemAlgorithm.MLKem512, MLKemTestData.IncrementalSeed);
+
+            AssertExportPkcs8PrivateKey(kem, pkcs8 =>
+            {
+                using MLKem imported = MLKem.ImportPkcs8PrivateKey(pkcs8);
+                Assert.Equal(MLKemAlgorithm.MLKem512, imported.Algorithm);
+                AssertExtensions.SequenceEqual(MLKemTestData.IncrementalSeed, kem.ExportPrivateSeed());
+            });
+        }
+
+        [Fact]
+        public void ExportPkcs8PrivateKey_DecapsulationKey_Roundtrip()
+        {
+            using MLKem kem = ImportDecapsulationKey(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512DecapsulationKey);
+
+            AssertExportPkcs8PrivateKey(kem, pkcs8 =>
+            {
+                using MLKem imported = MLKem.ImportPkcs8PrivateKey(pkcs8);
+                Assert.Equal(MLKemAlgorithm.MLKem512, imported.Algorithm);
+
+                Assert.Throws<CryptographicException>(() => kem.ExportPrivateSeed());
+                AssertExtensions.SequenceEqual(MLKemTestData.MLKem512DecapsulationKey, kem.ExportDecapsulationKey());
+            });
+        }
+
+        [Fact]
+        public void TryExportPkcs8PrivateKey_EncapsulationKey_Fails()
+        {
+            using MLKem kem = ImportEncapsulationKey(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512EncapsulationKey);
+            Assert.Throws<CryptographicException>(() => DoTryUntilDone(kem.TryExportPkcs8PrivateKey));
+            Assert.Throws<CryptographicException>(() => kem.ExportPkcs8PrivateKey());
+        }
+
+        private static void AssertExportPkcs8PrivateKey(MLKem kem, Action<byte[]> callback)
+        {
+            byte[] pkcs8 = DoTryUntilDone(kem.TryExportPkcs8PrivateKey);
+            callback(pkcs8);
+            callback(kem.ExportPkcs8PrivateKey());
+        }
+
+        private delegate bool TryExportFunc(Span<byte> destination, out int bytesWritten);
+
+        private static byte[] DoTryUntilDone(TryExportFunc func)
+        {
+            byte[] buffer = new byte[512];
+            int written;
+
+            while (!func(buffer, out written))
+            {
+                Array.Resize(ref buffer, buffer.Length * 2);
+            }
+
+            return buffer.AsSpan(0, written).ToArray();
+        }
+
+
         private static void Tamper(Span<byte> buffer)
         {
             buffer[buffer.Length - 1] ^= 0xFF;
diff --git a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs
index 07c8d498e6b087..a057b40d6e37e9 100644
--- a/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs
+++ b/src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs
@@ -789,6 +789,121 @@ public static void ExportSubjectPublicKeyInfo_Disposed()
             Assert.Throws<ObjectDisposedException>(() => kem.ExportSubjectPublicKeyInfo());
         }
 
+        [Fact]
+        public static void TryExportPkcs8PrivateKey_EarlyExitForSmallBuffer()
+        {
+            MLKemContract kem = new(MLKemAlgorithm.MLKem512);
+            byte[] destination = new byte[85];
+            AssertExtensions.FalseExpression(kem.TryExportPkcs8PrivateKey(destination, out int written));
+            Assert.Equal(0, written);
+        }
+
+        [Fact]
+        public static void TryExportPkcs8PrivateKey()
+        {
+            Random random;
+#if NET
+            random = Random.Shared;
+#else
+            random = new Random();
+#endif
+            int bufferSize = random.Next(87, 1024);
+            int writtenSize = random.Next(86, bufferSize);
+            bool success = (writtenSize & 1) == 1;
+            byte[] buffer = new byte[bufferSize];
+            MLKemContract kem = new(MLKemAlgorithm.MLKem512)
+            {
+                OnTryExportPkcs8PrivateKeyCore = (Span<byte> destination, out int bytesWritten) =>
+                {
+                    AssertSameBuffer(buffer, destination);
+                    bytesWritten = writtenSize;
+                    return success;
+                }
+            };
+
+            AssertExtensions.TrueExpression(success == kem.TryExportPkcs8PrivateKey(buffer, out int written));
+            Assert.Equal(writtenSize, written);
+        }
+
+        [Fact]
+        public static void ExportPkcs8PrivateKey_OneExportCall()
+        {
+            int size = -1;
+            MLKemContract kem = new(MLKemAlgorithm.MLKem512)
+            {
+                OnTryExportPkcs8PrivateKeyCore = (Span<byte> destination, out int bytesWritten) =>
+                {
+                    destination.Fill(0x88);
+                    bytesWritten = destination.Length;
+                    size = destination.Length;
+                    return true;
+                }
+            };
+
+            byte[] exported = kem.ExportPkcs8PrivateKey();
+            AssertExtensions.FilledWith<byte>(0x88, exported);
+            Assert.Equal(size, exported.Length);
+        }
+
+        [Fact]
+        public static void ExportPkcs8PrivateKey_ExpandAndRetry()
+        {
+            const int TargetSize = 4567;
+            MLKemContract kem = new(MLKemAlgorithm.MLKem512)
+            {
+                OnTryExportPkcs8PrivateKeyCore = (Span<byte> destination, out int bytesWritten) =>
+                {
+                    if (destination.Length < TargetSize)
+                    {
+                        bytesWritten = 0;
+                        return false;
+                    }
+
+                    destination.Fill(0x88);
+                    bytesWritten = TargetSize;
+                    return true;
+                }
+            };
+
+            byte[] exported = kem.ExportPkcs8PrivateKey();
+            AssertExtensions.FilledWith<byte>(0x88, exported);
+            Assert.Equal(TargetSize, exported.Length);
+
+            // The exact number of calls that made varies depending on the behavior of how the ArrayPool
+            // behaves. Though the algorithm is to double the buffer size, the pool may rent more than requested from
+            // the doubling. However we know it should be more than one.
+            AssertExtensions.GreaterThan(kem.TryExportPkcs8PrivateKeyCoreCount, 1);
+
+            // If the implementation follows a doubling scheme exactly, the ML-KEM 512 decapsulation key size
+            // should take no more than 3 calls to reach 4567. The initial size is 1,664 bytes.
+            AssertExtensions.LessThan(kem.TryExportPkcs8PrivateKeyCoreCount, 4);
+        }
+
+        [Fact]
+        public static void ExportPkcs8PrivateKey_MisbehavingBytesWritten()
+        {
+            MLKemContract kem = new(MLKemAlgorithm.MLKem512)
+            {
+                OnTryExportPkcs8PrivateKeyCore = (Span<byte> destination, out int bytesWritten) =>
+                {
+                    // This is not possible and indiciates a derived type is misimplemented.
+                    bytesWritten = destination.Length + 1;
+                    return true;
+                }
+            };
+
+            Assert.Throws<CryptographicException>(() => kem.ExportPkcs8PrivateKey());
+        }
+
+        [Fact]
+        public static void ExportPkcs8PrivateKey_Disposed()
+        {
+            MLKemContract kem = new(MLKemAlgorithm.MLKem512);
+            kem.Dispose();
+            Assert.Throws<ObjectDisposedException>(() => kem.ExportPkcs8PrivateKey());
+            Assert.Throws<ObjectDisposedException>(() => kem.TryExportPkcs8PrivateKey(new byte[512], out _));
+        }
+
         private static string MapAlgorithmOid(MLKemAlgorithm algorithm)
         {
             if (algorithm == MLKemAlgorithm.MLKem512)
@@ -860,13 +975,15 @@ internal sealed class MLKemContract : MLKem
         internal ExportKeyCoreCallback OnExportPrivateSeedCore { get; set; }
         internal ExportKeyCoreCallback OnExportEncapsulationKeyCore { get; set; }
         internal ExportKeyCoreCallback OnExportDecapsulationKeyCore { get; set; }
+        internal TryExportPkcs8PrivateKeyCoreCallback OnTryExportPkcs8PrivateKeyCore { get; set; }
         internal Action<bool> OnDispose { get; set; } = (bool disposing) => { };
 
-        private int DecapsulateCoreCount { get; set; }
-        private int EncapsulateCoreCount { get; set; }
-        private int ExportPrivateSeedCoreCount { get; set; }
-        private int ExportEncapsulationKeyCoreCount { get; set; }
-        private int ExportDecapsulationKeyCoreCount { get; set; }
+        internal int DecapsulateCoreCount { get; set; }
+        internal int EncapsulateCoreCount { get; set; }
+        internal int ExportPrivateSeedCoreCount { get; set; }
+        internal int ExportEncapsulationKeyCoreCount { get; set; }
+        internal int ExportDecapsulationKeyCoreCount { get; set; }
+        internal int TryExportPkcs8PrivateKeyCoreCount { get; set; }
 
         private bool _disposed;
 
@@ -904,6 +1021,12 @@ protected override void ExportEncapsulationKeyCore(Span<byte> destination)
             GetCallback(OnExportEncapsulationKeyCore)(destination);
         }
 
+        protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten)
+        {
+            TryExportPkcs8PrivateKeyCoreCount++;
+            return GetCallback(OnTryExportPkcs8PrivateKeyCore)(destination, out bytesWritten);
+        }
+
         protected override void Dispose(bool disposing)
         {
             GetCallback(OnDispose)(disposing);
@@ -933,11 +1056,16 @@ private void VerifyCalledOnDispose()
             {
                 Assert.Fail($"Expected call to {nameof(ExportEncapsulationKeyCore)}.");
             }
+            if (OnTryExportPkcs8PrivateKeyCore is not null && TryExportPkcs8PrivateKeyCoreCount == 0)
+            {
+                Assert.Fail($"Expected call to {nameof(TryExportPkcs8PrivateKeyCore)}.");
+            }
         }
 
         internal delegate void DecapsulateCoreCallback(ReadOnlySpan<byte> ciphertext, Span<byte> sharedSecret);
         internal delegate void EncapsulateCoreCallback(Span<byte> ciphertext, Span<byte> sharedSecret);
         internal delegate void ExportKeyCoreCallback(Span<byte> destination);
+        internal delegate bool TryExportPkcs8PrivateKeyCoreCallback(Span<byte> destination, out int bytesWritten);
 
         private T GetCallback<T>(T callback, [CallerMemberNameAttribute]string caller = null) where T : Delegate
         {
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 a2775eeac3fc6f..0930925517942f 100644
--- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs
+++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs
@@ -1874,6 +1874,7 @@ public void ExportDecapsulationKey(System.Span<byte> destination) { }
         public byte[] ExportEncapsulationKey() { throw null; }
         public void ExportEncapsulationKey(System.Span<byte> destination) { }
         protected abstract void ExportEncapsulationKeyCore(System.Span<byte> destination);
+        public byte[] ExportPkcs8PrivateKey() { throw null; }
         public byte[] ExportPrivateSeed() { throw null; }
         public void ExportPrivateSeed(System.Span<byte> destination) { }
         protected abstract void ExportPrivateSeedCore(System.Span<byte> destination);
@@ -1898,6 +1899,8 @@ public void ExportPrivateSeed(System.Span<byte> destination) { }
         public static System.Security.Cryptography.MLKem ImportSubjectPublicKeyInfo(byte[] source) { throw null; }
         public static System.Security.Cryptography.MLKem ImportSubjectPublicKeyInfo(System.ReadOnlySpan<byte> source) { throw null; }
         protected void ThrowIfDisposed() { }
+        public bool TryExportPkcs8PrivateKey(System.Span<byte> destination, out int bytesWritten) { throw null; }
+        protected abstract bool TryExportPkcs8PrivateKeyCore(System.Span<byte> destination, out int bytesWritten);
         public bool TryExportSubjectPublicKeyInfo(System.Span<byte> destination, out int bytesWritten) { throw null; }
     }
     [System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")]
@@ -1936,6 +1939,7 @@ protected override void EncapsulateCore(System.Span<byte> ciphertext, System.Spa
         protected override void ExportDecapsulationKeyCore(System.Span<byte> destination) { }
         protected override void ExportEncapsulationKeyCore(System.Span<byte> destination) { }
         protected override void ExportPrivateSeedCore(System.Span<byte> destination) { }
+        protected override bool TryExportPkcs8PrivateKeyCore(System.Span<byte> destination, out int bytesWritten) { throw null; }
     }
     public sealed partial class Oid
     {
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 8ffe8bc69f2488..dceaa60c3eb3ea 100644
--- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj
+++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj
@@ -400,6 +400,8 @@
             Link="Common\System\Security\Cryptography\MLKem.cs" />
     <Compile Include="$(CommonPath)System\Security\Cryptography\MLKemAlgorithm.cs"
             Link="Common\System\Security\Cryptography\MLKemAlgorithm.cs" />
+    <Compile Include="$(CommonPath)System\Security\Cryptography\MLKemPkcs8.cs"
+            Link="Common\System\Security\Cryptography\MLKemPkcs8.cs" />
     <Compile Include="$(CommonPath)System\Security\Cryptography\Oids.cs"
              Link="Common\System\Security\Cryptography\Oids.cs" />
     <Compile Include="$(CommonPath)System\Security\Cryptography\Oids.Shared.cs"
diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemImplementation.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemImplementation.OpenSsl.cs
index 1ce4ed9c1aecea..66bf1410e2e6d2 100644
--- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemImplementation.OpenSsl.cs
+++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemImplementation.OpenSsl.cs
@@ -13,9 +13,18 @@ internal sealed class MLKemImplementation : MLKem
         // OpenSSL is expected to give "all or none" support.
         internal static new bool IsSupported => Interop.Crypto.EvpKemAlgs.MlKem512 is not null;
 
-        private MLKemImplementation(MLKemAlgorithm algorithm, SafeEvpPKeyHandle key) : base(algorithm)
+        private readonly bool _hasSeed;
+        private readonly bool _hasDecapsulationKey;
+
+        private MLKemImplementation(
+            MLKemAlgorithm algorithm,
+            SafeEvpPKeyHandle key,
+            bool hasSeed,
+            bool hasDecapsulationKey) : base(algorithm)
         {
             _key = key;
+            _hasSeed = hasSeed;
+            _hasDecapsulationKey = hasDecapsulationKey;
         }
 
         internal static MLKem GenerateKeyImpl(MLKemAlgorithm algorithm)
@@ -23,7 +32,7 @@ internal static MLKem GenerateKeyImpl(MLKemAlgorithm algorithm)
             Debug.Assert(IsSupported);
             string kemName = MapAlgorithmToName(algorithm);
             SafeEvpPKeyHandle key = Interop.Crypto.EvpKemGeneratePkey(kemName);
-            return new MLKemImplementation(algorithm, key);
+            return new MLKemImplementation(algorithm, key, hasSeed: true, hasDecapsulationKey: true);
         }
 
         internal static MLKem ImportPrivateSeedImpl(MLKemAlgorithm algorithm, ReadOnlySpan<byte> source)
@@ -32,7 +41,7 @@ internal static MLKem ImportPrivateSeedImpl(MLKemAlgorithm algorithm, ReadOnlySp
             Debug.Assert(source.Length == algorithm.PrivateSeedSizeInBytes);
             string kemName = MapAlgorithmToName(algorithm);
             SafeEvpPKeyHandle key = Interop.Crypto.EvpKemGeneratePkey(kemName, source);
-            return new MLKemImplementation(algorithm, key);
+            return new MLKemImplementation(algorithm, key, hasSeed: true, hasDecapsulationKey: true);
         }
 
         internal static MLKem ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan<byte> source)
@@ -41,7 +50,7 @@ internal static MLKem ImportDecapsulationKeyImpl(MLKemAlgorithm algorithm, ReadO
             Debug.Assert(source.Length == algorithm.DecapsulationKeySizeInBytes);
             string kemName = MapAlgorithmToName(algorithm);
             SafeEvpPKeyHandle key = Interop.Crypto.EvpPKeyFromData(kemName, source, privateKey: true);
-            return new MLKemImplementation(algorithm, key);
+            return new MLKemImplementation(algorithm, key, hasSeed: false, hasDecapsulationKey: true);
         }
 
         internal static MLKem ImportEncapsulationKeyImpl(MLKemAlgorithm algorithm, ReadOnlySpan<byte> source)
@@ -50,7 +59,7 @@ internal static MLKem ImportEncapsulationKeyImpl(MLKemAlgorithm algorithm, ReadO
             Debug.Assert(source.Length == algorithm.EncapsulationKeySizeInBytes);
             string kemName = MapAlgorithmToName(algorithm);
             SafeEvpPKeyHandle key = Interop.Crypto.EvpPKeyFromData(kemName, source, privateKey: false);
-            return new MLKemImplementation(algorithm, key);
+            return new MLKemImplementation(algorithm, key, hasSeed: false, hasDecapsulationKey: false);
         }
 
         protected override void Dispose(bool disposing)
@@ -88,6 +97,16 @@ protected override void ExportEncapsulationKeyCore(Span<byte> destination)
             Interop.Crypto.EvpKemExportEncapsulationKey(_key, destination);
         }
 
+        protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten)
+        {
+            return MLKemPkcs8.TryExportPkcs8PrivateKey(
+                this,
+                _hasSeed,
+                _hasDecapsulationKey,
+                destination,
+                out bytesWritten);
+        }
+
         private static string MapAlgorithmToName(MLKemAlgorithm algorithm)
         {
             string? name = null;
diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.NotSupported.cs
index 56c8a55405a0ec..cd9fd90cd0e0f8 100644
--- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.NotSupported.cs
+++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.NotSupported.cs
@@ -7,14 +7,11 @@ namespace System.Security.Cryptography
 {
     public sealed partial class MLKemOpenSsl : MLKem
     {
-#pragma warning disable CA1822 // Member does not access instance data and can be marked static
-        private partial void Initialize(SafeEvpPKeyHandle upRefHandle)
-#pragma warning restore CA1822
-        {
-            throw new PlatformNotSupportedException();
-        }
-
-        private static partial MLKemAlgorithm AlgorithmFromHandle(SafeEvpPKeyHandle pkeyHandle, out SafeEvpPKeyHandle upRefHandle)
+        private static partial MLKemAlgorithm AlgorithmFromHandle(
+            SafeEvpPKeyHandle pkeyHandle,
+            out SafeEvpPKeyHandle upRefHandle,
+            out bool hasSeed,
+            out bool hasDecapsulationKey)
         {
             throw new PlatformNotSupportedException();
         }
@@ -60,5 +57,11 @@ protected override void ExportEncapsulationKeyCore(Span<byte> destination)
             Debug.Fail("Caller should have checked platform availability.");
             throw new PlatformNotSupportedException();
         }
+
+        protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten)
+        {
+            Debug.Fail("Caller should have checked platform availability.");
+            throw new PlatformNotSupportedException();
+        }
     }
 }
diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.OpenSsl.cs
index 71aee4ead2db7a..034bbb7bafe326 100644
--- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.OpenSsl.cs
+++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.OpenSsl.cs
@@ -2,7 +2,6 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
 using System.Runtime.Versioning;
 using Microsoft.Win32.SafeHandles;
 
@@ -10,25 +9,27 @@ namespace System.Security.Cryptography
 {
     public sealed partial class MLKemOpenSsl
     {
-        private SafeEvpPKeyHandle _key;
-
-        [MemberNotNull(nameof(_key))]
-        private partial void Initialize(SafeEvpPKeyHandle upRefHandle) => _key = upRefHandle;
-
         public partial SafeEvpPKeyHandle DuplicateKeyHandle()
         {
             ThrowIfDisposed();
             return _key.DuplicateHandle();
         }
 
-        private static partial MLKemAlgorithm AlgorithmFromHandle(SafeEvpPKeyHandle pkeyHandle, out SafeEvpPKeyHandle upRefHandle)
+        private static partial MLKemAlgorithm AlgorithmFromHandle(
+            SafeEvpPKeyHandle pkeyHandle,
+            out SafeEvpPKeyHandle upRefHandle,
+            out bool hasSeed,
+            out bool hasDecapsulationKey)
         {
             ArgumentNullException.ThrowIfNull(pkeyHandle);
             upRefHandle = pkeyHandle.DuplicateHandle();
 
             try
             {
-                Interop.Crypto.PalKemAlgorithmId kemId = Interop.Crypto.EvpKemGetKemIdentifier(upRefHandle);
+                Interop.Crypto.PalKemAlgorithmId kemId = Interop.Crypto.EvpKemGetKemIdentifier(
+                    upRefHandle,
+                    out hasSeed,
+                    out hasDecapsulationKey);
 
                 switch (kemId)
                 {
@@ -89,5 +90,16 @@ protected override void ExportEncapsulationKeyCore(Span<byte> destination)
         {
             Interop.Crypto.EvpKemExportEncapsulationKey(_key, destination);
         }
+
+        /// <inheritdoc />
+        protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten)
+        {
+            return MLKemPkcs8.TryExportPkcs8PrivateKey(
+                this,
+                _hasSeed,
+                _hasDecapsulationKey,
+                destination,
+                out bytesWritten);
+        }
     }
 }
diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.cs
index a2c4a6e8e588a6..68121d039204b0 100644
--- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.cs
+++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl.cs
@@ -23,6 +23,10 @@ namespace System.Security.Cryptography
     [Experimental(Experimentals.PostQuantumCryptographyDiagId)]
     public sealed partial class MLKemOpenSsl : MLKem
     {
+        private readonly SafeEvpPKeyHandle _key;
+        private readonly bool _hasSeed;
+        private readonly bool _hasDecapsulationKey;
+
         /// <summary>
         ///   Initializes a new instance of the <see cref="MLKemOpenSsl" /> class from an existing OpenSSL key
         ///   represented as an <c>EVP_PKEY*</c>.
@@ -47,15 +51,19 @@ public sealed partial class MLKemOpenSsl : MLKem
         [UnsupportedOSPlatform("osx")]
         [UnsupportedOSPlatform("tvos")]
         [UnsupportedOSPlatform("windows")]
-        public MLKemOpenSsl(SafeEvpPKeyHandle pkeyHandle) : base(AlgorithmFromHandle(pkeyHandle, out SafeEvpPKeyHandle upRefHandle))
+        public MLKemOpenSsl(SafeEvpPKeyHandle pkeyHandle) : base(
+            AlgorithmFromHandle(pkeyHandle, out SafeEvpPKeyHandle upRefHandle, out bool hasSeed, out bool hasDecapsulationKey))
         {
-            Initialize(upRefHandle);
+            _key = upRefHandle;
+            _hasSeed = hasSeed;
+            _hasDecapsulationKey = hasDecapsulationKey;
         }
 
-        // This partial can go away if partial constructors are available.
-        // https://github.com/dotnet/csharplang/issues/9058
-        private partial void Initialize(SafeEvpPKeyHandle upRefHandle);
-        private static partial MLKemAlgorithm AlgorithmFromHandle(SafeEvpPKeyHandle pkeyHandle, out SafeEvpPKeyHandle upRefHandle);
+        private static partial MLKemAlgorithm AlgorithmFromHandle(
+            SafeEvpPKeyHandle pkeyHandle,
+            out SafeEvpPKeyHandle upRefHandle,
+            out bool hasSeed,
+            out bool hasDecapsulationKey);
 
         /// <summary>
         /// Creates a duplicate handle.
diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.c b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.c
index d432d055319e6e..29ec92a988fba6 100644
--- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.c
+++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.c
@@ -30,10 +30,10 @@ int32_t CryptoNative_EvpKemAvailable(const char* algorithm)
     return 0;
 }
 
-int32_t CryptoNative_EvpKemGetPalId(const EVP_PKEY* pKey, int32_t* kemId)
+int32_t CryptoNative_EvpKemGetPalId(const EVP_PKEY* pKey, int32_t* kemId, int32_t* hasSeed, int32_t* hasDecapsulationKey)
 {
 #ifdef NEED_OPENSSL_3_0
-    assert(pKey && kemId);
+    assert(pKey && kemId && hasSeed && hasDecapsulationKey);
 
     if (API_EXISTS(EVP_PKEY_is_a))
     {
@@ -54,14 +54,20 @@ int32_t CryptoNative_EvpKemGetPalId(const EVP_PKEY* pKey, int32_t* kemId)
         else
         {
             *kemId = PalKemId_Unknown;
+            *hasSeed = 0;
+            *hasDecapsulationKey = 0;
+            return 1;
         }
 
+        *hasSeed = EvpPKeyHasKeyOctetStringParam(pKey, OSSL_PKEY_PARAM_ML_KEM_SEED);
+        *hasDecapsulationKey = EvpPKeyHasKeyOctetStringParam(pKey, OSSL_PKEY_PARAM_PRIV_KEY);
         return 1;
     }
 #endif
-
     (void)pKey;
     *kemId = PalKemId_Unknown;
+    *hasSeed = 0;
+    *hasDecapsulationKey = 0;
     return 0;
 }
 
diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.h
index 269fccc14ffb92..eda8c054f67fa3 100644
--- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.h
+++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.h
@@ -14,7 +14,10 @@ typedef enum
 } PalKemId;
 
 PALEXPORT int32_t CryptoNative_EvpKemAvailable(const char* algorithm);
-PALEXPORT int32_t CryptoNative_EvpKemGetPalId(const EVP_PKEY* pKey, int32_t* kemId);
+PALEXPORT int32_t CryptoNative_EvpKemGetPalId(const EVP_PKEY* pKey,
+                                              int32_t* kemId,
+                                              int32_t* hasSeed,
+                                              int32_t* hasDecapsulationKey);
 PALEXPORT EVP_PKEY* CryptoNative_EvpKemGeneratePkey(const char* kemName, uint8_t* seed, int32_t seedLength);
 PALEXPORT int32_t CryptoNative_EvpKemExportPrivateSeed(const EVP_PKEY* pKey, uint8_t* destination, int32_t destinationLength);
 PALEXPORT int32_t CryptoNative_EvpKemExportDecapsulationKey(const EVP_PKEY* pKey, uint8_t* destination, int32_t destinationLength);
diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.c b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.c
index 25dc5cd42270e7..a7477ff9f72514 100644
--- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.c
+++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.c
@@ -828,6 +828,35 @@ EVP_PKEY* CryptoNative_EvpPKeyFromData(const char* algorithmName, uint8_t* key,
     return NULL;
 }
 
+int32_t EvpPKeyHasKeyOctetStringParam(const EVP_PKEY* pKey, const char* name)
+{
+    assert(pKey);
+    assert(name);
+
+#ifdef NEED_OPENSSL_3_0
+    if (API_EXISTS(EVP_PKEY_get_octet_string_param))
+    {
+        ERR_clear_error();
+        size_t outLength = 0;
+
+        int ret = EVP_PKEY_get_octet_string_param(pKey, name, NULL, 0, &outLength);
+
+        if (ret == 1)
+        {
+            return outLength > 0 ? 1 : 0;
+        }
+        else
+        {
+            return 0;
+        }
+    }
+#endif
+
+    (void)pKey;
+    (void)name;
+    return 0;
+}
+
 int32_t EvpPKeyGetKeyOctetStringParam(const EVP_PKEY* pKey,
                                       const char* name,
                                       uint8_t* destination,
diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.h
index a07f7fb55cd9df..d0aea8c196e3c5 100644
--- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.h
+++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.h
@@ -147,3 +147,8 @@ int32_t EvpPKeyGetKeyOctetStringParam(const EVP_PKEY* pKey,
     const char* name,
     uint8_t* destination,
     int32_t destinationLength);
+
+/*
+Internal function to determine if an EVP_PKEY has a given octet string property.
+*/
+int32_t EvpPKeyHasKeyOctetStringParam(const EVP_PKEY* pKey, const char* name);


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds support for PKCS#8 key exports to MLKem and MLKemOpenSsl by updating the native interop functions, managed API, and test coverage. Key changes include modifications in the native lib functions for extracting key parameters, updates to MLKemOpenSsl and MLKem implementation to propagate seed and decapsulation key information, and new tests for PKCS#8 exporting functionality.

### Reviewed Changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey.[ch] | Added a new function to check for an octet string parameter in EVP_PKEY. |
| src/native/libs/System.Security.Cryptography.Native/pal_evp_kem.[ch] | Updated the KEM functions to report additional key attribute information. |
| src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemOpenSsl*.cs | Modified MLKemOpenSsl to consume the additional outputs for seed and decapsulation key. |
| src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/MLKemImplementation.OpenSsl.cs | Updated key generation and import methods to pass constant attribute values. |
| src/libraries/System/Security/Cryptography/MLKem.cs and ref/System.Security.Cryptography.cs | Added the TryExportPkcs8PrivateKey API and expanded buffer export logic. |
| src/libraries/Common/* | Added the MLKemPkcs8 helper and updated tests to cover the new PKCS#8 key export functionality. |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/libraries/Common/src/System/Security/Cryptography/MLKemPkcs8.cs:30**
* If both 'hasSeed' and 'hasDecapsulationKey' are true, the current logic prioritizes using the seed for PKCS#8 export. Please document this precedence explicitly to improve clarity for future maintainers.
```
if (hasSeed)
```
**src/libraries/Common/src/System/Security/Cryptography/MLKem.cs:808**
* [nitpick] Consider replacing the magic number with a more descriptive constant name or adding a clarifying comment that explains why 86 bytes is the minimum size required for a PKCS#8 ML-KEM key export.
```
const int MinimumPossiblePkcs8MLKemKey = 86;
```
</details>

**Tip:** Copilot code review supports C#, Go, Java, JavaScript, Markdown, Python, Ruby and TypeScript, with more languages coming soon.  [Learn more](https://gh.io/copilot-code-reviews-docs)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T16:01:02Z
user: login: bartonjs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T17:09:21Z
user: login: bartonjs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T17:22:34Z
user: login: bartonjs
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T17:27:59Z
user: login: vcsjones
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T18:30:56Z
user: login: bartonjs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T18:32:09Z

Review comments:
diff_hunk: @@ -789,6 +789,115 @@ public static void ExportSubjectPublicKeyInfo_Disposed()
             Assert.Throws<ObjectDisposedException>(() => kem.ExportSubjectPublicKeyInfo());
         }
 
+        [Fact]
+        public static void TryExportPkcs8PrivateKey_EarlyExitForSmallBuffer()
+        {
+            MLKemContract kem = new(MLKemAlgorithm.MLKem512);
+            byte[] destination = new byte[85];
+            AssertExtensions.FalseExpression(kem.TryExportPkcs8PrivateKey(destination, out int written));
+            Assert.Equal(0, written);
+        }
+
+        [Fact]
+        public static void TryExportPkcs8PrivateKey()
+        {
+            int bufferSize = RandomNumberGenerator.GetInt32(87, 1024);
+            int writtenSize = RandomNumberGenerator.GetInt32(86, bufferSize);
+            bool success = (writtenSize & 1) == 1;
path: src/libraries/Common/tests/System/Security/Cryptography/MLKemContractTests.cs
user: login: bartonjs
body: OK... this took a bit of thought, but I see it.

* Make a random length buffer.
* Decide how many bytes were written, not exceeding the buffer.  (Because the upper bound is exclusive, it'll actually never say buffer.Length)
* Flip a coin (more or less) to decide if we return success.

So then the callback asserts it saw the fully sized buffer (no Slicing by the caller), reports the bytesWritten, and success (or not).

Now, back in the test method, assert that the things we wanted to return got returned.

Much better than where I started: "uh... why would Kevin expect that ML-KEM P8 exports are always odd lengthed?"
created_at: 2025-04-04T17:09:21Z
author_association: MEMBER
diff_hunk: @@ -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 System.Formats.Asn1;
+using System.Security.Cryptography.Asn1;
+
+namespace System.Security.Cryptography
+{
+    internal static class MLKemPkcs8
+    {
+        internal static bool TryExportPkcs8PrivateKey(
+            MLKem kem,
+            bool hasSeed,
+            bool hasDecapsulationKey,
path: src/libraries/Common/src/System/Security/Cryptography/MLKemPkcs8.cs
user: login: bartonjs
body: Two bools is... fine, but every time I saw it I was thinking "this is really just one value".

```C#
internal enum SeededKeyState
{
    PublicOnly = 0,
    PrivateOnly = 1,
    Seed = 2,
}
```

(or flip 1 and 2).

Because `hasSeed:true` implies `hasDecapsulationKey: true`.

Yeah, the ldfld is cheap, and the registers are below the standard callconv count... but it's a thought I had every time, and decided to hold my tongue on.... until right before hitting approve, I guess :smile:
created_at: 2025-04-04T17:22:34Z
author_association: MEMBER
diff_hunk: @@ -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 System.Formats.Asn1;
+using System.Security.Cryptography.Asn1;
+
+namespace System.Security.Cryptography
+{
+    internal static class MLKemPkcs8
+    {
+        internal static bool TryExportPkcs8PrivateKey(
+            MLKem kem,
+            bool hasSeed,
+            bool hasDecapsulationKey,
path: src/libraries/Common/src/System/Security/Cryptography/MLKemPkcs8.cs
user: login: vcsjones
body: Your logic is correct but I decided to treat them as distinct value on purpose because I don't know, or want to depend, on what a 3rd party OpenSSL Provider can do. Maybe there is some provider out there that supports retrieving the seed but not the decapsulation key from the EVP_PKEY. Is that sane, probably not. Will it work? I am skeptical. But I didn't want to make an assumption.

I might be overly cautious here, but that was the thought process.
created_at: 2025-04-04T18:30:56Z
author_association: MEMBER
diff_hunk: @@ -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 System.Formats.Asn1;
+using System.Security.Cryptography.Asn1;
+
+namespace System.Security.Cryptography
+{
+    internal static class MLKemPkcs8
+    {
+        internal static bool TryExportPkcs8PrivateKey(
+            MLKem kem,
+            bool hasSeed,
+            bool hasDecapsulationKey,
path: src/libraries/Common/src/System/Security/Cryptography/MLKemPkcs8.cs
user: login: bartonjs
body: Fair enough, I suppose :smile:
created_at: 2025-04-04T18:32:09Z
author_association: MEMBER

PR comments:
user: login: ghost
created_at: 2025-04-04T16:00:45Z
body: Note regarding the `new-api-needs-documentation` label:

    This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.
author_association: NONE
user: login: ghost
created_at: 2025-04-04T16:00:47Z
body: Note regarding the `new-api-needs-documentation` label:

    This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.
author_association: NONE
user: login: dotnet-policy-service[bot]
created_at: 2025-04-04T16:01:29Z
body: Tagging subscribers to this area: @dotnet/area-system-security, @bartonjs, @vcsjones
See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
<!-- Policy app identification https://img.shields.io/static/v1?label=PullRequestIssueManagement. -->
author_association: CONTRIBUTOR