Skip to main content

offline_protocol_sealed/
constants.rs

1//! Numbers a phone and a leaf node must configure identically.
2//!
3//! Each of these is a value that appears on both sides of a sealed
4//! conversation. A copy that drifts does not fail a build or a test: it
5//! produces a peer that is configured differently from the one it is talking
6//! to, and the symptom arrives later, on a device, as a message that will not
7//! decrypt.
8
9use core::time::Duration;
10
11/// The metadata key naming the message a delivery acknowledgement answers.
12///
13/// An acknowledgement is not a control frame. It carries no prefix and no
14/// signature: it is an ordinary message with empty content whose whole meaning
15/// is this one metadata entry, so a receiver that does not know this string
16/// reads a blank message and a sender that does not write it is never
17/// answered. That makes it exactly the kind of number this module holds, and
18/// the reason it is not left in the engine, where a leaf node cannot reach it
19/// and would have to spell it again.
20///
21/// It is deliberately the *only* entry a leaf writes. The other two the engine
22/// puts on its own acknowledgements say which hop count and which carrier the
23/// answer came back over, and both have defaults that are already right for a
24/// device: no hops, because a leaf is a direct peer, and BLE, because that is
25/// the radio a leaf is paired over. A device does not own its transport, so
26/// naming one would be firmware's guess crossing the wire as fact.
27pub const ACK_FOR_KEY: &str = "ack_for";
28
29/// How many generations behind the latest decrypted message a sender ratchet
30/// key is kept, so late/reordered messages remain decryptable.
31///
32/// The OpenMLS default (5) is smaller than the windowed media transfer's
33/// in-flight budget (up to 8 chunks on internet transports, interleaved with
34/// text on the same 1:1 session ratchet), which would make a sufficiently
35/// delayed chunk *permanently* undecryptable and stall the transfer. 32 gives
36/// 4x headroom over the largest window at the cost of retaining up to 32
37/// unused message keys per sender ratchet.
38///
39/// The protocol layer keeps the combined in-flight budget within this bound
40/// by capping concurrent media transfers per peer
41/// (`MAX_CONCURRENT_MEDIA_TRANSFERS_PER_PEER` in `offline-protocol`); revisit
42/// both together if either changes.
43pub const SENDER_RATCHET_OUT_OF_ORDER_TOLERANCE: u32 = 32;
44
45/// How far ahead of the highest seen generation a sender ratchet may be
46/// fast-forwarded when messages are lost.
47///
48/// A receiver more than this far behind cannot decrypt anything from that
49/// sender again until the pair re-keys, so the window has to sit well above
50/// the gap a congested link can open. The engine keeps up to
51/// `DEFAULT_MAX_PENDING_ACKS` (1000) messages in flight and re-seals each
52/// resend, which spends a fresh generation every time; a receiver that drops
53/// a resend as a duplicate of a message it already has skips that generation.
54/// The OpenMLS default of 1000 was therefore reachable, and the 2026-10-19
55/// volume test reached it: the sender logged 12,152 retries against 2,434
56/// messages the receiver decrypted. 8192 keeps more than four in-flight
57/// ceilings of headroom (pinned in `offline-protocol`), and the cost of using
58/// it is one fast-forward of up to 8192 key derivations when a gap opens.
59///
60/// It is receiver-local policy: nothing on the wire carries it, and a peer
61/// configured with a different value still interoperates. It configures the
62/// phone's OpenMLS groups only: a leaf's receiver window is
63/// [`LEAF_SENDER_RATCHET_MAXIMUM_FORWARD_DISTANCE`], which mls-rs does not let
64/// anyone set.
65pub const SENDER_RATCHET_MAXIMUM_FORWARD_DISTANCE: u32 = 8192;
66
67/// How far ahead a leaf node's receiver ratchet may be fast-forwarded: a frame
68/// more than this many generations past the next one it expects is refused.
69///
70/// Not configuration. mls-rs fixes it (`MAX_RATCHET_BACK_HISTORY` in its
71/// secret tree, 1024 in the pinned 0.56.0) and exposes no setting, so a phone
72/// sealing for a leaf has this much room, not
73/// [`SENDER_RATCHET_MAXIMUM_FORWARD_DISTANCE`]. Anything a phone derives from
74/// "how far ahead a receiver tolerates" has to take the smaller of the two:
75/// the re-seal budget in `offline-protocol` does. Stated here so that
76/// derivation has one name to read, and pinned against the real mls-rs by
77/// `tools/mls-interop`, which fails if an mls-rs upgrade moves it.
78pub const LEAF_SENDER_RATCHET_MAXIMUM_FORWARD_DISTANCE: u32 = 1024;
79
80/// How long a leaf node's key package claims to be valid.
81///
82/// mls-rs defaults to a year. 28 days is what RFC 9420 asks an application to
83/// define, and it bounds how long an unused init key stays usable.
84///
85/// It clears [`MAX_ACCEPTED_KEY_PACKAGE_LIFETIME`] with three weeks to spare,
86/// which is not a coincidence to be tidied away: a leaf that minted a window
87/// wider than the phone accepts would be refused at the one moment it has no
88/// way to hear why, and the pairing would fail on a device with no screen.
89/// The assertion below is what stops one number moving without the other.
90pub const LEAF_KEY_PACKAGE_LIFETIME: Duration = Duration::from_secs(28 * 24 * 3600);
91
92/// The widest validity window this SDK will admit in a key package it did not
93/// mint, at import and on every read of the cache.
94///
95/// RFC 9420 requires an application to define this and reject anything longer,
96/// and until issue 396 nothing here did. OpenMLS ships both halves of the rule
97/// and wires up neither: it declares `MAX_LEAF_NODE_LIFETIME_RANGE_SECONDS`
98/// (an hour plus three months) and never calls `Lifetime::has_acceptable_range`,
99/// so `KeyPackageIn::validate` checks only that *now* falls inside the window.
100/// A package claiming a century passed.
101///
102/// # Why the window is what ages a key package out
103///
104/// A key package is admitted once and cached in the install-scoped
105/// protocol-state store, where it stays usable for establishing new sessions
106/// until the window closes. That window is the only thing that expires it, so
107/// an unbounded one turns a single leaked init key into a permanent capability
108/// to open a session as its owner. Bounding what we mint and admitting anything
109/// inbound bounds nothing.
110///
111/// # Why 90 days and not the OpenMLS constant
112///
113/// The tempting cap is the one OpenMLS declares, and it is the wrong number by
114/// exactly zero seconds. An `openmls` key package built without an explicit
115/// lifetime carries the library's default of three months plus a one-hour skew
116/// margin, which *is* `MAX_LEAF_NODE_LIFETIME_RANGE_SECONDS`. Every key package
117/// this SDK minted before issue 396 is one of those, so a cap set there admits
118/// them with no margin at all, and any peer whose skew margin is a second wider
119/// is refused. 90 days sits above every window this SDK has ever put on the
120/// wire and far below the year mls-rs hands out by default.
121pub const MAX_ACCEPTED_KEY_PACKAGE_LIFETIME: Duration = Duration::from_secs(90 * 24 * 3600);
122
123// A leaf's own package has to survive the phone's gate. Checked at compile time
124// because the failure is otherwise a pairing that fails in the field, on the
125// one device that cannot report why.
126const _: () = assert!(
127    LEAF_KEY_PACKAGE_LIFETIME.as_secs() <= MAX_ACCEPTED_KEY_PACKAGE_LIFETIME.as_secs(),
128    "a leaf mints a window the phone refuses"
129);
130
131/// How far into the past a leaf's key package validity window has to start.
132///
133/// OpenMLS tests `not_before < now`, strictly. mls-rs's client builder writes
134/// `not_before` as exactly the timestamp it is handed, without the backdating
135/// its own `Lifetime::seconds` helper applies, so a package stamped with the
136/// current second is refused for being not yet valid. This is also the margin
137/// that absorbs clock skew between the two devices.
138///
139/// A leaf has no clock of its own to stamp with. ADR 0021 records the
140/// consequence: it needs a time source at pairing.
141pub const LEAF_KEY_PACKAGE_NOT_BEFORE_BACKDATE_SECONDS: u64 = 3600;