open-standards/wai/extensions/confidentiality.md
Transaction Science a3524eed43 wai: register confidentiality (MLS+SFrame) capability in the standard
The e2e_encrypted private-channel path shipped end-to-end (SFrame crypto,
MLS key agreement, live /private-channel demo) but the standard did not
describe it. Close the drift:

- SPEC.md gains section 6 Confidentiality: an OPTIONAL security.encryption
  manifest field, keys as a requirement-of-existence (never transported),
  relay-sees-only-ciphertext transparency, and a decrypt-then-dispatch
  refinement of the capability-dispatch algorithm. Conformance and
  Versioning renumbered to 7 and 8; no cross-references broken.
- New extensions/confidentiality.md sub-spec: byte-level SFrame (RFC 9605,
  suite 0x0006) framing + HKDF-SHA-512 key schedule + nonce, the MLS
  (RFC 9420) exporter binding with single- vs multi-sender keying, and
  how it composes with the JWP receipt chain over ciphertext.
- jwp-receipts.md cross-links payload-confidentiality (distinct from
  receipt-confidentiality) to the new sub-spec.

Grounded byte-for-byte in the moq-sframe / joule-comm-mls reference impl.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 05:15:45 -04:00

9.9 KiB
Raw Permalink Blame History

WAI Extension: Confidentiality (MLS + SFrame)

Status: Draft. Sub-spec of WAI v1.0. Reference implementation: the moq-sframe (SFrame object encryption) and joule-comm-mls (MLS group key agreement) crates of the WAI reference tree, with browser bindings in joule-comm-mls-wasm. Live demonstration: https://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.md §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.md §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.md; §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 id 0x00 0x06 is 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.md §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 is LEN+1 bytes, big-endian, minimal (no leading zeros; a zero counter is one 0x00).
  • 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 are KLEN = kid_len − 1 and the kid_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 is header ‖ 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 parsed ctr, 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 reference sframe_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 SFrame kid then 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.md §6.4. Before capability dispatch (SPEC.md §4) a sink MUST:

  1. If the manifest has no security object, or security.encryption is null / "none", dispatch the payload unchanged.
  2. Else, for "mls-sframe": if the sink is not a member of the group (holds no MLS state) or holds no base key for security.kid, the file is inert at this sink — return a clear "no key" error. The sink MUST NOT feed ciphertext to a codec.
  3. Else open the SFrame object (§3.4) to recover the plaintext payload, then run SPEC.md §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.md) 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.encryption in 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-none security.encryption cleanly (SPEC.md §6.4) — never mis-decode ciphertext as a codec.
  • Conformance is verified against the reference moq-sframe / joule-comm-mls crates: 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.