Skip to main content

macula_rust/frame/
neighbour.rs

1//! Neighbour signatures (D17). In pq_hybrid a control frame travels as
2//! `{version, frame_type, neighbour}`: `neighbour` is a held signed object
3//! under MACULA-PQ-NEIGHBOUR-V1 by the sender's identity key, which the
4//! receiver holds from the handshake. Its tbs holds the frame's fields
5//! (without version), alg, the connection hash (the SHA-384 of the CHALLENGE
6//! frame's bytes) and seq: 0 on the first neighbour-signed frame in each
7//! direction, one more on each after. In pq_pure no frame carries one.
8//!
9//! The builders here are the control frames a client link sends: ADVERTISE
10//! and UNADVERTISE carry a signed record, SUBSCRIBE and UNSUBSCRIBE a topic,
11//! and GOODBYE a reason.
12
13use crate::cbor::Value;
14use crate::node_key::NodeKey;
15use crate::profile::Profile;
16use crate::signed_object::{sign_held_object, verify_held_object};
17
18use super::{
19    base, bounded_text, entry, has_fields, object_refusal, protocol_uint, read_fields, uint,
20    with_field, FrameError, Rule, MAX_TOPIC_BYTES, PROTOCOL_VERSION,
21};
22
23const NEIGHBOUR_LABEL: &str = "MACULA-PQ-NEIGHBOUR-V1";
24const MAX_GOODBYE_REASON_BYTES: usize = 256;
25const MAX_GOODBYE_DETAIL_BYTES: usize = 256;
26
27/// The control frames pq_hybrid neighbour-signs, as macula_frame lists them.
28/// Data frames carry their own end-to-end signatures.
29const NEIGHBOUR_SIGNED_TYPES: &[&str] = &[
30    "swim_ping",
31    "swim_ack",
32    "swim_suspect",
33    "swim_confirm",
34    "ping",
35    "pong",
36    "find_node",
37    "nodes",
38    "find_value",
39    "value",
40    "store",
41    "store_ack",
42    "advertise",
43    "unadvertise",
44    "subscribe",
45    "unsubscribe",
46    "overlay_relay",
47    "hyparview_join",
48    "hyparview_forward_join",
49    "hyparview_neighbor",
50    "hyparview_disconnect",
51    "hyparview_shuffle",
52    "hyparview_shuffle_reply",
53    "plumtree_ihave",
54    "plumtree_graft",
55    "plumtree_prune",
56    "goodbye",
57];
58
59/// Whether `profile` neighbour-signs frames of `frame_type`: every control
60/// frame in pq_hybrid, none in pq_pure.
61pub fn neighbour_signed(profile: Profile, frame_type: &str) -> bool {
62    profile == Profile::PqHybrid && NEIGHBOUR_SIGNED_TYPES.contains(&frame_type)
63}
64
65/// Where a sender neighbour-signs a frame: the connection hash and the seq of
66/// this frame in the sender's direction.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct NeighbourLink {
69    pub connection: [u8; 48],
70    pub seq: u64,
71}
72
73/// What a receiver checks a frame against: the connection's profile, the
74/// peer's identity key as carried, the connection hash, and the seq it
75/// expects next from that peer.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct NeighbourPeer {
78    pub profile: Profile,
79    pub peer_key: Vec<u8>,
80    pub connection: [u8; 48],
81    pub seq: u64,
82}
83
84/// Neighbour-signs `frame` with the sender's identity key, for one connection
85/// and one seq, when the key's profile signs its type; otherwise `frame` goes
86/// as it is. A frame that already carries a neighbour signature is refused.
87pub fn sign_neighbour(
88    frame: &Value,
89    key: &NodeKey,
90    link: &NeighbourLink,
91) -> Result<Value, FrameError> {
92    let Value::Map(pairs) = frame else {
93        return Err(FrameError::Malformed);
94    };
95    let (frame_type, version, has_neighbour) = control_header(pairs);
96    if has_neighbour {
97        return Err(FrameError::NeighbourSigned);
98    }
99    if !neighbour_signed(key.profile(), &frame_type) {
100        return Ok(frame.clone());
101    }
102    let mut fields: Vec<(Value, Value)> = pairs
103        .iter()
104        .filter(|(k, _)| *k != Value::text("version"))
105        .cloned()
106        .collect();
107    fields.push(entry("connection", Value::Bytes(link.connection.to_vec())));
108    fields.push(entry("seq", uint(link.seq)));
109    let held = sign_held_object(NEIGHBOUR_LABEL, &fields, key).map_err(object_refusal)?;
110    Ok(Value::Map(vec![
111        entry("version", version),
112        entry("frame_type", Value::text(frame_type)),
113        entry("neighbour", held.to_value()),
114    ]))
115}
116
117/// Reads a received frame under the connection's profile. A frame type the
118/// profile signs must be exactly `{version, frame_type, neighbour}`, signed by
119/// the peer's identity key for this connection and this seq, and comes back as
120/// the frame its tbs holds. Any other frame must not carry `neighbour` and
121/// comes back as it is.
122pub fn verify_neighbour(frame: &Value, peer: &NeighbourPeer) -> Result<Value, FrameError> {
123    let Value::Map(pairs) = frame else {
124        return Err(FrameError::Malformed);
125    };
126    let (frame_type, version, has_neighbour) = control_header(pairs);
127    if !neighbour_signed(peer.profile, &frame_type) {
128        return if has_neighbour {
129            Err(FrameError::Malformed)
130        } else {
131            Ok(frame.clone())
132        };
133    }
134    if pairs.len() != 3 || !has_neighbour {
135        return Err(FrameError::Malformed);
136    }
137    let neighbour = frame.get("neighbour").ok_or(FrameError::Malformed)?;
138    let verified = verify_held_object(NEIGHBOUR_LABEL, neighbour, &peer.peer_key, peer.profile)
139        .map_err(object_refusal)?;
140    opened_control_frame(&verified.fields, &frame_type, version, peer)
141}
142
143/// The frame a neighbour tbs holds: read under its type's table with alg,
144/// connection and seq, which must name this connection and this seq, and
145/// returned without them and with version back.
146fn opened_control_frame(
147    tbs: &Value,
148    frame_type: &str,
149    version: Value,
150    peer: &NeighbourPeer,
151) -> Result<Value, FrameError> {
152    let mut table = control_table(frame_type).ok_or(FrameError::Malformed)?;
153    table.extend([
154        ("alg", Rule::Any),
155        ("connection", Rule::BytesOf(48)),
156        ("seq", Rule::ProtocolUint),
157    ]);
158    let fields = read_fields(tbs, &table).ok_or(FrameError::Malformed)?;
159    if !has_fields(&fields, &["frame_type", "alg", "connection", "seq"])
160        || fields["frame_type"] != Value::text(frame_type)
161        || fields["connection"] != Value::Bytes(peer.connection.to_vec())
162        || protocol_uint(&fields["seq"]) != Some(peer.seq)
163    {
164        return Err(FrameError::Malformed);
165    }
166    let Value::Map(pairs) = tbs else {
167        return Err(FrameError::Malformed);
168    };
169    let mut opened = vec![entry("version", version)];
170    opened.extend(
171        pairs
172            .iter()
173            .filter(|(k, _)| !matches!(k, Value::Text(t) if t == "alg" || t == "connection" || t == "seq"))
174            .cloned(),
175    );
176    Ok(Value::Map(opened))
177}
178
179/// The field table of a control frame a client link exchanges, without
180/// version and neighbour: the base every frame carries and the type's own.
181fn control_table(frame_type: &str) -> Option<Vec<(&'static str, Rule)>> {
182    let own: &[(&'static str, Rule)] = match frame_type {
183        "advertise" => &[("advertisement", Rule::AnyBytes)],
184        "unadvertise" => &[("withdrawal", Rule::AnyBytes)],
185        "subscribe" => &[
186            ("topic", Rule::Any),
187            ("subscriber", Rule::BytesOf(32)),
188            ("options", Rule::Any),
189        ],
190        "unsubscribe" => &[("topic", Rule::Any), ("subscriber", Rule::BytesOf(32))],
191        "goodbye" => &[
192            ("reason", Rule::TextWithin(MAX_GOODBYE_REASON_BYTES)),
193            ("detail", Rule::Any),
194        ],
195        _ => return None,
196    };
197    let types: &'static [&'static str] = match frame_type {
198        "advertise" => &["advertise"],
199        "unadvertise" => &["unadvertise"],
200        "subscribe" => &["subscribe"],
201        "unsubscribe" => &["unsubscribe"],
202        _ => &["goodbye"],
203    };
204    let mut table = vec![
205        ("frame_type", Rule::TextIn(types)),
206        ("frame_id", Rule::Any),
207        ("sent_at_ms", Rule::ProtocolUint),
208        ("capabilities", Rule::ProtocolUint),
209        ("realm", Rule::Any),
210        ("call_id", Rule::Any),
211        ("source_route", Rule::Any),
212    ];
213    table.extend_from_slice(own);
214    Some(table)
215}
216
217/// A frame map's frame_type, version, and whether it carries neighbour.
218fn control_header(pairs: &[(Value, Value)]) -> (String, Value, bool) {
219    let mut frame_type = String::new();
220    let mut version = Value::Int(i128::from(PROTOCOL_VERSION));
221    let mut has_neighbour = false;
222    for (k, v) in pairs {
223        match (k, v) {
224            (Value::Text(n), Value::Text(t)) if n == "frame_type" => frame_type = t.clone(),
225            (Value::Text(n), _) if n == "version" => version = v.clone(),
226            (Value::Text(n), _) if n == "neighbour" => has_neighbour = true,
227            _ => {}
228        }
229    }
230    (frame_type, version, has_neighbour)
231}
232
233/// macula 12's ADVERTISE: the signed procedure_advertisement record, as
234/// encoded bytes.
235pub fn advertise_frame(advertisement: &[u8]) -> Value {
236    let mut fields = base("advertise");
237    fields.push(entry("advertisement", Value::Bytes(advertisement.to_vec())));
238    Value::Map(fields)
239}
240
241/// macula 12's UNADVERTISE: the signed withdrawal record, as encoded bytes.
242pub fn unadvertise_frame(withdrawal: &[u8]) -> Value {
243    let mut fields = base("unadvertise");
244    fields.push(entry("withdrawal", Value::Bytes(withdrawal.to_vec())));
245    Value::Map(fields)
246}
247
248/// macula 12's SUBSCRIBE of `subscriber` to `topic` in `realm`, with no
249/// options. A topic over 512 bytes or not UTF-8 is refused.
250pub fn subscribe_frame(
251    topic: &[u8],
252    realm: &[u8; 32],
253    subscriber: &[u8; 32],
254) -> Result<Value, FrameError> {
255    let mut fields = topic_frame("subscribe", topic, realm, subscriber)?;
256    fields.push(entry("options", Value::Map(Vec::new())));
257    Ok(Value::Map(fields))
258}
259
260/// macula 12's UNSUBSCRIBE of `subscriber` from `topic` in `realm`, with
261/// [`subscribe_frame`]'s bound on the topic.
262pub fn unsubscribe_frame(
263    topic: &[u8],
264    realm: &[u8; 32],
265    subscriber: &[u8; 32],
266) -> Result<Value, FrameError> {
267    topic_frame("unsubscribe", topic, realm, subscriber).map(Value::Map)
268}
269
270fn topic_frame(
271    frame_type: &str,
272    topic: &[u8],
273    realm: &[u8; 32],
274    subscriber: &[u8; 32],
275) -> Result<Vec<(Value, Value)>, FrameError> {
276    bounded_text("topic", topic, MAX_TOPIC_BYTES)?;
277    let mut fields = with_field(base(frame_type), "realm", Value::Bytes(realm.to_vec()));
278    fields.push(entry("topic", Value::Bytes(topic.to_vec())));
279    fields.push(entry("subscriber", Value::Bytes(subscriber.to_vec())));
280    Ok(fields)
281}
282
283/// macula 12's GOODBYE: a reason of at most 256 bytes, and a detail of at
284/// most 256 bytes of UTF-8, or none.
285pub fn goodbye_frame(reason: &str, detail: Option<&[u8]>) -> Result<Value, FrameError> {
286    bounded_text("reason", reason.as_bytes(), MAX_GOODBYE_REASON_BYTES)?;
287    let detail = match detail {
288        Some(d) => {
289            bounded_text("detail", d, MAX_GOODBYE_DETAIL_BYTES)?;
290            Value::Bytes(d.to_vec())
291        }
292        None => Value::Null,
293    };
294    let mut fields = base("goodbye");
295    fields.push(entry("reason", Value::text(reason)));
296    fields.push(entry("detail", detail));
297    Ok(Value::Map(fields))
298}