WAI Extension: Confidentiality (MLS + SFrame)
Status: Draft. Sub-spec of WAI v1.0. Reference implementation: the
moq-sframe(SFrame object encryption) andjoule-comm-mls(MLS group key agreement) crates of the WAI reference tree, with browser bindings injoule-comm-mls-wasm. Live demonstration: wai.transaction.science/private-channel
Keywords MUST, MUST NOT, SHOULD, MAY, REQUIRED, OPTIONAL are RFC 2119 / RFC 8174.
1. Scope and model
This extension makes a WAI payload private: only members of a group
may recover the content, while any relay or multiplexer that carries the
payload can still route, meter, and attest the bytes without reading
them. It is the e2e_encrypted path — the confidential counterpart to
the public floor, where a shared world or a broadcast film is plaintext
by design.
The construction is the draft-jennings-moq-e2ee-mls binding:
- MLS (RFC 9420) — the group agrees a shared secret out of band and every member derives the same SFrame base key from it. Membership, not a transported key, is the requirement of existence.
- SFrame (RFC 9605) — each object payload is sealed under that base key with an AEAD, so the wire carries only ciphertext.
Confidentiality is orthogonal to the capability (WAI SPEC §5). SFrame wraps the payload; the capability still names how to decode the plaintext once recovered. A sink decrypts first, then runs capability dispatch (SPEC §4) on the plaintext.
In scope: the SFrame object format, its key schedule, the MLS exporter binding, the decrypt-then-dispatch integration, and relay transparency.
Out of scope:
- A transport. Sealed objects ride in the carrier protocol exactly as plaintext objects do; this extension does not define how they move.
- MLS itself. Group formation, commits, welcomes, and epoch advancement are RFC 9420; this extension only names the ciphersuite and the exporter call that yields the SFrame base key.
- The receipt chain. JWP attestation is jwp-receipts; §6 states only how the two compose.
2. Ciphersuites
- SFrame: cipher suite
0x0006,AES_256_GCM_SHA512_128(RFC 9605 §4.5): AEAD = AES-256-GCM (Nk = 32,Nn = 12,Nt = 16), KDF = HKDF-SHA-512. The two-byte suite id0x00 0x06is a constant of the key schedule (§3.2), not carried on the wire. - MLS: ciphersuite
0x0001,MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519(RFC 9420 §17.1) — the mandatory-to-implement baseline. Credentials are BasicCredential over Ed25519.
A sink MUST reject an envelope whose declared security.encryption
names a scheme or suite it does not implement (SPEC §6.4), rather than
attempt to decode ciphertext.
3. SFrame object format
A sealed payload is header ‖ AEAD.Seal(...). The manifest’s payload
bytes ARE this sealed object; nothing else wraps it.
3.1 Header (RFC 9605 §4.3)
The first byte is:
bit 7 6 5 4 3 2 1 0
+---+-------+---+-------+
| R | LEN | X | KID/ |
|=0 | (3) | | KLEN |
+---+-------+---+-------+
R(1 bit) — reserved, MUST be 0.LEN(3 bits) —ctr_len − 1; the counter isLEN+1bytes, big-endian, minimal (no leading zeros; a zero counter is one0x00).X(1 bit) — key-id form.X = 0: the key id is the low 3 bits of byte 0 (values 0–7).X = 1: the low 3 bits areKLEN = kid_len − 1and thekid_len-byte big-endian minimal key id follows byte 0.- The counter bytes follow the key id.
The header is therefore 1 + (X ? kid_len : 0) + ctr_len bytes and is
self-delimiting: (kid, ctr) and the header length are recoverable from
the payload alone.
3.2 Key schedule (RFC 9605 §4.4.3)
From the 32-byte base key (§4), with suite = 0x00 0x06:
secret = HKDF-Extract(salt = "", IKM = base_key) ; HMAC-SHA-512
key = HKDF-Expand(secret, "SFrame 1.0 Secret key " ‖ suite, Nk=32)
salt = HKDF-Expand(secret, "SFrame 1.0 Secret salt " ‖ suite, Nn=12)
The label strings include their trailing space, immediately followed by the two suite bytes.
3.3 Nonce (RFC 9605 §4.4.4)
The per-object nonce is salt XOR ctr, where ctr is the 64-bit
counter written big-endian and right-aligned into the 12-byte nonce
(the low 8 bytes are XORed; the high 4 bytes are salt unchanged).
3.4 Seal / open
- Seal:
AES-256-GCM(key, nonce, plaintext, aad = header)→ct ‖ tag; the object isheader ‖ ct ‖ tag. The header is the AEAD associated data, so any edit to(kid, ctr)fails authentication. - Open: parse the header, derive
(key, salt), recompute the nonce from the parsedctr, and AEAD-open with the header as AAD. A wrong key, a tampered header, or a tampered ciphertext/tag MUST fail with a single indistinguishable authentication error.
The sender MUST NOT reuse a (base_key, ctr) pair — GCM nonce reuse is
catastrophic. Counters are per base key and strictly increasing.
4. MLS binding — deriving the base key
Every group member derives the SFrame base key from the MLS group’s exporter secret (RFC 9420 §8.5):
base_key = MLS-Exporter(label = "moq-jwp sframe base key",
context,
length = 32)
- All members at the same MLS epoch derive the same key; a non-member has no group state and cannot derive it. Advancing the epoch (any MLS commit — add, remove, update) rotates the base key, giving the channel MLS’s forward secrecy and post-compromise security for free.
- Single-sender channel:
context = "". One publisher, monotonic counter, no(key, ctr)collision. This is the profile the referencesframe_base_key()and the live private-channel demo implement. - Multi-sender channel: a sender MUST derive a per-sender base key by
binding its identity into the exporter —
context = KID(or the member’s MLS leaf index) — so two senders never share a(key, ctr)space. The SFramekidthen selects the sender whose base key opens the object. A multi-sender sink MUST use per-sender derivation; it MUST NOT seal two senders under one base key.
The SFrame kid (§3.1) names which base key opens an object; the MLS
epoch names which generation of that key. A sink resolves both before
open.
5. Decrypt-then-dispatch
This refines SPEC §6.4. Before capability dispatch (SPEC §4) a sink MUST:
- If the manifest has no
securityobject, orsecurity.encryptionisnull/"none", dispatch the payload unchanged. - Else, for
"mls-sframe": if the sink is not a member of the group (holds no MLS state) or holds no base key forsecurity.kid, the file is inert at this sink — return a clear “no key” error. The sink MUST NOT feed ciphertext to a codec. - Else
openthe SFrame object (§3.4) to recover the plaintext payload, then run SPEC §4 on the plaintext.
media, intent, conditioning.kind, and
model_requirement.capability describe the plaintext codec and are
NOT encrypted: confidentiality protects the content, not the shape,
so a multiplexer can still route by capability without a key.
6. Relay transparency and JWP composition
A relay or multiplexer that carries a sealed payload MUST treat it as opaque: it MUST NOT require the plaintext to route, meter, or attest, and MUST NOT attempt capability dispatch on ciphertext.
Attestation composes with confidentiality because the JWP receipt chain (jwp-receipts) commits over the ciphertext content hash. A relay signs that it forwarded the exact sealed bytes it was handed — with the per-object energy and delivery lineage bound into the Merkle leaf — while never learning what they say. Content secrecy (SFrame/MLS) and delivery attestation (JWP) are independent layers over the same bytes:
- SFrame answers what does it say — only members can.
- JWP answers who delivered it and at what energy — anyone can verify, no key required.
7. Security considerations
- Counter uniqueness (§3.4) is the sender’s obligation; a persisted or monotonic counter per base key is REQUIRED. Epoch rotation (§4) resets the counter space safely because it rotates the key.
- Replay. SFrame authenticates each object but does not by itself reject a replayed object. Freshness is enforced above this layer — the MLS epoch bounds a key’s lifetime, and the application op-log (e.g. a Lamport/causal clock) orders and de-duplicates. Note that a channel which legitimately re-sends old sealed objects for late-joiner resync MUST NOT treat re-delivery as an attack.
- Metadata. The manifest shape (capability, dimensions, timing) and object sizes/timing are visible to relays by design. This extension protects payload content, not traffic analysis.
- Trust boundary. A relay is untrusted for confidentiality (it never holds a key) and semi-trusted only for the attestation it signs under its own key; the two roles do not overlap.
8. Conformance
- A confidentiality-aware sink MUST implement §3 (SFrame open), §4 (MLS base-key derivation for the profiles it supports), and §5 (decrypt-then-dispatch), and MUST fail closed on a missing key or unsupported scheme.
- A confidentiality-aware encoder MUST emit
security.encryptionin the manifest whenever the payload is sealed, and MUST seal per §3 under a base key derived per §4. - A sink that does NOT implement this extension MUST refuse an envelope
whose manifest carries a non-
nonesecurity.encryptioncleanly (SPEC §6.4) — never mis-decode ciphertext as a codec. - Conformance is verified against the reference
moq-sframe/joule-comm-mlscrates: byte-level SFrame round-trip and rejection vectors, MLS group derivation of a shared base key, and the end-to-end check that a member opens what a non-member cannot — the same flow the live private-channel demo runs in-browser over the real relay.