Skip to content

Latest commit

 

History

History
116 lines (96 loc) · 7.5 KB

File metadata and controls

116 lines (96 loc) · 7.5 KB

Offline Protocol Specification

This directory specifies the Offline Protocol as a wire and behaviour contract, independent of the Rust implementation in this repository. A second implementation written against these documents, in any language, should interoperate with this one.

The Rust crates are the reference implementation, not the definition. Where a document here and the code disagree, that is a bug in one of them, and the document says which reading is normative for the wire.

Documents

Document Scope
Identity and addressing Address derivation, canonical form, self-certification, session and group identifiers
Message model and wire format The abstract message, the JSON encoding floor, the binary v1 encoding, the extension TLV registry
Control messages The reserved prefix registry, control-plane signing, and the two exemption classes
Document replication Document sync frames, the anti-entropy exchange, attachment references, and what travels the media path
Encryption envelopes The __MLS_ENC__ envelope forms, the media chunk envelope, and the sealed rich payload
Group protocol Group frames, membership commits, leaf identity binding, relay broadcast and the delivery report
Capability negotiation What peers advertise, what each capability gates, and what happens on absence
Leaf node provisioning What a constrained device owes at pairing, the never-committing profile, and the provisioning-time adversary
Bluetooth LE framing The GATT contract, the fragment header, and what a receiver owes on reassembly
Username discovery and invites The self-certifying invite payload, and the non-authoritative username directory
The gateway contract What a gateway is, the five verbs it implements, the gateway-daemon wire protocol, and the backbone
Conformance The two profiles, what every implementation owes, and how the vectors decide it

Conformance vectors

Frozen test vectors accompany the chapters that have them, and are the practical definition of interoperating with this implementation. They live in the crate whose code they pin, so a packaged build carries its own vectors:

Vectors Chapter
crates/offline-protocol-core/tests/data/wire-v1.vectors.json Message model and wire format
crates/offline-protocol-core/tests/data/address-v1.vectors.json Identity and addressing
crates/offline-protocol-sealed/tests/data/derive-address-v1.vectors.json Identity and addressing
crates/offline-protocol-sealed/tests/data/control-signing-v1.vectors.json Control messages
crates/offline-protocol-sealed/tests/data/gateway-address-proof-v1.vectors.json The gateway contract
crates/offline-protocol-sealed/tests/data/mls-envelope-v1.vectors.json Encryption envelopes
crates/offline-protocol-sealed/tests/data/key-package-v1.vectors.json Capability negotiation
crates/offline-protocol/tests/data/data-sync-v1.vectors.json Document replication
crates/offline-protocol-transport/tests/data/ble-framing-v1.vectors.json Bluetooth LE framing
crates/offline-protocol-transport/tests/data/nip44.vectors.json None. These are the NIP-44 spec's own published vectors, vendored for the Nostr carrier's sealing and pinned to the checksum that spec publishes. Transport framing is out of scope here, so there is no chapter for them to pin

Every file above was computed independently of the code it pins. A vector computed by running the code under test agrees with any format that code happens to emit, including a wrong one, so it can never report a break.

How that independence is held differs by row, and the difference matters to anyone editing one:

  • The first seven rows are generated by tools/spec-vectors/generate.py, a second implementation of these encodings written from the chapters, which never reads the crates it pins. CI regenerates them and fails on any difference, so an expectation cannot be edited without an edit to the generator beside it.
  • The document-replication and Bluetooth LE rows were computed by hand from their chapters, before that generator existed, and each states so in its own header. Nothing regenerates them, so their independence rests on review rather than on a job: an edit to one of those files is exactly as load-bearing as an edit to the wire format, and gets read that way.
  • The NIP-44 row is the upstream specification's own published file, vendored and pinned to the checksum that specification publishes.

When one fails, the wire format has changed and needs a new version identifier. Editing the expected value to make a test pass converts a caught break into a shipped one. Conformance states the full rule, what the vectors deliberately do not cover, and how a second implementation runs them against itself.

Conformance language

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as defined in RFC 2119 and RFC 8174, and appear in capitals only when used in that sense.

Layering

The protocol has four independent layers. Each has its own versioning and its own failure mode, and a reader should keep them apart:

  1. Transport framing. How bytes reach the next hop. Bluetooth LE, Wi-Fi Direct, Reticulum, Nostr, or an internet relay. Out of scope here except where a transport constrains a payload size, and except Bluetooth LE, which has its own chapter. That exception exists because a leaf node's firmware implements the radio itself, so on that one carrier the framing became part of the interop surface rather than an agreement between two builds of this source tree.
  2. Hop-local encoding. How a Message becomes bytes for one hop. Either the JSON floor or the binary v1 codec. Negotiated per peer via wire_versions, and re-negotiated on every connection.
  3. End-to-end envelope. How ciphertext and its addressing survive an arbitrary number of relay hops. The __MLS_ENC__ and media envelopes, negotiated via env_versions.
  4. Sealed payload. What travels inside the MLS AEAD boundary. Plain text, or a __RICH_V1__ body, negotiated via rich_versions.

A change at one layer does not imply a change at another. The compact MLS envelope and the binary wire codec ship independently and are gated by separate switches, because a message can take the binary encoding on one hop and JSON on the next while its envelope stays byte-identical end to end.

Two invariants that outrank everything else

JSON is the permanent floor. Every conforming receiver MUST accept the JSON encoding of a Message. A sender MAY emit a compact encoding only to a peer that advertised it. Persistence and the internet relay path use JSON unconditionally. No negotiation, capability, or configuration removes the obligation to decode JSON.

Frozen encodings never change in place. The binary v1 DTO field order and the numeric enum mappings are a wire contract. Additive data goes in the extension TLV section. A change that cannot be expressed additively takes a new version identifier and is negotiated, never assumed.