Skip to content

Commit d2ee366

Browse files
SPKI and PKCS8 for Composite ML-DSA (#119837)
1 parent 2db45f1 commit d2ee366

File tree

9 files changed

+1156
-60
lines changed

9 files changed

+1156
-60
lines changed

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -866,19 +866,13 @@ static void PrivateKeyReader(
866866
out CompositeMLDsa dsa)
867867
{
868868
CompositeMLDsaAlgorithm algorithm = GetAlgorithmIdentifier(in algorithmIdentifier);
869-
AsnValueReader reader = new AsnValueReader(privateKeyContents.Span, AsnEncodingRules.BER);
870869

871-
if (!reader.TryReadPrimitiveOctetString(out ReadOnlySpan<byte> key) || reader.HasData)
872-
{
873-
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
874-
}
875-
876-
if (!algorithm.IsValidPrivateKeySize(key.Length))
870+
if (!algorithm.IsValidPrivateKeySize(privateKeyContents.Length))
877871
{
878872
throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm);
879873
}
880874

881-
dsa = CompositeMLDsaImplementation.ImportCompositeMLDsaPrivateKeyImpl(algorithm, key);
875+
dsa = CompositeMLDsaImplementation.ImportCompositeMLDsaPrivateKeyImpl(algorithm, privateKeyContents.Span);
882876
}
883877
}
884878

@@ -1863,9 +1857,7 @@ private AsnWriter WriteSubjectPublicKeyToAsnWriter()
18631857

18641858
ReadOnlySpan<byte> publicKey = buffer.AsSpan(0, written);
18651859

1866-
// TODO verify overhead
1867-
1868-
// TODO: The ASN.1 overhead of a SubjectPublicKeyInfo encoding a public key is ___ bytes.
1860+
// The ASN.1 overhead of a SubjectPublicKeyInfo encoding a public key is around 24 bytes.
18691861
// Round it off to 32. This checked operation should never throw because the inputs are not
18701862
// user provided.
18711863
int capacity = checked(32 + publicKey.Length);

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
8+
using System.Formats.Asn1;
89
using System.Runtime.CompilerServices;
910
using System.Runtime.Versioning;
1011
using Internal.Cryptography;
@@ -367,8 +368,43 @@ protected override bool VerifyDataCore(ReadOnlySpan<byte> data, ReadOnlySpan<byt
367368
return And(_mldsa.VerifyData(M_prime, mldsaSig, AlgorithmDetails.DomainSeparator), _componentAlgorithm.VerifyData(M_prime, tradSig));
368369
}
369370

370-
protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten) =>
371-
throw new PlatformNotSupportedException();
371+
protected override bool TryExportPkcs8PrivateKeyCore(Span<byte> destination, out int bytesWritten)
372+
{
373+
AsnWriter? writer = null;
374+
375+
try
376+
{
377+
using (CryptoPoolLease lease = CryptoPoolLease.Rent(Algorithm.MaxPrivateKeySizeInBytes))
378+
{
379+
int privateKeySize = ExportCompositeMLDsaPrivateKeyCore(lease.Span);
380+
381+
// Add some overhead for the ASN.1 structure.
382+
int initialCapacity = 32 + privateKeySize;
383+
384+
writer = new AsnWriter(AsnEncodingRules.DER, initialCapacity);
385+
386+
using (writer.PushSequence())
387+
{
388+
writer.WriteInteger(0); // Version
389+
390+
using (writer.PushSequence())
391+
{
392+
writer.WriteObjectIdentifier(Algorithm.Oid);
393+
}
394+
395+
writer.WriteOctetString(lease.Span.Slice(0, privateKeySize));
396+
}
397+
398+
Debug.Assert(writer.GetEncodedLength() <= initialCapacity);
399+
}
400+
401+
return writer.TryEncode(destination, out bytesWritten);
402+
}
403+
finally
404+
{
405+
writer?.Reset();
406+
}
407+
}
372408

