Some checks failed
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 4s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 8s
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
The corpus is regenerated from its seeds since history entries name whether the principal was admitted. The room test now has a removed member post a message it sealed before its removal: the service refuses it (not_a_member) and a verifier refuses it (sender_not_member). The crate README names the messages and call pages and their e2e scenarios. Clippy notes in the slice's files are resolved. Co-Authored-By: Claude Opus 5.5 <[email protected]>
835 lines
48 KiB
Markdown
835 lines
48 KiB
Markdown
# WAI Extension: Live Messaging (conversations and calls on live rooms)
|
||
|
||
> Status: Draft. Registers two records. `wai.live.message`: a **conversation** is a room of
|
||
> [live-rooms](live-rooms.md) whose authority signs a conversation statement, `WMC1`, and
|
||
> which is end-to-end encrypted ([confidentiality](confidentiality.md)). Each member publishes
|
||
> its messages on a **messages track** of its own: each message one object, sealed under the
|
||
> room's MLS group and signed into the member's receipt chain as a room's media track is. A
|
||
> message is a signed record, `WMG1`, of one of five kinds: text, a reply in a thread, a
|
||
> reaction, an edit and a retraction, the last four naming the message they concern by its
|
||
> link hash. Membership is the room's: the clock records who the authority admits and
|
||
> removes, its proposals order every change, the members commit them, and the clock signs an
|
||
> **epoch statement**, `WME1`, naming each epoch's roster. Delivery and reading are signed
|
||
> acknowledgements, `WMA1`. A **history store** keeps the conversation's log, ciphertext,
|
||
> receipts and signed records only, so that a member that was offline catches up, and serves
|
||
> each principal only what it is owed. `wai.live.call`: a **call** is a room with a call
|
||
> descriptor, `WCT1`, signed by its authority; ringing is an invitation, `WCI1`, signed by the
|
||
> caller and held in the callee's inbox at the room service; accepting, declining, cancelling
|
||
> and a missed call are signed answers, `WCA1`; a call between two principals and a call among
|
||
> many are the same mechanism, and its media is the room's own audio and video tracks. Every
|
||
> record verifies offline. No new transport and no new receipt. The reference implementation
|
||
> is the `live_messaging` module of `wai-rs` (feature `live_messaging`); the corpus is
|
||
> [`live-conformance/messaging/`](../live-conformance/messaging/), and its second
|
||
> implementation is `live-conformance/live_messaging_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: the live family this extension builds on, as merged
|
||
> ([live-rooms](live-rooms.md), [live-events](live-events.md),
|
||
> [confidentiality](confidentiality.md), [live-recording](live-recording.md),
|
||
> [live-moderation](live-moderation.md)), [jwp-receipts](jwp-receipts.md) and
|
||
> [moq-streaming-format](moq-streaming-format.md); RFC 9420 (MLS) and RFC 9750 (the MLS
|
||
> architecture); `draft-ietf-moq-transport-22` (MOQT), an Internet-Draft, a section number of
|
||
> which in this text is that revision's; RFC 8032, RFC 3629 and FIPS 180-4.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the conversation statement, and how a conversation's lifecycle records membership
|
||
(§3); the messages track, its objects, their sealing and their receipts (§4); the message
|
||
record and its kinds (§5); threads, reactions, edits and retractions (§6); membership,
|
||
commits and the epoch statement (§7); acknowledgements (§8); the history store: what it holds,
|
||
what it takes, what each principal is owed (§9); calls: the call descriptor, the invitation,
|
||
the answers, the inbox and the media (§10); verification of a conversation (§11).
|
||
|
||
Out of scope: a transport; electronic mail, calendars and address books; interworking with
|
||
telephone networks; push notification services; how a principal obtains a grant; and how a
|
||
member's device keeps its group state between sessions (§7.5 states what it needs).
|
||
|
||
Relationship to the core spec: `wai.live.message` and `wai.live.call` are **records**
|
||
([SPEC](../SPEC.md) §5): signed records a WAI tool emits and verifies. They name no payload
|
||
format and are not dispatchable.
|
||
|
||
Relationship to [confidentiality](confidentiality.md): a conversation is an end-to-end
|
||
encrypted room, and everything that extension fixes holds in it, but for two rules: a
|
||
conversation's lifecycle records membership rather than presence (§3.4), and any member may
|
||
commit, the log ordering the commits (§7.2). This extension adds the messages track to the
|
||
tracks confidentiality §1 seals. A message is not a live-events record: the events tracks of
|
||
a conversation stay in the clear as confidentiality §1 leaves them, and carry the clock's
|
||
lifecycle alone.
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**, **room id**, **room authority**, **clock**, **principal**, **grant**: as in
|
||
live-rooms §2.
|
||
- **Member**, **epoch**, **leaf index**, **roster**, **KID**: as in confidentiality §2 and
|
||
§5.7. An epoch is named by its **generation** and its number (confidentiality §5.1).
|
||
- **Conversation**: a room whose authority signs a conversation statement (§3) and a
|
||
confidentiality record (confidentiality §3).
|
||
- **Messages track**: a member's track of messages (§4.1). Its owner is the message's
|
||
**author**.
|
||
- **Message**: one object of a messages track. Its plaintext is a message record (§5).
|
||
- **Link hash** of a message: the link hash of its group receipt (jwp-receipts §3 step 3),
|
||
`BLAKE3("moq-jwp:receipt-link\x01" ‖ canonical_bytes(G))`. It names the message in the
|
||
clear, and every reference (§6) names a message by it.
|
||
- **Log**: the history store's sequence of the conversation's entries, each at a
|
||
**position** from 0 (§9.1).
|
||
- **Latest epoch** at a position: the epoch of the last epoch statement before that position
|
||
in the log.
|
||
- **Call**, **caller**, **invitee**: a room with a call descriptor (§10.1), the principal it
|
||
names as placing the call, each principal it names as invited.
|
||
- **Inbox**: the invitations and answers a room service holds for one principal (§10.4).
|
||
|
||
Integers are little-endian, as in the records of live-rooms. A *long* field is a u16 length
|
||
and that many bytes. Every signature is Ed25519 (RFC 8032), 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`.
|
||
|
||
Every record of this extension is read the same way: a reader reads exactly the bytes it is
|
||
given as one record, in the order of its layout, and refuses at the first of these that
|
||
holds: fewer than 4 bytes, `truncated`; a magic other than the record's, `malformed`; no byte
|
||
after the magic, `truncated`; a version other than 1, `unknown_version`; the bytes end before
|
||
a field the layout requires, `truncated`; bytes follow `sig`, `malformed`. The **digest** of a
|
||
record is SHA-256 (FIPS 180-4) over its bytes from `magic` up to `sig`, and its signature is
|
||
over its domain followed by those same bytes.
|
||
|
||
## 3. The conversation statement
|
||
|
||
### 3.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WMC1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id (live-rooms §3.2) |
|
||
| `max_text` | 2 | u16: the most bytes of a text body (§5.3), from 1 to 16 384 |
|
||
| `max_reaction` | 1 | u8: the most bytes of a reaction body (§5.3), from 1 to 64 |
|
||
| `retention_days` | 2 | u16: the most days the history store keeps a message (§9.4); 0 states no bound |
|
||
| `sig` | 64 | §3.2 |
|
||
|
||
### 3.2 Signature
|
||
|
||
Ed25519 by the room descriptor's `authority` over the 22 bytes `wai:live-conversation`
|
||
(ASCII) and 0x01, followed by the bytes the digest covers.
|
||
|
||
### 3.3 Reading and rules
|
||
|
||
A verifier is given the room's descriptor, its confidentiality record (or none) and the
|
||
statement. It reads the descriptor (live-rooms §3.4: its reason, at `descriptor`), then, at
|
||
`record`: none given is `not_encrypted`; the record is checked as confidentiality §3.3 checks
|
||
it, with its reason. Then, at `statement`, in this order:
|
||
|
||
1. The statement reads (§2): `truncated`, `malformed` or `unknown_version`.
|
||
2. `max_text` is from 1 to 16 384 and `max_reaction` from 1 to 64: otherwise
|
||
`conversation_invalid`.
|
||
3. `room` is the room id: `wrong_room`.
|
||
4. The signature verifies under the descriptor's `authority` (§3.2): `bad_key` or
|
||
`bad_signature`.
|
||
|
||
A room has at most one conversation statement: a conversation that needs other bounds is
|
||
another room. A conversation is never recorded: its descriptor's recording `mode` is 0, which
|
||
confidentiality §3.3 step 5 already requires.
|
||
|
||
### 3.4 Membership is the room's lifecycle
|
||
|
||
In a conversation the room's clock records **membership**, not presence:
|
||
|
||
- **`join`**: when the authority admits a principal to the conversation, naming the grant it
|
||
admits it under (live-rooms §7.2). Every grant of a conversation may subscribe, so it is
|
||
keyed (confidentiality §2), and its principal is to be a member.
|
||
- **`leave`**: reason 1 when the member leaves the conversation, reason 2 when the authority
|
||
removes it. A member whose device is offline, or whose connection is lost, stays a member:
|
||
the clock records no `leave` for it, and reason 4 is not used. A grant's end is not a
|
||
`leave` either: a member whose grant ends asks the authority for another, and its
|
||
membership goes on.
|
||
|
||
The group follows the lifecycle as confidentiality §5.4 has it: the clock proposes the Add of
|
||
each principal it records joining and the Remove of each it records leaving. A grant of a
|
||
conversation authorises its principal's requests (to the history store, §9; to a relay,
|
||
live-rooms §10): it is not what makes it a member.
|
||
|
||
## 4. The messages track
|
||
|
||
### 4.1 Naming
|
||
|
||
A member `P` of conversation `R` publishes one messages track. With `R` and `P` as 64
|
||
lowercase hexadecimal digits, its Track Namespace is `[R, P, "messages"]` and its Track Name
|
||
is `0`; its receipts track (moq-streaming-format §4.1) is in the same namespace, named
|
||
`0_receipts`. The names keep live-rooms §6.1's rules. A messages track is not a media track:
|
||
it has no layer, profile or slot, and it is never composed. A grant of a conversation maps
|
||
onto one scope beyond live-rooms §10.2's, after its step 4: `[[2, 6], [R, P, "messages",
|
||
nil]]` (`PUBLISH_NAMESPACE` and `PUBLISH` of its messages track and that track's receipts
|
||
track); a relay that honours a room's grants refuses a PUBLISH under `[R, P, "messages"]` from
|
||
any session but one whose SETUP proved a grant for `P` in `R`.
|
||
|
||
A room service **MAY** carry a conversation's messages tracks itself, as confidentiality §15.2
|
||
lets it carry the group's messages: an object it carries keeps its Full Track Name, its Group
|
||
ID, its Object ID and its Publisher Priority, which its sealing binds (confidentiality §4.7),
|
||
and its receipts-track object, so nothing below changes with the carrier.
|
||
|
||
### 4.2 Objects
|
||
|
||
Each group of a messages track holds exactly one object, Object ID 0, of Publisher Priority
|
||
64: one message. The groups are numbered from 0 with no gap: the author's *n*th message is
|
||
group *n* − 1, whatever epoch it is sealed in.
|
||
|
||
Every object is sealed as confidentiality §4 seals the object of a media track, under the
|
||
author's keys of its current epoch (confidentiality §5.7), and opened as confidentiality §6
|
||
opens one. Its Immutable Properties hold the Key ID property alone; it carries no envelope
|
||
flag and no other property. The original payload (confidentiality §4.4) is the message record
|
||
(§5). Its plaintext **MAY** end with an Encrypted Properties List holding padding, which hides
|
||
a message's exact length from the store and every relay, and **MAY** carry
|
||
`WAI_INNER_DIGEST`; a receiver reads the list as confidentiality §4.4 reads it. A message is
|
||
sealed only in the latest epoch the author holds; one whose epoch has since ended is sealed
|
||
again in the new one before it is posted (§9.2).
|
||
|
||
### 4.3 Receipts
|
||
|
||
A messages track carries receipts exactly as a room's media track does
|
||
(moq-streaming-format §4.1, §5; confidentiality §8.1): each group's object is signed by the
|
||
author's key into a group receipt of one object ([jwp-receipts](jwp-receipts.md) §2.2, §2.3),
|
||
its content hash taken over the sealed payload, ciphertext and tag, as jwp-receipts §2.1 takes
|
||
it over an envelope; the group's receipts, in the binary form of jwp-receipts §2.7.3, are the
|
||
receipts track's object of the same group. Its receipts' `track_alias` (jwp-receipts §2.4.1)
|
||
is 5, the messages track's receipt alias.
|
||
|
||
The group receipt is in the **labelled** form (jwp-receipts §2.3, `moq-jwp:group-sign\x02`),
|
||
whose signing payload covers the parent link: a message's place in its author's chain is then
|
||
signed, and a reference by link hash (§6) names one message and its whole chain before it. A
|
||
group in the legacy form is refused `receipt_unlabelled`. Its figure is the unmetered marker:
|
||
`joules_total` 0, class tag 0, coverage `Whole`; its object receipt's leaf is labelled
|
||
unmetered (`leaf_version` 2, tag 0), `joules_micro` 0. A reader shows the marker as
|
||
unmetered, never as a figure of zero joules. Each group receipt's parent is the link hash of
|
||
the group receipt before it on the track, and group 0's is none.
|
||
|
||
### 4.4 What a relay and a store see
|
||
|
||
A relay, and the history store, see each message's track, its Group ID, its Key ID (and so the
|
||
epoch and the author's leaf), its size, its receipt and its link hash; they open nothing. The
|
||
plaintext, and with it the kind, the body and every reference, is the members' alone.
|
||
|
||
## 5. The message record
|
||
|
||
### 5.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WMG1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id |
|
||
| `author` | 32 | the author's Ed25519 key: the messages track's owner |
|
||
| `group` | 8 | u64: the Group ID of the object the record is sealed in |
|
||
| `t` | 8 | u64: the room clock (live-events §2), in microseconds, when the author sent it |
|
||
| `kind` | 1 | §5.3 |
|
||
| `ref` | 32 | the link hash of the message it concerns (§6); 32 zero bytes for a text |
|
||
| `body` | long | §5.3 |
|
||
| `sig` | 64 | §5.2 |
|
||
|
||
`room`, `author` and `group` repeat the object's position, so that a record opened from one
|
||
place cannot be shown as another's (`message_mismatch`, §11.3). `t` is the author's claim, as
|
||
live-events §11 has every `t`, and it does not decrease along the author's track
|
||
(`time_reversed`).
|
||
|
||
### 5.2 Signature
|
||
|
||
Ed25519 by `author` over the 17 bytes `wai:live-message` (ASCII) and 0x01, followed by the
|
||
bytes the digest covers. The record is signed inside the seal, so a member that opened it can
|
||
show it to anyone, who verifies it with the author's key alone.
|
||
|
||
### 5.3 Kinds and bodies
|
||
|
||
| `kind` | name | `ref` | `body` |
|
||
|---|---|---|---|
|
||
| 1 | `text` | 32 zero bytes | UTF-8 (RFC 3629), 1 to `max_text` bytes |
|
||
| 2 | `reply` | the thread's root (§6.1) | as `text` |
|
||
| 3 | `reaction` | the message reacted to (§6.2) | UTF-8, 1 to `max_reaction` bytes |
|
||
| 4 | `edit` | the message edited (§6.3) | as `text`: the message's new text |
|
||
| 5 | `retraction` | the message retracted (§6.4) | empty |
|
||
|
||
A kind outside the table is `unknown_kind`. A body that is not UTF-8 is `bad_utf8`; one longer
|
||
than its bound is `text_too_long`; an empty body where the table asks for one, and a body
|
||
beside a retraction, are `body_invalid`. A `text` with a `ref` that is not zero, and any other
|
||
kind with a zero `ref`, are `ref_invalid`.
|
||
|
||
## 6. References
|
||
|
||
A reply, a reaction, an edit and a retraction each name an earlier message of the
|
||
conversation by its link hash (§2), its **target**. The target is a message accepted before
|
||
it in the verification order (§11.3): otherwise `ref_unknown`, which includes a message the
|
||
verifier refused, and one later in the log. A target that a retraction accepted before has
|
||
retracted is `ref_retracted`, for every kind.
|
||
|
||
### 6.1 Threads
|
||
|
||
A reply's target is the **root** of its thread: a `text`. A target of another kind is
|
||
`thread_not_root`, so a thread is one level deep, and every reply of a thread names the same
|
||
root. A thread is its root and its replies, in the verification order.
|
||
|
||
### 6.2 Reactions
|
||
|
||
A reaction's target is a `text` or a `reply`: otherwise `target_kind`. Its body is the
|
||
reaction, a short string the sink shows as it is. A member's second reaction with the same body
|
||
to one target, while its first is not retracted, is `reaction_repeated`. Removing a reaction
|
||
is retracting it (§6.4).
|
||
|
||
### 6.3 Edits
|
||
|
||
An edit's target is a `text` or a `reply` (`target_kind`) whose author is the edit's
|
||
(`edit_not_author`): a member edits only what it wrote. The target's **current text** is the
|
||
body of the last edit accepted for it, or its own body when there is none.
|
||
|
||
A verifier keeps the target and every edit of it, each verified with its own receipt and
|
||
signature, and states the target's current text and its number of edits. The original stays in
|
||
the conversation, verified: an edit changes what a sink shows, and nothing a member holds. A
|
||
sink **MUST** show a message with its current text, and **MUST** show that it was edited.
|
||
|
||
### 6.4 Retractions
|
||
|
||
A retraction's target is a `text`, a `reply` or a `reaction` (`target_kind`) whose author is
|
||
the retraction's (`retract_not_author`).
|
||
|
||
A retraction **states**; it removes nothing from verification. The verifier keeps the target in
|
||
the conversation, verified as it was, with its receipt, its link hash and its place in its
|
||
author's chain, and states it **retracted**, naming the retraction by its link hash. A
|
||
retracted message has no current text: a sink **MUST NOT** show its body, its edits or (for a
|
||
reaction) the reaction, and **MAY** show that a message was retracted. Its link hash still
|
||
chains the author's track, so the messages after it verify, and a reference to it is
|
||
`ref_retracted` (§6). The ciphertext stays in the history store until its retention ends (§9.4):
|
||
a retraction is the author's statement, and a member that opened the message before keeps what
|
||
it opened.
|
||
|
||
## 7. Membership and epochs
|
||
|
||
### 7.1 The group
|
||
|
||
A conversation's group is confidentiality §5's: one MLS group (RFC 9420) at a time, whose
|
||
group ID is the room id and a generation, whose credentials are the principals' room keys
|
||
(confidentiality §5.2), whose one external sender is the room's clock (§5.3 there), which
|
||
proposes every Add and Remove the lifecycle calls for (§5.4 there) and holds no epoch's
|
||
secret; each epoch's per-sender keys are derived by the exporter (§5.7 there). The room
|
||
service carries the group's messages in one order, as confidentiality §15.2 has it, in the
|
||
conversation's log (§9.1).
|
||
|
||
### 7.2 Who commits
|
||
|
||
In a conversation, **any member that a commit does not remove may commit the clock's pending
|
||
proposals**, and the log orders the commits: the first commit for an epoch that the clock's
|
||
checks pass is carried, every other for that epoch is refused, and a member applies a commit
|
||
only when the log returns it (confidentiality §15.2). This replaces confidentiality §5.5's rule
|
||
of the lowest leaf; every other check of §5.5 holds: a commit holds by reference the clock's
|
||
first *k* proposals for the epoch, each once (`proposal_not_clock`, `commit_mismatch`), and
|
||
carries an UpdatePath. A commit by a sender that is not a member of the epoch, or that the
|
||
commit removes, is `commit_not_committer`.
|
||
|
||
Why: confidentiality §5.5's rule heals by removing a committer that stops responding, through a
|
||
`leave` for its lost connection. A conversation records no such `leave` (§3.4), so a lowest
|
||
member that is offline would hold every change back until it returned. Any member committing,
|
||
in the log's order, keeps every change moving while one member is online, and the log makes
|
||
two commits for one epoch impossible to apply both.
|
||
|
||
### 7.3 The epoch statement
|
||
|
||
Each time the log carries a GroupInfo that creates the group, or a commit, the clock signs an
|
||
**epoch statement** for the epoch it opens and the log carries it right after. Members check
|
||
each against the epoch their group reached; an offline verifier, which runs no MLS, takes each
|
||
epoch's roster from it.
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WME1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id |
|
||
| `generation` | 4 | u32: the group's generation (confidentiality §5.1) |
|
||
| `epoch` | 8 | u64: the epoch it opens, below 2^40 |
|
||
| `opened_by` | 32 | SHA-256 of the framed GroupInfo (epoch 0) or the framed commit that opened it, as the log carries them (confidentiality §5.1) |
|
||
| `prev` | 32 | the digest of the conversation's previous epoch statement; 32 zero bytes for the first |
|
||
| `roster` | 4 + 36 × *n* | a u32 count *n*, then each member: its leaf index (u32, below 2^24) and its key (32), in ascending order of leaf index |
|
||
| `sig` | 64 | Ed25519 by the descriptor's `clock` over the 15 bytes `wai:live-epoch` (ASCII) and 0x01, followed by the bytes the digest covers |
|
||
|
||
A verifier takes the conversation's epoch statements in the log's order, and refuses at the
|
||
first of these that fails, at the statement's position:
|
||
|
||
1. It reads (§2): `truncated`, `malformed` or `unknown_version`.
|
||
2. Its `room` is the room id: `wrong_room`.
|
||
3. Its signature verifies under the descriptor's `clock`: `bad_key` or `bad_signature`.
|
||
4. `epoch` is below 2^40; the roster has at least one member, its leaf indexes strictly
|
||
ascending and each below 2^24, and no key twice: otherwise `epoch_invalid`.
|
||
5. `prev` is the digest of the epoch statement before it in the log, or 32 zero bytes for the
|
||
first: `epoch_unlinked`.
|
||
6. The first is epoch 0 of any generation; each later is the next epoch of the same
|
||
generation, or epoch 0 of a higher one: `epoch_out_of_order`.
|
||
|
||
A member **MUST** refuse an epoch statement whose generation, epoch or roster is not the one
|
||
its group reached when it applied the GroupInfo or commit the statement names, and then stop
|
||
reading the log: the log is not the conversation's. The clock follows the group from its
|
||
public handshake messages and checks every commit as a member does (confidentiality §5.3,
|
||
§15.2), so the roster it states is the one every member computes (confidentiality §5.6).
|
||
|
||
### 7.4 Removal
|
||
|
||
A principal is **removed** at the first epoch statement after one that lists it, whose roster
|
||
does not list it. The epoch it opens is the first whose keys exclude it (confidentiality §12),
|
||
and from its position on the history store takes nothing of the principal's (§9.2) and serves
|
||
it nothing more (§9.3).
|
||
|
||
### 7.5 A member that was offline
|
||
|
||
A member keeps its group state while its device is offline, and when it returns reads the log
|
||
from the position it stopped at: every handshake message in order, each commit applied as it
|
||
comes, each epoch statement checked against the epoch it reaches, and each message opened with
|
||
the keys of the epoch its KID names. Since the store takes a message only in the latest epoch
|
||
(§9.2), every message of an epoch stands in the log before the epoch statement of the next, and
|
||
a member deletes an epoch's keys once it has read that statement (confidentiality §5.8). How a
|
||
device keeps its group state between sessions is the device's: a member that has lost it is a
|
||
principal whose keys are gone, and it opens nothing it held before, as confidentiality §12 says
|
||
of any member.
|
||
|
||
## 8. Acknowledgements
|
||
|
||
A member acknowledges that it holds, or has shown, every message of one author's track up to a
|
||
group, with a record it signs in the clear. An acknowledgement names messages by their link
|
||
hash and position alone, never their content.
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WMA1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id |
|
||
| `member` | 32 | the acknowledging member's key |
|
||
| `author` | 32 | the author of the track it acknowledges |
|
||
| `through` | 8 | u64: the last group of `author`'s track it acknowledges |
|
||
| `link` | 32 | the link hash of `author`'s message at `through` |
|
||
| `kind` | 1 | 1 **delivered**: the member holds every message of the track up to `through`, opened; 2 **read**: it has shown them |
|
||
| `t` | 8 | u64: the room clock, in microseconds, when it was signed |
|
||
| `sig` | 64 | Ed25519 by `member` over the 21 bytes `wai:live-message-ack` (ASCII) and 0x01, followed by the bytes the digest covers |
|
||
|
||
An acknowledgement is cumulative: it covers every group of the track up to `through`. A
|
||
verifier checks each, in the log's order, and refuses one at the first of these that fails:
|
||
|
||
1. It reads (§2): `truncated`, `malformed` or `unknown_version`.
|
||
2. Its `room` is the room id: `wrong_room`.
|
||
3. Its signature verifies under `member`: `bad_key` or `bad_signature`.
|
||
4. `kind` is 1 or 2: `ack_invalid`.
|
||
5. `member` is in the roster of the latest epoch at its position: `ack_not_member`.
|
||
6. `author`'s track holds, earlier in the log, a message at group `through` that passed
|
||
§11.3 step 6, whose link hash is `link`: `ack_unknown`. A verifier without the epoch's secret
|
||
checks an acknowledgement as one with it does.
|
||
7. `through` is at least that of the last acknowledgement of the same `member`, `author` and
|
||
`kind` the verifier accepted: `ack_regressed`.
|
||
|
||
A refused acknowledgement changes nothing. For each author and each member, the verifier states
|
||
the last group acknowledged delivered and the last acknowledged read. A read implies delivery:
|
||
a member that has shown a message holds it.
|
||
|
||
## 9. The history store
|
||
|
||
### 9.1 What it holds
|
||
|
||
A history store keeps one **log** per conversation: one sequence of entries, each at a position
|
||
from 0, in the order the store took them:
|
||
|
||
- the clock's lifecycle records, as it signed them (live-events §3);
|
||
- the group's handshake messages, framed (confidentiality §5.1): the clock's proposals, each
|
||
GroupInfo, commit and Welcome a member posts;
|
||
- the clock's epoch statements (§7.3);
|
||
- messages: for each, its author, its Group ID, its property list as carried, its sealed
|
||
payload and its receipts-track object (§4);
|
||
- acknowledgements (§8).
|
||
|
||
It holds **no plaintext and no key**: nothing in it opens without an epoch's secret, which only
|
||
members derive (confidentiality §5.7), and a Welcome is encrypted to the member it adds. What it
|
||
holds of each message is what a relay forwards (§4.4). A store **MUST NOT** take a message as
|
||
anything else, nor keep a plaintext a member sends it.
|
||
|
||
### 9.2 What it takes
|
||
|
||
A store takes an entry only after the checks a verifier of the conversation makes that need no
|
||
key, against the log as it stands; a post that fails one is refused with its reason and not
|
||
taken:
|
||
|
||
- a handshake message: confidentiality §15.2's checks, with §7.2's rule of who commits;
|
||
- a message, posted by a request its author authorised (live-rooms §10.5.3), in this order:
|
||
the author is in the roster of the latest epoch (`sender_not_member`; `epoch_unknown` before
|
||
any epoch); its Key ID reads (confidentiality §6 step 1); its KID names the latest epoch
|
||
(`epoch_stale`), at a leaf the roster gives the author (`key_id_unknown`,
|
||
`sender_not_owner`); its Group ID is the author's next (`message_gap`); its receipts verify
|
||
as §11.3 step 6 has them;
|
||
- an acknowledgement, posted by a request its member authorised: §8's checks.
|
||
|
||
The store answers a post only once the entry is on stable storage: an entry it acknowledged
|
||
taking survives the store's restart. It keeps every entry at its position; a restart neither
|
||
reorders nor renumbers the log.
|
||
|
||
### 9.3 What a principal is owed
|
||
|
||
The store serves a principal `P` the log from a position it names, authorised by a grant of the
|
||
room for `P` and its proof of possession over a challenge the store issued (live-rooms §10.5.3).
|
||
`P` is **owed**:
|
||
|
||
- every **membership entry** (lifecycle record, handshake message, epoch statement) from the
|
||
conversation's start up to, and including, the epoch statement that removes it (§7.4), once
|
||
the clock has recorded its `join`. A principal admitted and not yet a member reads them to
|
||
follow the group from outside until its Welcome (confidentiality §15.2);
|
||
- every **content entry** (message, acknowledgement) at a position whose latest epoch lists
|
||
`P` in its roster: what was sealed and acknowledged while it was a member, and nothing
|
||
before or after.
|
||
|
||
`P` is **admitted** while the clock's last lifecycle record of it is a `join`. The store
|
||
answers with the entries owed from that position, each with its position, and with the
|
||
positions it withheld. It refuses the request `not_a_member` when `P` is not admitted and is
|
||
owed no content entry at or after that position: a principal never admitted, and a removed
|
||
member asking past its removal. An admitted principal with nothing new, a member's or one
|
||
waiting for its Welcome, is answered with nothing, not refused.
|
||
|
||
This is how a member proves it is owed a range: its key, by the proof; its membership in each
|
||
epoch, by the clock's epoch statements the store holds, which list it. A store **MUST NOT**
|
||
serve a content entry to a principal the rule does not owe it, whatever grant it presents: the
|
||
ciphertext would open nothing for it, but its sizes, timing and authors are the members'
|
||
metadata.
|
||
|
||
### 9.4 Retention
|
||
|
||
A store keeps every entry while the conversation lasts, but that it **MAY** drop a message
|
||
older than the statement's `retention_days`, counted from when the store took it, as a whole
|
||
entry: its position stays, empty, and a reader is told the position was dropped. It never drops
|
||
a lifecycle record, a handshake message, an epoch statement or a message's receipts while the
|
||
conversation lasts: a member's chain of link hashes, and every reference into it, stays
|
||
checkable after a message's ciphertext is gone.
|
||
|
||
## 10. Calls
|
||
|
||
A **call** is a room of live-rooms with a call descriptor. Its media is the room's own: each
|
||
participant that enters publishes its audio and video tracks (live-rooms §6.1) under a grant,
|
||
and the clock records its `join` and `publish` as in any room. A call between two principals
|
||
and a call among many differ only in how many invitees the descriptor names.
|
||
|
||
### 10.1 The call descriptor
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WCT1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the call's room id |
|
||
| `caller` | 32 | the caller's key |
|
||
| `media` | 1 | bit 0 audio, bit 1 video: what the call carries |
|
||
| `ring_ms` | 4 | u32: how long an invitation rings, in milliseconds, from 1 000 to 300 000 |
|
||
| `invitees` | 1 + 32 × *n* | a u8 count *n* from 1 to 32, then each invitee's key |
|
||
| `sig` | 64 | Ed25519 by the descriptor's `authority` over the 14 bytes `wai:live-call` (ASCII) and 0x01, followed by the bytes the digest covers |
|
||
|
||
A verifier reads the room's descriptor (live-rooms §3.4, at `descriptor`), then the call
|
||
descriptor, at `call`, in this order: it reads (§2); `media` is 1, 2 or 3, `ring_ms` within
|
||
its bounds, the invitees 1 to 32, no two the same and none the caller, otherwise
|
||
`call_invalid`; `room` is the room id, `wrong_room`; its signature verifies, `bad_key` or
|
||
`bad_signature`.
|
||
|
||
### 10.2 The invitation
|
||
|
||
The caller rings each invitee with an invitation it signs.
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WCI1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the call's room id |
|
||
| `call` | 32 | the call descriptor's digest |
|
||
| `caller` | 32 | the caller's key |
|
||
| `callee` | 32 | the invitee rung |
|
||
| `t` | 8 | u64: the room clock, in microseconds, from which it rings |
|
||
| `sig` | 64 | Ed25519 by `caller` over the 21 bytes `wai:live-call-invite` (ASCII) and 0x01, followed by the bytes the digest covers |
|
||
|
||
It rings until `t` + `ring_ms` × 1 000, its **deadline**. A verifier takes the invitations in
|
||
order and refuses at the first of these that fails, at the invitation's index: it reads;
|
||
`room`, `call` and `caller` are the room id, the descriptor's digest and its caller
|
||
(`invitation_mismatch`); its signature verifies under `caller` (`bad_key`, `bad_signature`);
|
||
`callee` is an invitee (`callee_not_invited`); no earlier invitation rang it
|
||
(`already_invited`).
|
||
|
||
A callee's device **MUST** verify the invitation and the call descriptor it names, and their
|
||
signatures, before it rings: an invitation that does not verify is not shown.
|
||
|
||
### 10.3 Answers
|
||
|
||
Every invitation ends with one signed answer:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WCA1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the call's room id |
|
||
| `invitation` | 32 | the invitation's digest |
|
||
| `signer` | 32 | the key that signs it |
|
||
| `kind` | 1 | 1 **accept**, 2 **decline**, 3 **cancel**, 4 **missed** |
|
||
| `t` | 8 | u64: the room clock, in microseconds |
|
||
| `sig` | 64 | Ed25519 by `signer` over the 21 bytes `wai:live-call-answer` (ASCII) and 0x01, followed by the bytes the digest covers |
|
||
|
||
An accept or a decline is the callee's, a cancel the caller's, and a missed call the room
|
||
clock's: the clock signs `missed` for an invitation unanswered at its deadline, so a missed
|
||
call is recorded rather than inferred from silence. A verifier takes the answers in order and
|
||
refuses at the first of these that fails, at the answer's index:
|
||
|
||
1. It reads (§2); its `room` is the room id (`wrong_room`).
|
||
2. `invitation` is the digest of an invitation it holds: `answer_unknown`.
|
||
3. `kind` is 1 to 4: `answer_invalid`.
|
||
4. `signer` is the one its kind names: the invitation's `callee` for 1 and 2, its `caller` for
|
||
3, the descriptor's `clock` for 4: `answer_wrong_signer`.
|
||
5. Its signature verifies under `signer`: `bad_key` or `bad_signature`.
|
||
6. No earlier answer ended that invitation: `already_answered`.
|
||
7. `t` is at least the invitation's `t`: `answer_early`. An accept, a decline or a cancel is
|
||
before the deadline (`answer_late`); a missed call is at or after it (`missed_early`).
|
||
|
||
Each invitation's state is then `ringing` (no answer yet), `accepted`, `declined`,
|
||
`cancelled` or `missed`, with the answer's `t`.
|
||
|
||
### 10.4 The inbox
|
||
|
||
A room service that keeps calls holds, for each principal, an **inbox**: every invitation that
|
||
names it as callee, the call descriptor each names, and every answer to them, the clock's
|
||
`missed` among them. It serves a principal its inbox on a proof of possession of its key over a
|
||
challenge the service issued (live-rooms §10.5.3), in which the proof's `grant` is 32 zero
|
||
bytes: a callee holds no grant of a call it has not answered. The service takes an invitation
|
||
into an inbox only when §10.2's checks pass and the caller's request presented the caller's
|
||
grant and proof; it takes an answer only when §10.3's pass and the request proved the signer's
|
||
key. Nothing in an inbox is the service's statement but the `missed` answers: every other
|
||
record in it is signed by the principal it names.
|
||
|
||
### 10.5 Media
|
||
|
||
A principal enters a call's room as a publisher under a grant (live-rooms §4), which the
|
||
authority issues to the caller, and to an invitee only once it holds the invitee's accept. Its
|
||
audio and video tracks are the room's (live-rooms §6.1), and in an end-to-end encrypted call
|
||
are sealed as confidentiality has it. A call ends as a room does: when its last participant
|
||
leaves.
|
||
|
||
## 11. Verifying a conversation
|
||
|
||
### 11.1 Inputs
|
||
|
||
A verifier is given the room's descriptor, its confidentiality record, the conversation
|
||
statement, and the log's epoch statements, messages and acknowledgements in the log's order,
|
||
each with its position. It is not given the lifecycle records or the handshake messages: the
|
||
epoch statements carry what it needs of them (§7.3). To open the messages it is given, for each
|
||
epoch whose messages it opens, that epoch's application secret (confidentiality §5.7); a member
|
||
holds those of its own epochs. A message of an epoch whose secret it is not given is checked
|
||
through step 6 below and stated `sealed`.
|
||
|
||
### 11.2 The statement and the epochs
|
||
|
||
It checks the descriptor, the record and the statement (§3.3), and every epoch statement
|
||
(§7.3) at its place in the log, and refuses the conversation at the first failure, naming
|
||
where: `descriptor`, `record`, `statement`, or the epoch statement's position.
|
||
|
||
### 11.3 Each message
|
||
|
||
Each message, in the log's order, is **accepted** or **refused** with a reason; a refusal does
|
||
not stop the verification, and a refused message is not a target (§6). It runs these steps and
|
||
stops at the first that fails:
|
||
|
||
1. An epoch statement precedes it: `epoch_unknown`.
|
||
2. The author is in the roster of the latest epoch: `sender_not_member`. A removed member's
|
||
message after its removal fails here.
|
||
3. The property list reads and holds one Key ID inside the Immutable Properties
|
||
(confidentiality §6 step 1): `properties_malformed`, `key_id_missing`.
|
||
4. The KID's epoch (confidentiality §5.7) is the latest epoch's number: `epoch_stale`. Its leaf
|
||
index is in that epoch's roster (`key_id_unknown`), and the member there is the author
|
||
(`sender_not_owner`).
|
||
5. Its Group ID is the group after the author's last message that passed this step and step
|
||
6, or 0 for its first: `message_gap`. A refused post takes no group (§9.2), so its author
|
||
posts that group again.
|
||
6. Its receipts-track object reads, holds one group receipt and one object receipt of that
|
||
group (`malformed`), in the labelled form (`receipt_unlabelled`), signed by the author's
|
||
key, with `track_alias` 5, both naming the Group ID, the object receipt Object ID 0
|
||
(`moqt_mismatch`); and it verifies by jwp-receipts §3 against the sealed payload, with the
|
||
reason that section names: the group's signature, the content hash, the object's proof, the
|
||
group's total, and the chain, the group receipt's parent the link hash of the group receipt
|
||
of the author's last message that passed this step, and group 0's none. The verifier keeps
|
||
that receipt alone (jwp-receipts §3 step 3): a parent where none is held, and a parent that
|
||
is not its link hash, are `parent_not_held`; none where one is held is `chain_break`.
|
||
7. With the epoch's secret, it derives the author's track base key for the namespace
|
||
`[R, author, "messages"]` (confidentiality §5.7) and the key and salt of the object, Track
|
||
Name `0`, under the KID (§4.5 there), and opens the sealed payload with the nonce and AAD of
|
||
Object ID 0 at priority 64 (§4.6, §4.7, §4.10 there): `bad_tag`. The plaintext reads
|
||
(§4.4 there): `plaintext_malformed`; where it carries `WAI_INNER_DIGEST`, the digest is the
|
||
original payload's (§6 step 11 there): `inner_digest_mismatch`. Without the secret, the
|
||
message is `sealed`, and steps 8 to 13 are not run.
|
||
8. The original payload reads as a message record (§2: `truncated`, `malformed`,
|
||
`unknown_version`) and its signature verifies under `author` (`bad_key`, `bad_signature`).
|
||
9. Its `room`, `author` and `group` are the room id, the track's owner and the Group ID:
|
||
`message_mismatch`.
|
||
10. Its `t` is at least that of the author's last message that passed this step:
|
||
`time_reversed`.
|
||
11. Its kind and body keep §5.3: `unknown_kind`, `bad_utf8`, `text_too_long`, `body_invalid`,
|
||
`ref_invalid`.
|
||
12. Its target keeps §6 for its kind: `ref_unknown`, `ref_retracted`, `thread_not_root`,
|
||
`target_kind`, `reaction_repeated`, `edit_not_author`, `retract_not_author`.
|
||
13. It is accepted, and applied: a text is a message of the conversation; a reply joins its
|
||
root's thread; a reaction is added to its target's; an edit sets its target's current
|
||
text; a retraction marks its target retracted, and a retracted reaction leaves its
|
||
target's reactions.
|
||
|
||
A message refused at steps 1 to 6 does not advance its author's track: the next is checked
|
||
against the last message that passed step 6, its group and its group receipt. One refused at
|
||
steps 7 to 12 has a receipt that verified, and its group and link hash chain the track as any
|
||
other.
|
||
|
||
### 11.4 Each acknowledgement
|
||
|
||
Each acknowledgement, in the log's order, by §8.
|
||
|
||
### 11.5 The outcome
|
||
|
||
A verification that refuses states the reason and where (§11.2). One that does not states:
|
||
|
||
- every epoch: its generation, its number and its roster;
|
||
- every message, at its position: its author, its Group ID, its link hash where its
|
||
receipts-track object reads, and its outcome:
|
||
accepted, `sealed`, or the reason it was refused; for one accepted, its kind, its `t` and
|
||
its target, and, for a text or a reply, its status (`shown`, `edited` or `retracted`), its
|
||
current text (none once retracted), its number of edits, its reactions (each member and
|
||
body, in the order accepted, retracted ones left out), the retraction that retracted it, and
|
||
for a root the replies of its thread; for an accepted reaction, whether it was retracted;
|
||
- every acknowledgement, at its position: accepted, or its reason;
|
||
- for each author, for each member, the last group acknowledged delivered and read.
|
||
|
||
## 12. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `conversation_invalid` | §3.3 step 2 |
|
||
| `not_encrypted` | §3.3 |
|
||
| `epoch_invalid` | §7.3 step 4 |
|
||
| `epoch_unlinked` | §7.3 step 5 |
|
||
| `epoch_out_of_order` | §7.3 step 6 |
|
||
| `epoch_unknown` | §9.2, §11.3 step 1 |
|
||
| `sender_not_member` | §9.2, §11.3 step 2 |
|
||
| `epoch_stale` | §4.2, §9.2, §11.3 step 4 |
|
||
| `message_gap` | §4.2, §9.2, §11.3 step 5 |
|
||
| `receipt_unlabelled` | §4.3, §11.3 step 6 |
|
||
| `message_mismatch` | §5.1, §11.3 step 9 |
|
||
| `body_invalid` | §5.3, §11.3 step 11 |
|
||
| `ref_invalid` | §5.3, §11.3 step 11 |
|
||
| `ref_unknown` | §6, §11.3 step 12 |
|
||
| `ref_retracted` | §6, §6.4, §11.3 step 12 |
|
||
| `thread_not_root` | §6.1 |
|
||
| `target_kind` | §6.2, §6.3, §6.4 |
|
||
| `reaction_repeated` | §6.2 |
|
||
| `edit_not_author` | §6.3 |
|
||
| `retract_not_author` | §6.4 |
|
||
| `ack_invalid` | §8 step 4 |
|
||
| `ack_not_member` | §8 step 5 |
|
||
| `ack_unknown` | §8 step 6 |
|
||
| `ack_regressed` | §8 step 7 |
|
||
| `not_a_member` | §9.3 |
|
||
| `call_invalid` | §10.1 |
|
||
| `invitation_mismatch` | §10.2 |
|
||
| `callee_not_invited` | §10.2 |
|
||
| `already_invited` | §10.2 |
|
||
| `answer_unknown` | §10.3 step 2 |
|
||
| `answer_invalid` | §10.3 step 3 |
|
||
| `answer_wrong_signer` | §10.3 step 4 |
|
||
| `already_answered` | §10.3 step 6 |
|
||
| `answer_early` | §10.3 step 7 |
|
||
| `answer_late` | §10.3 step 7 |
|
||
| `missed_early` | §10.3 step 7 |
|
||
|
||
It also raises codes other extensions define: `truncated`, `malformed`, `unknown_version`,
|
||
`wrong_room`, `bad_key` and `bad_signature` for a record that does not read, is of another room
|
||
or does not verify; `unknown_kind`, `bad_utf8`, `text_too_long` and `time_reversed` as
|
||
live-events §9 names them, for a message's kind, body and `t`; live-rooms §11's for a
|
||
descriptor; confidentiality §13's for the confidentiality record (§3.3), for a message's Key ID
|
||
and opening (`properties_malformed`, `key_id_missing`, `key_id_unknown`, `sender_not_owner`,
|
||
`bad_tag`, `plaintext_malformed`, `inner_digest_mismatch`), and for a commit (`commit_not_committer`,
|
||
`proposal_not_clock`, `commit_mismatch`, §7.2); and jwp-receipts §3.3's for a message's
|
||
receipts (§11.3 step 6).
|
||
|
||
## 13. Conformance
|
||
|
||
[`live-conformance/messaging/`](../live-conformance/messaging/) holds the corpus, in one file,
|
||
`vectors.json`, written from fixed seeds by `wai_live_messaging_vectors gen`. Its keys and
|
||
epoch secrets are derived from fixed seeds; the epoch secrets stand where an MLS exporter
|
||
gives them (confidentiality §14's boundary: the corpus runs no MLS), and every message is
|
||
sealed under them by confidentiality §4, so a verifier opens every message it is given a
|
||
secret for. It has:
|
||
|
||
- **Conversations**, each a descriptor, a confidentiality record, a statement, the epoch
|
||
secrets given and the log's entries, with the outcome §11.5 states:
|
||
- accepted: three members exchanging texts; a thread with replies and reactions; an edit and
|
||
a retraction, of a text and of a reaction; a member removed, its message after the removal
|
||
refused `sender_not_member` and its earlier messages accepted; a member that joins later;
|
||
a message of an epoch whose secret the verifier is not given, stated `sealed`;
|
||
acknowledgements of delivery and reading, and one of each refusal of §8;
|
||
- one message refused for each reason of §11.3 that a message can raise, each a control of
|
||
one defect: an edit of another author's message, a retraction of another's, a reaction to
|
||
a retracted message, a reply to a reply, a reaction repeated, a body that breaks §5.3, a
|
||
record whose position differs from its object's, a record signed by another key, a Key ID
|
||
of a stale epoch and of another member's leaf, a gap, a receipt in the legacy form, a
|
||
receipt whose chain breaks, a payload changed after it was signed, and a sealed payload
|
||
that does not open;
|
||
- refused: each reason of §3.3 and §7.3.
|
||
- **History**, in the file's `history`: requests against a conversation's log, each naming the
|
||
requester, whether it is admitted, and the position it reads from, with the content
|
||
positions it is owed or `not_a_member`: a member, a member that joined later (owed nothing sealed before it joined),
|
||
a removed member before and after its removal, and a principal never a member.
|
||
- **Calls**, in the file's `calls`: a call between two principals rung and accepted; a call
|
||
among three in which one accepts, one declines and one is missed; a call cancelled; and one
|
||
refused for each reason of §10.1 to §10.3.
|
||
- **Records**, in the file's `records`: each record of this extension, and a copy of it for each
|
||
reading refusal of §2.
|
||
|
||
A conforming verifier gives every vector the outcome the corpus states.
|
||
`wai_live_messaging_vectors verify` and the test `tests/live_messaging.rs` check that the file
|
||
regenerates byte for byte, that `wai-rs` gives every vector its outcome, and that §12 lists
|
||
exactly the reference's codes. `live-conformance/live_messaging_verify.py` is the second
|
||
implementation, written from this text: it shares no code with `wai-rs`; it reads descriptors
|
||
with the live-rooms corpus's own second implementation (`live_rooms_verify.py`), checks
|
||
confidentiality records, reads Key IDs and opens sealed objects with the confidentiality
|
||
corpus's (`e2ee_verify.py`), verifies signatures with WAI's strict rule written from RFC 8032
|
||
(`claim-conformance/_ed25519.py`) and computes BLAKE3 with `claim-conformance/_blake3.py`; it
|
||
gives every vector the same outcome, and requires every mutated copy of the corpus to fail.
|
||
|
||
The room service `joule-moq-room` keeps conversations and calls (`[messaging]`, `[call]`), and
|
||
its tests run them with members keying through real MLS groups (`joule-moq-mls`) and sealing
|
||
with `joule-moq`'s secure objects: three members exchange messages, one is offline across a
|
||
restart of the service and catches up from its history store, a thread, a reaction, an edit and
|
||
a retraction are verified by `wai-rs`, a removed member opens nothing sealed after its removal
|
||
and is refused what it is not owed, and a call rings, is accepted and carries audio and video,
|
||
while another is missed.
|
||
|
||
## 14. Security and privacy considerations
|
||
|
||
- **Who reads.** The members of an epoch, and no one else: not the relay, not the store, not
|
||
the clock, not the authority (confidentiality §12). The authority decides who is admitted, so
|
||
an authority that wants to read admits a principal of its own, which every epoch statement
|
||
then lists.
|
||
- **Who wrote.** Every message is signed twice by its author: its record inside the seal, so a
|
||
member can show what it opened to anyone; its receipt over the ciphertext in the clear, so the
|
||
store and every relay hold each author to its own track. Within the group every member holds
|
||
every sender's keys (confidentiality §12); the owner check (§11.3 step 4) and the two
|
||
signatures are what keep a member from writing as another.
|
||
- **Removal.** A removed member keeps the keys of the epochs it held and what it opened in them.
|
||
It opens nothing of a later epoch, the store takes nothing more from it and serves it nothing
|
||
sealed after its removal (§9.2, §9.3), and its earlier messages stay verified.
|
||
- **What retraction and editing are.** A statement by the author that a sink honours. A member
|
||
that opened a message before its retraction, or its text before an edit, keeps them; the
|
||
original record verifies for anyone who holds it. A sender that needs a message never to
|
||
persist does not send it.
|
||
- **Metadata.** The store, and a relay, see who posts, when, how much and in which epoch, the
|
||
membership over time, the acknowledgements and the link hashes (§4.4, §8); acknowledgements
|
||
are in the clear so that a sender and the store can check them. A member that needs its
|
||
acknowledgements unseen sends none: acknowledging is a member's act, and this extension adds
|
||
no consent step to taking part in a conversation. Where a regime asks the operator of a store
|
||
for a lawful basis for what it keeps, the operator records it as a `lawful-basis` claim
|
||
([jwp-receipts](jwp-receipts.md) §2.4.6).
|
||
- **The store's order.** The store orders the log and can withhold or delay an entry, as a
|
||
relay can; it cannot forge or change one: every entry is signed by its author, the clock or a
|
||
member, each message's chain is signed, each epoch statement chained, and a member checks each
|
||
epoch statement against its own group (§7.3).
|
||
- **The clock's statements.** An epoch statement is the clock's; a member checks it against the
|
||
MLS group it holds, and an offline verifier takes it as the clock's signed word, as it takes
|
||
the clock's lifecycle (live-rooms §13).
|
||
- **Calls.** An invitation is the caller's, and its call descriptor the authority's: a device
|
||
rings only for an invitation whose signatures verify (§10.2), so a party that is neither
|
||
cannot make a device ring. A missed call is the clock's statement that no answer reached it by
|
||
the deadline. An inbox is served only on a proof of its owner's key.
|
||
- **Replay.** A message record names its room, author and group, and its seal binds its track
|
||
and position (confidentiality §4.7); an acknowledgement and an answer name their room; an
|
||
invitation names its call's digest. None can be moved to another conversation or call.
|