Skip to content

Commit ec0a902

Browse files
authored
ML-KEM: SubjectPublicKeyInfo import and export
1 parent 659321f commit ec0a902

File tree

14 files changed

+641
-48
lines changed

14 files changed

+641
-48
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Formats.Asn1;
67
using System.Runtime.CompilerServices;
78
using System.Runtime.InteropServices;
89
using System.Runtime.Versioning;
@@ -111,5 +112,18 @@ internal static int HashOidToByteLength(string hashOid)
111112
_ => throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashOid)),
112113
};
113114
}
115+
116+
internal static CryptographicException CreateAlgorithmUnknownException(AsnWriter encodedId)
117+
{
118+
#if NET10_0_OR_GREATER
119+
return encodedId.Encode(static encoded =>
120+
new CryptographicException(
121+
SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, Convert.ToHexString(encoded))));
122+
#else
123+
return new CryptographicException(
124+
SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier,
125+
HexConverter.ToString(encodedId.Encode(), HexConverter.Casing.Upper)));
126+
#endif
127+
}
114128
}
115129
}

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

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Formats.Asn1;
88
using System.Security.Cryptography.Asn1;
9+
using Internal.Cryptography;
910

1011
// The type being internal is making unused parameter warnings fire for
1112
// not-implemented methods. Suppress those warnings.
@@ -832,8 +833,7 @@ public static MLDsa ImportSubjectPublicKeyInfo(ReadOnlySpan<byte> source)
832833
{
833834
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
834835
spki.Algorithm.Encode(writer);
835-
ThrowAlgorithmUnknown(writer);
836-
Debug.Fail("Execution should have halted in the throw-helper.");
836+
throw Helpers.CreateAlgorithmUnknownException(writer);
837837
}
838838

839839
return MLDsaImplementation.ImportPublicKey(info, spki.SubjectPublicKey.Span);
@@ -883,8 +883,7 @@ public static MLDsa ImportPkcs8PrivateKey(ReadOnlySpan<byte> source)
883883
{
884884
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
885885
pki.PrivateKeyAlgorithm.Encode(writer);
886-
ThrowAlgorithmUnknown(writer);
887-
Debug.Fail("Execution should have halted in the throw-helper.");
886+
throw Helpers.CreateAlgorithmUnknownException(writer);
888887
}
889888

890889
return MLDsaImplementation.ImportPkcs8PrivateKeyValue(info, pki.PrivateKey.Span);
@@ -1366,19 +1365,6 @@ internal static void ThrowIfNotSupported()
13661365
}
13671366
}
13681367

