§1's scope now covers the browser and states what a browser member does not do in this revision (§15.3): watch without being a member, ingest by WHIP or WebRTC, seek back. §15.2 makes the carrier's order binding on a member's own create and commit: applied only when the log returns it, dropped when another's commit for the epoch comes first. CI: joule-moq-room and joule-moq-web run on a joule-moq-mls change; the conformance job runs joule-moq-web's tests with feature e2ee, which run only with it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
855 lines
50 KiB
Markdown
855 lines
50 KiB
Markdown
# WAI Extension: Confidentiality (end-to-end encrypted rooms)
|
||
|
||
> Status: Draft. Sub-spec of WAI v1.0, and Lane B of SPEC §6.5: end-to-end encryption at the
|
||
> application layer. Registers the record `wai.live.e2ee`: the room authority's signed
|
||
> statement, `WRE1`, that a room of [live-rooms](live-rooms.md) is end-to-end encrypted and
|
||
> with which ciphersuites. In such a room every object of a media track is sealed by its
|
||
> publisher as an **MoQ secure object** (§4), so a relay routes, caches, meters and receipts
|
||
> ciphertext alone, and only the room's current members open it. The keys come from an MLS
|
||
> group (RFC 9420) whose membership follows the room's grants and lifecycle (§5): a principal
|
||
> that joins under a grant that may receive or publish is added, a principal that leaves,
|
||
> whose grant is revoked or whose grant ends is removed, and the next epoch's keys exclude it.
|
||
> The room's clock proposes every change and a member commits it, by a rule every member
|
||
> computes alike (§5.5). A recording of such a room is refused (§9). The reference
|
||
> implementation is open and in this tree: the `secure_objects` module of `joule-moq`
|
||
> (feature `secure-objects`) seals and opens objects and holds a receiver's keys and checks;
|
||
> the `joule-moq-mls` crate binds a room to an MLS group and exports each epoch's
|
||
> per-sender keys; `joule-moq-web` (feature `e2ee`) builds both for a browser page, and the
|
||
> room service `joule-moq-room` carries a room's MLS messages in one order (§15). The corpus
|
||
> is [`live-conformance/e2ee/`](../live-conformance/e2ee/), and its second implementation is
|
||
> `live-conformance/e2ee_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Documents read for this revision, each pinned to the revision named:
|
||
> `draft-ietf-moq-secure-objects-01` (6 July 2026, "secure objects"),
|
||
> `draft-jennings-moq-mocha-mls-keying-00` (6 July 2026, "the keying draft") and
|
||
> `draft-ietf-moq-transport-22` (1 October 2026, "MOQT"), all Internet-Drafts and none past
|
||
> Working Group Last Call; RFC 9605 (SFrame), RFC 9420 (MLS) and RFC 9750 (the MLS
|
||
> architecture); with RFC 5869 (HKDF), RFC 5116, NIST SP 800-38D (GCM) and FIPS 197 (AES)
|
||
> for the primitives. A section number of one of those documents in this text is that
|
||
> revision's. Where the documents disagree with one another, §11 says which reading this
|
||
> extension takes and why.
|
||
|
||
## 1. Scope and model
|
||
|
||
A room ([live-rooms](live-rooms.md)) carries each publisher's media on tracks named by the
|
||
room id, the publisher and the media kind (live-rooms §6.1), through relays that the room's
|
||
participants do not trust with the content. This extension makes those media tracks
|
||
private: the publisher seals each object before it leaves the device, every relay forwards
|
||
the sealed bytes as they are, and a member opens them after they arrive.
|
||
|
||
In scope:
|
||
|
||
- the room's confidentiality record (§3);
|
||
- object sealing, exactly as secure objects specifies it, on the objects of MOQT -22 (§4);
|
||
- keying: one MLS group per room, its credentials bound to the principals' room keys, its
|
||
membership driven by the room's lifecycle, the rule that says who commits, and each
|
||
epoch's per-sender keys (§5);
|
||
- what a receiver checks, in order, and the reason for each refusal (§6);
|
||
- rooms where several principals publish at once (§7);
|
||
- receipts and metering over ciphertext, and what a relay can and cannot prove (§8);
|
||
- recording and clips (§9);
|
||
- decrypt-then-dispatch and relay transparency (§10);
|
||
- a member in a browser, and a room service as the carrier of a room's MLS messages (§15).
|
||
|
||
Sealed in this revision: the objects of every **media track** of the room (live-rooms §6.1:
|
||
`audio`, `video`, `screen`, each layer). In the clear, as in a room without this extension:
|
||
|
||
- each publisher's **catalog track**, which states codecs and dimensions: the shape of what
|
||
is sent, never its content;
|
||
- each media track's **receipts track**, whose receipts commit to the sealed bytes (§8);
|
||
- every **events track** (live-events §8.1). The clock's lifecycle events must be read by
|
||
the clock itself, which is not a member (§5.3), and by every relay that admits grants; a
|
||
participant's events (chat, reactions, markers) are held to its grant's policy by relays
|
||
and by any room verifier (live-rooms §9), and sealing them would move those checks to the
|
||
members alone. Events stay signed in the clear in this revision; a participant that needs
|
||
a private message sends it on a media track.
|
||
|
||
Out of scope:
|
||
|
||
- **A transport.** Sealed objects ride MOQT exactly as plaintext objects do, through the
|
||
same relays, with no relay change (§10).
|
||
- **MLS itself.** Group state, the ratchet tree, commits, welcomes and the key schedule are
|
||
RFC 9420's; this extension fixes the ciphersuite, the credentials, the external sender,
|
||
who commits and what a commit holds, and the exporter call (§5).
|
||
- **What a browser member does not do in this revision** (§15.3): watch without being a
|
||
member, ingest by WHIP or WebRTC into such a room, or seek back.
|
||
- **Hardware-protected content** (SPEC §6.5, Lane A). This extension is Lane B: the
|
||
plaintext is in the member's application memory (§12).
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**, **room id**, **room authority**, **clock**, **principal**, **grant**,
|
||
**grant in effect**, **present**: as in live-rooms §2 and §9.3.
|
||
- **Keyed grant**: a grant that may subscribe (`subscribe` 1) or may publish any media kind
|
||
(`publish` not 0). A principal present under a keyed grant needs the room's keys: to open
|
||
what it receives, or to seal what it publishes.
|
||
- **Member**: a principal that holds a leaf of the room's MLS group in an epoch.
|
||
- **Epoch**, **leaf index**, **commit**, **proposal**, **Welcome**, **KeyPackage**: as in
|
||
RFC 9420. The **roster** of an epoch maps each occupied leaf index to its member.
|
||
- **Committer**: the member that commits the end of an epoch (§5.5).
|
||
- **Sealed object**: an object of a media track whose payload is a secure-objects
|
||
ciphertext and whose Immutable Properties carry its Key ID (§4).
|
||
- **KID**: the Key ID of a sealed object (§5.7).
|
||
- **vi64**: the variable-length integer of MOQT §8.1. Where this text writes one, it is the
|
||
shortest encoding of its value; where it reads one, any encoding MOQT allows is read.
|
||
|
||
## 3. The room's confidentiality record (`wai.live.e2ee`)
|
||
|
||
A room is end-to-end encrypted when its authority signs a **confidentiality record** for
|
||
it. The record names the room and the two ciphersuites; members and publishers seal and open
|
||
under it, and a verifier is given it beside the room's descriptor.
|
||
|
||
### 3.1 Layout
|
||
|
||
Integers are little-endian, as in the records of live-rooms.
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WRE1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id (live-rooms §3.2) |
|
||
| `suite` | 2 | u16: the secure-objects ciphersuite (secure objects §7.2) every sealed object of the room uses |
|
||
| `mls_suite` | 2 | u16: the MLS ciphersuite (RFC 9420 §17.1) of the room's group |
|
||
| `sig` | 64 | §3.2 |
|
||
|
||
### 3.2 Signature
|
||
|
||
The **signature** is Ed25519 by the descriptor's `authority` over the 18 bytes
|
||
`wai:live-room-e2ee` (ASCII) and 0x01, followed by the record from `magic` through
|
||
`mls_suite`. It is verified strictly, as [SPEC](../SPEC.md) §7 *Signatures* requires: a key
|
||
that fails step 1 or 2 is `bad_key`, and a signature that fails a later step is
|
||
`bad_signature`.
|
||
|
||
### 3.3 Checking the record
|
||
|
||
A verifier is given the record, the room id, the descriptor's `authority` and the
|
||
descriptor's recording `mode` (live-rooms §3.7), and refuses at the first of these that
|
||
holds:
|
||
|
||
1. Fewer than 4 bytes: `truncated`. A magic other than `WRE1`: `malformed`. No byte after
|
||
the magic: `truncated`. A version other than 1: `unknown_version`. Fewer than 105 bytes:
|
||
`truncated`. More: `malformed`.
|
||
2. `room` is not the room id: `wrong_room`.
|
||
3. `suite` is not `0x0004` (`AES_128_GCM_SHA256_128`) or `0x0005`
|
||
(`AES_256_GCM_SHA512_128`): `suite_unsupported`. Secure objects makes `0x0004`
|
||
mandatory to implement; the counter-mode suites are not used in rooms in this revision
|
||
(§11.2).
|
||
4. `mls_suite` is not `0x0001` (`MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`):
|
||
`mls_suite_unsupported`. It is the suite whose signatures are Ed25519, the scheme of the
|
||
principals' room keys (§5.2).
|
||
5. The descriptor's recording `mode` is not 0: `recording_refused` (§9).
|
||
6. The signature (§3.2): `bad_key` or `bad_signature`.
|
||
|
||
A room has at most one confidentiality record: a room id is the digest of its descriptor,
|
||
and a room whose confidentiality changed would be a new room.
|
||
|
||
## 4. Object sealing
|
||
|
||
Sealing is secure objects' construction, applied to the objects of a room's media tracks
|
||
as MOQT -22 carries them. Nothing below changes a cryptographic step of secure objects;
|
||
§4.3 and §4.4 say how its fields sit on a -22 object, and §11 where the documents leave a
|
||
choice.
|
||
|
||
### 4.1 What is protected
|
||
|
||
| protection | fields |
|
||
|---|---|
|
||
| hop by hop only (TLS of each MOQT session) | Track Alias; the object's mutable properties, those outside Immutable Properties |
|
||
| end to end, authenticated | the Full Track Name; Group ID; Object ID; Publisher Priority; the Immutable Properties, the Key ID among them |
|
||
| end to end, encrypted and authenticated | the original payload; the Encrypted Properties List |
|
||
|
||
This is secure objects' Table 1. A relay reads everything in the first two rows, which is
|
||
all it needs to route, cache, prioritise and meter (MOQT §8.4, §10.7); it can change only
|
||
the first row without the receiver noticing.
|
||
|
||
### 4.2 The Serialized Full Track Name
|
||
|
||
The Serialized Full Track Name is the object's Track Namespace as MOQT §8.7 encodes it, a
|
||
vi64 count of fields and each field as a vi64 length and its bytes, followed by its Track
|
||
Name as a vi64 length and its bytes (secure objects §3.3.1). In a room the fields are those
|
||
of live-rooms §6.1: for `<room>-<principal>-video--1`, the count 3, the 64 hexadecimal
|
||
digits of the room id, the principal's 64 digits, `video`, then the Track Name `1`.
|
||
|
||
### 4.3 The Key ID property
|
||
|
||
The Key ID is an Object Property, `SECURE_OBJECT_KEY_ID`, whose value is a vi64, the KID
|
||
(§5.7), carried inside the object's Immutable Properties (MOQT §10.7), exactly once (secure
|
||
objects §4.1). Its type in this revision is **`0x3A4A`**, from the range MOQT §8.4 sets aside for applications, and not
|
||
the `0x2` secure objects §7.1 asks for: MOQT -22 §16.8 has given `0x02` to
|
||
OBJECT_DELIVERY_TIMEOUT, so a -22 relay that read a Key ID at `0x2` would hold it for a
|
||
delivery timeout of that many milliseconds (§11.1). The type is even, so the value is one
|
||
vi64 (MOQT §8.3). A sealed object carries no Key ID property outside its Immutable
|
||
Properties.
|
||
|
||
The **Serialized Immutable Properties** that secure objects §3.5 puts in the AAD are the
|
||
value of the object's Immutable Properties property: its Key-Value-Pairs exactly as the
|
||
object carries them, which MOQT §10.7 forbids anyone after the publisher to re-encode. A
|
||
publisher writes them in the order MOQT §8.3 requires, each type the delta from the one
|
||
before, each vi64 the shortest.
|
||
|
||
The object receipt properties of [moq-streaming-format](moq-streaming-format.md) §5
|
||
(`WAI_RECEIPT`, `JWP_OBJECT_RECEIPT`) commit to the sealed bytes, which depend on the
|
||
Immutable Properties through the AAD; a sealed object therefore carries neither inside its
|
||
Immutable Properties. A sealed track's receipts ride its receipts track (live-rooms §6.1;
|
||
§8).
|
||
|
||
### 4.4 The plaintext
|
||
|
||
The AEAD's plaintext is the original payload and, optionally, the Encrypted Properties List
|
||
(secure objects §3.3.2):
|
||
|
||
```
|
||
pt = vi64(len(payload)) ‖ payload ‖ [ list ]
|
||
list = 0x000A (2 bytes) ‖ vi64(len(entries)) ‖ entries
|
||
entry = type (2 bytes, not 0x0032) ‖ vi64(len(value)) ‖ value
|
||
padding = 0x0032 (2 bytes) ‖ length (2 bytes) ‖ length zero bytes
|
||
```
|
||
|
||
Types and the padding length are big-endian. The list's form is secure objects §3.3.3
|
||
step 4 and §4.2, an entry's form and the padding's are the forms of secure objects'
|
||
Appendix B and §4.3 (§11.3). The **original payload** is what the object would carry in a
|
||
room without this extension: one WAI envelope ([moq-streaming-format](moq-streaming-format.md)
|
||
§3) or one media frame.
|
||
|
||
This extension defines one entry:
|
||
|
||
- **`WAI_INNER_DIGEST`** (type `0x3A4B`): the 32-byte BLAKE3 hash of the original payload,
|
||
the content hash [jwp-receipts](jwp-receipts.md) §2.1 computes over an object's payload.
|
||
It is the hash a receipt of the object would carry in the clear, sealed with the object, so
|
||
a member checks the plaintext against a digest the publisher committed to (§8.2) and
|
||
plaintext-level records attach to it. A publisher **SHOULD** carry it on every sealed
|
||
object of a video or screen track and **MAY** leave it out where its 35 bytes weigh on a
|
||
small object, as a frame of speech is.
|
||
|
||
Padding (secure objects §4.3) is the last entry when present; a receiver ignores its bytes.
|
||
|
||
A receiver reads the plaintext so, and refuses with `plaintext_malformed` where it does not
|
||
read: a vi64 length and that many bytes of payload; if nothing remains, no list; otherwise
|
||
2 bytes that are `0x000A`, a vi64 length that is exactly the number of bytes left, and
|
||
entries that fill them, each read by its form, padding only as the last, at most one
|
||
`WAI_INNER_DIGEST`, of exactly 32 bytes. An entry of another type is skipped.
|
||
|
||
### 4.5 Key and salt
|
||
|
||
For the object's KID, the base key of its track (§5.7) and its Serialized Full Track Name,
|
||
with the suite's hash (`SHA-256` for `0x0004`, `SHA-512` for `0x0005`), as secure objects
|
||
§3.7 derives them:
|
||
|
||
```
|
||
moq_secret = HKDF-Extract("", track_base_key)
|
||
moq_key = HKDF-Expand(moq_secret, "MOQ 1.0 Secure Objects Secret key " ‖ ftn ‖ suite ‖ kid, Nk)
|
||
moq_salt = HKDF-Expand(moq_secret, "MOQ 1.0 Secret salt " ‖ ftn ‖ suite ‖ kid, Nn)
|
||
```
|
||
|
||
`suite` is the record's `suite` as 2 bytes big-endian, `kid` the KID as 8 bytes big-endian,
|
||
and the label strings end in the space shown.
|
||
|
||
### 4.6 Nonce
|
||
|
||
`CTR` is the Group ID as 8 bytes big-endian followed by the Object ID as 4 bytes big-endian,
|
||
and the nonce is `moq_salt XOR CTR` (secure objects §3.6). An Object ID above 2^32 − 1 has
|
||
no nonce: such an object is not sealed, and a receiver refuses it `object_id_too_large`. A
|
||
publisher of a sealed track numbers its objects within that bound.
|
||
|
||
### 4.7 AAD
|
||
|
||
```
|
||
aad = Group ID (8 bytes) ‖ Object ID (4 bytes) ‖ Publisher Priority (1 byte) ‖ Serialized Immutable Properties
|
||
```
|
||
|
||
big-endian (secure objects §3.5). The Publisher Priority is the object's as its publisher
|
||
sent it; a publisher of a sealed track states it in every subgroup header and datagram, so
|
||
it never falls back to a track default a relay could hold differently (MOQT §10.4).
|
||
|
||
### 4.8 Ciphersuites
|
||
|
||
| `suite` | name | AEAD | hash | Nk | Nn | Nt |
|
||
|---|---|---|---|---|---|---|
|
||
| `0x0004` | `AES_128_GCM_SHA256_128` | AES-128-GCM | SHA-256 | 16 | 12 | 16 |
|
||
| `0x0005` | `AES_256_GCM_SHA512_128` | AES-256-GCM | SHA-512 | 32 | 12 | 16 |
|
||
|
||
GCM is SP 800-38D's with a 96-bit nonce and a 128-bit tag, the tag following the ciphertext
|
||
(RFC 5116). The reference implements RFC 9605's counter-mode suites as well (§11.2), and
|
||
checks them against RFC 9605's vectors; a room does not use them in this revision.
|
||
|
||
### 4.9 Seal
|
||
|
||
To seal `payload` as object (`group`, `object`) of a track, at priority `p`, with the KID and
|
||
track base key of the publisher's current epoch (§5.7):
|
||
|
||
1. Build the Immutable Properties: the Key ID property (§4.3) and any other property the
|
||
publisher fixes before sealing (the layer properties of moq-streaming-format §4.1, the
|
||
envelope flag), serialized per §4.3.
|
||
2. Build the plaintext (§4.4).
|
||
3. Derive `moq_key` and `moq_salt` (§4.5); form the nonce (§4.6) and the AAD (§4.7).
|
||
4. The object's payload is `AEAD.Encrypt(moq_key, nonce, aad, pt)`, the tag included.
|
||
|
||
A publisher counts the objects it seals under each `moq_key` and stops before the limit of
|
||
secure objects §6.3 for its suite; a key is one track's in one epoch, and the next epoch
|
||
gives a fresh one (§5.8).
|
||
|
||
### 4.10 Open
|
||
|
||
A receiver opens an object only through the checks of §6, which end in
|
||
`AEAD.Decrypt(moq_key, nonce, aad, payload)`. A wrong key, a changed property, priority,
|
||
Group ID, Object ID or track, and a changed byte of the payload or tag all fail alike, as
|
||
`bad_tag`, and the receiver discards the object (secure objects §3.9).
|
||
|
||
## 5. Keying from MLS
|
||
|
||
### 5.1 The group
|
||
|
||
Each room has one MLS group at a time, of the record's `mls_suite`. Its group ID is the 32
|
||
bytes of the room id followed by a 4-byte big-endian **generation**, 0 for the room's first
|
||
group. Handshake messages (proposals and commits) are sent as PublicMessage (RFC 9420
|
||
§6.2), which RFC 9750 §8.1.2 weighs: here membership is already public in the room's
|
||
lifecycle events, and the clock, which is not a member, follows each epoch from them. MLS protocol messages ride the tracks of the
|
||
keying draft §3 within the room's namespace, under the names live-rooms §6.1 leaves free:
|
||
each member's `mls` track and `keypackage` track, owner its principal. A deployment that
|
||
carries them otherwise, a room service passing them directly, changes nothing below.
|
||
|
||
### 5.2 Credentials: the principal's room key
|
||
|
||
A member's leaf carries a BasicCredential (RFC 9420 §5.3) whose `identity` is the 32-byte
|
||
Ed25519 key of the principal its grant names, and whose leaf `signature_key` is that same
|
||
key: the principal signs its MLS messages with the key that signs its events and its proofs
|
||
of possession (live-rooms §10.5). MLS signs with its own labels (`"MLS 1.0 "` and the
|
||
structure's name, RFC 9420 §5.1.2) and WAI with its own domains, so no signature of one can
|
||
be taken for the other.
|
||
|
||
A member validates a credential (RFC 9420 §5.3.1, RFC 9750 §4) against the room: an Add
|
||
whose leaf's identity is not its signature key is refused `credential_mismatch`, and one
|
||
whose principal is not the subject the clock owes an Add (§5.4) `proposal_mismatch`; a
|
||
commit that holds a proposal a member refused never applies for that member. The room authority stands as the authentication service of RFC 9750: its grants say who
|
||
the principals are, and the clock's lifecycle says which of them are present.
|
||
|
||
### 5.3 The clock as the external sender
|
||
|
||
The group's GroupContext carries the `external_senders` extension (RFC 9420 §12.1.8.1) with
|
||
exactly one entry: the descriptor's `clock` key, with a BasicCredential whose identity is
|
||
that key. The clock is never a member and holds no epoch's secret; it signs external Add and
|
||
Remove proposals (RFC 9420 §12.1.8), as PublicMessage, for the group's current epoch. A
|
||
member that joins a group whose group ID or external sender is not the room's (§5.1) refuses
|
||
it `group_mismatch`.
|
||
|
||
### 5.4 Membership follows the room
|
||
|
||
The clock turns each lifecycle event of live-rooms §7.2 that changes who holds keys into one
|
||
proposal, in the order of the events:
|
||
|
||
- **`join`** under a keyed grant: an Add of the subject's KeyPackage. When no group exists
|
||
(the room's first keyed join, or every member has left), there is nothing to add to: the
|
||
subject creates the group, of the next generation, at epoch 0, itself at leaf 0, with the
|
||
clock as its external sender.
|
||
- **`leave`** of a member, for any reason of live-rooms §7.2: a Remove of its leaf. The
|
||
clock records a `leave` when a principal leaves (reason 1), when the authority removes it,
|
||
a revocation through the key log among the ways (live-rooms §5; reason 2), when its grant's
|
||
window ends (reason 3), and when its connection is lost (reason 4). Each is a Remove, and
|
||
the epoch the commit opens excludes the principal.
|
||
- **`grant_exchange`** from a keyed grant to one that is not: a Remove; from a grant that is
|
||
not keyed to a keyed one: an Add. An exchange between keyed grants changes nothing here.
|
||
|
||
Nothing else is proposed: a principal present under a grant that is not keyed (it sends
|
||
events only) is never a member, and the clock sends no Update and no other proposal.
|
||
|
||
What the clock owes the group is the difference between the principals present under keyed
|
||
grants and the group's membership once its pending proposals are committed. It proposes, one
|
||
at a time, the earliest owed change, in the order of the lifecycle events that call for
|
||
them, that can be proposed now: an Add into any group, a Remove only of a principal that
|
||
holds a leaf. So a principal that leaves before the clock proposes its Add is never added,
|
||
and one that leaves while its Add is pending is removed once a commit has added it. When a
|
||
commit ends an epoch without some of the clock's proposals for it, those are void, as RFC
|
||
9420 makes every proposal for an ended epoch, and the same rule proposes them again for the
|
||
new epoch, before anything owed later.
|
||
|
||
A group in which no member is still present under a keyed grant can never commit again: it
|
||
ends, and the next keyed join creates the group of the next generation.
|
||
|
||
A KeyPackage reaches the clock as the keying draft §4.7 has it: the joining principal
|
||
publishes it on its `keypackage` track, signed by its room key (§5.2), and each is used
|
||
once. A Welcome reaches the new member on the committer's `mls` track (the keying draft
|
||
§4.8).
|
||
|
||
### 5.5 Who commits
|
||
|
||
**The members commit, by one rule: the committer of the commit that ends epoch *E* is the
|
||
member at the lowest leaf index of epoch *E* that the commit's proposals do not remove.**
|
||
|
||
The commit holds, by reference, the first *k* proposals the clock sent for epoch *E*, for
|
||
some *k*, each once, in any order, and no other proposal: the clock's proposals reach the
|
||
committer in order on the clock's track, so the ones it holds when it commits are always the
|
||
first. It carries an UpdatePath, so the committer's own leaf is renewed with every epoch. A
|
||
commit with no proposal is allowed, from the committer alone: it renews the epoch's keys and
|
||
nothing else.
|
||
|
||
A member checks each proposal when it arrives, and keeps it only if it passes: the clock's
|
||
must be for the current epoch, its Add's credential bound as §5.2 requires
|
||
(`credential_mismatch`), and the change the clock owes (`proposal_mismatch`). A member's own
|
||
proposal is kept but never committed. A member applies a commit only after checking it, in
|
||
this order:
|
||
|
||
- its sender is the committer: otherwise `commit_not_committer`;
|
||
- every proposal it holds is the clock's: otherwise `proposal_not_clock`;
|
||
- the proposals it holds are the clock's first *k* for the epoch, each once: otherwise
|
||
`commit_mismatch`.
|
||
|
||
A commit that fails is discarded, and its epoch never exists for that member.
|
||
|
||
Why the members, and why this rule:
|
||
|
||
- **The clock cannot commit.** In MLS only a member commits, and a member holds every
|
||
epoch's secret. The clock is the room's own service; were it the committer, the room's
|
||
host could open every object, and "only the room's current members" would include a party
|
||
no participant joined with. As an external sender it orders every membership change and
|
||
holds no key.
|
||
- **One committer, computed alike everywhere.** The keying draft §4.2 has one committer per
|
||
group so that every epoch has exactly one commit and nothing forks. Here every member
|
||
computes the committer from the epoch's tree and the commit's own proposals, with no
|
||
election and no extra message: the rule is the keying draft's failover rule (§4.4 there),
|
||
the lowest eligible member, applied at every epoch rather than only after a timeout.
|
||
- **It heals itself.** A committer that goes away is a member whose connection is lost: the
|
||
clock records its `leave` (reason 4), and the commit that removes it is, by the rule, the
|
||
next member's. A committer that stays but does not commit is, to the room, a member that
|
||
has stopped responding, and is removed the same way.
|
||
- **It can be checked.** The rule needs the tree and the commit alone, so a member checks
|
||
every commit before applying it, and a verifier checks a room's whole transcript (§13).
|
||
|
||
### 5.6 Roster
|
||
|
||
A commit applies its proposals as RFC 9420 §12.3 orders them: every Remove blanks its leaf,
|
||
then every Add, in the order the commit lists them, takes the leftmost blank leaf, or a new
|
||
leaf at the right edge when none is blank (RFC 9420 §7.7, §12.1.1): the lowest leaf index
|
||
no member holds. The roster of the new epoch follows from the
|
||
roster before it and the commit alone, so the clock, which is not a member, knows each
|
||
member's leaf index, and so does a verifier.
|
||
|
||
### 5.7 Per-epoch, per-sender keys
|
||
|
||
At each epoch *E*, every member derives the epoch's **application secret** with the MLS
|
||
exporter (RFC 9420 §8.5):
|
||
|
||
```
|
||
application_secret[E] = MLS-Exporter("wai live e2ee", room_id, 32)
|
||
```
|
||
|
||
and from it the **track base key** of each namespace, as the keying draft §7.1 derives a
|
||
track key, with HKDF-Expand of the MLS suite's hash (SHA-256):
|
||
|
||
```
|
||
track_base_key[E, ns] = HKDF-Expand(application_secret[E], "mocha_track" ‖ Serialize(ns), 32)
|
||
```
|
||
|
||
`Serialize(ns)` is the namespace as §4.2 serializes it, and `mocha_track` is ASCII with no
|
||
separator. The **KID** of an object a member at leaf index *L* seals in epoch *E* is the
|
||
keying draft's §7.2:
|
||
|
||
```
|
||
KID = E × 2^24 + L (E < 2^40, L < 2^24)
|
||
```
|
||
|
||
The base key is the namespace's, not the track's: secure objects §3.2 scopes a Key ID to a
|
||
namespace, so the tracks of one namespace (a publisher's layers of one media kind) share it,
|
||
and secure objects' own derivation (§4.5) mixes in the Full Track Name, so each layer's key
|
||
and salt are its own. Every sender's own tracks are in a namespace of their own (live-rooms
|
||
§6.1), and the KID names the sender, so a sender's key is its own in every epoch: two
|
||
members never seal under one `(moq_key, nonce)`, even in a slot where a live publisher and
|
||
its standby send on the same track (live-rooms §7.1). A receiver reads the epoch and the
|
||
leaf index from the KID and derives the one key that opens the object, with no trial
|
||
decryption.
|
||
|
||
The KID's 40 bits of epoch and 24 of leaf index fit in one vi64 (MOQT §8.1); a group whose
|
||
epoch would reach 2^40 or whose tree would exceed 2^24 leaves ends, and the next join starts
|
||
the next generation.
|
||
|
||
### 5.8 Rotation and retention
|
||
|
||
Every commit opens an epoch whose application secret, and so every key derived from it, is
|
||
new. A member derives an epoch's keys when it applies the commit that opens it, and a
|
||
publisher seals with them from then on. A receiver that holds an object whose KID names an
|
||
epoch it has not reached yet **MAY** keep it until it applies the commit (secure objects
|
||
§3.9), and opens it then.
|
||
|
||
A member keeps an epoch's keys while objects of that epoch can still arrive, and deletes
|
||
them after; for a receiver, an epoch whose keys it deleted is one it no longer joins (§6
|
||
step 4). Keeping them no longer than needed is what gives MLS's forward secrecy to the room's
|
||
media: a member's keys taken later open nothing sent before the epochs it still holds.
|
||
|
||
## 6. Receiving
|
||
|
||
A receiver is a member. For an object of a media track of the room it runs these steps in
|
||
order, and refuses at the first that fails:
|
||
|
||
1. The object's properties read (MOQT §8.3, §10.7), and its Immutable Properties hold
|
||
exactly one Key ID property and the mutable list none: `properties_malformed` for a list
|
||
that does not read or a Key ID twice, outside, or not a vi64; `key_id_missing` for none.
|
||
A media object of an end-to-end encrypted room is always sealed: one without a Key ID is
|
||
refused, never decoded as plaintext.
|
||
2. The Object ID is at most 2^32 − 1: `object_id_too_large`.
|
||
3. The KID gives the epoch *E* and leaf index *L* (§5.7).
|
||
4. The receiver was a member in epoch *E*: `epoch_not_joined` when *E* precedes the epoch it
|
||
joined at, or it deleted *E*'s keys (§5.8); `removed_before_epoch` when it was removed at
|
||
or before *E*. (*E* later than the receiver's latest epoch is held, §5.8, not refused.)
|
||
5. *L* is occupied in epoch *E*'s roster: `key_id_unknown`.
|
||
6. The member at *L* in epoch *E* is the track's owner (live-rooms §6.1: the namespace's
|
||
second field is its key; for a slot's track, a publisher the room state binds to the
|
||
slot): `sender_not_owner`. A member holds every sender's keys in its epochs, so it could
|
||
seal under another's KID; this step, and the publisher's signed receipts (§8), are what
|
||
keep a member from speaking as another.
|
||
7. The member at *L* held *L* in every epoch from *E* to the receiver's latest: otherwise
|
||
`sender_removed`. Once a receiver has applied the commit that removed a member, it takes
|
||
nothing more sealed under that member's earlier KIDs, so a removed member cannot go on
|
||
publishing under the last epoch it held.
|
||
8. The receiver has not opened an object of this Full Track Name, Group ID and Object ID
|
||
before: `replayed`. MOQT forbids two objects at one location (secure objects §6.1); one
|
||
presented again is a relay's replay, and a receiver keeps the locations it opened while it
|
||
keeps the epoch's keys.
|
||
9. `AEAD.Decrypt` (§4.10): `bad_tag`.
|
||
10. The plaintext reads (§4.4): `plaintext_malformed`.
|
||
11. Where the plaintext carries `WAI_INNER_DIGEST`, it is BLAKE3 of the original payload:
|
||
`inner_digest_mismatch`.
|
||
|
||
The original payload is then dispatched (§10). Steps 4 to 7 need only the receiver's epochs,
|
||
their rosters and the track's name, so a receiver refuses them before deriving any key, and
|
||
records a location as opened (step 8) only once the object has opened.
|
||
|
||
## 7. Multi-sender rooms
|
||
|
||
Every member may publish, each on its own tracks, at once. Nothing in a sealed object needs
|
||
coordinating between them: each sender's KID carries its leaf index (§5.7), so its key is
|
||
its own and its nonces never meet another's; a receiver selects the key from the KID alone.
|
||
Each publisher numbers its own tracks' groups and objects, as MOQT has it, and each signs its
|
||
own tracks' receipts with its own room key (§8.1).
|
||
|
||
When a commit opens epoch *E* + 1, each member moves to it when it applies the commit, at its
|
||
own pace: a publisher's objects of *E* and *E* + 1 can interleave at a receiver, and each
|
||
opens under its own epoch (§5.8). Two commits for one epoch cannot both be applied: the
|
||
rule of §5.5 names one committer, and a member discards any other's commit.
|
||
|
||
A slot (live-rooms §7.1) has one live publisher and, with redundancy, a standby; both are
|
||
members, and their objects on the slot's tracks carry their own KIDs. A takeover or failover
|
||
changes who publishes, not who is a member, and needs no commit.
|
||
|
||
## 8. Receipts and metering over ciphertext
|
||
|
||
### 8.1 What receipts bind
|
||
|
||
A sealed object's receipt is the receipt of [jwp-receipts](jwp-receipts.md), unchanged,
|
||
over the object's payload as it travels: its content hash is BLAKE3 over the sealed bytes,
|
||
ciphertext and tag. The publisher signs its group receipts with its room key, and they ride
|
||
the track's receipts track (live-rooms §6.1, moq-streaming-format §4.1), in the clear. A
|
||
relay with a receipt door ([jwp-receipts](jwp-receipts.md) §4.1) checks each sealed object
|
||
against its receipt exactly as it checks a plaintext one, and holds each publisher's tracks
|
||
to its own key (the namespace names it).
|
||
|
||
### 8.2 What a member checks besides
|
||
|
||
A member that opens an object knows the plaintext came from the sealed bytes the publisher
|
||
signed for, under a key of the publisher's leaf: the AEAD authenticates the plaintext
|
||
under the KID's key, and the receipt binds those exact bytes to the publisher's room key.
|
||
Where the object carries `WAI_INNER_DIGEST` (§4.4), the member also checks the plaintext
|
||
against the digest the publisher sealed into it, the content hash a receipt of the plaintext
|
||
would carry; records made over plaintext (a decoded output's digest, a reconstruction
|
||
binding) attach to that hash.
|
||
|
||
### 8.3 What a relay can prove, and what it cannot
|
||
|
||
A relay, and anyone holding the receipts, can prove: which sealed bytes each publisher signed
|
||
for, at which track, group and object; that the relay forwarded exactly those bytes, since a
|
||
changed byte fails the receipt; how many objects and bytes of each track it carried, received
|
||
and dropped, and the energy each receipt states, which the per-hop records of
|
||
[live-metering](live-metering.md) count from what it carried (their byte counts are of sealed
|
||
bytes, the AEAD's tag and the Key ID among them); the track names, priorities and Immutable
|
||
Properties, which the AEAD authenticates but does not hide; and the room's membership over
|
||
time, which the room's events and the group's public handshake messages state.
|
||
|
||
A relay cannot prove, or learn: what any object says; that a sealed payload opens at all, or
|
||
that its plaintext is a frame of the codec its catalog names; that a member received or
|
||
opened what it was sent; or anything a measurement over plaintext states (staged-measurement
|
||
counts no sealed object, code `encrypted`; a decoded output's tier and digest are the
|
||
member's). A moderator who reviews media is a member and reviews what it opens; a relay can
|
||
withhold an object a moderator's decision names by its receipt leaf, and cannot review one by
|
||
its content ([live-moderation](live-moderation.md)).
|
||
|
||
## 9. Recording and clips: refused
|
||
|
||
**A room with a confidentiality record is never recorded.** Its descriptor's recording
|
||
`mode` is 0 (live-rooms §3.7), and §3.3 step 5 refuses a record for a room whose mode is not
|
||
(`recording_refused`): no principal holds the record power to any effect, no device commits a
|
||
take (capture-binding needs a `record_start`), no recording manifest is signed, no clip is
|
||
cut, and a relay keeps nothing of the room's tracks beyond delivery: no DVR window either,
|
||
which a relay that keeps one stops at the room's first sealed object
|
||
([live-recording](live-recording.md) §8.2).
|
||
|
||
The alternative was a recording of the relay's ciphertext with each epoch's keys escrowed to
|
||
the room authority. It is not taken, for these reasons:
|
||
|
||
- **Escrow makes a reader the members cannot see.** The MLS roster is where a member sees
|
||
who can open what it sends. An authority holding every epoch's keys from outside the tree
|
||
would open everything while no roster named it, and the room's claim to be end-to-end
|
||
encrypted would be false for every recorded room.
|
||
- **Escrowed keys outlive their epoch.** Forward secrecy, and a removed member being cut off,
|
||
rest on each epoch's keys being deleted (§5.8). Keys kept for a recording are kept for its
|
||
retention, and one later compromise of the escrow opens the whole session.
|
||
- **It is not what a recording is here.** A recording of [live-recording](live-recording.md)
|
||
is a package of the participants' takes, masters their devices upload to the room's store
|
||
in the clear, under a manifest the authority signs. A store of ciphertext and keys would be
|
||
a second format that live-recording's verifier does not read, and the takes would still
|
||
reach the store as plaintext. A room whose session must be recorded is not end-to-end
|
||
encrypted, and says so by having no confidentiality record.
|
||
- **It needs no new field.** Mode 0 already means "never recorded", so the rule is a check of
|
||
what the descriptor states.
|
||
|
||
A member keeps what it receives as any participant of an end-to-end encrypted system can;
|
||
that is the member's own act, outside the room's recording policy, and no record of this
|
||
family attests it.
|
||
|
||
## 10. Decrypt-then-dispatch and relay transparency
|
||
|
||
This refines SPEC §6.4. A receiver of a room's media track opens each object by §6, then
|
||
treats the original payload as it would the payload of a plaintext room: a WAI envelope is
|
||
read and dispatched by SPEC §4, a media frame decoded by the track's codec. It **MUST NOT**
|
||
feed a payload that has not opened to a decoder. The envelope inside a sealed object carries
|
||
no `security` member: the object around it was sealed, and the envelope is plaintext once
|
||
opened.
|
||
|
||
A relay forwards a sealed object as it forwards any object: it reads the track name, the
|
||
Group ID, the Object ID, the priority and the properties, applies its delivery timeouts,
|
||
caches and serves FETCH, checks receipts at a door, and meters; it never needs a key, and
|
||
the Key ID property is one more property it forwards unchanged (MOQT §8.4). Nothing in a
|
||
relay's forwarding changes for a sealed object, and the reference relay
|
||
(`joule-moq-relay`) forwards them with no change of its own.
|
||
|
||
## 11. Reading the documents together
|
||
|
||
The documents of this revision were written against different revisions of one another.
|
||
Where they disagree, this extension takes the reading below.
|
||
|
||
### 11.1 The Key ID's code point
|
||
|
||
Secure objects (§4.1, §7.1) gives the Key ID property the type `0x2`, written against MOQT
|
||
-18. MOQT -22 §16.8 gives `0x02` to OBJECT_DELIVERY_TIMEOUT, a Track and Object Property a
|
||
relay acts on, and its table of provisional registrations lists secure objects' `0x0A` and
|
||
`0x32` but no Key ID. This extension carries the Key ID at `0x3A4A` (§4.3), in the
|
||
application range of MOQT §8.4, until the two agree; every other rule of the property is
|
||
secure objects'.
|
||
|
||
### 11.2 The counter-mode suites' tag
|
||
|
||
Secure objects §7.2 says its suites' algorithms are those of RFC 9605. RFC 9605 §4.5.1
|
||
computes the counter-mode suites' tag as HMAC over the lengths of the AAD, the ciphertext and
|
||
the tag, then the nonce, the AAD and the ciphertext. The ciphertexts of secure objects'
|
||
Appendix B for suites `0x0001` and `0x0002` carry instead the HMAC of the AAD, the nonce and
|
||
the ciphertext, with no lengths; their counter-mode encryption, keys, salts, nonces and AADs
|
||
are RFC 9605's. This extension implements RFC 9605's construction and keeps the
|
||
counter-mode suites out of rooms until the text and the vectors agree; its corpus checks
|
||
both readings against those two vectors, so the difference is stated exactly.
|
||
|
||
### 11.3 Property forms
|
||
|
||
Secure objects' Appendix B serializes properties as a 16-bit type, a variable-length length
|
||
and the value, whatever the type's parity; MOQT -22 §8.3 serializes them with delta-coded
|
||
types, an even type's value a single integer. On a -22 object the Immutable Properties are
|
||
-22's (§4.3), which is what MOQT §10.7 keeps unchanged end to end; inside the plaintext, which
|
||
no relay reads, the Encrypted Properties List keeps Appendix B's form (§4.4). Padding is
|
||
secure objects §4.3's form, a 16-bit type and a 16-bit length; §4.3.1's processing steps
|
||
speak of a 1-byte type and a 4-byte length, and are not followed. Appendix B's own vectors are
|
||
checked with the property bytes they state, which the AEAD takes as they are.
|
||
|
||
### 11.4 The keying draft's track key
|
||
|
||
The keying draft §7.1 derives a track key and a track nonce from an "application secret"
|
||
and the Track Name alone. RFC 9420 gives an application one interface to its key schedule,
|
||
the exporter (§8.5), so the application secret here is an exported value (§5.7); a room's
|
||
Track Names are layer numbers that every publisher shares (live-rooms §6.1), and secure
|
||
objects §3.2 scopes a Key ID to a namespace, so the derivation is over the namespace; and
|
||
secure objects derives its own key and salt from a base key (§4.5), so the derived value is
|
||
that base key, and no nonce is derived beside it. The KID is the keying draft's §7.2 as
|
||
written.
|
||
|
||
### 11.5 Variable-length integers
|
||
|
||
Secure objects writes its lengths and its Key ID as the varints of the MOQT revision it was
|
||
written against. This extension writes them as MOQT -22's (§8.1), shortest, everywhere: in
|
||
the Serialized Full Track Name, the payload's length and the list's. Every value in secure
|
||
objects' Appendix B has the same bytes in both encodings, so those vectors hold unchanged.
|
||
|
||
## 12. Security and privacy considerations
|
||
|
||
- **Who can open.** The members of an epoch, and no one else: not a relay, not the clock,
|
||
not the room authority. A member is a principal present under a keyed grant, admitted by
|
||
the clock's proposal and a commit every member checked. The authority decides who is
|
||
present, so an authority that wants to read can admit a principal of its own, and that
|
||
principal is a member, named in the roster every member holds; the authority cannot read
|
||
without being seen to.
|
||
- **Within the group.** MLS gives every member every sender's keys (RFC 9605 §7.2:
|
||
SFrame has no per-sender authentication). A member's objects are bound to it by the
|
||
receiver's owner check (§6 step 6), by the relay's door holding each publisher's
|
||
namespace to its key, and by its signed receipts (§8.1). A grant that may publish but not
|
||
subscribe is a member, and can open what others send: subscription is a relay's
|
||
authorisation, not a line inside a group.
|
||
- **Removal.** A removed member keeps the keys of the epochs it held. It opens nothing of a
|
||
later epoch (§6 step 4), and once its removal is applied a receiver takes nothing more
|
||
sealed under its earlier KIDs (§6 step 7). Objects of earlier epochs that it received are
|
||
its own to keep.
|
||
- **Late joiners.** A member added at epoch *E* derives nothing before *E*: a Welcome carries
|
||
the epoch's secrets, not earlier ones, and objects a relay serves by FETCH from before the
|
||
join are refused `epoch_not_joined`.
|
||
- **Replay and reordering.** The AEAD binds each payload to its track, group, object,
|
||
priority and properties (secure objects §6.2); a receiver refuses a location presented
|
||
twice (§6 step 8). A relay can still drop, delay and reorder objects (secure objects §6.1);
|
||
the receipts chain, End of Group and End of Track statuses and the gap properties are how
|
||
drops are seen (secure objects §6.4, §6.5).
|
||
- **Forgery against fan-out.** A relay could try forgeries on many subscribers at once
|
||
(secure objects §6.1.1). The suites of rooms have 128-bit tags and the base keys are 256
|
||
bits, and a receiver discards a failed object indistinguishably from one it processed.
|
||
- **Metadata.** Track names, sizes, timing, priorities, the catalog and the membership are
|
||
visible by design. Padding (§4.4) hides an object's exact size from a relay where a
|
||
deployment needs it.
|
||
- **Key reuse.** The principal's room key signs its events, its proofs of possession and its
|
||
MLS messages, each under its own domain or label (§5.2). A compromise of that key lets an
|
||
attacker speak as the principal in all three until the authority revokes the grant (and,
|
||
with it, the member's leaf at the next commit).
|
||
- **The device.** The plaintext, and every key a member derives, are in the application's
|
||
memory; Lane B is not a protected-path robustness level (SPEC §6.5).
|
||
|
||
## 13. Reasons
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `truncated`, `malformed`, `unknown_version` | §3.3 step 1 |
|
||
| `wrong_room` | §3.3 step 2 |
|
||
| `suite_unsupported` | §3.3 step 3 |
|
||
| `mls_suite_unsupported` | §3.3 step 4 |
|
||
| `recording_refused` | §3.3 step 5, §9 |
|
||
| `bad_key`, `bad_signature` | §3.3 step 6 |
|
||
| `group_mismatch` | §5.3 |
|
||
| `credential_mismatch` | §5.2 |
|
||
| `proposal_not_clock` | §5.5 |
|
||
| `proposal_mismatch` | §5.4, §5.5 |
|
||
| `commit_not_committer` | §5.5 |
|
||
| `commit_mismatch` | §5.5 |
|
||
| `properties_malformed` | §6 step 1 |
|
||
| `key_id_missing` | §6 step 1 |
|
||
| `object_id_too_large` | §4.6, §6 step 2 |
|
||
| `epoch_not_joined` | §6 step 4 |
|
||
| `removed_before_epoch` | §6 step 4 |
|
||
| `key_id_unknown` | §6 step 5 |
|
||
| `sender_not_owner` | §6 step 6 |
|
||
| `sender_removed` | §6 step 7 |
|
||
| `replayed` | §6 step 8 |
|
||
| `bad_tag` | §4.10, §6 step 9 |
|
||
| `plaintext_malformed` | §4.4, §6 step 10 |
|
||
| `inner_digest_mismatch` | §6 step 11 |
|
||
|
||
## 14. Conformance
|
||
|
||
[`live-conformance/e2ee/`](../live-conformance/e2ee/) holds the corpus. `vectors.json` is
|
||
written from fixed seeds by `joule_moq_e2ee_vectors gen`; beside it, `rfc9605.json` and
|
||
`secure-objects-01.json` hold the test vectors of RFC 9605 Appendix C and of secure objects'
|
||
Appendix B as those documents print them. The corpus has:
|
||
|
||
- **Published vectors.** Every RFC 9605 vector (header encoding, the counter-mode AEAD, and
|
||
SFrame encryption in all five suites), and every secure-objects vector (the Full Track
|
||
Name, key derivation, nonce and AAD, and encryption: exact in suites `0x0004` and `0x0005`,
|
||
and in `0x0001` and `0x0002` the difference of §11.2 stated).
|
||
- **Records**: an accepted confidentiality record, and one refused for each reason of §3.3.
|
||
- **Key schedule**: per-epoch application secrets and, for each sender and namespace, the
|
||
track base key, the KID, and each track's `moq_key` and `moq_salt`.
|
||
- **Objects**: sealed objects with every input and key stated (the Immutable Properties as
|
||
carried, the plaintext, the AAD, the nonce, the sealed payload and its content hash), with
|
||
and without an inner digest and padding, in both suites.
|
||
- **A room**: three members joining in turn, each publishing, the third removed (its grant
|
||
revoked), a fourth joining late at the third's old leaf, and the first leaving so that the
|
||
second commits, with the lifecycle, the clock's proposals, each commit and its committer,
|
||
each epoch's roster and application secret, and every sealed object delivered to its
|
||
receivers with the outcome §6 gives it: opened, with its payload's BLAKE3 hash, or refused
|
||
with its reason. Every epoch's keys open at least one object.
|
||
- **Refused rooms**: one transcript for each reason a proposal, a commit or a group's
|
||
creation raises, cut at the step it refuses.
|
||
- **Negatives**: one control for each reason of §13 an object, a transcript or a record
|
||
raises.
|
||
|
||
A conforming implementation gives every vector its stated outcome.
|
||
`joule_moq_e2ee_vectors verify` and the crate's tests check that the file regenerates byte for
|
||
byte, that the reference gives every vector its outcome, and that §13 lists exactly the
|
||
reference's codes. `live-conformance/e2ee_verify.py` is the second implementation, written
|
||
from this text and the documents it reads, standard library only: AES (FIPS 197) and GCM (SP
|
||
800-38D) written from their specifications for vectors only, HKDF and HMAC from Python's
|
||
standard library, BLAKE3 from `claim-conformance/_blake3.py`, and Ed25519 under WAI's strict
|
||
rule from `claim-conformance/_ed25519.py`. It requires every mutated copy of the corpus to
|
||
fail.
|
||
|
||
**The verifier's boundary.** It does not implement MLS. It takes each epoch's application
|
||
secret (§5.7) from the corpus as given and checks everything after it: the roster of each
|
||
epoch from the commits (§5.6), the committer of each commit and what it holds (§5.5), the
|
||
proposals against the room's lifecycle (§5.4), each track base key and KID, each key, salt,
|
||
nonce and AAD, every seal and open, and every refusal. That the application secrets are what
|
||
an MLS group of the room exports is checked by the reference's tests, which run the group.
|
||
|
||
## 15. A member in a browser
|
||
|
||
A browser page is a member as any other: the reference's member (`joule-moq-mls`), the
|
||
sealed objects of `joule-moq` and the codec, compiled for WebAssembly as one module that the
|
||
page loads for an end-to-end encrypted room alone (`joule-moq-web`, feature `e2ee`). Its MLS
|
||
state and every key it derives are in the page's memory and nowhere else: nothing of them is
|
||
written to the browser's storage or sent anywhere, and a page that is closed is a principal
|
||
whose connection is lost (§5.4).
|
||
|
||
### 15.1 Sealing frames
|
||
|
||
The page encodes its camera and microphone with W3C WebCodecs. Each encoded chunk, packed as
|
||
the room's original payload (§4.4), is sealed under the page's latest epoch (§4.9), and its
|
||
receipt is signed over the sealed bytes (§8.1) before it is published; until the page holds an
|
||
epoch, nothing is published. Each object the page receives is verified against its receipt,
|
||
then opened by §6, and its plaintext reaches a decoder only once it opened (§10). An object
|
||
whose KID names an epoch the page has not reached, or that arrives before the page's Welcome,
|
||
is held for a bounded time and opened when the page reaches that epoch (§5.8); an object that
|
||
does not open is counted with its reason and dropped. The page deletes an epoch's keys a fixed
|
||
time after the next epoch opens (§5.8).
|
||
|
||
### 15.2 A room service as the carrier
|
||
|
||
A room service that passes the group's messages directly (§5.1) keeps one log per room, in
|
||
which the clock's lifecycle records, as the clock signed them, its proposals, and every
|
||
GroupInfo, commit and Welcome a member posts stand in one order. The service's clock checks
|
||
each post as a member checks it (§5.4, §5.5), and a post it refuses is not carried. A member
|
||
reached through such a log:
|
||
|
||
- reads it from its first entry, and checks each lifecycle record as the clock's: its
|
||
signature, its room, and its place in the clock's chain;
|
||
- before its Welcome, follows the group from its GroupInfo and its handshake messages as the
|
||
clock does (§5.3), so that it knows whether a group exists and who holds which leaf, and
|
||
creates the group only when §5.4 owes it one;
|
||
- leaves its KeyPackage with the service before it publishes, so the clock's Add for it is
|
||
ready when the clock records its `join`;
|
||
- **MUST** apply a GroupInfo or a commit it made only when the log returns it in the log's
|
||
order, and **MUST** drop it when the service refuses it or the log carries another commit
|
||
for the same epoch first.
|
||
|
||
The last rule is what keeps one epoch one epoch. §5.5 names the committer from the commit's own
|
||
proposals, so two members whose views of the pending proposals differ can each be the
|
||
committer of the commit it makes: a member that has read the clock's Add and not yet a Remove
|
||
of itself, and the next member, which has read both. The log orders the two, the clock carries
|
||
the first and refuses the second, and every member applies the first.
|
||
|
||
The log adds no trust. Every message on it is signed by its sender and checked by every
|
||
member as §5 requires, and every lifecycle record by the clock's key; the service can withhold
|
||
or delay a message, as a relay can (§12), and cannot forge or change one.
|
||
|
||
### 15.3 What a browser member does not do in this revision
|
||
|
||
- **Watch without being a member.** A grant that may subscribe is keyed (§2), so its principal
|
||
is a member once present. The reference room service records presence from a publisher's
|
||
tracks, so a viewer it grants is never a member and opens nothing; its page offers no
|
||
watching in such a room.
|
||
- **Ingest by WHIP or WebRTC** ([live-ingest](live-ingest.md)) into such a room. A WebRTC
|
||
sender's frames would be sealed through the W3C encoded-transform interface, which is not
|
||
built.
|
||
- **Seek back.** The relay keeps nothing of such a room (§9).
|
||
|
||
The reference checks this section in `joule-moq-mls`'s tests (members behind a log that orders
|
||
their messages: a create and a commit applied only when the log returns them, two commits for
|
||
one epoch, a commit overtaken by another, a create the log refuses), `joule-moq-room`'s tests
|
||
(members keying through the service on a relay), `joule-moq-web`'s tests (its WebAssembly
|
||
surface on the host), and the `e2ee` scenario of `joule-moq-web/e2e/` in a browser.
|