Skip to content

Commit 00b280a

Browse files
PranavSenthilnathanvcsjonesbartonjs
authored
PKCS#8 support for ML-DSA (#115569)
Co-authored-by: Kevin Jones <[email protected]> Co-authored-by: Jeremy Barton <[email protected]>
1 parent 9c6be5f commit 00b280a

27 files changed

+4753
-281
lines changed

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

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,35 @@ internal enum PalMLDsaAlgorithmId
2424
[LibraryImport(Libraries.CryptoNative)]
2525
private static partial int CryptoNative_MLDsaGetPalId(
2626
SafeEvpPKeyHandle mldsa,
27-
out PalMLDsaAlgorithmId mldsaId);
28-
29-
internal static PalMLDsaAlgorithmId MLDsaGetPalId(SafeEvpPKeyHandle key)
27+
out PalMLDsaAlgorithmId mldsaId,
28+
out int hasSeed,
29+
out int hasSecretKey);
30+
31+
internal static PalMLDsaAlgorithmId MLDsaGetPalId(
32+
SafeEvpPKeyHandle key,
33+
out bool hasSeed,
34+
out bool hasSecretKey)
3035
{
3136
const int Success = 1;
37+
const int Yes = 1;
3238
const int Fail = 0;
33-
int result = CryptoNative_MLDsaGetPalId(key, out PalMLDsaAlgorithmId mldsaId);
34-
35-
return result switch
36-
{
37-
Success => mldsaId,
38-
Fail => throw CreateOpenSslCryptographicException(),
39-
int other => throw FailThrow(other),
40-
};
39+
int result = CryptoNative_MLDsaGetPalId(
40+
key,
41+
out PalMLDsaAlgorithmId mldsaId,
42+
out int pKeyHasSeed,
43+
out int pKeyHasSecretKey);
4144

42-
static Exception FailThrow(int result)
45+
switch (result)
4346
{
44-
Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_MLDsaGetPalId)}.");
45-
return new CryptographicException();
47+
case Success:
48+
hasSeed = pKeyHasSeed == Yes;
49+
hasSecretKey = pKeyHasSecretKey == Yes;
50+
return mldsaId;
51+
case Fail:
52+
throw CreateOpenSslCryptographicException();
53+
default:
54+
Debug.Fail($"Unexpected return value {result} from {nameof(CryptoNative_MLDsaGetPalId)}.");
55+
throw new CryptographicException();
4656
}
4757
}
4858

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<asn:Choice
3+
xmlns:asn="http://schemas.dot.net/asnxml/201808/"
4+
name="MLDsaPrivateKeyAsn"
5+
namespace="System.Security.Cryptography.Asn1">
6+
7+
<!--
8+
https://github.com/lamps-wg/dilithium-certificates/blob/5b23428b08a53aacdb89d93422b81228433e34d8/draft-ietf-lamps-dilithium-certificates.md
9+
10+
ML-DSA-44-PrivateKey ::= CHOICE {
11+
seed [0] OCTET STRING (SIZE (32)),
12+
expandedKey OCTET STRING (SIZE (2560)),
13+
both SEQUENCE {
14+
seed OCTET STRING (SIZE (32)),
15+
expandedKey OCTET STRING (SIZE (2560))
16+
}
17+
}
18+
19+
ML-DSA-65-PrivateKey ::= CHOICE {
20+
seed [0] OCTET STRING (SIZE (32)),
21+
expandedKey OCTET STRING (SIZE (4032)),
22+
both SEQUENCE {
23+
seed OCTET STRING (SIZE (32)),
24+
expandedKey OCTET STRING (SIZE (4032))
25+
}
26+
}
27+
28+
ML-DSA-87-PrivateKey ::= CHOICE {
29+
seed [0] OCTET STRING (SIZE (32)),
30+
expandedKey OCTET STRING (SIZE (4896)),
31+
both SEQUENCE {
32+
seed OCTET STRING (SIZE (32)),
33+
expandedKey OCTET STRING (SIZE (4896))
34+
}
35+
}
36+
-->
37+
<asn:OctetString name="Seed" implicitTag="0" />
38+
<asn:OctetString name="ExpandedKey" />
39+
<asn:AsnType name="Both" typeName="System.Security.Cryptography.Asn1.MLDsaPrivateKeyBothAsn" />
40+
</asn:Choice>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
#pragma warning disable SA1028 // ignore whitespace warnings for generated code
5+
using System;
6+
using System.Formats.Asn1;
7+
using System.Runtime.InteropServices;
8+
9+
namespace System.Security.Cryptography.Asn1
10+
{
11+
[StructLayout(LayoutKind.Sequential)]
12+
internal partial struct MLDsaPrivateKeyAsn
13+
{
14+
internal ReadOnlyMemory<byte>? Seed;
15+
internal ReadOnlyMemory<byte>? ExpandedKey;
16+
internal System.Security.Cryptography.Asn1.MLDsaPrivateKeyBothAsn? Both;
17+
18+
#if DEBUG
19+
static MLDsaPrivateKeyAsn()
20+
{
21+
var usedTags = new System.Collections.Generic.Dictionary<Asn1Tag, string>();
22+
Action<Asn1Tag, string> ensureUniqueTag = (tag, fieldName) =>
23+
{
24+
if (usedTags.TryGetValue(tag, out string? existing))
25+
{
26+
throw new InvalidOperationException($"Tag '{tag}' is in use by both '{existing}' and '{fieldName}'");
27+
}
28+
29+
usedTags.Add(tag, fieldName);
30+
};
31+
32+
ensureUniqueTag(new Asn1Tag(TagClass.ContextSpecific, 0), "Seed");
33+
ensureUniqueTag(Asn1Tag.PrimitiveOctetString, "ExpandedKey");
34+
ensureUniqueTag(Asn1Tag.Sequence, "Both");
35+
}
36+
#endif
37+
38+
internal readonly void Encode(AsnWriter writer)
39+
{
40+
bool wroteValue = false;
41+
42+
if (Seed.HasValue)
43+
{
44+
if (wroteValue)
45+
throw new CryptographicException();
46+
47+
writer.WriteOctetString(Seed.Value.Span, new Asn1Tag(TagClass.ContextSpecific, 0));
48+
wroteValue = true;
49+
}
50+
51+
if (ExpandedKey.HasValue)
52+
{
53+
if (wroteValue)
54+
throw new CryptographicException();
55+
56+
writer.WriteOctetString(ExpandedKey.Value.Span);
57+
wroteValue = true;
58+
}
59+
60+
if (Both.HasValue)
61+
{
62+
if (wroteValue)
63+
throw new CryptographicException();
64+
65+
Both.Value.Encode(writer);
66+
wroteValue = true;
67+
}
68+
69+
if (!wroteValue)
70+
{
71+
throw new CryptographicException();
72+
}
73+
}
74+
75+
internal static MLDsaPrivateKeyAsn Decode(ReadOnlyMemory<byte> encoded, AsnEncodingRules ruleSet)
76+
{
77+
try
78+
{
79+
AsnValueReader reader = new AsnValueReader(encoded.Span, ruleSet);
80+
81+
DecodeCore(ref reader, encoded, out MLDsaPrivateKeyAsn decoded);
82+
reader.ThrowIfNotEmpty();
83+
return decoded;
84+
}
85+
catch (AsnContentException e)
86+
{
87+
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
88+
}
89+
}
90+
91+
internal static void Decode(ref AsnValueReader reader, ReadOnlyMemory<byte> rebind, out MLDsaPrivateKeyAsn decoded)
92+
{
93+
try
94+
{
95+
DecodeCore(ref reader, rebind, out decoded);
96+
}
97+
catch (AsnContentException e)
98+
{
99+
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
100+
}
101+
}
102+
103+
private static void DecodeCore(ref AsnValueReader reader, ReadOnlyMemory<byte> rebind, out MLDsaPrivateKeyAsn decoded)
104+
{
105+
decoded = default;
106+
Asn1Tag tag = reader.PeekTag();
107+
ReadOnlySpan<byte> rebindSpan = rebind.Span;
108+
int offset;
109+
ReadOnlySpan<byte> tmpSpan;
110+
111+
if (tag.HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 0)))
112+
{
113+
114+
if (reader.TryReadPrimitiveOctetString(out tmpSpan, new Asn1Tag(TagClass.ContextSpecific, 0)))
115+
{
116+
decoded.Seed = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
117+
}
118+
else
119+
{
120+
decoded.Seed = reader.ReadOctetString(new Asn1Tag(TagClass.ContextSpecific, 0));
121+
}
122+
123+
}
124+
else if (tag.HasSameClassAndValue(Asn1Tag.PrimitiveOctetString))
125+
{
126+
127+
if (reader.TryReadPrimitiveOctetString(out tmpSpan))
128+
{
129+
decoded.ExpandedKey = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
130+
}
131+
else
132+
{
133+
decoded.ExpandedKey = reader.ReadOctetString();
134+
}
135+
136+
}
137+
else if (tag.HasSameClassAndValue(Asn1Tag.Sequence))
138+
{
139+
System.Security.Cryptography.Asn1.MLDsaPrivateKeyBothAsn tmpBoth;
140+
System.Security.Cryptography.Asn1.MLDsaPrivateKeyBothAsn.Decode(ref reader, rebind, out tmpBoth);
141+
decoded.Both = tmpBoth;
142+
143+
}
144+
else
145+
{
146+
throw new CryptographicException();
147+
}
148+
}
149+
}
150+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<asn:Sequence
3+
xmlns:asn="http://schemas.dot.net/asnxml/201808/"
4+
name="MLDsaPrivateKeyBothAsn"
5+
namespace="System.Security.Cryptography.Asn1">
6+
7+
<!--
8+
https://github.com/lamps-wg/dilithium-certificates/blob/5b23428b08a53aacdb89d93422b81228433e34d8/draft-ietf-lamps-dilithium-certificates.md
9+
10+
both SEQUENCE {
11+
seed OCTET STRING (SIZE (32)),
12+
expandedKey OCTET STRING (SIZE (2560))
13+
}
14+
-->
15+
<asn:OctetString name="Seed" />
16+
<asn:OctetString name="ExpandedKey" />
17+
</asn:Sequence>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
#pragma warning disable SA1028 // ignore whitespace warnings for generated code
5+
using System;
6+
using System.Formats.Asn1;
7+
using System.Runtime.InteropServices;
8+
9+
namespace System.Security.Cryptography.Asn1
10+
{
11+
[StructLayout(LayoutKind.Sequential)]
12+
internal partial struct MLDsaPrivateKeyBothAsn
13+
{
14+
internal ReadOnlyMemory<byte> Seed;
15+
internal ReadOnlyMemory<byte> ExpandedKey;
16+
17+
internal readonly void Encode(AsnWriter writer)
18+
{
19+
Encode(writer, Asn1Tag.Sequence);
20+
}
21+
22+
internal readonly void Encode(AsnWriter writer, Asn1Tag tag)
23+
{
24+
writer.PushSequence(tag);
25+
26+
writer.WriteOctetString(Seed.Span);
27+
writer.WriteOctetString(ExpandedKey.Span);
28+
writer.PopSequence(tag);
29+
}
30+
31+
internal static MLDsaPrivateKeyBothAsn Decode(ReadOnlyMemory<byte> encoded, AsnEncodingRules ruleSet)
32+
{
33+
return Decode(Asn1Tag.Sequence, encoded, ruleSet);
34+
}
35+
36+
internal static MLDsaPrivateKeyBothAsn Decode(Asn1Tag expectedTag, ReadOnlyMemory<byte> encoded, AsnEncodingRules ruleSet)
37+
{
38+
try
39+
{
40+
AsnValueReader reader = new AsnValueReader(encoded.Span, ruleSet);
41+
42+
DecodeCore(ref reader, expectedTag, encoded, out MLDsaPrivateKeyBothAsn decoded);
43+
reader.ThrowIfNotEmpty();
44+
return decoded;
45+
}
46+
catch (AsnContentException e)
47+
{
48+
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
49+
}
50+
}
51+
52+
internal static void Decode(ref AsnValueReader reader, ReadOnlyMemory<byte> rebind, out MLDsaPrivateKeyBothAsn decoded)
53+
{
54+
Decode(ref reader, Asn1Tag.Sequence, rebind, out decoded);
55+
}
56+
57+
internal static void Decode(ref AsnValueReader reader, Asn1Tag expectedTag, ReadOnlyMemory<byte> rebind, out MLDsaPrivateKeyBothAsn decoded)
58+
{
59+
try
60+
{
61+
DecodeCore(ref reader, expectedTag, rebind, out decoded);
62+
}
63+
catch (AsnContentException e)
64+
{
65+
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e);
66+
}
67+
}
68+
69+
private static void DecodeCore(ref AsnValueReader reader, Asn1Tag expectedTag, ReadOnlyMemory<byte> rebind, out MLDsaPrivateKeyBothAsn decoded)
70+
{
71+
decoded = default;
72+
AsnValueReader sequenceReader = reader.ReadSequence(expectedTag);
73+
ReadOnlySpan<byte> rebindSpan = rebind.Span;
74+
int offset;
75+
ReadOnlySpan<byte> tmpSpan;
76+
77+
78+
if (sequenceReader.TryReadPrimitiveOctetString(out tmpSpan))
79+
{
80+
decoded.Seed = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
81+
}
82+
else
83+
{
84+
decoded.Seed = sequenceReader.ReadOctetString();
85+
}
86+
87+
88+
if (sequenceReader.TryReadPrimitiveOctetString(out tmpSpan))
89+
{
90+
decoded.ExpandedKey = rebindSpan.Overlaps(tmpSpan, out offset) ? rebind.Slice(offset, tmpSpan.Length) : tmpSpan.ToArray();
91+
}
92+
else
93+
{
94+
decoded.ExpandedKey = sequenceReader.ReadOctetString();
95+
}
96+
97+
98+
sequenceReader.ThrowIfNotEmpty();
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)