Skip to content

Commit 333bddc

Browse files
authored
ML-KEM: PKCS12/PFX Loading and Exporting
1 parent bf37cde commit 333bddc

File tree

13 files changed

+1673
-65
lines changed

13 files changed

+1673
-65
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ internal static int UpRefEvpPkey(SafeEvpPKeyHandle handle)
4747
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpPKeyType")]
4848
internal static partial EvpAlgorithmId EvpPKeyType(SafeEvpPKeyHandle handle);
4949

50+
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpPKeyFamily")]
51+
internal static partial EvpAlgorithmFamilyId EvpPKeyFamily(SafeEvpPKeyHandle handle);
52+
5053
[LibraryImport(Libraries.CryptoNative)]
5154
private static unsafe partial SafeEvpPKeyHandle CryptoNative_DecodeSubjectPublicKeyInfo(
5255
byte* buf,
@@ -327,5 +330,14 @@ internal enum EvpAlgorithmId
327330
DSA = 116,
328331
ECC = 408,
329332
}
333+
334+
internal enum EvpAlgorithmFamilyId
335+
{
336+
Unknown = 0,
337+
RSA = 1,
338+
DSA = 2,
339+
ECC = 3,
340+
MLKem = 4,
341+
}
330342
}
331343
}

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

Lines changed: 1146 additions & 0 deletions
Large diffs are not rendered by default.

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslExportProvider.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,33 @@ protected override byte[] ExportPkcs8(
3030
throw new CryptographicException(SR.Cryptography_OpenInvalidHandle);
3131
}
3232

33-
Interop.Crypto.EvpAlgorithmId evpAlgId = Interop.Crypto.EvpPKeyType(privateKey);
33+
Interop.Crypto.EvpAlgorithmFamilyId evpAlgId = Interop.Crypto.EvpPKeyFamily(privateKey);
3434

35-
AsymmetricAlgorithm alg = evpAlgId switch
35+
AsymmetricAlgorithm? alg = evpAlgId switch
3636
{
37-
Interop.Crypto.EvpAlgorithmId.RSA => new RSAOpenSsl(privateKey),
38-
Interop.Crypto.EvpAlgorithmId.ECC => new ECDsaOpenSsl(privateKey),
39-
Interop.Crypto.EvpAlgorithmId.DSA => new DSAOpenSsl(privateKey),
40-
_ => throw new CryptographicException(SR.Cryptography_InvalidHandle),
37+
Interop.Crypto.EvpAlgorithmFamilyId.RSA => new RSAOpenSsl(privateKey),
38+
Interop.Crypto.EvpAlgorithmFamilyId.ECC => new ECDsaOpenSsl(privateKey),
39+
Interop.Crypto.EvpAlgorithmFamilyId.DSA => new DSAOpenSsl(privateKey),
40+
_ => null,
4141
};
4242

43-
return alg.ExportEncryptedPkcs8PrivateKey(password, pbeParameters);
43+
if (alg is not null)
44+
{
45+
using (alg)
46+
{
47+
return alg.ExportEncryptedPkcs8PrivateKey(password, pbeParameters);
48+
}
49+
}
50+
51+
if (evpAlgId == Interop.Crypto.EvpAlgorithmFamilyId.MLKem)
52+
{
53+
using (MLKem kem = new MLKemOpenSsl(privateKey))
54+
{
55+
return kem.ExportEncryptedPkcs8PrivateKey(password, pbeParameters);
56+
}
57+
}
58+
59+
throw new CryptographicException(SR.Cryptography_InvalidHandle);
4460
}
4561

4662
private static void PushHandle(IntPtr certPtr, SafeX509StackHandle publicCerts)

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Android.cs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Formats.Asn1;
56
using System.IO;
67

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

55-
if (certAndKey.Key != null)
56+
if (certAndKey.Key is not null)
5657
{
57-
pal.SetPrivateKey(GetPrivateKey(certAndKey.Key));
58-
certAndKey.Key.Dispose();
58+
if (certAndKey.Key is { Key: AsymmetricAlgorithm alg })
59+
{
60+
pal.SetPrivateKey(GetPrivateKey(alg));
61+
certAndKey.Key.Dispose();
62+
}
63+
else
64+
{
65+
Debug.Fail($"Unhandled key type '{certAndKey.Key.Key?.GetType()?.FullName}'.");
66+
throw new CryptographicException();
67+
}
5968
}
6069

6170
return new Pkcs12Return(pal);
6271
}
6372

