Skip to main content

macula_rust/
frame.rs

1//! macula 12's frames, as macula_frame and macula-go build and read them: the
2//! requests, replies, relay errors, publications and stream frames that carry
3//! signed objects, the control frames a pq_hybrid link neighbour-signs, the
4//! decoding rule's payload bounds, and the length-prefixed wire codec.
5//!
6//! A wire frame is `<Length:4 bytes big-endian><Cbor>`, the deterministic
7//! encoding of one map with `version` and `frame_type`. No frame carries a
8//! frame-level signature: what is signed is the signed object a frame holds,
9//! and, in pq_hybrid, a control frame's neighbour signature.
10
11mod check_payload;
12mod neighbour;
13mod publication;
14mod reply;
15mod request;
16mod sealed;
17mod session;
18mod stream;
19
20pub use check_payload::{
21    check_frame, check_payload, FRAME_RESERVED_ELEMENTS, MAX_PAYLOAD_ELEMENTS, MAX_PAYLOAD_NESTING,
22};
23pub use neighbour::{
24    advertise_frame, goodbye_frame, neighbour_signed, sign_neighbour, subscribe_frame,
25    unadvertise_frame, unsubscribe_frame, verify_neighbour, NeighbourLink, NeighbourPeer,
26};
27pub use publication::{sign_publish, verify_publication, PublicationSpec, VerifiedPublication};
28pub use reply::{
29    claimed_reply_ids, sign_provider_error, sign_relay_error, sign_result,
30    sign_sealed_provider_error, sign_sealed_result, verify_relay_error, verify_reply,
31    RelayErrorSpec, RelayErrorType, ReplyType, VerifiedRelayError, VerifiedReply,
32};
33pub use request::{
34    request_fields_accepted, sign_call, sign_stream_open, verify_request, RequestSpec, RequestType,
35    VerifiedRequest, MAX_PROOFS, MAX_PROOFS_BYTES,
36};
37pub use sealed::{Sealed, SEALED_SCHEME};
38pub use session::{
39    liveness_nonce, liveness_ping_frame, liveness_pong_frame, verify_session_frame, Liveness,
40    LIVENESS_NONCE_SIZE,
41};
42pub use stream::{
43    open_stream, sign_caller_stream, sign_provider_stream, verify_caller_stream,
44    verify_provider_stream, StreamEncoding, StreamFields, StreamMode, StreamRole, StreamState,
45    VerifiedStreamFrame,
46};
47
48use std::fmt;
49
50use crate::cbor::{self, Value};
51use crate::node_key::{KeyError, NodeKey, Purpose};
52use crate::signed_object::ObjectError;
53
54/// The version field every frame carries.
55pub const PROTOCOL_VERSION: i64 = 2;
56
57/// The CBOR payload size cap: 16 MiB minus one byte, as macula's.
58pub const MAX_FRAME_BYTES: usize = 0x00FF_FFFF;
59
60/// The labels of the signed objects frames carry (D25, D17).
61const REQUEST_LABEL: &str = "MACULA-PQ-REQUEST-V1";
62const REPLY_LABEL: &str = "MACULA-PQ-REPLY-V1";
63const RELAY_ERROR_LABEL: &str = "MACULA-PQ-RELAY-ERROR-V1";
64const STREAM_LABEL: &str = "MACULA-PQ-STREAM-V1";
65const CALLER_STREAM_LABEL: &str = "MACULA-PQ-CALLER-STREAM-V1";
66const PUBLICATION_LABEL: &str = "MACULA-PQ-PUBLICATION-V1";
67
68/// A protocol integer stays below 2^53; a procedure name is at most 512
69/// bytes, an error code at most 64, and an error's text at most 256.
70const MAX_PROTOCOL_INT: u64 = 1 << 53;
71const MAX_PROCEDURE_BYTES: usize = 512;
72const MAX_ERROR_CODE_BYTES: usize = 64;
73const MAX_ERROR_TEXT_BYTES: usize = 256;
74const MAX_TOPIC_BYTES: usize = 512;
75
76/// The refusals of a frame, named as macula_frame names them.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum FrameError {
79    /// An encoding longer than [`MAX_FRAME_BYTES`], or a header claiming one.
80    TooLarge(usize),
81    /// A frame, or the signed object it carries, without exactly the shape and
82    /// fields of its type.
83    Malformed,
84    /// A payload the decoding rule would refuse where it arrives, and where.
85    Payload(String),
86    /// A whole frame the decoding rule would refuse, and where.
87    BreaksDecodingRule(String),
88    /// A request's delegation chain proofs outside their bound.
89    ProofsOutOfBound,
90    /// A signed object whose signer is not the key it verified with.
91    KeyIdMismatch,
92    /// A reply, relay error or stream frame naming another request.
93    RequestMismatch,
94    /// A reply, or a provider's first stream frame, from a node other than its
95    /// request's target.
96    NotTheTarget,
97    /// A relay error from another station than the connection's.
98    NotTheConnection,
99    /// A key that cannot sign this frame.
100    Unsignable,
101    /// A text longer than its bound, naming the field.
102    TextTooLong(String),
103    /// A text that is not valid UTF-8, naming the field.
104    InvalidText(String),
105    /// A relay error code outside its closed set.
106    RelayCodeOutsideItsSet,
107    /// A field outside its range, and which.
108    OutOfRange(String),
109    /// A sealed payload built with another shape than its frame's: a
110    /// request's without a kem_ct of a profile's size or with a nonce, a
111    /// reply's or a provider stream frame's without a 12-byte nonce or with a
112    /// kem_ct, or a caller stream frame's with either.
113    SealedShape,
114    /// A stream frame its side does not send, and which.
115    NotAllowed(String),
116    /// A stream frame out of its side's order.
117    SeqMismatch,
118    /// A stream frame after its side's STREAM_END.
119    StreamEnded,
120    /// A frame given to sign that already carries a neighbour signature.
121    NeighbourSigned,
122    /// A publication published too far ahead, by how many milliseconds.
123    NotYetValid(i64),
124    /// A publication past its expiry, by how many milliseconds.
125    Expired(i64),
126    /// A signed object's signature that does not verify.
127    SignatureInvalid,
128    /// A key that could not sign.
129    Key(KeyError),
130}
131
132impl fmt::Display for FrameError {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            FrameError::TooLarge(n) => write!(
136                f,
137                "a frame of {n} bytes, over the {MAX_FRAME_BYTES}-byte cap"
138            ),
139            FrameError::Malformed => f.write_str("malformed frame"),
140            FrameError::Payload(why) | FrameError::BreaksDecodingRule(why) => f.write_str(why),
141            FrameError::ProofsOutOfBound => {
142                f.write_str("the request's proofs are outside the bound")
143            }
144            FrameError::KeyIdMismatch => {
145                f.write_str("the signer the frame names is not the key it verified with")
146            }
147            FrameError::RequestMismatch => f.write_str("the frame names another request"),
148            FrameError::NotTheTarget => f.write_str("the frame is not from the request's target"),
149            FrameError::NotTheConnection => {
150                f.write_str("the relay error is not from the connection's station")
151            }
152            FrameError::Unsignable => f.write_str("the key cannot sign this frame"),
153            FrameError::TextTooLong(what) => write!(f, "text longer than its bound: {what}"),
154            FrameError::InvalidText(what) => write!(f, "text that is not valid UTF-8: {what}"),
155            FrameError::RelayCodeOutsideItsSet => {
156                f.write_str("a relay error code outside its closed set")
157            }
158            FrameError::OutOfRange(what) => write!(f, "a field outside its range: {what}"),
159            FrameError::SealedShape => {
160                f.write_str("a sealed payload of another shape than its frame's")
161            }
162            FrameError::NotAllowed(what) => {
163                write!(f, "a stream frame its side does not send: {what}")
164            }
165            FrameError::SeqMismatch => f.write_str("a stream frame out of its side's order"),
166            FrameError::StreamEnded => f.write_str("a stream frame after its side's STREAM_END"),
167            FrameError::NeighbourSigned => {
168                f.write_str("the frame already carries a neighbour signature")
169            }
170            FrameError::NotYetValid(ms) => write!(f, "a publication not yet valid, by {ms} ms"),
171            FrameError::Expired(ms) => write!(f, "a publication past its expiry, by {ms} ms"),
172            FrameError::SignatureInvalid => {
173                f.write_str("the signed object's signature does not verify")
174            }
175            FrameError::Key(e) => write!(f, "{e}"),
176        }
177    }
178}
179
180impl std::error::Error for FrameError {}
181
182/// The refusal of a frame whose signed object did not verify: a signature
183/// that does not verify as it is, anything else as [`FrameError::Malformed`].
184fn object_refusal(e: ObjectError) -> FrameError {
185    match e {
186        ObjectError::SignatureInvalid => FrameError::SignatureInvalid,
187        ObjectError::Key(k) => FrameError::Key(k),
188        _ => FrameError::Malformed,
189    }
190}
191
192/// Wraps `frame` as `<Length:4 bytes big-endian><Cbor>`, refusing one over
193/// the frame cap.
194pub fn encode(frame: &Value) -> Result<Vec<u8>, FrameError> {
195    let payload = cbor::encode(frame).map_err(|e| FrameError::Payload(e.to_string()))?;
196    if payload.len() > MAX_FRAME_BYTES {
197        return Err(FrameError::TooLarge(payload.len()));
198    }
199    let mut out = Vec::with_capacity(4 + payload.len());
200    out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
201    out.extend_from_slice(&payload);
202    Ok(out)
203}
204
205/// What decoding the head of a buffer found.
206#[derive(Debug, Clone, PartialEq)]
207pub enum Decoded {
208    /// A whole frame, and how many bytes of the buffer it took.
209    Complete { frame: Value, consumed: usize },
210    /// At least this many more bytes are needed before trying again.
211    NeedMore(usize),
212}
213
214/// Decodes one length-prefixed frame from the head of `buf`, under the
215/// decoding rule.
216pub fn decode(buf: &[u8]) -> Result<Decoded, FrameError> {
217    let Some((header, rest)) = buf.split_first_chunk::<4>() else {
218        return Ok(Decoded::NeedMore(4 - buf.len()));
219    };
220    let length = u32::from_be_bytes(*header) as usize;
221    if length > MAX_FRAME_BYTES {
222        return Err(FrameError::TooLarge(length));
223    }
224    if rest.len() < length {
225        return Ok(Decoded::NeedMore(length - rest.len()));
226    }
227    let frame = cbor::decode(&rest[..length]).map_err(|_| FrameError::Malformed)?;
228    Ok(Decoded::Complete {
229        frame,
230        consumed: 4 + length,
231    })
232}
233
234/// A field's rule, as a frame's field table names it.
235#[derive(Debug, Clone, Copy)]
236enum Rule {
237    Any,
238    AnyBytes,
239    BytesOf(usize),
240    TextWithin(usize),
241    TextIn(&'static [&'static str]),
242    ProtocolUint,
243    ProtocolVersion,
244    CarriedObject,
245    HeldObject,
246    StreamObject,
247    Proofs,
248    Sealed(sealed::SealedContext),
249}
250
251impl Rule {
252    fn accepts(self, v: &Value) -> bool {
253        match self {
254            Rule::Any => true,
255            Rule::AnyBytes => matches!(v, Value::Bytes(_)),
256            Rule::BytesOf(n) => matches!(v, Value::Bytes(b) if b.len() == n),
257            Rule::TextWithin(n) => matches!(v, Value::Text(t) if t.len() <= n),
258            Rule::TextIn(names) => matches!(v, Value::Text(t) if names.contains(&t.as_str())),
259            Rule::ProtocolUint => protocol_uint(v).is_some(),
260            Rule::ProtocolVersion => {
261                matches!(v, Value::Int(n) if *n == i128::from(PROTOCOL_VERSION))
262            }
263            Rule::CarriedObject => crate::signed_object::Object::from_value(v).is_ok(),
264            Rule::HeldObject => crate::signed_object::HeldObject::from_value(v).is_ok(),
265            Rule::StreamObject => Rule::CarriedObject.accepts(v) || Rule::HeldObject.accepts(v),
266            Rule::Proofs => request::proofs_within_bound(v),
267            Rule::Sealed(context) => sealed::read_sealed(v, context).is_some(),
268        }
269    }
270}
271
272/// A frame's or signed object's fields by name.
273type Fields = std::collections::HashMap<String, Value>;
274
275/// A map read through its table, as macula_frame's read_fields does: every
276/// key text, named in the table and there once, with a value its rule
277/// accepts.
278fn read_fields(v: &Value, table: &[(&str, Rule)]) -> Option<Fields> {
279    let Value::Map(pairs) = v else {
280        return None;
281    };
282    let mut fields = Fields::with_capacity(pairs.len());
283    for (key, value) in pairs {
284        let Value::Text(name) = key else {
285            return None;
286        };
287        let rule = table.iter().find(|(n, _)| *n == name)?.1;
288        if fields.contains_key(name) || !rule.accepts(value) {
289            return None;
290        }
291        fields.insert(name.clone(), value.clone());
292    }
293    Some(fields)
294}
295
296fn has_fields(fields: &Fields, names: &[&str]) -> bool {
297    names.iter().all(|n| fields.contains_key(*n))
298}
299
300/// A received frame that carries its fields in one signed object: exactly
301/// version, frame_type, the object under `object_name` and the routing fields
302/// `routes` names; the protocol's version; a frame type of `types`; the object
303/// in a shape `object_rule` accepts; and each routing field of its rule.
304fn received_frame(
305    v: &Value,
306    object_name: &str,
307    object_rule: Rule,
308    routes: &[(&str, Rule)],
309    types: &'static [&'static str],
310) -> Option<(String, Value)> {
311    let mut table = vec![
312        ("version", Rule::ProtocolVersion),
313        ("frame_type", Rule::TextIn(types)),
314        (object_name, object_rule),
315    ];
316    table.extend_from_slice(routes);
317    let fields = read_fields(v, &table)?;
318    if !has_fields(&fields, &["version", "frame_type", object_name]) {
319        return None;
320    }
321    Some((text_of(&fields["frame_type"]), fields[object_name].clone()))
322}
323
324/// Refuses text longer than `max` bytes, judged first, then text that is not
325/// UTF-8, naming the field.
326fn bounded_text(field: &str, text: &[u8], max: usize) -> Result<(), FrameError> {
327    if text.len() > max {
328        return Err(FrameError::TextTooLong(format!(
329            "a {field} of {} bytes, over {max}",
330            text.len()
331        )));
332    }
333    if std::str::from_utf8(text).is_err() {
334        return Err(FrameError::InvalidText(format!("the {field}")));
335    }
336    Ok(())
337}
338
339/// Refuses a key that is not an identity key.
340fn identity_signer(key: &NodeKey) -> Result<(), FrameError> {
341    if key.purpose() != Purpose::Identity {
342        return Err(FrameError::Unsignable);
343    }
344    Ok(())
345}
346
347fn protocol_uint(v: &Value) -> Option<u64> {
348    match v {
349        Value::Int(n) if *n >= 0 && *n < i128::from(MAX_PROTOCOL_INT) => Some(*n as u64),
350        _ => None,
351    }
352}
353
354fn text_of(v: &Value) -> String {
355    match v {
356        Value::Text(t) => t.clone(),
357        _ => String::new(),
358    }
359}
360
361fn bytes_of(v: &Value) -> Vec<u8> {
362    match v {
363        Value::Bytes(b) => b.clone(),
364        _ => Vec::new(),
365    }
366}
367
368fn fixed<const N: usize>(v: &Value) -> [u8; N] {
369    let mut out = [0u8; N];
370    if let Value::Bytes(b) = v {
371        if b.len() == N {
372            out.copy_from_slice(b);
373        }
374    }
375    out
376}
377
378fn entry(name: &str, value: Value) -> (Value, Value) {
379    (Value::text(name), value)
380}
381
382fn uint(n: u64) -> Value {
383    Value::Int(i128::from(n))
384}
385
386/// Whether a reply's, relay error's or stream frame's request_id and
387/// request_hash are `request`'s.
388fn names_request(fields: &Fields, request: &VerifiedRequest) -> bool {
389    fields.get("request_id") == Some(&Value::Bytes(request.request_id.to_vec()))
390        && fields.get("request_hash") == Some(&Value::Bytes(request.request_hash.to_vec()))
391}
392
393/// The envelope a control frame carries, as macula_frame's base/2: version,
394/// frame_type, a fresh frame_id (UUID v7), sent_at_ms, capabilities, and the
395/// null realm, call_id and source_route.
396fn base(frame_type: &str) -> Vec<(Value, Value)> {
397    vec![
398        entry("version", Value::Int(i128::from(PROTOCOL_VERSION))),
399        entry("frame_type", Value::text(frame_type)),
400        entry("frame_id", Value::Bytes(crate::uuid_v7::new().to_vec())),
401        entry("sent_at_ms", uint(crate::uuid_v7::now_ms())),
402        entry("capabilities", uint(0)),
403        entry("realm", Value::Null),
404        entry("call_id", Value::Null),
405        entry("source_route", Value::Null),
406    ]
407}
408
409/// Replaces `fields`' entry for `key`, or appends one: a raw push over a base
410/// field would put two entries under one key.
411fn with_field(mut fields: Vec<(Value, Value)>, key: &str, value: Value) -> Vec<(Value, Value)> {
412    match fields.iter_mut().find(|(k, _)| *k == Value::text(key)) {
413        Some(slot) => slot.1 = value,
414        None => fields.push(entry(key, value)),
415    }
416    fields
417}