lib/reticulum/src/crypto/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const pretty = @import("pretty");
3 const reticulum = @import("../root.zig");
4
5 const conformance = reticulum.conformance.token;
6 const crypto = reticulum.crypto;
7
8 const nist_iv =
9 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f";
10 const nist_plaintext =
11 "\x6b\xc1\xbe\xe2\x2e\x40\x9f\x96\xe9\x3d\x7e\x11\x73\x93\x17\x2a" ++
12 "\xae\x2d\x8a\x57\x1e\x03\xac\x9c\x9e\xb7\x6f\xac\x45\xaf\x8e\x51" ++
13 "\x30\xc8\x1c\x46\xa3\x5c\xe4\x11\xe5\xfb\xc1\x19\x1a\x0a\x52\xef" ++
14 "\xf6\x9f\x24\x45\xdf\x4f\x9b\x17\xad\x2b\x41\x7b\xe6\x6c\x37\x10";
15 const nist_aes128_key =
16 "\x2b\x7e\x15\x16\x28\xae\xd2\xa6\xab\xf7\x15\x88\x09\xcf\x4f\x3c";
17 const nist_aes128_ciphertext =
18 "\x76\x49\xab\xac\x81\x19\xb2\x46\xce\xe9\x8e\x9b\x12\xe9\x19\x7d" ++
19 "\x50\x86\xcb\x9b\x50\x72\x19\xee\x95\xdb\x11\x3a\x91\x76\x78\xb2" ++
20 "\x73\xbe\xd6\xb8\xe3\xc1\x74\x3b\x71\x16\xe6\x9e\x22\x22\x95\x16" ++
21 "\x3f\xf1\xca\xa1\x68\x1f\xac\x09\x12\x0e\xca\x30\x75\x86\xe1\xa7";
22 const nist_aes256_key =
23 "\x60\x3d\xeb\x10\x15\xca\x71\xbe\x2b\x73\xae\xf0\x85\x7d\x77\x81" ++
24 "\x1f\x35\x2c\x07\x3b\x61\x08\xd7\x2d\x98\x10\xa3\x09\x14\xdf\xf4";
25 const nist_aes256_ciphertext =
26 "\xf5\x8c\x4c\x04\xd6\xe5\xf1\xba\x77\x9e\xab\xfb\x5f\x7b\xfb\xd6" ++
27 "\x9c\xfc\x4e\x96\x7e\xdb\x80\x8d\x67\x9f\x77\x7b\xc6\x70\x2c\x7d" ++
28 "\x39\xf2\x33\x69\xa9\xd9\xba\xcf\xa5\x30\xe2\x63\x04\x23\x14\x61" ++
29 "\xb2\xeb\x05\xe2\xc3\x9b\xe9\xfc\xda\x6c\x19\x07\x8c\x6a\x9d\x1b";
30
31 fn failBytes(
32 vector_name: []const u8,
33 field_name: []const u8,
34 expected: []const u8,
35 actual: []const u8,
36 ) !void {
37 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
38 defer arena.deinit();
39 var report = try pretty.diagnostic.Report.init(arena.allocator(), "Crypto mismatch");
40 defer report.deinit();
41 try report.field("vector", "{s}", .{vector_name});
42 try report.field("field", "{s}", .{field_name});
43 try report.field("expected", "{any}", .{expected});
44 try report.field("actual", "{any}", .{actual});
45 pretty.diagnostic.writeStderr(&report, .{ .width = 100 });
46 return error.ConformanceMismatch;
47 }
48
49 fn expectBytes(
50 vector_name: []const u8,
51 field_name: []const u8,
52 expected: []const u8,
53 actual: []const u8,
54 ) !void {
55 if (!std.mem.eql(u8, expected, actual)) {
56 return failBytes(vector_name, field_name, expected, actual);
57 }
58 }
59
60 fn expectBool(
61 vector_name: []const u8,
62 field_name: []const u8,
63 expected: bool,
64 actual: bool,
65 ) !void {
66 if (expected == actual) return;
67 const expected_bytes = if (expected) "true" else "false";
68 const actual_bytes = if (actual) "true" else "false";
69 return failBytes(vector_name, field_name, expected_bytes, actual_bytes);
70 }
71
72 test "Reticulum@1.5.0 RNS/Cryptography/Token.py:61-114 differential corpus" {
73 for (conformance.vectors) |vector| {
74 const token = try crypto.token.Token.init(vector.key);
75 var encrypted_storage: [512]u8 = undefined;
76 const encrypted = try token.encrypt(
77 vector.iv[0..crypto.token.iv_length].*,
78 vector.plaintext,
79 &encrypted_storage,
80 );
81 try expectBytes(vector.name, "token", vector.token, encrypted);
82
83 var decrypted_storage: [512]u8 = undefined;
84 const decrypted = try token.decrypt(vector.token, &decrypted_storage);
85 try expectBytes(vector.name, "plaintext", vector.plaintext, decrypted);
86 try expectBool(vector.name, "verify", true, token.verify(vector.token));
87 try expectBool(
88 vector.name,
89 "corrupted_verify_hmac",
90 vector.corrupted_verify_hmac,
91 token.verify(vector.corrupted_token),
92 );
93 const corrupted_decrypt = if (token.decrypt(
94 vector.corrupted_token,
95 &decrypted_storage,
96 )) |_| true else |_| false;
97 try expectBool(vector.name, "corrupted_decrypt", false, corrupted_decrypt);
98 }
99 }
100
101 test "Reticulum@1.5.0 RNS/Cryptography/HKDF.py:35-62 differential corpus" {
102 for (conformance.hkdf_vectors) |vector| {
103 var output_storage: [128]u8 = undefined;
104 const output = try crypto.hkdf.derive(
105 vector.length,
106 vector.key_material,
107 vector.salt,
108 vector.context,
109 &output_storage,
110 );
111 try expectBytes(vector.name, "output", vector.output, output);
112 }
113 }
114
115 test "Reticulum@1.5.0 RNS/Cryptography/PKCS7.py:32-48 differential corpus" {
116 for (conformance.pkcs7_vectors) |vector| {
117 var padded_storage: [512]u8 = undefined;
118 const padded = try crypto.pkcs7.pad(vector.input, &padded_storage);
119 try expectBytes(vector.name, "padded", vector.padded, padded);
120 const unpadded = crypto.pkcs7.unpad(vector.padded) catch {
121 try expectBool(vector.name, "unpad_verdict", vector.unpad_verdict, false);
122 continue;
123 };
124 try expectBool(vector.name, "unpad_verdict", vector.unpad_verdict, true);
125 try expectBytes(vector.name, "unpadded", vector.unpadded, unpadded);
126 }
127 }
128
129 test "NIST SP 800-38A F.2.1 and F.2.2 CBC-AES128" {
130 var ciphertext: [nist_plaintext.len]u8 = undefined;
131 const encrypted = try crypto.cbc.Aes128Cbc.encrypt(
132 nist_aes128_key[0..16].*,
133 nist_iv[0..16].*,
134 nist_plaintext,
135 &ciphertext,
136 );
137 try std.testing.expectEqualSlices(u8, nist_aes128_ciphertext, encrypted);
138 var plaintext: [nist_plaintext.len]u8 = undefined;
139 const decrypted = try crypto.cbc.Aes128Cbc.decrypt(
140 nist_aes128_key[0..16].*,
141 nist_iv[0..16].*,
142 encrypted,
143 &plaintext,
144 );
145 try std.testing.expectEqualSlices(u8, nist_plaintext, decrypted);
146 }
147
148 test "NIST SP 800-38A F.2.5 and F.2.6 CBC-AES256" {
149 var ciphertext: [nist_plaintext.len]u8 = undefined;
150 const encrypted = try crypto.cbc.Aes256Cbc.encrypt(
151 nist_aes256_key[0..32].*,
152 nist_iv[0..16].*,
153 nist_plaintext,
154 &ciphertext,
155 );
156 try std.testing.expectEqualSlices(u8, nist_aes256_ciphertext, encrypted);
157 var plaintext: [nist_plaintext.len]u8 = undefined;
158 const decrypted = try crypto.cbc.Aes256Cbc.decrypt(
159 nist_aes256_key[0..32].*,
160 nist_iv[0..16].*,
161 encrypted,
162 &plaintext,
163 );
164 try std.testing.expectEqualSlices(u8, nist_plaintext, decrypted);
165 }
166
167 test "RFC 4231 test cases 1 and 2 HMAC-SHA256" {
168 const key_one: [20]u8 = @splat(0x0b);
169 const expected_one =
170 "\xb0\x34\x4c\x61\xd8\xdb\x38\x53\x5c\xa8\xaf\xce\xaf\x0b\xf1\x2b" ++
171 "\x88\x1d\xc2\x00\xc9\x83\x3d\xa7\x26\xe9\x37\x6c\x2e\x32\xcf\xf7";
172 const expected_two =
173 "\x5b\xdc\xc1\x46\xbf\x60\x75\x4e\x6a\x04\x24\x26\x08\x95\x75\xc7" ++
174 "\x5a\x00\x3f\x08\x9d\x27\x39\x83\x9d\xec\x58\xb9\x64\xec\x38\x43";
175 const actual_one = crypto.hmac.sign(&key_one, "Hi There");
176 const actual_two = crypto.hmac.sign("Jefe", "what do ya want for nothing?");
177 try std.testing.expectEqualSlices(u8, expected_one, &actual_one);
178 try std.testing.expectEqualSlices(u8, expected_two, &actual_two);
179 try std.testing.expect(crypto.hmac.verify(&key_one, "Hi There", actual_one));
180 var corrupted = actual_one;
181 corrupted[0] ^= 1;
182 try std.testing.expect(!crypto.hmac.verify(&key_one, "Hi There", corrupted));
183 }
184
185 fn expectHkdf(
186 name: []const u8,
187 key_material: []const u8,
188 salt: ?[]const u8,
189 context: ?[]const u8,
190 expected: []const u8,
191 ) !void {
192 var output_storage: [82]u8 = undefined;
193 std.debug.assert(expected.len <= crypto.hkdf.max_output_length);
194 const output = try crypto.hkdf.derive(
195 @intCast(expected.len),
196 key_material,
197 salt,
198 context,
199 &output_storage,
200 );
201 try expectBytes(name, "okm", expected, output);
202 }
203
204 test "RFC 5869 test cases 1 to 3 HKDF-SHA256" {
205 const ikm_one: [22]u8 = @splat(0x0b);
206 const salt_one =
207 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c";
208 const info_one = "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9";
209 const okm_one =
210 "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a" ++
211 "\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf" ++
212 "\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65";
213 try expectHkdf("RFC-5869-1", &ikm_one, salt_one, info_one, okm_one);
214 try expectRfc5869CaseTwo();
215 const okm_three =
216 "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31" ++
217 "\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d" ++
218 "\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8";
219 try expectHkdf("RFC-5869-3", &ikm_one, null, null, okm_three);
220 }
221
222 fn expectRfc5869CaseTwo() !void {
223 const ikm =
224 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" ++
225 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" ++
226 "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f" ++
227 "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f" ++
228 "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f";
229 const salt =
230 "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f" ++
231 "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f" ++
232 "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f" ++
233 "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f" ++
234 "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf";
235 const info =
236 "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf" ++
237 "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf" ++
238 "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf" ++
239 "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef" ++
240 "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff";
241 const okm =
242 "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34" ++
243 "\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c" ++
244 "\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09" ++
245 "\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71" ++
246 "\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f" ++
247 "\x1d\x87";
248 try expectHkdf("RFC-5869-2", ikm, salt, info, okm);
249 }
250
251 test "CBC u16 maximum and maximum plus one admission" {
252 const maximum: usize = crypto.cbc.max_input_length;
253 var plaintext: [maximum]u8 = @splat(0);
254 var ciphertext: [maximum]u8 = undefined;
255 var decrypted: [maximum]u8 = undefined;
256 const key: [16]u8 = @splat(0x31);
257 const iv: [16]u8 = @splat(0x52);
258 const encrypted = try crypto.cbc.Aes128Cbc.encrypt(key, iv, &plaintext, &ciphertext);
259 const output = try crypto.cbc.Aes128Cbc.decrypt(key, iv, encrypted, &decrypted);
260 try std.testing.expectEqualSlices(u8, &plaintext, output);
261 var too_long: [maximum + 1]u8 = undefined;
262 try std.testing.expectError(
263 error.InputTooLong,
264 crypto.cbc.Aes128Cbc.decrypt(key, iv, &too_long, &decrypted),
265 );
266 }
267
268 test "PKCS#7 maximum and maximum plus one admission" {
269 const maximum: u17 = crypto.pkcs7.max_data_length;
270 var input: [maximum]u8 = @splat(0x5a);
271 var output: [crypto.pkcs7.max_padded_length]u8 = undefined;
272 const padded = try crypto.pkcs7.pad(&input, &output);
273 try std.testing.expectEqual(@as(usize, crypto.pkcs7.max_padded_length), padded.len);
274 try std.testing.expectEqualSlices(u8, &input, (try crypto.pkcs7.unpad(padded)));
275 try std.testing.expectError(
276 error.InputTooLong,
277 crypto.pkcs7.paddedLength(maximum + 1),
278 );
279 }
280
281 test "Reticulum@1.5.0 RNS/Cryptography/PKCS7.py:41-48 exact rejection rules" {
282 try std.testing.expectError(error.EmptyInput, crypto.pkcs7.unpad(""));
283 try std.testing.expectError(error.InvalidPadding, crypto.pkcs7.unpad("\x11"));
284 try std.testing.expectEqualSlices(u8, "\x42\x00", try crypto.pkcs7.unpad("\x42\x00"));
285 try std.testing.expectEqualSlices(u8, "\xaa", try crypto.pkcs7.unpad("\xaa\x03"));
286 }
287
288 test "HKDF u16 maximum and maximum plus one admission" {
289 const maximum: u17 = crypto.hkdf.max_output_length;
290 var output: [maximum]u8 = undefined;
291 const derived = try crypto.hkdf.derive(maximum, "k", null, null, &output);
292 try std.testing.expectEqual(@as(usize, maximum), derived.len);
293 try std.testing.expectError(
294 error.OutputTooLong,
295 crypto.hkdf.derive(maximum + 1, "k", null, null, &output),
296 );
297 }
298
299 test "token u16 maximum and maximum plus one admission" {
300 try std.testing.expectEqual(
301 crypto.token.max_encrypted_length,
302 try crypto.token.encryptedLength(crypto.token.max_plaintext_length),
303 );
304 try std.testing.expectError(
305 error.PlaintextTooLong,
306 crypto.token.encryptedLength(@as(u17, crypto.token.max_plaintext_length) + 1),
307 );
308 const key: [32]u8 = @splat(0x41);
309 const short_token: [32]u8 = @splat(0x00);
310 const token = try crypto.token.Token.init(&key);
311 try std.testing.expect(!token.verify(&short_token));
312 try std.testing.expectError(error.InvalidKeyLength, crypto.token.Token.init("short"));
313 }
314
315 test "Reticulum@1.5.0 RNS/Cryptography/Token.py:77-114 authenticated short token" {
316 const key: [32]u8 = @splat(0x29);
317 const token = try crypto.token.Token.init(&key);
318 var malformed: [33]u8 = undefined;
319 malformed[0] = 0x70;
320 const tag = crypto.hmac.sign(key[0..16], malformed[0..1]);
321 @memcpy(malformed[1..], &tag);
322 try std.testing.expect(token.verify(&malformed));
323 var output: [16]u8 = undefined;
324 try std.testing.expectError(error.InvalidToken, token.decrypt(&malformed, &output));
325 }