Skip to content

Commit a739534

Browse files
authored
ML-KEM: PKCS#8 Exports
Adds `ExportPkcs8PrivateKey`, `TryExportPkcs8PrivateKey`, and `TryExportPkcs8PrivateKeyCore` to `MLKem`.
1 parent 283b64d commit a739534

File tree

16 files changed

+525
-49
lines changed

16 files changed

+525
-49
lines changed

src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Kem.cs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ private static partial int CryptoNative_EvpKemDecapsulate(
3434
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpKemGetPalId")]
3535
private static partial int CryptoNative_EvpKemGetPalId(
3636
SafeEvpPKeyHandle kem,
37-
out PalKemAlgorithmId kemId);
37+
out PalKemAlgorithmId kemId,
38+
out int hasSeed,
39+
out int hasDecapsulationKey);
3840

3941
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpKemGeneratePkey", StringMarshalling = StringMarshalling.Utf8)]
4042
private static partial SafeEvpPKeyHandle CryptoNative_EvpKemGeneratePkey(
@@ -103,23 +105,31 @@ internal static SafeEvpPKeyHandle EvpKemGeneratePkey(string kemName, ReadOnlySpa
103105
return handle;
104106
}
105107

106-
internal static PalKemAlgorithmId EvpKemGetKemIdentifier(SafeEvpPKeyHandle key)
108+
internal static PalKemAlgorithmId EvpKemGetKemIdentifier(
109+
SafeEvpPKeyHandle key,
110+
out bool hasSeed,
111+
out bool hasDecapsulationKey)
107112
{
108113
const int Success = 1;
114+
const int Yes = 1;
109115
const int Fail = 0;
110-
int result = CryptoNative_EvpKemGetPalId(key, out PalKemAlgorithmId kemId);
111-
112-
return result switch
113-
{
114-
Success => kemId,
115-
Fail => throw CreateOpenSslCryptographicException(),
116-
int other => throw FailThrow(other),
117-
};
116+
int result = CryptoNative_EvpKemGetPalId(
117+
key,
118+
out PalKemAlgorithmId kemId,
119+
out int pKeyHasSeed,
120+
out int pKeyHasDecapsulationKey);
118121

119-
static Exception FailThrow(int result)
122+
switch (result)
120123
{
121-
Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_EvpKemGetPalId)}.");
122-
return new CryptographicException();
124+
case Success:
125+
hasSeed = pKeyHasSeed == Yes;
126+
hasDecapsulationKey = pKeyHasDecapsulationKey == Yes;
127+
return kemId;
128+
case Fail:
129+
throw CreateOpenSslCryptographicException();
130+
default:
131+
Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_EvpKemGetPalId)}.");
132+
throw new CryptographicException();
123133
}
124134
}
125135

src/libraries/Common/src/System/Security/Cryptography/MLKem.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,115 @@ public byte[] ExportSubjectPublicKeyInfo()
778778
return ExportSubjectPublicKeyInfoCore().Encode();
779779
}
780780

