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