offline_protocol_sealed/prefixes.rs
1//! The control-frame prefixes a 1:1 sealed conversation is carried on.
2//!
3//! A frame's type is the reserved prefix its content begins with. The six
4//! here are the ones both ends of a pair speak: everything needed to
5//! establish a session, carry sealed payloads over it, and confirm it came
6//! up. They live in this crate because a leaf node emits and parses them
7//! with a different MLS implementation than the engine, and two ends that
8//! disagree about a prefix byte do not have a conversation at all.
9//!
10//! # What is not here
11//!
12//! The engine reserves many more: connection lifecycle, group frames, relay
13//! answers, presence, the sealed rich body, document sync. None of them
14//! reach a leaf node, and the registry that gates them is engine
15//! machinery. The split is deliberately "what a pair needs" rather than
16//! "every prefix", because the second would put group and relay vocabulary
17//! into a crate that builds for a part with 1536 KB of flash.
18//!
19//! # The registry still lives in the engine
20//!
21//! Naming a prefix here does **not** reserve it. Reservation is the
22//! `INTERNAL_PREFIXES` array in `offline-protocol`, which drives refusal of
23//! application content that begins with one, and which is generated from the
24//! same macro invocation that names these. Adding a prefix to this module
25//! without adding it there registers nothing.
26
27/// Carries a key package payload, JSON, signed on the control plane.
28///
29/// This is the only channel by which capabilities are advertised, which is
30/// why a device that cannot mint one is served the protocol floor forever.
31pub const KEY_PACKAGE: &str = "__MLS_KEY_PKG__";
32
33/// Carries an MLS Welcome, JSON, signed on the control plane.
34///
35/// The MLS bytes inside are a JSON byte array rather than base64, unlike
36/// every group frame.
37pub const WELCOME: &str = "__MLS_WELCOME__";
38
39/// Carries a sealed payload: the compact envelope in base64, or the JSON
40/// envelope that is the permanent floor.
41///
42/// This is a data-plane frame. It carries no control-plane signature,
43/// because MLS authenticates its own sender and a second signature would
44/// state on the outside what the AEAD already proves on the inside.
45pub const ENCRYPTED: &str = "__MLS_ENC__";
46
47/// Asks a peer to confirm it holds a working session. Empty body, signed.
48pub const SESSION_CONFIRM_PROBE: &str = "__MLS_CONFIRM_PROBE__";
49
50/// Answers [`SESSION_CONFIRM_PROBE`]. Empty body, signed.
51pub const SESSION_CONFIRM_ACK: &str = "__MLS_CONFIRM_ACK__";
52
53/// A confirmation that travels **inside** an [`ENCRYPTED`] envelope and
54/// never on the wire as a frame of its own.
55///
56/// Its whole purpose is to be a group-aware decrypt. A peer that created a
57/// session of its own confirms only on a successful decrypt, so a plaintext
58/// acknowledgement leaves it unconfirmed no matter how many times it is
59/// sent. The receiver consumes this marker and never surfaces it.
60pub const SESSION_CONFIRM_ENCRYPTED: &str = "__MLS_ENC_CONFIRM__";
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 /// Pins every prefix byte for byte.
67 ///
68 /// These are wire constants shared with an implementation that is not
69 /// compiled against this crate, so a rename here is not a refactor: it is
70 /// a frame the other end stops recognizing, and it surfaces as a session
71 /// that never comes up rather than as an error anyone can read. Nothing
72 /// else in the workspace compares these to a literal.
73 #[test]
74 fn prefixes_are_pinned() {
75 assert_eq!(KEY_PACKAGE, "__MLS_KEY_PKG__");
76 assert_eq!(WELCOME, "__MLS_WELCOME__");
77 assert_eq!(ENCRYPTED, "__MLS_ENC__");
78 assert_eq!(SESSION_CONFIRM_PROBE, "__MLS_CONFIRM_PROBE__");
79 assert_eq!(SESSION_CONFIRM_ACK, "__MLS_CONFIRM_ACK__");
80 assert_eq!(SESSION_CONFIRM_ENCRYPTED, "__MLS_ENC_CONFIRM__");
81 }
82
83 /// No prefix may be a prefix of another.
84 ///
85 /// Dispatch picks a frame type by the first prefix that matches, so an
86 /// overlap makes the shorter one shadow the longer one and routes a frame
87 /// to the wrong handler. `__MLS_ENC__` and `__MLS_ENC_CONFIRM__` are the
88 /// near miss: they differ only after the point where the shorter one ends,
89 /// and the trailing underscores are what keep them apart.
90 #[test]
91 fn no_prefix_shadows_another() {
92 let all = [
93 KEY_PACKAGE,
94 WELCOME,
95 ENCRYPTED,
96 SESSION_CONFIRM_PROBE,
97 SESSION_CONFIRM_ACK,
98 SESSION_CONFIRM_ENCRYPTED,
99 ];
100 for (i, a) in all.iter().enumerate() {
101 for (j, b) in all.iter().enumerate() {
102 if i != j {
103 assert!(
104 !a.starts_with(b),
105 "prefix {b} shadows {a}: dispatch would never reach {a}"
106 );
107 }
108 }
109 }
110 }
111}