781+
/// <summary>
782+
/// Attempts to export the current key in the PKCS#8 PrivateKeyInfo format
783+
/// into the provided buffer.
784+
/// </summary>
785+
/// <param name="destination">
786+
/// The buffer to receive the PKCS#8 PrivateKeyInfo value.
787+
/// </param>
788+
/// <param name="bytesWritten">
789+
/// When this method returns, contains the number of bytes written to the <paramref name="destination"/> buffer.
790+
/// This parameter is treated as uninitialized.
791+
/// </param>
792+
/// <returns>
793+
/// <see langword="true" /> if <paramref name="destination"/> was large enough to hold the result;
794+
/// otherwise, <see langword="false" />.
795+
/// </returns>
796+
/// <exception cref="ObjectDisposedException">
797+
/// This instance has been disposed.
798+
/// </exception>
799+
/// <exception cref="CryptographicException">
800+
/// An error occurred while exporting the key.
801+
/// </exception>
802+
public bool TryExportPkcs8PrivateKey(Span<byte> destination, out int bytesWritten)
803+
{
804+
ThrowIfDisposed();
805+
806+
// An ML-KEM-512 "seed" export with no attributes is 86 bytes. A buffer smaller than that cannot hold a
807+
// PKCS#8 encoded key. If we happen to get a buffer smaller than that, it won't export.
808+
const int MinimumPossiblePkcs8MLKemKey = 86;
809+
810+
if (destination.Length < MinimumPossiblePkcs8MLKemKey)
811+
{
812+
bytesWritten = 0;
813+
return false;
814+
}
815+
816+
return TryExportPkcs8PrivateKeyCore(destination, out bytesWritten);
817+
}
818+
819+
/// <summary>
820+
/// Export the current key in the PKCS#8 PrivateKeyInfo format.
821+
/// </summary>
822+
/// <returns>
823+
/// A byte array containing the PKCS#8 PrivateKeyInfo representation of the this key.
824+
/// </returns>
825+
/// <exception cref="ObjectDisposedException">
826+
/// This instance has been disposed.
827+
/// </exception>
828+
/// <exception cref="CryptographicException">
829+
/// An error occurred while exporting the key.
830+
/// </exception>
831+
public byte[] ExportPkcs8PrivateKey()
832+
{
833+
ThrowIfDisposed();
834+
835+
// A PKCS#8 ML-KEM-1024 ExpandedKey has an ASN.1 overhead of 28 bytes, assuming no attributes.
836+
// Make it an even 32 and that should give a good starting point for a buffer size.
837+
// Decapsulation keys are always larger than the seed, so if we end up with a seed export it should
838+
// fit in the initial buffer.
839+
int size = Algorithm.DecapsulationKeySizeInBytes + 32;
840+
byte[] buffer = ArrayPool<byte>.Shared.Rent(size); // Released to callers, do not use CryptoPool.
841+
int written;
842+
843+
while (!TryExportPkcs8PrivateKeyCore(buffer, out written))
844+
{
845+
ClearAndReturnToPool(buffer, written);
846+
size = checked(size * 2);
847+
buffer = ArrayPool<byte>.Shared.Rent(size);
848+
}
849+
850+
if (written > buffer.Length)
851+
{
852+
// We got a nonsense value written back. Clear the buffer, but don't put it back in the pool.
853+
CryptographicOperations.ZeroMemory(buffer);
854+
throw new CryptographicException();
855+
}
856+
857+
byte[] result = buffer.AsSpan(0, written).ToArray();
858+
ClearAndReturnToPool(buffer, written);
859+
return result;
860+
861+
static void ClearAndReturnToPool(byte[] buffer, int clearSize)
862+
{
863+
CryptographicOperations.ZeroMemory(buffer.AsSpan(0, clearSize));
864+
ArrayPool<byte>.Shared.Return(buffer);
865+
}
866+
}
867+
868+
/// <summary>
869+
/// When overridden in a derived class, attempts to export the current key in the PKCS#8 PrivateKeyInfo format
870+
/// into the provided buffer.
871+
/// </summary>
872+
/// <param name="destination">
873+
/// The buffer to receive the PKCS#8 PrivateKeyInfo value.
874+
/// </param>
875+
/// <param name="bytesWritten">
876+
/// When this method returns, contains the number of bytes written to the <paramref name="destination"/> buffer.
877+
/// </param>
878+
/// <returns>
879+
/// <see langword="true" /> if <paramref name="destination"/> was large enough to hold the result;
880+
/// otherwise, <see langword="false" />.
881+
/// </returns>
882+
/// <exception cref="ObjectDisposedException">
883+
/// This instance has been disposed.
884+
/// </exception>
885+
/// <exception cref="CryptographicException">
886+
/// An error occurred while exporting the key.
887+
/// </exception>
888+
protected abstract bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten);
889+
781890
/// <summary>
782891
/// Imports an ML-KEM encapsulation key from an X.509 SubjectPublicKeyInfo structure.
783892
/// </summary>

src/libraries/Common/src/System/Security/Cryptography/MLKemImplementation.NotSupported.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,11 @@ protected override void ExportEncapsulationKeyCore(Span<byte> destination)
7575
Debug.Fail("Caller should have checked platform availability.");
7676
throw new PlatformNotSupportedException();
7777
}
78+
79+
protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten)
80+
{
81+
Debug.Fail("Caller should have checked platform availability.");
82+
throw new PlatformNotSupportedException();
83+
}
7884
}
7985
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Formats.Asn1;
5+
using System.Security.Cryptography.Asn1;
6+
7+
namespace System.Security.Cryptography
8+
{
9+
internal static class MLKemPkcs8
10+
{
11+
internal static bool TryExportPkcs8PrivateKey(
12+
MLKem kem,
13+
bool hasSeed,
14+
bool hasDecapsulationKey,
15+
Span<byte> destination,
16+
out int bytesWritten)
17+
{
18+
AlgorithmIdentifierAsn algorithmIdentifier = new()
19+
{
20+
Algorithm = kem.Algorithm.Oid,
21+
Parameters = default(ReadOnlyMemory<byte>?),
22+
};
23+
24+
MLKemPrivateKeyAsn privateKeyAsn = default;
25+
byte[]? rented = null;
26+
int written = 0;
27+
28+
try
29+
{
30+
if (hasSeed)
31+
{
32+
int seedSize = kem.Algorithm.PrivateSeedSizeInBytes;
33+
rented = CryptoPool.Rent(seedSize);
34+
Memory<byte> buffer = rented.AsMemory(0, seedSize);
35+
kem.ExportPrivateSeed(buffer.Span);
36+
written = buffer.Length;
37+
privateKeyAsn.Seed = buffer;
38+
}
39+
else if (hasDecapsulationKey)
40+
{
41+
int decapsulationKeySize = kem.Algorithm.DecapsulationKeySizeInBytes;
42+
rented = CryptoPool.Rent(decapsulationKeySize);
43+
Memory<byte> buffer = rented.AsMemory(0, decapsulationKeySize);
44+
kem.ExportDecapsulationKey(buffer.Span);
45+
written = buffer.Length;
46+
privateKeyAsn.ExpandedKey = buffer;
47+
}
48+
else
49+
{
50+
throw new CryptographicException(SR.Cryptography_NotValidPrivateKey);
51+
}
52+
53+
AsnWriter algorithmWriter = new(AsnEncodingRules.DER);
54+
algorithmIdentifier.Encode(algorithmWriter);
55+
AsnWriter privateKeyWriter = new(AsnEncodingRules.DER);
56+
privateKeyAsn.Encode(privateKeyWriter);
57+
AsnWriter pkcs8Writer = KeyFormatHelper.WritePkcs8(algorithmWriter, privateKeyWriter);
58+
59+
bool result = pkcs8Writer.TryEncode(destination, out bytesWritten);
60+
privateKeyWriter.Reset();
61+
pkcs8Writer.Reset();
62+
return result;
63+
}
64+
finally
65+
{
66+
if (rented is not null)
67+
{
68+
CryptoPool.Return(rented, written);
69+
}
70+
}
71+
}
72+
}
73+
}