373409
protected override int ExportCompositeMLDsaPublicKeyCore(Span<byte> destination)
374410
{

src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/CompositeMLDsa/CompositeMLDsaContractTests.cs

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

44
using System.Collections.Generic;
5+
using System.Formats.Asn1;
56
using System.Linq;
7+
using System.Security.Cryptography.Asn1;
68
using Xunit;
79

810
using CompositeMLDsaTestVector = System.Security.Cryptography.Tests.CompositeMLDsaTestData.CompositeMLDsaTestVector;
@@ -32,6 +34,20 @@ public static void NullArgumentValidation(CompositeMLDsaAlgorithm algorithm, boo
3234
AssertExtensions.Throws<ArgumentNullException>("data", () => dsa.VerifyData(null, null));
3335

3436
AssertExtensions.Throws<ArgumentNullException>("signature", () => dsa.VerifyData(Array.Empty<byte>(), null));
37+
38+
AssertExtensions.Throws<ArgumentNullException>("password", () => dsa.ExportEncryptedPkcs8PrivateKey((string)null, null));
39+
AssertExtensions.Throws<ArgumentNullException>("password", () => dsa.ExportEncryptedPkcs8PrivateKeyPem((string)null, null));
40+
AssertExtensions.Throws<ArgumentNullException>("password", () => dsa.TryExportEncryptedPkcs8PrivateKey((string)null, null, Span<byte>.Empty, out _));
41+
42+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte>.Empty, null));
43+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(ReadOnlySpan<char>.Empty, null));
44+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKey(string.Empty, null));
45+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan<byte>.Empty, null));
46+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKeyPem(ReadOnlySpan<char>.Empty, null));
47+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.ExportEncryptedPkcs8PrivateKeyPem(string.Empty, null));
48+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan<byte>.Empty, null, Span<byte>.Empty, out _));
49+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.TryExportEncryptedPkcs8PrivateKey(ReadOnlySpan<char>.Empty, null, Span<byte>.Empty, out _));
50+
AssertExtensions.Throws<ArgumentNullException>("pbeParameters", () => dsa.TryExportEncryptedPkcs8PrivateKey(string.Empty, null, Span<byte>.Empty, out _));
3551
}
3652

3753
[Fact]
@@ -62,6 +78,37 @@ public static void ArgumentValidation(CompositeMLDsaAlgorithm algorithm, bool sh
6278
AssertExtensions.Throws<ArgumentOutOfRangeException>("context", () => dsa.VerifyData(Array.Empty<byte>(), new byte[maxSignatureSize], new byte[256]));
6379
}
6480

81+
[Theory]
82+
[MemberData(nameof(ArgumentValidationData))]
83+
public static void ArgumentValidation_PbeParameters(CompositeMLDsaAlgorithm algorithm, bool shouldDispose)
84+
{
85+
using CompositeMLDsa dsa = CompositeMLDsaMockImplementation.Create(algorithm);
86+
87+
if (shouldDispose)
88+
{
89+
// Test that argument validation exceptions take precedence over ObjectDisposedException
90+
dsa.Dispose();
91+
}
92+
93+
CompositeMLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export =>
94+
{
95+
// Unknown algorithm
96+
AssertExtensions.Throws<CryptographicException>(() =>
97+
export(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.Unknown, HashAlgorithmName.SHA1, 42)));
98+
99+
// TripleDes3KeyPkcs12 only works with SHA1
100+
AssertExtensions.Throws<CryptographicException>(() =>
101+
export(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA512, 42)));
102+
});
103+
104+
CompositeMLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(export =>
105+
{
106+
// Bytes not allowed in TripleDes3KeyPkcs12
107+
AssertExtensions.Throws<CryptographicException>(() =>
108+
export(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 42)));
109+
}, CompositeMLDsaTestHelpers.EncryptionPasswordType.Byte);
110+
}
111+
65112
[Theory]
66113
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
67114
public static void TryExportCompositeMLDsaPublicKey_LowerBound(CompositeMLDsaAlgorithm algorithm)
@@ -926,6 +973,223 @@ public static void Dispose_CallsVirtual(CompositeMLDsaAlgorithm algorithm)
926973
CompositeMLDsaTestHelpers.VerifyDisposed(dsa);
927974
}
928975

