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();
}
+ ///
+ /// Attempts to export the current key in the PKCS#8 PrivateKeyInfo format
+ /// into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the PKCS#8 PrivateKeyInfo value.
+ ///
+ ///
+ /// When this method returns, contains the number of bytes written to the buffer.
+ /// This parameter is treated as uninitialized.
+ ///
+ ///
+ /// if was large enough to hold the result;
+ /// otherwise, .
+ ///
+ ///
+ /// This instance has been disposed.
+ ///
+ ///
+ /// An error occurred while exporting the key.
+ ///
+ public bool TryExportPkcs8PrivateKey(Span 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);
+ }
+
+ ///
+ /// Export the current key in the PKCS#8 PrivateKeyInfo format.
+ ///
+ ///
+ /// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
+ ///
+ ///
+ /// This instance has been disposed.
+ ///
+ ///
+ /// An error occurred while exporting the key.
+ ///
+ 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.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.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.Shared.Return(buffer);
+ }
+ }
+
+ ///
+ /// When overridden in a derived class, attempts to export the current key in the PKCS#8 PrivateKeyInfo format
+ /// into the provided buffer.
+ ///
+ ///
+ /// The buffer to receive the PKCS#8 PrivateKeyInfo value.
+ ///
+ ///
+ /// When this method returns, contains the number of bytes written to the buffer.
+ ///
+ ///
+ /// if was large enough to hold the result;
+ /// otherwise, .
+ ///
+ ///
+ /// This instance has been disposed.
+ ///
+ ///
+ /// An error occurred while exporting the key.
+ ///
+ protected abstract bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten);
+
///
/// Imports an ML-KEM encapsulation key from an X.509 SubjectPublicKeyInfo structure.
///
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 destination)
Debug.Fail("Caller should have checked platform availability.");
throw new PlatformNotSupportedException();
}
+
+ protected override bool TryExportPkcs8PrivateKeyCore(Span 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 destination,
+ out int bytesWritten)
+ {
+ AlgorithmIdentifierAsn algorithmIdentifier = new()
+ {
+ Algorithm = kem.Algorithm.Oid,
+ Parameters = default(ReadOnlyMemory?),
+ };
+
+ MLKemPrivateKeyAsn privateKeyAsn = default;
+ byte[]? rented = null;
+ int written = 0;
+
+ try
+ {
+ if (hasSeed)
+ {
+ int seedSize = kem.Algorithm.PrivateSeedSizeInBytes;
+ rented = CryptoPool.Rent(seedSize);
+ Memory 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 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(() => kem.ExportPrivateSeed());
+ AssertExtensions.SequenceEqual(MLKemTestData.MLKem512DecapsulationKey, kem.ExportDecapsulationKey());
+ });
+ }
+
+ [Fact]
+ public void TryExportPkcs8PrivateKey_EncapsulationKey_Fails()
+ {
+ using MLKem kem = ImportEncapsulationKey(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512EncapsulationKey);
+ Assert.Throws(() => DoTryUntilDone(kem.TryExportPkcs8PrivateKey));
+ Assert.Throws(() => kem.ExportPkcs8PrivateKey());
+ }
+
+ private static void AssertExportPkcs8PrivateKey(MLKem kem, Action callback)
+ {
+ byte[] pkcs8 = DoTryUntilDone(kem.TryExportPkcs8PrivateKey);
+ callback(pkcs8);
+ callback(kem.ExportPkcs8PrivateKey());
+ }
+
+ private delegate bool TryExportFunc(Span 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 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(() => 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 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 destination, out int bytesWritten) =>
+ {
+ destination.Fill(0x88);
+ bytesWritten = destination.Length;
+ size = destination.Length;
+ return true;
+ }
+ };
+
+ byte[] exported = kem.ExportPkcs8PrivateKey();
+ AssertExtensions.FilledWith(0x88, exported);
+ Assert.Equal(size, exported.Length);
+ }
+
+ [Fact]
+ public static void ExportPkcs8PrivateKey_ExpandAndRetry()
+ {
+ const int TargetSize = 4567;
+ MLKemContract kem = new(MLKemAlgorithm.MLKem512)
+ {
+ OnTryExportPkcs8PrivateKeyCore = (Span 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(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 destination, out int bytesWritten) =>
+ {
+ // This is not possible and indiciates a derived type is misimplemented.
+ bytesWritten = destination.Length + 1;
+ return true;
+ }
+ };
+
+ Assert.Throws(() => kem.ExportPkcs8PrivateKey());
+ }
+
+ [Fact]
+ public static void ExportPkcs8PrivateKey_Disposed()
+ {
+ MLKemContract kem = new(MLKemAlgorithm.MLKem512);
+ kem.Dispose();
+ Assert.Throws(() => kem.ExportPkcs8PrivateKey());
+ Assert.Throws(() => 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 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 destination)
GetCallback(OnExportEncapsulationKeyCore)(destination);
}
+ protected override bool TryExportPkcs8PrivateKeyCore(Span 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 ciphertext, Span sharedSecret);
internal delegate void EncapsulateCoreCallback(Span ciphertext, Span sharedSecret);
internal delegate void ExportKeyCoreCallback(Span destination);
+ internal delegate bool TryExportPkcs8PrivateKeyCoreCallback(Span destination, out int bytesWritten);
private T GetCallback(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 destination) { }
public byte[] ExportEncapsulationKey() { throw null; }
public void ExportEncapsulationKey(System.Span destination) { }
protected abstract void ExportEncapsulationKeyCore(System.Span destination);
+ public byte[] ExportPkcs8PrivateKey() { throw null; }
public byte[] ExportPrivateSeed() { throw null; }
public void ExportPrivateSeed(System.Span destination) { }
protected abstract void ExportPrivateSeedCore(System.Span destination);
@@ -1898,6 +1899,8 @@ public void ExportPrivateSeed(System.Span destination) { }
public static System.Security.Cryptography.MLKem ImportSubjectPublicKeyInfo(byte[] source) { throw null; }
public static System.Security.Cryptography.MLKem ImportSubjectPublicKeyInfo(System.ReadOnlySpan source) { throw null; }
protected void ThrowIfDisposed() { }
+ public bool TryExportPkcs8PrivateKey(System.Span destination, out int bytesWritten) { throw null; }
+ protected abstract bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten);
public bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; }
}
[System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SYSLIB5006")]
@@ -1936,6 +1939,7 @@ protected override void EncapsulateCore(System.Span ciphertext, System.Spa
protected override void ExportDecapsulationKeyCore(System.Span destination) { }
protected override void ExportEncapsulationKeyCore(System.Span destination) { }
protected override void ExportPrivateSeedCore(System.Span destination) { }
+ protected override bool TryExportPkcs8PrivateKeyCore(System.Span 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" />
+
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 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 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 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 destination)
Interop.Crypto.EvpKemExportEncapsulationKey(_key, destination);
}
+ protected override bool TryExportPkcs8PrivateKeyCore(Span 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 destination)
Debug.Fail("Caller should have checked platform availability.");
throw new PlatformNotSupportedException();
}
+
+ protected override bool TryExportPkcs8PrivateKeyCore(Span 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 destination)
{
Interop.Crypto.EvpKemExportEncapsulationKey(_key, destination);
}
+
+ ///
+ protected override bool TryExportPkcs8PrivateKeyCore(Span 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;
+
///
/// Initializes a new instance of the class from an existing OpenSSL key
/// represented as an EVP_PKEY*.
@@ -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);
///
/// 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);