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 @@ -47,6 +47,9 @@ internal static int UpRefEvpPkey(SafeEvpPKeyHandle handle)
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpPKeyType")]
internal static partial EvpAlgorithmId EvpPKeyType(SafeEvpPKeyHandle handle);

[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpPKeyFamily")]
internal static partial EvpAlgorithmFamilyId EvpPKeyFamily(SafeEvpPKeyHandle handle);

[LibraryImport(Libraries.CryptoNative)]
private static unsafe partial SafeEvpPKeyHandle CryptoNative_DecodeSubjectPublicKeyInfo(
byte* buf,
Expand Down Expand Up @@ -327,5 +330,14 @@ internal enum EvpAlgorithmId
DSA = 116,
ECC = 408,
}

internal enum EvpAlgorithmFamilyId
{
Unknown = 0,
RSA = 1,
DSA = 2,
ECC = 3,
MLKem = 4,
}
}
}
1,146 changes: 1,146 additions & 0 deletions src/libraries/Common/tests/System/Security/Cryptography/MLKemTestData.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,33 @@ protected override byte[] ExportPkcs8(
throw new CryptographicException(SR.Cryptography_OpenInvalidHandle);
}

Interop.Crypto.EvpAlgorithmId evpAlgId = Interop.Crypto.EvpPKeyType(privateKey);
Interop.Crypto.EvpAlgorithmFamilyId evpAlgId = Interop.Crypto.EvpPKeyFamily(privateKey);

AsymmetricAlgorithm alg = evpAlgId switch
AsymmetricAlgorithm? alg = evpAlgId switch
{
Interop.Crypto.EvpAlgorithmId.RSA => new RSAOpenSsl(privateKey),
Interop.Crypto.EvpAlgorithmId.ECC => new ECDsaOpenSsl(privateKey),
Interop.Crypto.EvpAlgorithmId.DSA => new DSAOpenSsl(privateKey),
_ => throw new CryptographicException(SR.Cryptography_InvalidHandle),
Interop.Crypto.EvpAlgorithmFamilyId.RSA => new RSAOpenSsl(privateKey),
Interop.Crypto.EvpAlgorithmFamilyId.ECC => new ECDsaOpenSsl(privateKey),
Interop.Crypto.EvpAlgorithmFamilyId.DSA => new DSAOpenSsl(privateKey),
_ => null,
};

return alg.ExportEncryptedPkcs8PrivateKey(password, pbeParameters);
if (alg is not null)
{
using (alg)
{
return alg.ExportEncryptedPkcs8PrivateKey(password, pbeParameters);
}
}

if (evpAlgId == Interop.Crypto.EvpAlgorithmFamilyId.MLKem)
{
using (MLKem kem = new MLKemOpenSsl(privateKey))
{
return kem.ExportEncryptedPkcs8PrivateKey(password, pbeParameters);
}
}

throw new CryptographicException(SR.Cryptography_InvalidHandle);
}

private static void PushHandle(IntPtr certPtr, SafeX509StackHandle publicCerts)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Formats.Asn1;
using System.IO;

Expand Down Expand Up @@ -52,24 +53,43 @@ private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, Import
{
AndroidCertificatePal pal = (AndroidCertificatePal)certAndKey.Cert!;

if (certAndKey.Key != null)
if (certAndKey.Key is not null)
{
pal.SetPrivateKey(GetPrivateKey(certAndKey.Key));
certAndKey.Key.Dispose();
if (certAndKey.Key is { Key: AsymmetricAlgorithm alg })
{
pal.SetPrivateKey(GetPrivateKey(alg));
certAndKey.Key.Dispose();
}
else
{
Debug.Fail($"Unhandled key type '{certAndKey.Key.Key?.GetType()?.FullName}'.");
throw new CryptographicException();
}
}

return new Pkcs12Return(pal);
}

