|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license. |
3 | 3 |
|
4 | 4 | using System.Collections.Generic; |
| 5 | +using System.Formats.Asn1; |
5 | 6 | using System.Linq; |
| 7 | +using System.Security.Cryptography.Asn1; |
6 | 8 | using Xunit; |
7 | 9 |
|
8 | 10 | using CompositeMLDsaTestVector = System.Security.Cryptography.Tests.CompositeMLDsaTestData.CompositeMLDsaTestVector; |
@@ -32,6 +34,20 @@ public static void NullArgumentValidation(CompositeMLDsaAlgorithm algorithm, boo |
32 | 34 | AssertExtensions.Throws<ArgumentNullException>("data", () => dsa.VerifyData(null, null)); |
33 | 35 |
|
34 | 36 | 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 _)); |
35 | 51 | } |
36 | 52 |
|
37 | 53 | [Fact] |
@@ -62,6 +78,37 @@ public static void ArgumentValidation(CompositeMLDsaAlgorithm algorithm, bool sh |
62 | 78 | AssertExtensions.Throws<ArgumentOutOfRangeException>("context", () => dsa.VerifyData(Array.Empty<byte>(), new byte[maxSignatureSize], new byte[256])); |
63 | 79 | } |
64 | 80 |
|
| 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 | + |
65 | 112 | [Theory] |
66 | 113 | [MemberData(nameof(CompositeMLDsaTestData.AllAlgorithmsTestData), MemberType = typeof(CompositeMLDsaTestData))] |
67 | 114 | public static void TryExportCompositeMLDsaPublicKey_LowerBound(CompositeMLDsaAlgorithm algorithm) |
@@ -926,6 +973,223 @@ public static void Dispose_CallsVirtual(CompositeMLDsaAlgorithm algorithm) |
926 | 973 | CompositeMLDsaTestHelpers.VerifyDisposed(dsa); |
927 | 974 | } |
928 | 975 |
|
| 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 | + |
929 | 1193 | private static void AssertExpectedFill(ReadOnlySpan<byte> buffer, ReadOnlySpan<byte> content, int offset, byte paddingElement) |
930 | 1194 | { |
931 | 1195 | // Ensure that the data was filled correctly |
|
0 commit comments