open-standards/wai/extensions/confidentiality.md
Transaction Science 59ab0d9489
All checks were successful
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m59s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m8s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m0s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m30s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m9s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m12s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m18s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m9s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m49s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 28s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 7m57s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 20s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 13m52s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 29s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 54s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m58s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m10s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m6s
WAI neural decoders end-to-end / synthetic fixtures → Rust decode → reference (±4) (pull_request) Successful in 1m11s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m12s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 4s
WAI section references / cited sections exist (pull_request) Successful in 10s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 33m46s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 4m40s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 2m53s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
standards workspaces build + test / aac-rs (pull_request) Successful in 9m8s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 3m2s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 4m3s
standards workspaces build + test / map-rs (pull_request) Successful in 7m9s
standards workspaces build + test / mesh-rs (pull_request) Successful in 4m14s
standards workspaces build + test / openpay (pull_request) Successful in 31m58s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 1m11s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 10m31s
WAI HTTP delivery / HTTP corpus · falsification and differential (pull_request) Successful in 1h31m39s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 2m1s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 3m36s
wai-pack / wai-pack · linux (pull_request) Successful in 1m7s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 24s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 1m47s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m21s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 3m18s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m10s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m16s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 30s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 12m1s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 31s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 3m31s
wai-pack / wai-pack · macos (pull_request) Successful in 2m11s
wai: SECURITY.md, SPEC §2 and §6.5 say what holds
SECURITY.md:
- "Allocation follows the input" was false for the manifest. The reference
  readers parse it into serde_json::Value; measured on the release build,
  a 94 MB prev of 2^20 runs peaks at about 7 times its length and a
  134,217,000-byte array of zeros at about 38 times (5.1 GB RSS, 7.6 s in
  from_bytes, which returned Ok). The text now says so, and the argument
  that a sink short of that memory could not decode such layers either is
  gone.
- CI does link ONNX Runtime and libzstd: wai-neural-gates.yml and
  wai-neural-e2e.yml build `neural`. No workflow builds `codecs`.
- No Kani harness is in int_kernels.rs; the harnesses are in rans.rs,
  int_entropy.rs, int_transform.rs, wai_video_policy.rs, staged/predict.rs
  and staged/wir1_arith.rs. Its SIMD kernels, the only unsafe outside the
  C ABI, are held to the scalar kernels by equality tests, not by a model
  checker.
- The bare "(§5.1)" names http-delivery; the writer test is described as
  it now runs; the Python verifiers' checks are described as they now run.

SPEC §2: the 2^20-unit cap is staged-delivery Appendix A.3's (section 3
bounds a count only by 32 bits); the limit bounds what a reader reads, not
the memory its parse takes; the paragraph is reflowed.

SPEC §6.5: Lane A's records are "among them", and include wai.video.ssai
and wai.video.channel. Lane B no longer says every record applies:
staged-measurement counts no encrypted object, and a verifier without the
key checks no payload-dependent rule of a reconstruct-session claim
(jwp-receipts §2.4.12 rule 3); nothing in the reference states a Lane B
tier. The confidentiality header and the CHANGELOG say the same.

SPEC §5 and determinism-tiers §5: only `photo` comes from a trained model;
gradient, bloom, facade and the cross-architecture golden use seeded
integer weights, as the CHANGELOG said both texts did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:16:04 -04:00

11 KiB
Raw Permalink Blame History

WAI Extension: Confidentiality (MLS + SFrame)

Status: Draft. Sub-spec of WAI v1.0. Its implementation is a companion implementation, outside this tree and under its own licence: the moq-sframe (SFrame object encryption) and joule-comm-mls (MLS group key agreement) crates, with browser bindings in joule-comm-mls-wasm. Live demonstration: https://wai.transaction.science/private-channel

This is Lane B of SPEC §6.5: end-to-end encryption at the application layer. The sink that holds the key decodes the plaintext, so the tier and the output digests it states hold as they do for plaintext; staged-measurement counts no encrypted object, and a verifier without the key checks none of a claim's rules that need a payload (SPEC §6.5). It is not a protected-path robustness level.

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 (2026-09-30: draft-jennings-moq-e2ee-mls is an individual Internet-Draft that has expired; the MoQ working group's draft-ietf-moq-secure-objects is related work):

  • 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. The publisher signs the group's Merkle root over the objects' leaves, and a Merkle leaf commits to the object's content hash and, for a labelled object, its figure, class and origin (jwp-receipts §2.2); an unlabelled object's figure is signed only where an energy claim restates it (jwp-receipts §2.4.5). A relay forwards the sealed bytes and their receipts unaltered, signs nothing in the publisher's name (jwp-receipts §4.3), and never learns what the bytes 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 which bytes the publisher signed for, and the group's declared energy — anyone can check it with the publisher's verifying key, which the receipt carries, and no content key; whose key it is, jwp-receipts.md §7 confirms.

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 content key) and signs nothing in the JWP chain (jwp-receipts §4.3). A relay attestation, where a deployment wants one, is a separate receipt under the relay's own key, and no class for it is registered yet.

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.