confidentiality §9 (merged with L10) says a relay keeps nothing of an end-to-end encrypted room's tracks beyond delivery; the DVR store kept every track its prefix rules matched. The store now knows such a room by its first object carrying a secure-objects Key ID (or properties that do not read), never writes that group, deletes what it held of the room (its events and receipts tracks are in the clear and can be whole first), drops the room's decisions from decisions.wdl, writes nothing more of it, and records it in sealed.wdl so a restart keeps refusing it. Moderation still honours the room's decisions for what the cache serves. live-recording §8.2 states the rule; confidentiality §9 points to it. The joule-code changelog's stale "a file per group" now describes the segments, and the cache's media/receipts pairing is documented. Co-Authored-By: Claude Opus 5.5 <[email protected]>
786 lines
46 KiB
Markdown
786 lines
46 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. 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).
|
||
|
||
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).
|
||
- **The browser.** A browser member needs MLS in WebAssembly and the sealing of encoded
|
||
frames as the W3C encoded-transform interface does it; that is a later slice, and nothing
|
||
here depends on it.
|
||
- **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.
|