lib/reticulum/src/wire/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;
6 const wire = reticulum.wire;
7 const AnnounceVector = conformance.announce.Vector;
8 const PacketVector = conformance.packet.Vector;
9
10 fn failValue(
11 vector_name: []const u8,
12 field_name: []const u8,
13 expected: anytype,
14 actual: anytype,
15 ) !void {
16 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
17 defer arena.deinit();
18 var report = try pretty.diagnostic.Report.init(
19 arena.allocator(),
20 "Reticulum wire conformance mismatch",
21 );
22 defer report.deinit();
23 try report.field("vector", "{s}", .{vector_name});
24 try report.field("field", "{s}", .{field_name});
25 try report.field("expected", "{any}", .{expected});
26 try report.field("actual", "{any}", .{actual});
27 pretty.diagnostic.writeStderr(&report, .{ .width = 100 });
28 return error.ConformanceMismatch;
29 }
30
31 fn failError(vector_name: []const u8, field_name: []const u8, actual: anyerror) !void {
32 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
33 defer arena.deinit();
34 var report = try pretty.diagnostic.Report.init(
35 arena.allocator(),
36 "Reticulum wire unexpected error",
37 );
38 defer report.deinit();
39 try report.field("vector", "{s}", .{vector_name});
40 try report.field("field", "{s}", .{field_name});
41 try report.field("error", "{s}", .{@errorName(actual)});
42 pretty.diagnostic.writeStderr(&report, .{ .width = 100 });
43 return error.ConformanceMismatch;
44 }
45
46 fn expectValue(
47 vector_name: []const u8,
48 field_name: []const u8,
49 expected: anytype,
50 actual: @TypeOf(expected),
51 ) !void {
52 if (!std.meta.eql(expected, actual)) {
53 return failValue(vector_name, field_name, expected, actual);
54 }
55 }
56
57 fn expectBytes(
58 vector_name: []const u8,
59 field_name: []const u8,
60 expected: []const u8,
61 actual: []const u8,
62 ) !void {
63 if (!std.mem.eql(u8, expected, actual)) {
64 return failValue(vector_name, field_name, expected, actual);
65 }
66 }
67
68 fn decodeVector(vector: PacketVector) !?wire.Packet {
69 const expected = vector.decode_verdict and vector.raw.len <= wire.mtu;
70 const packet = wire.decode(vector.raw) catch |err| {
71 if (expected) {
72 try failError(vector.name, "decode", err);
73 unreachable;
74 }
75 return null;
76 };
77 if (!expected) {
78 try failValue(vector.name, "decode", false, true);
79 unreachable;
80 }
81 return packet;
82 }
83
84 fn expectedTransportId(vector: PacketVector) !?[16]u8 {
85 if (vector.transport_id.len == 0) return null;
86 if (vector.transport_id.len != 16) {
87 try failValue(vector.name, "transport_id_length", 16, vector.transport_id.len);
88 unreachable;
89 }
90 return vector.transport_id[0..16].*;
91 }
92
93 fn checkDecodedFields(vector: PacketVector, packet: wire.Packet) !void {
94 if (vector.destination_hash.len != 16) {
95 return failValue(vector.name, "destination_length", 16, vector.destination_hash.len);
96 }
97 try expectValue(vector.name, "ifac", @as(u1, @truncate(vector.raw[0] >> 7)), packet.ifac);
98 try expectValue(
99 vector.name,
100 "header",
101 @as(wire.HeaderType, @fromBackingInt(@intCast(vector.header_type))),
102 packet.header,
103 );
104 try expectValue(vector.name, "context_flag", vector.context_flag, packet.context_flag);
105 try expectValue(
106 vector.name,
107 "transport",
108 @as(wire.TransportType, @fromBackingInt(@intCast(vector.transport_type))),
109 packet.transport,
110 );
111 try expectValue(
112 vector.name,
113 "destination_type",
114 @as(wire.DestinationType, @fromBackingInt(@intCast(vector.destination_type))),
115 packet.destination_type,
116 );
117 try expectValue(
118 vector.name,
119 "packet_type",
120 @as(wire.PacketType, @fromBackingInt(@intCast(vector.packet_type))),
121 packet.packet_type,
122 );
123 try expectValue(vector.name, "hops", vector.hops, packet.hops);
124 try expectValue(
125 vector.name,
126 "transport_id",
127 try expectedTransportId(vector),
128 packet.transport_id,
129 );
130 try expectBytes(vector.name, "destination", vector.destination_hash, &packet.destination);
131 try expectValue(vector.name, "context", wire.Context.decode(vector.context), packet.context);
132 try expectBytes(vector.name, "payload", vector.decoded_data, packet.payload);
133 }
134
135 fn packetFromVector(vector: PacketVector) !wire.Packet {
136 if (vector.destination_hash.len != 16) {
137 try failValue(vector.name, "destination_length", 16, vector.destination_hash.len);
138 unreachable;
139 }
140 return .{
141 .ifac = @truncate(vector.raw[0] >> 7),
142 .header = @fromBackingInt(@intCast(vector.header_type)),
143 .context_flag = vector.context_flag,
144 .transport = @fromBackingInt(@intCast(vector.transport_type)),
145 .destination_type = @fromBackingInt(@intCast(vector.destination_type)),
146 .packet_type = @fromBackingInt(@intCast(vector.packet_type)),
147 .hops = vector.hops,
148 .transport_id = try expectedTransportId(vector),
149 .destination = vector.destination_hash[0..16].*,
150 .context = wire.Context.decode(vector.context),
151 .payload = vector.decoded_data,
152 };
153 }
154
155 fn checkEncodedVector(vector: PacketVector) !void {
156 const expected = vector.pack_verdict and vector.hops < wire.pathfinder_hops;
157 var out: [wire.mtu]u8 = undefined;
158 const encoded = wire.encode(try packetFromVector(vector), &out) catch |err| {
159 if (expected) return failError(vector.name, "encode", err);
160 return;
161 };
162 if (!expected) return failValue(vector.name, "encode", false, true);
163 try expectBytes(vector.name, "encoded", vector.raw, encoded);
164 }
165
166 fn checkHashVector(vector: PacketVector) !void {
167 const full = wire.hash.full(vector.raw) catch |err|
168 return failError(vector.name, "full_hash", err);
169 const truncated = wire.hash.truncated(vector.raw) catch |err|
170 return failError(vector.name, "truncated_hash", err);
171 try expectBytes(vector.name, "packet_hash", vector.packet_hash, &full);
172 try expectBytes(vector.name, "truncated_hash", vector.truncated_hash, &truncated);
173 const offset: usize = if (vector.header_type == 0) 2 else 18;
174 try expectValue(
175 vector.name,
176 "hashable_length",
177 1 + vector.raw.len - offset,
178 try wire.hash.hashableLength(vector.raw),
179 );
180 }
181
182 test "Reticulum@1.5.0 RNS/Packet.py:178,243,349 packet corpus differential" {
183 for (conformance.packet.vectors) |vector| {
184 if (try decodeVector(vector)) |packet| try checkDecodedFields(vector, packet);
185 try checkEncodedVector(vector);
186 try checkHashVector(vector);
187 }
188 }
189
190 fn checkAnnounceVector(vector: AnnounceVector) !void {
191 const packet = wire.decode(vector.raw) catch |err|
192 return failError(vector.name, "decode", err);
193 try expectValue(vector.name, "packet_type", wire.PacketType.announce, packet.packet_type);
194 const expected_flag: u1 = @intFromBool(vector.ratchet_public_key.len != 0);
195 try expectValue(vector.name, "context_flag", expected_flag, packet.context_flag);
196 try expectBytes(vector.name, "destination", vector.destination_hash, &packet.destination);
197
198 var out: [wire.mtu]u8 = undefined;
199 const encoded = wire.encode(packet, &out) catch |err|
200 return failError(vector.name, "encode", err);
201 try expectBytes(vector.name, "encoded", vector.raw, encoded);
202 const original_hash = wire.hash.full(vector.raw) catch |err|
203 return failError(vector.name, "original_hash", err);
204 const encoded_hash = wire.hash.full(encoded) catch |err|
205 return failError(vector.name, "encoded_hash", err);
206 try expectBytes(vector.name, "packet_hash", &original_hash, &encoded_hash);
207 }
208
209 test "Reticulum@1.5.0 RNS/Destination.py:244 announce packet corpus differential" {
210 for (conformance.announce.vectors) |vector| try checkAnnounceVector(vector);
211 }
212
213 const LinkVector = conformance.link.Vector;
214 const LinkSignalling = ?wire.link.Signalling;
215
216 fn linkVector(name: []const u8) LinkVector {
217 for (conformance.link.vectors) |vector| {
218 if (std.mem.eql(u8, vector.name, name)) return vector;
219 }
220 unreachable;
221 }
222
223 fn decodeLinkFrame(vector: LinkVector, field_name: []const u8, raw: []const u8) !wire.Packet {
224 return wire.decode(raw) catch |err| {
225 try failError(vector.name, field_name, err);
226 unreachable;
227 };
228 }
229
230 fn expectLinkHeader(
231 vector: LinkVector,
232 packet: wire.Packet,
233 packet_type: wire.PacketType,
234 context: wire.Context,
235 ) !void {
236 try expectValue(vector.name, "packet_type", packet_type, packet.packet_type);
237 try expectValue(vector.name, "context", context, packet.context);
238 try expectValue(vector.name, "header", wire.HeaderType.one, packet.header);
239 const destination_type: wire.DestinationType = switch (packet_type) {
240 .link_request => .single,
241 else => .link,
242 };
243 try expectValue(vector.name, "destination_type", destination_type, packet.destination_type);
244 if (packet_type == .link_request) return;
245 try expectBytes(vector.name, "destination", vector.link_id, &packet.destination);
246 }
247
248 fn linkDestination() reticulum.identity.Public {
249 const session = conformance.link.session_vectors[0];
250 const private = reticulum.identity.Private.fromBytes(
251 session.destination_private_key[0..64].*,
252 );
253 return private.public();
254 }
255
256 fn linkToken() !reticulum.crypto.token.Token {
257 return reticulum.crypto.token.Token.init(conformance.link.key_vectors[0].derived_key);
258 }
259
260 test "Reticulum@1.5.0 RNS/Link.py:348-361 link key corpus derives the token key" {
261 const key = conformance.link.key_vectors[0];
262 const request = linkVector("link-request");
263 const proof = linkVector("link-proof");
264 const initiator_key = request.encryption_private_key;
265 try expectBytes(key.name, "initiator_key", initiator_key, key.initiator_encryption_private_key);
266 const responder_key = proof.encryption_private_key;
267 try expectBytes(key.name, "responder_key", responder_key, key.responder_encryption_private_key);
268 try expectBytes(key.name, "link_id", request.link_id, key.link_id);
269 const X25519 = std.crypto.dh.X25519;
270 const initiator = key.initiator_encryption_private_key[0..32].*;
271 const responder = key.responder_encryption_private_key[0..32].*;
272 const initiator_public = try X25519.recoverPublicKey(initiator);
273 const responder_public = try X25519.recoverPublicKey(responder);
274 const initiator_shared = try X25519.scalarmult(initiator, responder_public);
275 const responder_shared = try X25519.scalarmult(responder, initiator_public);
276 try expectBytes(key.name, "initiator_shared_key", key.shared_key, &initiator_shared);
277 try expectBytes(key.name, "responder_shared_key", key.shared_key, &responder_shared);
278 var derived: [64]u8 = undefined;
279 const link_id = key.link_id;
280 const output = try reticulum.crypto.hkdf.derive(64, key.shared_key, link_id, null, &derived);
281 try expectBytes(key.name, "derived_key", key.derived_key, output);
282 }
283
284 test "Reticulum@1.5.0 RNS/Link.py:304-346 link request corpus and its stripped link id" {
285 const request = linkVector("link-request");
286 const packet = try decodeLinkFrame(request, "output_raw", request.output_raw);
287 try expectLinkHeader(request, packet, .link_request, .none);
288 try expectBytes(request.name, "payload", request.plaintext, packet.payload);
289 const decoded = wire.link.Request.decode(packet.payload) catch |err|
290 return failError(request.name, "request", err);
291 const signalling: LinkSignalling = wire.link.default_signalling;
292 try expectValue(request.name, "signalling", signalling, decoded.signalling);
293 const encryption_public = try std.crypto.dh.X25519.recoverPublicKey(
294 request.encryption_private_key[0..32].*,
295 );
296 try expectBytes(
297 request.name,
298 "encryption_public",
299 &encryption_public,
300 &decoded.encryption_public,
301 );
302 const signing = try std.crypto.sign.Ed25519.KeyPair.generateDeterministic(
303 request.signing_private_key[0..32].*,
304 );
305 const signing_public = signing.public_key.toBytes();
306 try expectBytes(request.name, "signing_public", &signing_public, &decoded.signing_public);
307 var out: [wire.link.signalled_request_bytes]u8 = undefined;
308 try expectBytes(request.name, "encoded", packet.payload, try decoded.encode(&out));
309 const link_id = try wire.link.linkId(request.output_raw);
310 try expectBytes(request.name, "link_id", request.link_id, &link_id);
311
312 const stripped = linkVector("link-request-stripped");
313 const stripped_packet = try decodeLinkFrame(stripped, "input_raw", stripped.input_raw);
314 try expectLinkHeader(stripped, stripped_packet, .link_request, .none);
315 const unsignalled = wire.link.Request.decode(stripped_packet.payload) catch |err|
316 return failError(stripped.name, "request", err);
317 try expectValue(stripped.name, "signalling", @as(LinkSignalling, null), unsignalled.signalling);
318 try expectValue(stripped.name, "mode", wire.link.default_mode, unsignalled.mode());
319 const keys = request.plaintext[0..wire.link.request_bytes];
320 try expectBytes(stripped.name, "payload", keys, stripped_packet.payload);
321 const stripped_id = try wire.link.linkId(stripped.input_raw);
322 try expectBytes(stripped.name, "link_id", request.link_id, &stripped_id);
323 }
324
325 fn expectLinkProof(vector: LinkVector) !void {
326 const packet = try decodeLinkFrame(vector, "output_raw", vector.output_raw);
327 try expectLinkHeader(vector, packet, .proof, .lrproof);
328 try expectBytes(vector.name, "payload", vector.plaintext, packet.payload);
329 const proof = wire.link.Proof.decode(packet.payload) catch |err|
330 return failError(vector.name, "proof", err);
331 const signalling: LinkSignalling = wire.link.default_signalling;
332 try expectValue(vector.name, "signalling", signalling, proof.signalling);
333 const mode = wire.link.proofMode(packet.payload);
334 try expectValue(vector.name, "mode", wire.link.default_mode, mode);
335 const encryption_public = try std.crypto.dh.X25519.recoverPublicKey(
336 vector.encryption_private_key[0..32].*,
337 );
338 try expectBytes(vector.name, "encryption_public", &encryption_public, &proof.encryption_public);
339 const destination = linkDestination();
340 var signed: [wire.link.signed_proof_bytes_max]u8 = undefined;
341 const message = wire.link.signedProof(
342 vector.link_id[0..16].*,
343 proof.encryption_public,
344 destination.bytes[32..64].*,
345 proof.signalling,
346 &signed,
347 );
348 const valid = destination.validate(proof.signature, message);
349 try expectValue(vector.name, "signature_valid", true, valid);
350 var out: [wire.link.signalled_proof_bytes]u8 = undefined;
351 try expectBytes(vector.name, "encoded", packet.payload, try proof.encode(&out));
352 }
353
354 test "Reticulum@1.5.0 RNS/Link.py:366-375,391-418 link proof corpus verifies" {
355 const proof = linkVector("link-proof");
356 try expectLinkProof(proof);
357 try expectLinkProof(linkVector("link-request-stripped"));
358 const request = linkVector("link-request");
359 try expectBytes(proof.name, "input_raw", request.output_raw, proof.input_raw);
360 }
361
362 test "Reticulum@1.5.0 RNS/Link.py:419-437,516-522 link RTT corpus is a whole-second float64" {
363 const vector = linkVector("link-rtt");
364 const packet = try decodeLinkFrame(vector, "output_raw", vector.output_raw);
365 try expectLinkHeader(vector, packet, .data, .lrrtt);
366 const proof = linkVector("link-proof");
367 try expectBytes(vector.name, "input_raw", proof.output_raw, vector.input_raw);
368 const token = try linkToken();
369 var encrypted: [wire.mtu]u8 = undefined;
370 const ciphertext = try token.encrypt(vector.iv[0..16].*, vector.plaintext, &encrypted);
371 try expectBytes(vector.name, "encrypted", packet.payload, ciphertext);
372 const seconds = wire.link.decodeRtt(vector.plaintext) catch |err|
373 return failError(vector.name, "rtt", err);
374 const measured: f64 = @floatFromInt(vector.clock - linkVector("link-request").clock);
375 try expectValue(vector.name, "rtt", measured, seconds);
376 try expectBytes(vector.name, "encoded_rtt", vector.plaintext, &wire.link.encodeRtt(seconds));
377 }
378
379 fn expectLinkCiphertext(vector: LinkVector, context: wire.Context) ![]const u8 {
380 const packet = try decodeLinkFrame(vector, "output_raw", vector.output_raw);
381 try expectLinkHeader(vector, packet, .data, context);
382 const token = try linkToken();
383 var encrypted: [wire.mtu]u8 = undefined;
384 const ciphertext = try token.encrypt(vector.iv[0..16].*, vector.plaintext, &encrypted);
385 try expectBytes(vector.name, "encrypted", packet.payload, ciphertext);
386 return packet.payload;
387 }
388
389 test "Reticulum@1.5.0 RNS/Link.py:657-683,1166-1187 link data and close corpus" {
390 _ = try expectLinkCiphertext(linkVector("link-data"), .none);
391 const close = linkVector("link-close");
392 const ciphertext = try expectLinkCiphertext(close, .linkclose);
393 const token = try linkToken();
394 var decrypted: [wire.mtu]u8 = undefined;
395 const plaintext = try token.decrypt(ciphertext, &decrypted);
396 const closes = wire.link.closes(plaintext, close.link_id[0..16].*);
397 try expectValue(close.name, "closes", true, closes);
398 }
399
400 test "Reticulum@1.5.0 RNS/Link.py:378-389 link data proof corpus proves the data hash" {
401 const vector = linkVector("link-data-proof");
402 try expectBytes(vector.name, "input_raw", linkVector("link-data").output_raw, vector.input_raw);
403 const packet = try decodeLinkFrame(vector, "output_raw", vector.output_raw);
404 try expectLinkHeader(vector, packet, .proof, .none);
405 const proof = wire.proof.decode(packet.payload) catch |err|
406 return failError(vector.name, "proof", err);
407 const explicit = switch (proof) {
408 .explicit => |value| value,
409 .implicit => return failValue(vector.name, "proof_form", "explicit", "implicit"),
410 };
411 const data_hash = try wire.hash.full(vector.input_raw);
412 try expectBytes(vector.name, "proved_hash", &data_hash, &explicit.packet_hash);
413 const valid = linkDestination().validate(explicit.signature, &data_hash);
414 try expectValue(vector.name, "signature_valid", true, valid);
415 }
416
417 test "Reticulum@1.5.0 RNS/Link.py:799-802,1130-1135 link keepalive corpus" {
418 const keepalive = linkVector("link-keepalive");
419 const request = try decodeLinkFrame(keepalive, "output_raw", keepalive.output_raw);
420 try expectLinkHeader(keepalive, request, .data, .keepalive);
421 const request_byte = [1]u8{wire.link.keepalive_request};
422 try expectBytes(keepalive.name, "payload", &request_byte, request.payload);
423 const answer = linkVector("link-keepalive-answer");
424 try expectBytes(answer.name, "input_raw", keepalive.output_raw, answer.input_raw);
425 const reply = try decodeLinkFrame(answer, "output_raw", answer.output_raw);
426 try expectLinkHeader(answer, reply, .data, .keepalive);
427 const answer_byte = [1]u8{wire.link.keepalive_answer};
428 try expectBytes(answer.name, "payload", &answer_byte, reply.payload);
429 }
430
431 test "Reticulum@1.5.0 RNS/Packet.py:349-358 link corpus packet hashes" {
432 for (conformance.link.vectors) |vector| {
433 const raw = if (vector.output_raw.len > 0) vector.output_raw else vector.input_raw;
434 try expectBytes(vector.name, "packet_hash", vector.packet_hash, &try wire.hash.full(raw));
435 }
436 }