1369-
[DoesNotReturn]
1370-
private static void ThrowAlgorithmUnknown(AsnWriter encodedId)
1371-
{
1372-
#if NET9_0_OR_GREATER
1373-
throw encodedId.Encode(static encoded =>
1374-
new CryptographicException(
1375-
SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, Convert.ToHexString(encoded))));
1376-
#else
1377-
throw new CryptographicException(
1378-
SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier, Convert.ToHexString(encodedId.Encode())));
1379-
#endif
1380-
}
1381-
13821368
[DoesNotReturn]
13831369
private static ParameterSetInfo ThrowAlgorithmUnknown(string algorithmId)
13841370
{

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

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Buffers;
6+
using System.Diagnostics;
57
using System.Diagnostics.CodeAnalysis;
8+
using System.Formats.Asn1;
69
using System.Runtime.CompilerServices;
10+
using System.Security.Cryptography.Asn1;
11+
using Internal.Cryptography;
712

813
namespace System.Security.Cryptography
914
{
@@ -74,7 +79,7 @@ protected MLKem(MLKemAlgorithm algorithm)
7479
/// </exception>
7580
/// <exception cref="PlatformNotSupportedException">
7681
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
77-
/// to determine if the platform supports MK-KEM.
82+
/// to determine if the platform supports ML-KEM.
7883
/// </exception>
7984
public static MLKem GenerateKey(MLKemAlgorithm algorithm)
8085
{
@@ -462,7 +467,7 @@ public byte[] ExportPrivateSeed()
462467
/// </exception>
463468
/// <exception cref="PlatformNotSupportedException">
464469
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
465-
/// to determine if the platform supports MK-KEM.
470+
/// to determine if the platform supports ML-KEM.
466471
/// </exception>
467472
public static MLKem ImportPrivateSeed(MLKemAlgorithm algorithm, ReadOnlySpan<byte> source)
468473
{
@@ -495,7 +500,7 @@ public static MLKem ImportPrivateSeed(MLKemAlgorithm algorithm, ReadOnlySpan<byt
495500
/// </exception>
496501
/// <exception cref="PlatformNotSupportedException">
497502
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
498-
/// to determine if the platform supports MK-KEM.
503+
/// to determine if the platform supports ML-KEM.
499504
/// </exception>
500505
public static MLKem ImportPrivateSeed(MLKemAlgorithm algorithm, byte[] source)
501506
{
@@ -521,7 +526,7 @@ public static MLKem ImportPrivateSeed(MLKemAlgorithm algorithm, byte[] source)
521526
/// </exception>
522527
/// <exception cref="PlatformNotSupportedException">
523528
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
524-
/// to determine if the platform supports MK-KEM.
529+
/// to determine if the platform supports ML-KEM.
525530
/// </exception>
526531
public static MLKem ImportDecapsulationKey(MLKemAlgorithm algorithm, ReadOnlySpan<byte> source)
527532
{
@@ -553,7 +558,7 @@ public static MLKem ImportDecapsulationKey(MLKemAlgorithm algorithm, ReadOnlySpa
553558
/// </exception>
554559
/// <exception cref="PlatformNotSupportedException">
555560
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
556-
/// to determine if the platform supports MK-KEM.
561+
/// to determine if the platform supports ML-KEM.
557562
/// </exception>
558563
public static MLKem ImportDecapsulationKey(MLKemAlgorithm algorithm, byte[] source)
559564
{
@@ -578,7 +583,7 @@ public static MLKem ImportDecapsulationKey(MLKemAlgorithm algorithm, byte[] sour
578583
/// </exception>
579584
/// <exception cref="PlatformNotSupportedException">
580585
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
581-
/// to determine if the platform supports MK-KEM.
586+
/// to determine if the platform supports ML-KEM.
582587
/// </exception>
583588
public static MLKem ImportEncapsulationKey(MLKemAlgorithm algorithm, ReadOnlySpan<byte> source)
584589
{
@@ -610,7 +615,7 @@ public static MLKem ImportEncapsulationKey(MLKemAlgorithm algorithm, ReadOnlySpa
610615
/// </exception>
611616
/// <exception cref="PlatformNotSupportedException">
612617
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
613-
/// to determine if the platform supports MK-KEM.
618+
/// to determine if the platform supports ML-KEM.
614619
/// </exception>
615620
public static MLKem ImportEncapsulationKey(MLKemAlgorithm algorithm, byte[] source)
616621
{
@@ -727,6 +732,119 @@ public byte[] ExportEncapsulationKey()
727732
/// </param>
728733
protected abstract void ExportEncapsulationKeyCore(Span<byte> destination);
729734

735+
/// <summary>
736+
/// Attempts to export the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format
737+
/// into the provided buffer.
738+
/// </summary>
739+
/// <param name="destination">
740+
/// The buffer to receive the X.509 SubjectPublicKeyInfo value.
741+
/// </param>
742+
/// <param name="bytesWritten">
743+
/// When this method returns, contains the number of bytes written to the <paramref name="destination"/> buffer.
744+
/// This parameter is treated as uninitialized.
745+
/// </param>
746+
/// <returns>
747+
/// <see langword="true" /> if <paramref name="destination"/> was large enough to hold the result;
748+
/// otherwise, <see langword="false" />.
749+
/// </returns>
750+
/// <exception cref="ObjectDisposedException">
751+
/// This instance has been disposed.
752+
/// </exception>
753+
/// <exception cref="CryptographicException">
754+
/// An error occurred while exporting the key.
755+
/// </exception>
756+
public bool TryExportSubjectPublicKeyInfo(Span<byte> destination, out int bytesWritten)
757+
{
758+
ThrowIfDisposed();
759+
return ExportSubjectPublicKeyInfoCore().TryEncode(destination, out bytesWritten);
760+
}
761+
762+
/// <summary>
763+
/// Exports the public-key portion of the current key in the X.509 SubjectPublicKeyInfo format.
764+
/// </summary>
765+
/// <returns>
766+
/// A byte array containing the X.509 SubjectPublicKeyInfo representation of the public-key portion of this key.
767+
/// </returns>
768+
/// <exception cref="ObjectDisposedException">
769+
/// This instance has been disposed.
770+
/// </exception>
771+
/// <exception cref="CryptographicException">
772+
/// An error occurred while exporting the key.
773+
/// </exception>
774+
public byte[] ExportSubjectPublicKeyInfo()
775+
{
776+
ThrowIfDisposed();
777+
return ExportSubjectPublicKeyInfoCore().Encode();
778+
}
779+
780+
/// <summary>
781+
/// Imports an ML-KEM encapsulation key from an X.509 SubjectPublicKeyInfo structure.
782+
/// </summary>
783+
/// <param name="source">
784+
/// The bytes of an X.509 SubjectPublicKeyInfo structure in the ASN.1-DER encoding.
785+
/// </param>
786+
/// <returns>
787+
/// The imported key.
788+
/// </returns>
789+
/// <exception cref="CryptographicException">
790+
/// <para>
791+
/// The contents of <paramref name="source"/> do not represent an ASN.1-DER-encoded X.509 SubjectPublicKeyInfo structure.
792+
/// </para>
793+
/// <para>-or-</para>
794+
/// <para>
795+
/// The SubjectPublicKeyInfo value does not represent an ML-KEM key.
796+
/// </para>
797+
/// <para>-or-</para>
798+
/// <para>
799+
/// The algorithm-specific import failed.
800+
/// </para>
801+
/// </exception>
802+
/// <exception cref="PlatformNotSupportedException">
803+
/// The platform does not support ML-KEM. Callers can use the <see cref="IsSupported" /> property
804+
/// to determine if the platform supports ML-KEM.
805+
/// </exception>
806+
public static MLKem ImportSubjectPublicKeyInfo(ReadOnlySpan<byte> source)
807+
{
808+
ThrowIfNotSupported();
809+
810+
unsafe
811+
{
812+
fixed (byte* pointer = source)
813+
{
814+
using (PointerMemoryManager<byte> manager = new(pointer, source.Length))
815+
{
816+
AsnValueReader reader = new(source, AsnEncodingRules.DER);
817+
SubjectPublicKeyInfoAsn.Decode(ref reader, manager.Memory, out SubjectPublicKeyInfoAsn spki);
818+
MLKemAlgorithm algorithm = MLKemAlgorithm.FromOid(spki.Algorithm.Algorithm) ??
819+
throw new CryptographicException(
820+
SR.Format(SR.Cryptography_UnknownAlgorithmIdentifier,
821+
spki.Algorithm.Algorithm));
822+
823+
// draft-ietf-lamps-kyber-certificates-07:
824+
// The parameters field of the AlgorithmIdentifier for the ML-KEM public key MUST be absent.
825+
if (spki.Algorithm.Parameters.HasValue)
826+
{
827+
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
828+
spki.Algorithm.Encode(writer);
829+
throw Helpers.CreateAlgorithmUnknownException(writer);
830+
}
831+
832+
return MLKemImplementation.ImportEncapsulationKeyImpl(algorithm, spki.SubjectPublicKey.Span);
833+
}
834+
}
835+
}
836+
}
837+
838+
/// <inheritdoc cref="ImportSubjectPublicKeyInfo(ReadOnlySpan{byte})" />
839+
/// <exception cref="ArgumentNullException">
840+
/// <paramref name="source" /> is <see langword="null" />
841+
/// </exception>
842+
public static MLKem ImportSubjectPublicKeyInfo(byte[] source)
843+
{
844+
ThrowIfNull(source);
845+
return ImportSubjectPublicKeyInfo(new ReadOnlySpan<byte>(source));
846+
}
847+
730848
/// <summary>
731849
/// Releases all resources used by the <see cref="MLKem"/> class.
732850
/// </summary>
@@ -752,6 +870,40 @@ protected virtual void Dispose(bool disposing)
752870
{
753871
}
754872

873+
private AsnWriter ExportSubjectPublicKeyInfoCore()
874+
{
875+
int encapsulationKeySize = Algorithm.EncapsulationKeySizeInBytes;
876+
byte[] encapsulationKeyBuffer = CryptoPool.Rent(encapsulationKeySize);
877+
Memory<byte> encapsulationKey = encapsulationKeyBuffer.AsMemory(0, encapsulationKeySize);
878+
879+
try
880+
{
881+
ExportEncapsulationKeyCore(encapsulationKey.Span);
882+
883+
SubjectPublicKeyInfoAsn spki = new SubjectPublicKeyInfoAsn
884+
{
885+
Algorithm = new AlgorithmIdentifierAsn
886+
{
887+
Algorithm = Algorithm.Oid,
888+
Parameters = default(ReadOnlyMemory<byte>?),
889+
},
890+
SubjectPublicKey = encapsulationKey,
891+
};
892+
893+
// The ASN.1 overhead of a SubjectPublicKeyInfo encoding an encapsulation key is 22 bytes.
894+
// Round it off to 32. This checked operation should never throw because the inputs are not
895+
// user provided.
896+
int capacity = checked(32 + encapsulationKeySize);
897+
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER, capacity);
898+
spki.Encode(writer);
899+
return writer;
900+
}
901+
finally
902+
{
903+
CryptoPool.Return(encapsulationKeyBuffer, clearSize: 0); // SPKI is public info, skip clear.
904+
}
905+
}
906+
755907
private protected static void ThrowIfNotSupported()
756908
{
757909
if (!IsSupported)

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ private MLKemAlgorithm(
1818
string name,
1919
int encapsulationKeySizeInBytes,
2020
int decapsulationKeySizeInBytes,
21-
int ciphertextSizeInBytes)
21+
int ciphertextSizeInBytes,
22+
string oid)
2223
{
2324
Name = name;
2425
EncapsulationKeySizeInBytes = encapsulationKeySizeInBytes;
2526
DecapsulationKeySizeInBytes = decapsulationKeySizeInBytes;
2627
CiphertextSizeInBytes = ciphertextSizeInBytes;
28+
Oid = oid;
2729
}
2830

2931
// Values are from NIST FIPS-203 table 3.
@@ -34,23 +36,23 @@ private MLKemAlgorithm(
3436
/// <value>
3537
/// An ML-KEM algorithm identifier for the ML-KEM-512 algorithm.
3638
/// </value>
37-
public static MLKemAlgorithm MLKem512 { get; } = new("ML-KEM-512", 800, 1632, 768);
39+
public static MLKemAlgorithm MLKem512 { get; } = new("ML-KEM-512", 800, 1632, 768, Oids.MlKem512);
3840

3941
/// <summary>
4042
/// Gets an ML-KEM algorithm identifier for the ML-KEM-768 algorithm.
4143
/// </summary>
4244
/// <value>
4345
/// An ML-KEM algorithm identifier for the ML-KEM-768 algorithm.
4446
/// </value>
45-
public static MLKemAlgorithm MLKem768 { get; } = new("ML-KEM-768", 1184, 2400, 1088);
47+
public static MLKemAlgorithm MLKem768 { get; } = new("ML-KEM-768", 1184, 2400, 1088, Oids.MlKem768);
4648

4749
/// <summary>
4850
/// Gets an ML-KEM algorithm identifier for the ML-KEM-1024 algorithm.
4951
/// </summary>
5052
/// <value>
5153
/// An ML-KEM algorithm identifier for the ML-KEM-1024 algorithm.
5254
/// </value>
53-
public static MLKemAlgorithm MLKem1024 { get; } = new("ML-KEM-1024", 1568, 3168, 1568);
55+
public static MLKemAlgorithm MLKem1024 { get; } = new("ML-KEM-1024", 1568, 3168, 1568, Oids.MlKem1024);
5456

5557
/// <summary>
5658
/// Gets the name of the algorithm.
@@ -104,6 +106,8 @@ private MLKemAlgorithm(
104106
// size is needed, then it can be provided as input to the private constructor.
105107
public int PrivateSeedSizeInBytes { get; } = 64;
106108

109+
internal string Oid { get; }
110+
107111
/// <summary>
108112
/// Compares two <see cref="MLKemAlgorithm" /> objects.
109113
/// </summary>
@@ -155,5 +159,16 @@ private MLKemAlgorithm(
155159
{
156160
return !(left == right);
157161
}
162+
163+
internal static MLKemAlgorithm? FromOid(string? oid)
164+
{
165+
return oid switch
166+
{
167+
Oids.MlKem512 => MLKem512,
168+
Oids.MlKem768 => MLKem768,
169+
Oids.MlKem1024 => MLKem1024,
170+
_ => null,
171+
};
172+
}
158173
}
159174
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,5 +243,13 @@ internal static partial class Oids
243243

244244
// LDAP
245245
internal const string DomainComponent = "0.9.2342.19200300.100.1.25";
246+
247+
// ML-KEM
248+
// id-alg-ml-kem-512
249+
internal const string MlKem512 = "2.16.840.1.101.3.4.4.1";
250+
// id-alg-ml-kem-768
251+
internal const string MlKem768 = "2.16.840.1.101.3.4.4.2";
252+
// id-alg-ml-kem-1024
253+
internal const string MlKem1024 = "2.16.840.1.101.3.4.4.3";
246254
}
247255
}

0 commit comments

Comments
 (0)