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 }