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 }