64-
private static partial AsymmetricAlgorithm? CreateKey(string algorithm)
73+
private static partial Pkcs12Key? CreateKey(string algorithm, ReadOnlySpan<byte> pkcs8)
6574
{
66-
return algorithm switch
75+
switch (algorithm)
6776
{
68-
Oids.Rsa or Oids.RsaPss => new RSAImplementation.RSAAndroid(),
69-
Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDsaImplementation.ECDsaAndroid(),
70-
Oids.Dsa => new DSAImplementation.DSAAndroid(),
71-
_ => null,
72-
};
77+
case Oids.Rsa or Oids.RsaPss:
78+
return new AsymmetricAlgorithmPkcs12PrivateKey(
79+
pkcs8,
80+
static () => new RSAImplementation.RSAAndroid());
81+
case Oids.EcPublicKey or Oids.EcDiffieHellman:
82+
return new AsymmetricAlgorithmPkcs12PrivateKey(
83+
pkcs8,
84+
static () => new ECDsaImplementation.ECDsaAndroid());
85+
case Oids.Dsa:
86+
return new AsymmetricAlgorithmPkcs12PrivateKey(
87+
pkcs8,
88+
static () => new DSAImplementation.DSAAndroid());
89+
default:
90+
// No PQC support on Android.
91+
return null;
92+
}
7393
}
7494

7595
internal static SafeKeyHandle GetPrivateKey(AsymmetricAlgorithm key)

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.OpenSsl.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,30 +59,44 @@ private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, Import
5959
return new Pkcs12Return(pal);
6060
}
6161

62-
private static partial AsymmetricAlgorithm? CreateKey(string algorithm)
62+
private static partial Pkcs12Key? CreateKey(string algorithm, ReadOnlySpan<byte> pkcs8)
6363
{
64-
return algorithm switch
64+
switch (algorithm)
6565
{
66-
Oids.Rsa or Oids.RsaPss => new RSAOpenSsl(),
67-
Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDiffieHellmanOpenSsl(),
68-
Oids.Dsa => new DSAOpenSsl(),
69-
_ => null,
70-
};
66+
case Oids.Rsa or Oids.RsaPss:
67+
return new AsymmetricAlgorithmPkcs12PrivateKey(pkcs8, static () => new RSAOpenSsl());
68+
case Oids.EcPublicKey or Oids.EcDiffieHellman:
69+
return new AsymmetricAlgorithmPkcs12PrivateKey(pkcs8, static () => new ECDiffieHellmanOpenSsl());
70+
case Oids.Dsa:
71+
return new AsymmetricAlgorithmPkcs12PrivateKey(pkcs8, static () => new DSAOpenSsl());
72+
case Oids.MlKem512 or Oids.MlKem768 or Oids.MlKem1024:
73+
return new MLKemPkcs12PrivateKey(pkcs8);
74+
default:
75+
return null;
76+
}
7177
}
7278

73-
internal static SafeEvpPKeyHandle GetPrivateKey(AsymmetricAlgorithm key)
79+
internal static SafeEvpPKeyHandle GetPrivateKey(Pkcs12Key key)
7480
{
75-
if (key is RSAOpenSsl rsa)
81+
if (key.Key is RSAOpenSsl rsa)
7682
{
7783
return rsa.DuplicateKeyHandle();
7884
}
7985

80-
if (key is DSAOpenSsl dsa)
86+
if (key.Key is DSAOpenSsl dsa)
8187
{
8288
return dsa.DuplicateKeyHandle();
8389
}
8490

85-
return ((ECDiffieHellmanOpenSsl)key).DuplicateKeyHandle();
91+
if (key.Key is MLKem kem)
92+
{
93+
// We should always get back an MLKemImplementation from PKCS8 loading.
94+
MLKemImplementation? impl = kem as MLKemImplementation;
95+
Debug.Assert(impl is not null, "MLKem implementation is not handled for duplicating a handle.");
96+
return impl.DuplicateHandle();
97+
}
98+
99+
return ((ECDiffieHellmanOpenSsl)key.Key).DuplicateKeyHandle();
86100
}
87101

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

src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Unix.cs

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static partial class X509CertificateLoader
1414
{
1515
private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, ImportState importState);
1616

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

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

