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