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 }