@@ -196,7 +196,7 @@ internal ReadOnlySpan<SafeBagAsn> GetKeysSpan()
196196
private struct CertAndKey
197197
{
198198
internal ICertificatePalCore? Cert;
199-
internal AsymmetricAlgorithm? Key;
199+
internal Pkcs12Key? Key;
200200

201201
internal void Dispose()
202202
{
@@ -209,7 +209,7 @@ private struct CertKeyMatcher
209209
{
210210
private CertAndKey[] _certAndKeys;
211211
private int _certCount;
212-
private AsymmetricAlgorithm?[] _keys;
212+
private Pkcs12Key?[] _keys;
213213
private RentedSubjectPublicKeyInfo[] _rentedSpki;
214214
private int _keyCount;
215215

@@ -247,11 +247,11 @@ internal void LoadKeys(ref BagState bagState)
247247
return;
248248
}
249249

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

252252
foreach (SafeBagAsn safeBag in bagState.GetKeysSpan())
253253
{
254-
AsymmetricAlgorithm? key = null;
254+
Pkcs12Key? key = null;
255255

256256
try
257257
{
@@ -260,12 +260,10 @@ internal void LoadKeys(ref BagState bagState)
260260
PrivateKeyInfoAsn privateKeyInfo =
261261
PrivateKeyInfoAsn.Decode(safeBag.BagValue, AsnEncodingRules.BER);
262262

263-
key = CreateKey(privateKeyInfo.PrivateKeyAlgorithm.Algorithm);
263+
key = CreateKey(privateKeyInfo.PrivateKeyAlgorithm.Algorithm, safeBag.BagValue.Span);
264264

265265
if (key is not null)
266266
{
267-
ImportPrivateKey(key, safeBag.BagValue.Span);
268-
269267
if (_rentedSpki is null)
270268
{
271269
_rentedSpki =
@@ -376,7 +374,7 @@ internal CertAndKey[] MatchCertAndKeys(ref BagState bagState, bool allowDoubleBi
376374
if (allowDoubleBind)
377375
{
378376

379-
AsymmetricAlgorithm? key = CreateKey(cert.KeyAlgorithm);
377+
Pkcs12Key? key = CreateKey(cert.KeyAlgorithm, keyBag.BagValue.Span);
380378

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

390388
_certAndKeys[certBagIdx].Key = key;
391-
ImportPrivateKey(key, keyBag.BagValue.Span);
392389
}
393390
else
394391
{
@@ -492,6 +489,8 @@ certKeyParameters is not null &&
492489
publicKeyInfo.Algorithm.Parameters.Value.Span.SequenceEqual(certKeyParameters);
493490
}
494491

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

505504
private static void ExtractPublicKey(
506505
ref RentedSubjectPublicKeyInfo spki,
507-
AsymmetricAlgorithm key,
506+
Pkcs12Key key,
508507
int sizeHint)
509508
{
510509
Debug.Assert(sizeHint > 0);
@@ -550,7 +549,7 @@ internal void Dispose()
550549
_keys[i]?.Dispose();
551550
}
552551

553-
ArrayPool<AsymmetricAlgorithm?>.Shared.Return(_keys, clearArray: true);
552+
ArrayPool<Pkcs12Key?>.Shared.Return(_keys, clearArray: true);
554553
}
555554

556555
if (_rentedSpki is not null)
@@ -619,4 +618,73 @@ internal void Dispose()
619618
}
620619
}
621620
}
621+
622+
internal abstract class Pkcs12Key : IDisposable
623+
{
624+
internal abstract IDisposable Key { get; }
625+
626+
internal abstract bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten);
627+
628+
public void Dispose() => Key.Dispose();
629+
}
630+
631+
internal sealed class MLKemPkcs12PrivateKey : Pkcs12Key
632+
{
633+
private readonly MLKem _key;
634+
635+
internal MLKemPkcs12PrivateKey(ReadOnlySpan<byte> pkcs8)
636+
{
637+
try
638+
{
639+
_key = MLKem.ImportPkcs8PrivateKey(pkcs8);
640+
}
641+
catch (PlatformNotSupportedException nse)
642+
{
643+
// PKCS12 loader turns PNSE in to a CryptographicException
644+
throw new CryptographicException(SR.Cryptography_NotValidPrivateKey, nse);
645+
}
646+
}
647+
648+
internal override bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten) =>
649+
_key.TryExportSubjectPublicKeyInfo(destination, out bytesWritten);
650+
651+
internal override MLKem Key => _key;
652+
}
653+
654+
internal sealed class AsymmetricAlgorithmPkcs12PrivateKey : Pkcs12Key
655+
{
656+
private readonly AsymmetricAlgorithm _key;
657+
658+
internal AsymmetricAlgorithmPkcs12PrivateKey(ReadOnlySpan<byte> pkcs8, Func<AsymmetricAlgorithm> factory)
659+
{
660+
_key = factory();
661+
662+
try
663+
{
664+
_key.ImportPkcs8PrivateKey(pkcs8, out int bytesRead);
665+
Debug.Assert(bytesRead == pkcs8.Length);
666+
}
667+
catch (Exception ex)
668+
{
669+
_key.Dispose();
670+
671+
if (ex is PlatformNotSupportedException nse)
672+
{
673+
// Turn a "curve not supported" PNSE (or other PNSE)
674+
// into a standardized CryptographicException.
675+
throw new CryptographicException(SR.Cryptography_NotValidPrivateKey, nse);
676+
}
677+
else
678+
{
679+
throw;
680+
}
681+
}
682+
683+
}
684+
685+
internal override bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten) =>
686+
_key.TryExportSubjectPublicKeyInfo(destination, out bytesWritten);
687+
688+
internal override AsymmetricAlgorithm Key => _key;
689+
}
622690
}

0 commit comments

Comments
 (0)