src/libraries/Common/tests/System/Security/Cryptography/MLKemBaseTests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,65 @@ public void Encapsulate_Overlaps_WhenTrimmed_Works()
459459
AssertExtensions.SequenceEqual(sharedSecret.Slice(0, sharedSecretWritten), decapsulated);
460460
}
461461

462+
[Fact]
463+
public void TryExportPkcs8PrivateKey_Seed_Roundtrip()
464+
{
465+
using MLKem kem = ImportPrivateSeed(MLKemAlgorithm.MLKem512, MLKemTestData.IncrementalSeed);
466+
467+
AssertExportPkcs8PrivateKey(kem, pkcs8 =>
468+
{
469+
using MLKem imported = MLKem.ImportPkcs8PrivateKey(pkcs8);
470+
Assert.Equal(MLKemAlgorithm.MLKem512, imported.Algorithm);
471+
AssertExtensions.SequenceEqual(MLKemTestData.IncrementalSeed, kem.ExportPrivateSeed());
472+
});
473+
}
474+
475+
[Fact]
476+
public void ExportPkcs8PrivateKey_DecapsulationKey_Roundtrip()
477+
{
478+
using MLKem kem = ImportDecapsulationKey(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512DecapsulationKey);
479+
480+
AssertExportPkcs8PrivateKey(kem, pkcs8 =>
481+
{
482+
using MLKem imported = MLKem.ImportPkcs8PrivateKey(pkcs8);
483+
Assert.Equal(MLKemAlgorithm.MLKem512, imported.Algorithm);
484+
485+
Assert.Throws<CryptographicException>(() => kem.ExportPrivateSeed());
486+
AssertExtensions.SequenceEqual(MLKemTestData.MLKem512DecapsulationKey, kem.ExportDecapsulationKey());
487+
});
488+
}
489+
490+
[Fact]
491+
public void TryExportPkcs8PrivateKey_EncapsulationKey_Fails()
492+
{
493+
using MLKem kem = ImportEncapsulationKey(MLKemAlgorithm.MLKem512, MLKemTestData.MLKem512EncapsulationKey);
494+
Assert.Throws<CryptographicException>(() => DoTryUntilDone(kem.TryExportPkcs8PrivateKey));
495+
Assert.Throws<CryptographicException>(() => kem.ExportPkcs8PrivateKey());
496+
}
497+
498+
private static void AssertExportPkcs8PrivateKey(MLKem kem, Action<byte[]> callback)
499+
{
500+
byte[] pkcs8 = DoTryUntilDone(kem.TryExportPkcs8PrivateKey);
501+
callback(pkcs8);
502+
callback(kem.ExportPkcs8PrivateKey());
503+
}
504+
505+
private delegate bool TryExportFunc(Span<byte> destination, out int bytesWritten);
506+
507+
private static byte[] DoTryUntilDone(TryExportFunc func)
508+
{
509+
byte[] buffer = new byte[512];
510+
int written;
511+
512+
while (!func(buffer, out written))
513+
{
514+
Array.Resize(ref buffer, buffer.Length * 2);
515+
}
516+
517+
return buffer.AsSpan(0, written).ToArray();
518+
}
519+
520+
462521
private static void Tamper(Span<byte> buffer)
463522
{
464523
buffer[buffer.Length - 1] ^= 0xFF;

0 commit comments

Comments
 (0)