private static partial AsymmetricAlgorithm? CreateKey(string algorithm)
private static partial Pkcs12Key? CreateKey(string algorithm, ReadOnlySpan<byte> pkcs8)
{
return algorithm switch
switch (algorithm)
{
Oids.Rsa or Oids.RsaPss => new RSAImplementation.RSAAndroid(),
Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDsaImplementation.ECDsaAndroid(),
Oids.Dsa => new DSAImplementation.DSAAndroid(),
_ => null,
};
case Oids.Rsa or Oids.RsaPss:
return new AsymmetricAlgorithmPkcs12PrivateKey(
pkcs8,
static () => new RSAImplementation.RSAAndroid());
case Oids.EcPublicKey or Oids.EcDiffieHellman:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it was already like this, but this change is making me realize how often we repeat code.

It'd be nice (in a future change) to have just a central registration of "here's my RSA importer, or null if I don't support RSA". Something like

private static partial KeyLoader GetKeyLoader();

...
private static partial KeyLoader GetKeyLoader()
{
    return new KeyLoader(
        rsa: pkcs8 => new ...,
        ecc: ...

but that's a very half-formed idea

return new AsymmetricAlgorithmPkcs12PrivateKey(
pkcs8,
static () => new ECDsaImplementation.ECDsaAndroid());
case Oids.Dsa:
return new AsymmetricAlgorithmPkcs12PrivateKey(
pkcs8,
static () => new DSAImplementation.DSAAndroid());
default:
// No PQC support on Android.
return null;
}
}

internal static SafeKeyHandle GetPrivateKey(AsymmetricAlgorithm key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,44 @@ private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, Import
return new Pkcs12Return(pal);
}

private static partial AsymmetricAlgorithm? CreateKey(string algorithm)
private static partial Pkcs12Key? CreateKey(string algorithm, ReadOnlySpan<byte> pkcs8)
{
return algorithm switch
switch (algorithm)
{
Oids.Rsa or Oids.RsaPss => new RSAOpenSsl(),
Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDiffieHellmanOpenSsl(),
Oids.Dsa => new DSAOpenSsl(),
_ => null,
};
case Oids.Rsa or Oids.RsaPss:
return new AsymmetricAlgorithmPkcs12PrivateKey(pkcs8, static () => new RSAOpenSsl());
case Oids.EcPublicKey or Oids.EcDiffieHellman:
return new AsymmetricAlgorithmPkcs12PrivateKey(pkcs8, static () => new ECDiffieHellmanOpenSsl());
case Oids.Dsa:
return new AsymmetricAlgorithmPkcs12PrivateKey(pkcs8, static () => new DSAOpenSsl());
case Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024:
return new MLKemPkcs12PrivateKey(pkcs8);
default:
return null;
}
}

internal static SafeEvpPKeyHandle GetPrivateKey(AsymmetricAlgorithm key)
internal static SafeEvpPKeyHandle GetPrivateKey(Pkcs12Key key)
{
if (key is RSAOpenSsl rsa)
if (key.Key is RSAOpenSsl rsa)
{
return rsa.DuplicateKeyHandle();
}

if (key is DSAOpenSsl dsa)
if (key.Key is DSAOpenSsl dsa)
{
return dsa.DuplicateKeyHandle();
}

return ((ECDiffieHellmanOpenSsl)key).DuplicateKeyHandle();
if (key.Key is MLKem kem)
{
// We should always get back an MLKemImplementation from PKCS8 loading.
MLKemImplementation? impl = kem as MLKemImplementation;
Debug.Assert(impl is not null, "MLKem implementation is not handled for duplicating a handle.");
return impl.DuplicateHandle();
}

return ((ECDiffieHellmanOpenSsl)key.Key).DuplicateKeyHandle();
}

