lib/reticulum/src/destination/announce.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const reticulum = @import("../root.zig");
3
4 const identity = reticulum.identity;
5 const wire = reticulum.wire;
6
7 const public_bytes: usize = 64;
8 const name_hash_bytes: usize = 10;
9 const random_hash_bytes: usize = 10;
10 const rotating_bytes: usize = 32;
11 const signature_bytes: usize = 64;
12 const destination_hash_bytes: usize = 16;
13 const prefix_bytes: usize = public_bytes + name_hash_bytes + random_hash_bytes;
14 const fixed_payload_bytes: u16 = @intCast(prefix_bytes + signature_bytes);
15
16 /// 465 bytes, the most an announce payload holds, the room left behind a
17 /// 35-byte header, following Reticulum@1.5.0 RNS/Reticulum.py:93,151. The same
18 /// bound covers every announce this package builds and every announce a node
19 /// forwards behind a 35-byte header.
20 pub const payload_bytes_max: u16 = 465;
21 /// 317 bytes, the application data an announce carries once its fixed 148 bytes
22 /// are taken.
23 pub const app_bytes_max: u16 = payload_bytes_max - fixed_payload_bytes;
24 /// 285 bytes, the application data an announce carries once a 32-byte rotating
25 /// key takes its place in the payload.
26 pub const rotating_app_bytes_max: u16 = app_bytes_max - @as(u16, rotating_bytes);
27 /// 481 bytes, the most payload a packet holds behind a 19-byte header,
28 /// following Reticulum@1.5.0 RNS/Packet.py:236-237.
29 pub const received_payload_bytes_max: u16 = wire.mtu - wire.header_one_bytes;
30 /// 333 bytes, the application data an announce carries behind a 19-byte header.
31 pub const received_app_bytes_max: u16 = received_payload_bytes_max - fixed_payload_bytes;
32
33 const signed_bytes_max: usize = @as(usize, received_payload_bytes_max) - signature_bytes +
34 destination_hash_bytes;
35
36 comptime {
37 std.debug.assert(payload_bytes_max == wire.mtu - wire.header_two_bytes);
38 std.debug.assert(received_payload_bytes_max == wire.mtu - wire.header_one_bytes);
39 }
40
41 /// Returns how much payload a packet holds behind the given header shape, which
42 /// is 500 bytes less that header, following Reticulum@1.5.0
43 /// RNS/Packet.py:236-237.
44 pub fn payloadBytesMax(header: wire.HeaderType) u16 {
45 return wire.mtu - wire.headerLength(header);
46 }
47
48 pub const Fields = struct {
49 destination_hash: [destination_hash_bytes]u8,
50 name_hash: [name_hash_bytes]u8,
51 random_hash: [random_hash_bytes]u8,
52 rotating_public_key: ?[rotating_bytes]u8 = null,
53 app_data: []const u8 = &.{},
54 };
55
56 pub const BuildError = error{
57 AppDataTooLong,
58 OutputTooSmall,
59 };
60
61 pub const ValidateError = error{
62 PayloadTooShort,
63 PayloadTooLong,
64 InvalidSignature,
65 DestinationMismatch,
66 };
67
68 /// The fields of a checked announce: the public key, the name hash, the ten
69 /// random bytes, the rotating public key when the announce carries one, the
70 /// signature, the application data, and the context flag. Every slice points
71 /// into the payload the caller passed in, so it stays good as long as that
72 /// payload does.
73 pub const Announce = struct {
74 public_key: identity.BorrowedPublic,
75 name_hash: []const u8,
76 random_hash: []const u8,
77 rotating_public_key: ?[]const u8,
78 signature: []const u8,
79 app_data: []const u8,
80 context_flag: u1,
81 };
82
83 fn append(output: []u8, cursor: *usize, bytes: []const u8) void {
84 std.debug.assert(cursor.* <= output.len);
85 std.debug.assert(bytes.len <= output.len - cursor.*);
86 @memcpy(output[cursor.*..][0..bytes.len], bytes);
87 cursor.* += bytes.len;
88 }
89
90 fn signatureOffset(has_rotating: bool) usize {
91 return prefix_bytes + if (has_rotating) rotating_bytes else 0;
92 }
93
94 fn appMaximum(has_rotating: bool) usize {
95 return if (has_rotating) rotating_app_bytes_max else app_bytes_max;
96 }
97
98 fn writePrefix(fields: Fields, public: identity.KeyBytes, output: []u8) usize {
99 var cursor: usize = 0;
100 append(output, &cursor, &public);
101 append(output, &cursor, &fields.name_hash);
102 append(output, &cursor, &fields.random_hash);
103 if (fields.rotating_public_key) |rotating| append(output, &cursor, &rotating);
104 return cursor;
105 }
106
107 /// Writes the signed announce payload into `output` and returns it: the 64
108 /// public bytes, the name hash, the ten random bytes, the rotating public key
109 /// when the caller passes one, the signature, and the application data,
110 /// following Reticulum@1.5.0 RNS/Destination.py:244-304. What the signature
111 /// covers starts with the destination hash, then the payload up to the
112 /// signature, then the application data. The call returns
113 /// `error.AppDataTooLong` past 317 bytes, or past 285 when the announce carries
114 /// a rotating key, and `error.OutputTooSmall` when `output` is shorter than the
115 /// payload. The output buffer has to lie apart from the private key storage and
116 /// from the application data.
117 pub fn build(
118 fields: Fields,
119 private: *const identity.Private,
120 output: []u8,
121 ) BuildError![]u8 {
122 const has_rotating = fields.rotating_public_key != null;
123 if (fields.app_data.len > appMaximum(has_rotating)) return error.AppDataTooLong;
124 const signature_offset = signatureOffset(has_rotating);
125 const payload_bytes = signature_offset + signature_bytes + fields.app_data.len;
126 if (output.len < payload_bytes) return error.OutputTooSmall;
127
128 const public = private.publicBytes();
129 const prefix_end = writePrefix(fields, public, output);
130 std.debug.assert(prefix_end == signature_offset);
131
132 var signed: [signed_bytes_max]u8 = undefined;
133 var signed_end: usize = 0;
134 append(&signed, &signed_end, &fields.destination_hash);
135 append(&signed, &signed_end, output[0..prefix_end]);
136 append(&signed, &signed_end, fields.app_data);
137 const signature = private.sign(signed[0..signed_end]);
138 @memcpy(output[signature_offset..][0..signature_bytes], &signature);
139 @memcpy(output[signature_offset + signature_bytes ..][0..fields.app_data.len], fields.app_data);
140 return output[0..payload_bytes];
141 }
142
143 fn announcedDestination(announce: Announce) [destination_hash_bytes]u8 {
144 const identity_hash = announce.public_key.hash();
145 var hasher = reticulum.hash.Hasher.init();
146 hasher.update(announce.name_hash);
147 hasher.update(&identity_hash);
148 return hasher.finalTruncated();
149 }
150
151 fn signedTranscript(
152 destination_hash: [destination_hash_bytes]u8,
153 payload: []const u8,
154 signature_offset: usize,
155 app_data: []const u8,
156 output: *[signed_bytes_max]u8,
157 ) []const u8 {
158 var cursor: usize = 0;
159 append(output, &cursor, &destination_hash);
160 append(output, &cursor, payload[0..signature_offset]);
161 append(output, &cursor, app_data);
162 return output[0..cursor];
163 }
164
165 /// Checks an announce payload and returns its fields, following Reticulum@1.5.0
166 /// RNS/Identity.py:511-565. A context flag of one says the payload carries a
167 /// 32-byte rotating key between the random bytes and the signature. The
168 /// signature is checked against the public key in the payload, over the
169 /// destination hash followed by the payload up to the signature and then the
170 /// application data. The address is recomputed from the announce's own name
171 /// hash and the hash of its public key, and it has to equal the address the
172 /// packet was sent to. The call returns `error.PayloadTooShort` below the fixed
173 /// bytes, `error.PayloadTooLong` past what the header shape allows,
174 /// `error.InvalidSignature`, and `error.DestinationMismatch`.
175 pub fn validate(
176 destination_hash: [destination_hash_bytes]u8,
177 header: wire.HeaderType,
178 context_flag: u1,
179 payload: []const u8,
180 ) ValidateError!Announce {
181 const has_rotating = context_flag == 1;
182 const signature_offset = signatureOffset(has_rotating);
183 const fixed_bytes = signature_offset + signature_bytes;
184 if (payload.len < fixed_bytes) return error.PayloadTooShort;
185 if (payload.len > payloadBytesMax(header)) return error.PayloadTooLong;
186 std.debug.assert(payload.len - signature_bytes + destination_hash_bytes <= signed_bytes_max);
187
188 const public_pointer: *const identity.KeyBytes = @ptrCast(payload.ptr);
189 const app_data = payload[fixed_bytes..];
190 const announce = Announce{
191 .public_key = identity.BorrowedPublic.fromBytes(public_pointer),
192 .name_hash = payload[public_bytes..][0..name_hash_bytes],
193 .random_hash = payload[public_bytes + name_hash_bytes ..][0..random_hash_bytes],
194 .rotating_public_key = if (has_rotating)
195 payload[prefix_bytes..][0..rotating_bytes]
196 else
197 null,
198 .signature = payload[signature_offset..][0..signature_bytes],
199 .app_data = app_data,
200 .context_flag = context_flag,
201 };
202
203 var signed: [signed_bytes_max]u8 = undefined;
204 const transcript = signedTranscript(
205 destination_hash,
206 payload,
207 signature_offset,
208 app_data,
209 &signed,
210 );
211 if (!announce.public_key.validate(announce.signature[0..signature_bytes].*, transcript)) {
212 return error.InvalidSignature;
213 }
214 if (!std.mem.eql(u8, &announcedDestination(announce), &destination_hash)) {
215 return error.DestinationMismatch;
216 }
217 return announce;
218 }