976+
[Theory]
977+
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
978+
public static void ExportSubjectPublicKeyInfo_CallsExportPublicKey(CompositeMLDsaAlgorithm algorithm)
979+
{
980+
CompositeMLDsaTestHelpers.AssertExportSubjectPublicKeyInfo(export =>
981+
{
982+
using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm);
983+
984+
dsa.ExportCompositeMLDsaPublicKeyCoreHook = dest => dest.Length;
985+
dsa.AddLengthAssertion();
986+
dsa.AddFillDestination(1);
987+
988+
byte[] exported = export(dsa);
989+
AssertExtensions.GreaterThan(dsa.ExportCompositeMLDsaPublicKeyCoreCallCount, 0);
990+
991+
SubjectPublicKeyInfoAsn exportedSpki = SubjectPublicKeyInfoAsn.Decode(exported, AsnEncodingRules.DER);
992+
AssertExtensions.FilledWith<byte>(1, exportedSpki.SubjectPublicKey.Span);
993+
Assert.Equal(CompositeMLDsaTestHelpers.AlgorithmToOid(algorithm), exportedSpki.Algorithm.Algorithm);
994+
AssertExtensions.FalseExpression(exportedSpki.Algorithm.Parameters.HasValue);
995+
});
996+
}
997+
998+
[Theory]
999+
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
1000+
public static void TryExportPkcs8PrivateKey_DestinationTooSmall(CompositeMLDsaAlgorithm algorithm)
1001+
{
1002+
using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm);
1003+
1004+
// Early heuristic based bailout so no core methods are called
1005+
AssertExtensions.FalseExpression(
1006+
dsa.TryExportPkcs8PrivateKey(new byte[CompositeMLDsaTestHelpers.ExpectedPrivateKeySizeLowerBound(algorithm) - 1], out int bytesWritten));
1007+
Assert.Equal(0, bytesWritten);
1008+
}
1009+
1010+
[Theory]
1011+
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
1012+
public static void ExportPkcs8PrivateKey_DestinationInitialSize(CompositeMLDsaAlgorithm algorithm)
1013+
{
1014+
using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm);
1015+
1016+
dsa.TryExportPkcs8PrivateKeyCoreHook = (Span<byte> destination, out int bytesWritten) =>
1017+
{
1018+
// The first call should at least be the size of the private key
1019+
destination.Fill(42);
1020+
AssertExtensions.GreaterThanOrEqualTo(destination.Length, CompositeMLDsaTestHelpers.ExpectedPrivateKeySizeLowerBound(algorithm));
1021+
bytesWritten = destination.Length;
1022+
1023+
// Before we return, update the next callback so subsequent calls fail the test
1024+
dsa.TryExportPkcs8PrivateKeyCoreHook = (Span<byte> destination, out int bytesWritten) =>
1025+
{
1026+
Assert.Fail();
1027+
bytesWritten = 0;
1028+
return true;
1029+
};
1030+
1031+
return true;
1032+
};
1033+
1034+
byte[] exported = dsa.ExportPkcs8PrivateKey();
1035+
1036+
Assert.Equal(1, dsa.TryExportPkcs8PrivateKeyCoreCallCount);
1037+
AssertExtensions.FilledWith<byte>(42, exported);
1038+
}
1039+
1040+
[Theory]
1041+
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
1042+
public static void ExportPkcs8PrivateKey_Resizes(CompositeMLDsaAlgorithm algorithm)
1043+
{
1044+
using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm);
1045+
1046+
int originalSize = -1;
1047+
dsa.TryExportPkcs8PrivateKeyCoreHook = (Span<byte> destination, out int bytesWritten) =>
1048+
{
1049+
// Return false to force a resize
1050+
bool ret = false;
1051+
originalSize = destination.Length;
1052+
bytesWritten = 0;
1053+
1054+
// Before we return false, update the callback so the next call will succeed
1055+
dsa.TryExportPkcs8PrivateKeyCoreHook = (Span<byte> destination, out int bytesWritten) =>
1056+
{
1057+
// New buffer must be larger than the original
1058+
bool ret = true;
1059+
AssertExtensions.GreaterThan(destination.Length, originalSize);
1060+
destination.Fill(42);
1061+
bytesWritten = destination.Length;
1062+
1063+
// Before we return, update the next callback so subsequent calls fail the test
1064+
dsa.TryExportPkcs8PrivateKeyCoreHook = (Span<byte> destination, out int bytesWritten) =>
1065+
{
1066+
Assert.Fail();
1067+
bytesWritten = 0;
1068+
return true;
1069+
};
1070+
1071+
return ret;
1072+
};
1073+
1074+
return ret;
1075+
};
1076+
1077+
byte[] exported = dsa.ExportPkcs8PrivateKey();
1078+
1079+
Assert.Equal(2, dsa.TryExportPkcs8PrivateKeyCoreCallCount);
1080+
AssertExtensions.FilledWith<byte>(42, exported);
1081+
}
1082+
1083+
[Theory]
1084+
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
1085+
public static void ExportPkcs8PrivateKey_IgnoreReturnValue(CompositeMLDsaAlgorithm algorithm)
1086+
{
1087+
using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm);
1088+
1089+
int[] valuesToWrite = [-1, 0, int.MaxValue];
1090+
int index = 0;
1091+
1092+
int finalDestinationSize = -1;
1093+
dsa.TryExportPkcs8PrivateKeyCoreHook = (Span<byte> destination, out int bytesWritten) =>
1094+
{
1095+
// Go through all the values we want to test, and once we reach the last one,
1096+
// return true with a valid value
1097+
if (index >= valuesToWrite.Length)
1098+
{
1099+
finalDestinationSize = bytesWritten = 1;
1100+
return true;
1101+
}
1102+
1103+
// This returned value should should be ignored. There's no way to check
1104+
// what happens with it, but at the very least we should expect no exceptions
1105+
// and the correct number of calls.
1106+
bytesWritten = valuesToWrite[index];
1107+
index++;
1108+
return false;
1109+
};
1110+
1111+
int actualSize = dsa.ExportPkcs8PrivateKey().Length;
1112+
Assert.Equal(finalDestinationSize, actualSize);
1113+
Assert.Equal(valuesToWrite.Length + 1, dsa.TryExportPkcs8PrivateKeyCoreCallCount);
1114+
}
1115+
1116+
[Theory]
1117+
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
1118+
public static void ExportPkcs8PrivateKey_HandleBadReturnValue(CompositeMLDsaAlgorithm algorithm)
1119+
{
1120+
using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm);
1121+
1122+
Func<int, int> getBadReturnValue = (int destinationLength) => destinationLength + 1;
1123+
CompositeMLDsaMockImplementation.TryExportFunc hook = (Span<byte> destination, out int bytesWritten) =>
1124+
{
1125+
bool ret = true;
1126+
1127+
bytesWritten = getBadReturnValue(destination.Length);
1128+
1129+
// Before we return, update the next callback so subsequent calls fail the test
1130+
dsa.TryExportPkcs8PrivateKeyCoreHook = (Span<byte> destination, out int bytesWritten) =>
1131+
{
1132+
Assert.Fail();
1133+
bytesWritten = 0;
1134+
return true;
1135+
};
1136+
1137+
return ret;
1138+
};
1139+
1140+
dsa.TryExportPkcs8PrivateKeyCoreHook = hook;
1141+
Assert.Throws<CryptographicException>(dsa.ExportPkcs8PrivateKey);
1142+
Assert.Equal(1, dsa.TryExportPkcs8PrivateKeyCoreCallCount);
1143+
1144+
dsa.TryExportPkcs8PrivateKeyCoreHook = hook;
1145+
getBadReturnValue = (int destinationLength) => int.MaxValue;
1146+
Assert.Throws<CryptographicException>(dsa.ExportPkcs8PrivateKey);
1147+
Assert.Equal(2, dsa.TryExportPkcs8PrivateKeyCoreCallCount);
1148+
1149+
dsa.TryExportPkcs8PrivateKeyCoreHook = hook;
1150+
getBadReturnValue = (int destinationLength) => -1;
1151+
Assert.Throws<CryptographicException>(dsa.ExportPkcs8PrivateKey);
1152+
Assert.Equal(3, dsa.TryExportPkcs8PrivateKeyCoreCallCount);
1153+
}
1154+
1155+
[Theory]
1156+
[MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))]
1157+
public static void ExportPkcs8PrivateKey_HandleBadReturnBuffer(CompositeMLDsaAlgorithm algorithm)
1158+
{
1159+
CompositeMLDsaTestHelpers.AssertEncryptedExportPkcs8PrivateKey(exportEncrypted =>
1160+
{
1161+
using CompositeMLDsaMockImplementation dsa = CompositeMLDsaMockImplementation.Create(algorithm);
1162+
1163+
// Create a bad encoding
1164+
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
1165+
writer.WriteBitString("some string"u8);
1166+
byte[] validEncoding = writer.Encode();
1167+
Memory<byte> badEncoding = validEncoding.AsMemory(0, validEncoding.Length - 1); // Chop off the last byte
1168+
1169+
CompositeMLDsaMockImplementation.TryExportFunc hook = (Span<byte> destination, out int bytesWritten) =>
1170+
{
1171+
bool ret = badEncoding.Span.TryCopyTo(destination);
1172+
bytesWritten = ret ? badEncoding.Length : 0;
1173+
return ret;
1174+
};
1175+
1176+
dsa.TryExportPkcs8PrivateKeyCoreHook = hook;
1177+
1178+
// Exporting the key should work without any issues because there's no validation
1179+
AssertExtensions.SequenceEqual(badEncoding.Span, dsa.ExportPkcs8PrivateKey().AsSpan());
1180+
1181+
int numberOfCalls = dsa.TryExportPkcs8PrivateKeyCoreCallCount;
1182+
dsa.TryExportPkcs8PrivateKeyCoreCallCount = 0;
1183+
1184+
// However, exporting the encrypted key should fail because it validates the PKCS#8 private key encoding first
1185+
AssertExtensions.Throws<CryptographicException>(() =>
1186+
exportEncrypted(dsa, "PLACEHOLDER", new PbeParameters(PbeEncryptionAlgorithm.Aes128Cbc, HashAlgorithmName.SHA1, 1)));
1187+
1188+
// Sanity check that the code to export the private key was called
1189+
Assert.Equal(numberOfCalls, dsa.TryExportPkcs8PrivateKeyCoreCallCount);
1190+
});
1191+
}
1192+
9291193
private static void AssertExpectedFill(ReadOnlySpan<byte> buffer, ReadOnlySpan<byte> content, int offset, byte paddingElement)
9301194
{
9311195
// Ensure that the data was filled correctly

0 commit comments

Comments
 (0)