private static partial ICertificatePalCore LoadX509Der(ReadOnlyMemory<byte> data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static partial class X509CertificateLoader
{
private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, ImportState importState);

private static partial AsymmetricAlgorithm? CreateKey(string algorithm);
private static partial Pkcs12Key? CreateKey(string algorithm, ReadOnlySpan<byte> pkcs8);

private static partial ICertificatePalCore LoadX509Der(ReadOnlyMemory<byte> data);

Expand Down Expand Up @@ -196,7 +196,7 @@ internal ReadOnlySpan<SafeBagAsn> GetKeysSpan()
private struct CertAndKey
{
internal ICertificatePalCore? Cert;
internal AsymmetricAlgorithm? Key;
internal Pkcs12Key? Key;

internal void Dispose()
{
Expand All @@ -209,7 +209,7 @@ private struct CertKeyMatcher
{
private CertAndKey[] _certAndKeys;
private int _certCount;
private AsymmetricAlgorithm?[] _keys;
private Pkcs12Key?[] _keys;
private RentedSubjectPublicKeyInfo[] _rentedSpki;
private int _keyCount;

Expand Down Expand Up @@ -247,11 +247,11 @@ internal void LoadKeys(ref BagState bagState)
return;
}

_keys = ArrayPool<AsymmetricAlgorithm?>.Shared.Rent(bagState.KeyCount);
_keys = ArrayPool<Pkcs12Key?>.Shared.Rent(bagState.KeyCount);

foreach (SafeBagAsn safeBag in bagState.GetKeysSpan())
{
AsymmetricAlgorithm? key = null;
Pkcs12Key? key = null;

try
{
Expand All @@ -260,12 +260,10 @@ internal void LoadKeys(ref BagState bagState)
PrivateKeyInfoAsn privateKeyInfo =
PrivateKeyInfoAsn.Decode(safeBag.BagValue, AsnEncodingRules.BER);

key = CreateKey(privateKeyInfo.PrivateKeyAlgorithm.Algorithm);
key = CreateKey(privateKeyInfo.PrivateKeyAlgorithm.Algorithm, safeBag.BagValue.Span);

if (key is not null)
{
ImportPrivateKey(key, safeBag.BagValue.Span);

if (_rentedSpki is null)
{
_rentedSpki =
Expand Down Expand Up @@ -376,7 +374,7 @@ internal CertAndKey[] MatchCertAndKeys(ref BagState bagState, bool allowDoubleBi
if (allowDoubleBind)
{

AsymmetricAlgorithm? key = CreateKey(cert.KeyAlgorithm);
Pkcs12Key? key = CreateKey(cert.KeyAlgorithm, keyBag.BagValue.Span);

if (key is null)
{
Expand All @@ -388,7 +386,6 @@ internal CertAndKey[] MatchCertAndKeys(ref BagState bagState, bool allowDoubleBi
}

_certAndKeys[certBagIdx].Key = key;
ImportPrivateKey(key, keyBag.BagValue.Span);
}
else
{
Expand Down Expand Up @@ -492,6 +489,8 @@ certKeyParameters is not null &&
publicKeyInfo.Algorithm.Parameters.Value.Span.SequenceEqual(certKeyParameters);
}

// ML-KEM requires parameters to match exactly. ML-KEM also prohibits parameters, but that is checked
// by MLKem when loading the key.
// Any other algorithm matches null/empty parameters as equivalent
if (!publicKeyInfo.Algorithm.Parameters.HasValue)
{
Expand All @@ -504,7 +503,7 @@ certKeyParameters is not null &&

private static void ExtractPublicKey(
ref RentedSubjectPublicKeyInfo spki,
AsymmetricAlgorithm key,
Pkcs12Key key,
int sizeHint)
{
Debug.Assert(sizeHint > 0);
Expand Down Expand Up @@ -550,7 +549,7 @@ internal void Dispose()
_keys[i]?.Dispose();
}

ArrayPool<AsymmetricAlgorithm?>.Shared.Return(_keys, clearArray: true);
ArrayPool<Pkcs12Key?>.Shared.Return(_keys, clearArray: true);
}

if (_rentedSpki is not null)
Expand Down Expand Up @@ -619,4 +618,73 @@ internal void Dispose()
}
}
}

internal abstract class Pkcs12Key : IDisposable
Copy link
Member

@krwq krwq Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just implement AsymmetricAlgorithm at this point, there doesn't seem enough benefit not doing it. At least here you don't seem to need any new public APIs but IMO implementing will just make it easier for everyone

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m pretty firmly against it.

  1. It would only very minimally help this pull request. We still have quite a number of type tests if (alg is RSAOpenSsl), if (alg is DSAOpenSsl), etc. Looking at it from a fresh perspective, all it would really do is make the SubjectPublicKeyInfo export easier.
  2. Implementing AsymmetricAlgorithm significantly complicates implementation of the actual algorithm. It makes the algorithm instances mutable and it makes PKCS#8/SPKI import and export more difficult to support for .NET Framework because AsymmetricAlgorithm on .NET Framework doesn’t have any of the key loading methods.

Since it doesn’t help consumers of the algorithm, and it doesn’t help with the implementation of the algorithm, I guess I don’t see much of a reason to do it.

{
internal abstract IDisposable Key { get; }

internal abstract bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten);

public void Dispose() => Key.Dispose();
}

internal sealed class MLKemPkcs12PrivateKey : Pkcs12Key
{
private readonly MLKem _key;

internal MLKemPkcs12PrivateKey(ReadOnlySpan<byte> pkcs8)
{
try
{
_key = MLKem.ImportPkcs8PrivateKey(pkcs8);
}
catch (PlatformNotSupportedException nse)
{
// PKCS12 loader turns PNSE in to a CryptographicException
throw new CryptographicException(SR.Cryptography_NotValidPrivateKey, nse);
}
}

internal override bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten) =>
_key.TryExportSubjectPublicKeyInfo(destination, out bytesWritten);

internal override MLKem Key => _key;
}

internal sealed class AsymmetricAlgorithmPkcs12PrivateKey : Pkcs12Key
{
private readonly AsymmetricAlgorithm _key;

internal AsymmetricAlgorithmPkcs12PrivateKey(ReadOnlySpan<byte> pkcs8, Func<AsymmetricAlgorithm> factory)
{
_key = factory();

try
{
_key.ImportPkcs8PrivateKey(pkcs8, out int bytesRead);
Debug.Assert(bytesRead == pkcs8.Length);
}
catch (Exception ex)
{
_key.Dispose();

if (ex is PlatformNotSupportedException nse)
{
// Turn a "curve not supported" PNSE (or other PNSE)
// into a standardized CryptographicException.
throw new CryptographicException(SR.Cryptography_NotValidPrivateKey, nse);
}
else
{
throw;
}
}

}

internal override bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten) =>
_key.TryExportSubjectPublicKeyInfo(destination, out bytesWritten);

internal override AsymmetricAlgorithm Key => _key;
}
}
Loading
Loading