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);