Repository navigation
Conversation
…MEMBER_PROVISION__ A leaf cannot fetch a certificate, so its paired phone hands it one inside their 1:1 session (ADR 0037, "Leaves"). `MemberProvision` is that body: version 1, kind 3, the publisher key, the leaf's certificate and the phone's clock, 135 bytes, fixed layout. Kind 3 is no `__MEMBER__` step, so neither parser reads the other's body. `prefixes::MEMBER_PROVISION` names the prefix it travels under, inside an `__MLS_ENC__` envelope only. `CONFIGURED_WIRE_APP_ID` is the one spelling of the constant wire app id a configured node stamps, so the engine and a leaf cannot split into two groups by the field meant to name none. Vectors: a `provision` block and five rejects in app-membership-v1, computed by tools/spec-vectors/generate.py.
Step 9a of the shared network plan (ADR 0037, "Leaves").
- A `__MEMBER_PROVISION__` body is acted on only as a decrypted
application message in the pair's session, after the sealing member is
bound to the sender. The certificate must name this device and verify
under the publisher key it came with, and be current at the supplied
time. It is written to the store before the group state that spends the
frame's generation, and acknowledged after both: a refusal or a failed
store leaves the device unprovisioned, unacknowledged and able to open
the retransmission.
- `K_app` is derived from the publisher key and the app id the device was
opened with, exactly as the engine does. The exchange follows the
engine's rules step for step, with mls-rs primitives (ChaCha20-Poly1305
from the provider's ChaCha suite). `__MEMBER__` frames are never
acknowledged and are admitted under the older control payload, since a
neighbour that never paired with the device never learned it verifies
the newer one and the exchange's freshness is in its nonces.
- No clock is read: certificates are checked against the latest supplied
time, the provisioning's (never backwards) or the caller's, whichever
is later.
- Once provisioned, every frame stamps the constant wire app id
"offline". The provisioning survives a power cut.
- An unprovisioned device is unchanged: it sends no membership frame,
answers none and writes nothing. It remembers the last hello each
neighbour sent, in memory, so the exchange is answered the moment a
provisioning lands.
New API: `LeafDevice::neighbor_discovered`, `is_member_visible`,
`membership_not_after`; `LeafEvent::{MembershipProvisioned, MemberVisible}`;
`LeafError::MembershipRefused`; `store::KEY_TYPE_MEMBERSHIP`.
…embership `OfflineProtocol::provision_leaf_membership(peer, cert)` checks the certificate against the rules the leaf applies (it names the leaf, verifies under the configured publisher key, is current), then seals the publisher key, the certificate and this node's clock inside the confirmed 1:1 session with the leaf and puts it on the reliable ladder. It never starts a session: provisioning outside the pair's session is exactly what the frame must not be. It then restarts the membership exchange with the leaf, since the hello sent when the phone met it went unanswered and would hold off a new one for the whole interval. `__MEMBER_PROVISION__` is reserved, so application text cannot carry it, and an engine consumes it wherever it finds it, bare or sealed: an engine takes its own certificate from the application. Across the FFI as `provision_leaf_membership(peer_id, cert) -> string`. Every binding is regenerated from the one definition (Swift, Kotlin, Python, Ruby, Java), with the C dispatcher, the Python local API table and the C# generated sources; the method is a platform operation on the local API, like the rest of application membership. Tests over a real engine and a real leaf: the two see each other once provisioned and the provisioning is acknowledged; a member that never paired sees a provisioned leaf across a power cut; a provisioning frame outside the session provisions nothing; a store that cannot keep it means no acknowledgement and no visibility, and the retransmission provisions it once the store recovers; an unprovisioned leaf is unchanged.
…amed Under the inbound gate a configured phone drops an unprovisioned leaf's key package unless the application named the leaf, which it does by asking for a session with the address the anchor verified. The pairing rig models that step, a new test pins the refusal, and the leaf provisioning chapter states it. The leaf and the engine stamp one wire constant, WIRE_APP_ID.
This was referenced Nov 4, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 9a of the shared-network plan (ADR 0037). Stacked on #691.
A leaf can't fetch a certificate, so its paired phone hands it one inside their 1:1 session. From then on the leaf runs the membership exchange like any other node.
Sealed
MemberProvision: base64 ofversion(1)=1 || kind(1)=3 || publisher_key(32) || cert(93) || u64be(supplied_time), 135 bytes. Kind 3 is not a__MEMBER__step, so neither parser accepts the other's body.__MEMBER_PROVISION__is reserved, added to the pinned list and the shadow test.provisionblock and five rejects inapp-membership-v1.vectors.json.WIRE_APP_ID.Phone (engine and UDL)
provision_leaf_membership(peer, cert) -> MessageId:__MEMBER_PROVISION__on the wire, or in a decrypted plaintext on a node that is not a leaf, is consumed and acted on by nothing.Leaf (
offline-protocol-leaf/src/membership.rs)group.write_to_storage(), and the acknowledgement is minted after both. A failed store leaves no group write and no ack, so the phone's retransmission still opens.offlineon the wire after provisioning. It uses mls-rs crypto only, with no new dependency.nowfirmware passes in, and it never moves backwards.LeafDevice::neighbor_discovered(peer, now)for each neighbour its radio connects.Found while stacking it on the inbound gate (#689)
Because of the inbound gate, a configured phone drops an unprovisioned leaf's key package unless its application named the leaf. The application does that through the out-of-band anchor step, by asking for a session with the address it verified (
establish_secure_session). That call names the peer even before any key package has arrived.docs/spec/leaf-provisioning.mdstates this.a_configured_phone_never_pairs_a_leaf_its_application_did_not_namepins the refusal. Mutation-checked: forcing the inbound gate open makes it fail.Decisions to review
__MEMBER__under the older control payload, as it already does for__MLS_KEY_PKG__. Otherwise a provisioned leaf would be invisible to members it never paired with. Freshness comes from the nonces.Checks
cargo test --lib: offline-protocol 2222, sealed 108, leaf 6 and uniffi 318, all passing.cargo clippy --workspace -- -D warnings, rustdoc under-D warningsand fmt all pass.--checkpasses.CI runs only once the stack below merges into main.