Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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();
}
}

Expand Down
109 changes: 109 additions & 0 deletions src/libraries/Common/src/System/Security/Cryptography/MLKem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading