Relay API v1 and channel protocol v3
This document is normative. The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY
are to be read as in RFC 2119. The JSON test vectors in vectors/ are part of the
specification: an implementation that disagrees with a vector is wrong, and where a
vector and this text disagree, report it as a bug.
1. Layers¶
| Layer | What it does | Defined here |
|---|---|---|
| L0 relay | Channels with two sides, host and guest. Binary frames passed through untouched, presence events. |
Section 2 |
| L1 channel | Pairing, a Noise handshake on every connection, transport frames, control messages. Its output is an ordered, authenticated, confidential stream of messages between two static keys. | Sections 3 to 9 |
| L2 application | What the messages mean (for AInBox, HTTP and WebSocket operations, an allow-list and a session). | No. See section 10. |
A relay is never trusted with confidentiality or integrity. Everything L1 promises
holds against a relay that drops, delays, replays, reorders, duplicates or alters
frames. What a relay can still do is listed in SECURITY.md.
The host is the long-lived side (a server install). The guest is the side that pairs with it (a phone). One guest and one host share one channel per relay.
JSON in this document is UTF-8 (RFC 8259). A receiver MUST refuse a JSON object with a duplicate key, and MUST treat an integer field as valid only when it is a JSON number with no fraction or exponent in the range 0 to 2^53 - 1, unless a field says otherwise. Lengths of strings are counted in Unicode code points.
2. Relay API v1 (L0)¶
2.1 Identifiers and tokens¶
A channel id is 16 random bytes and a side token 32 random bytes, both encoded as
base64url without padding (22 and 43 characters from A-Z a-z 0-9 - _). Wherever
this document uses a channel id or a token, it means that text, not the decoded
bytes. A relay MUST draw them from a cryptographically secure generator. A relay
MUST NOT store tokens in clear: it stores their SHA-256 digests and finds a presented
token by its digest, never by comparing it with stored tokens. A relay MUST NOT log request lines, URLs or Authorization
headers.
2.2 Project keys¶
Creating a channel takes a project key, sent as Authorization: Bearer <key>. A
key carries a quota with three limits:
| Limit | Counts |
|---|---|
channels |
Channels created with the key that are still alive. |
creates_per_minute |
Channel creations with the key in any 60-second window. |
bytes_per_day |
Data frame bytes (section 2.4, first byte included) the relay forwarded on the key's channels since 00:00 UTC. |
A key is one of two kinds:
- An operator key: any bearer string that does not start with
rt1.. The relay operator lists the SHA-256 digests of the keys it accepts with their quotas. How that list is stored is the operator's business. -
A relay token: a statement signed by an issuer the operator trusts, so that one issuer (a vendor) can hand out keys to many installs without the operator listing each one. Its form is
rt1.
.
where <payload> is the base64url (no padding) of a UTF-8 JSON object and
<signature> is the base64url (no padding) of the 64-byte Ed25519 signature
(RFC 8032) over the ASCII bytes rt1.<payload>. The whole token is at most 4096
bytes. The payload holds:
| Field | Type | Meaning |
|---|---|---|
iss |
string, 1 to 64 | Issuer id. The relay looks up the issuer's Ed25519 public key by it. |
aud |
string, 1 to 64 | The relay this token is for. Each relay has its own id, unique among the relays that trust the issuer; the operator assigns it and tells the issuer. Two relays MUST NOT share an id. |
sub |
string, 1 to 128 | An opaque id for the holder. It MUST NOT name the holder. |
exp |
integer | Expiry, Unix seconds. The token is valid while now < exp. |
channels, creates_per_minute, bytes_per_day |
integers | The quota. |
The relay MUST refuse a token that is longer than 4096 bytes, not of the form
above, signed by an issuer it does not know or not by that issuer's key, whose
payload is not a JSON object with every field above at its type, whose aud is
not the relay's own id, or whose exp has passed. Unknown fields are ignored.
The quota is accounted per (iss, sub); when two valid tokens for the same
holder carry different quotas, the relay applies the one in the token presented
with each request. Revocation is expiry: an issuer that needs to cut a holder
issues tokens with short lives, and SHOULD NOT issue tokens that live longer than
the holder's entitlement.
A relay operator MAY give its own channels (a relay run inside one install for its local network) a key with no limits.
2.3 HTTP¶
A relay's base URL (section 8.1) may carry a path; every route below is relative to it.
POST /v1/channels creates a channel. Request body: empty. Answers:
| Status | When | Body |
|---|---|---|
| 201 | Created. | {"channel": <id>, "host_token": <token>, "guest_token": <token>} |
| 401 | No key, an unknown key, or a refused relay token. | any |
| 429 | Over channels or creates_per_minute. SHOULD carry Retry-After. |
any |
| 503 | The relay holds as many channels as it can. | any |
The relay checks in that order: the key first, then its quota, then its capacity.
DELETE /v1/channels/{id} with Authorization: Bearer <host_token> closes both
sockets of the channel with code 4410 and forgets it. Answers 204 when deleted, 404
for an unknown channel or a wrong token alike. Only the host can delete a channel.
The host token authenticates the host's socket and also deletes the channel, and the
project key creates channels. A host MUST NOT send either over a ws:// or http://
connection that crosses a network other machines share; over plain text it may only
reach a relay on its own machine (loopback) or on a network that only it and the
relay are members of (a container network). A host MAY reach a relay by a different
URL than the one it gives guests, for this reason: the URL in a relay entry is the
guests' URL.
2.4 WebSocket¶
GET /v1/ws/{id} opens a WebSocket for one side of a channel. The URL carries no
token. The client's first frame MUST be a text frame holding a JSON object with a
string field auth, the token, sent within 5 seconds of the socket opening; other
fields are ignored. The auth object follows section 1: a duplicate key makes the
frame invalid. The token decides the side. If the channel is unknown, the token
is not one of its two, the frame is not such a text frame, or 5 seconds pass, the
relay closes with 4401. It uses 4401 for all these cases so that a client learns
nothing about which channels exist, and checks authentication before quota.
After a valid auth the relay sends a ready system frame and then, if the other
side holds a socket, a peer_connected system frame. From then on every frame in
both directions is binary and its first byte says what it is:
| First byte | Name | Body |
|---|---|---|
0x00 |
system | JSON {"type": "system", "event": <event>}. Only the relay sends it. Receivers ignore other fields. |
0x01 |
data | Opaque bytes, at most 1 MiB (1,048,576 bytes) after the first byte. |
A client MUST send only data frames. The relay MUST forward a data frame from one side to the other byte for byte, first byte included, in the order received. If the other side holds no socket, the relay drops the frame: there is no store and forward.
System events:
| Event | Sent to a side when |
|---|---|
ready |
Its auth was accepted. |
peer_connected |
The other side's auth was accepted, or, right after ready, the other side is already present. |
peer_disconnected |
The other side's socket closed and no newer one holds its place. |
replaced |
A newer socket authenticated as the same side. The relay then closes this socket with 4409. |
Clients ignore an event they do not know. Presence events are hints for user interfaces; L1 never takes them as evidence of anything (section 6.3).
A second socket that authenticates as a side already held replaces the first: the
relay sends replaced to the old socket and closes it with 4409. Only the socket
that holds a side frees it; a replaced socket that closes late MUST NOT produce a
peer_disconnected.
Close codes:
| Code | Meaning | What a client does |
|---|---|---|
| 4400 | Protocol violation: a text frame after auth, a client frame whose first byte is not 0x01. |
Fix itself; treat as a failed relay. |
| 4401 | Authentication failed (unknown channel, wrong token, malformed or late auth). |
The channel is gone for it (section 8.4). |
| 4409 | Replaced by a newer socket for the same side. | Keep the newer socket if it opened it itself; otherwise reconnect after the backoff of section 8.2. |
| 4410 | The channel was deleted. | The channel is gone for it (section 8.4). |
| 4413 | A data frame whose body is longer than 1 MiB. | Treat as a failed relay. |
| 4429 | The channel's key is over its bytes_per_day. The relay also refuses auth on the key's channels with 4429 until 00:00 UTC. |
Treat as a failed relay until 00:00 UTC. |
2.5 Lifetime¶
A relay MUST keep channel ids, token digests and the identifier of the key each
channel was created with (for an operator key its digest, for a relay token its
(iss, sub)) across restarts, in storage only the relay can read. It SHOULD forget
a channel whose guest never authenticated within 1 hour of creation, and MUST forget
any channel that had no authenticated socket for 30 days.
2.6 v0¶
A relay MAY also serve the earlier API (POST /channels, /ws/{id}?token=, sides
mobile and bridge) during a migration. v0 is not specified here and new clients
MUST NOT use it.
3. Channel protocol v3 (L1): keys and primitives¶
L1 uses the Noise Protocol Framework, revision 34, with two handshake patterns and one suite:
Noise_IKpsk1_25519_AESGCM_SHA256for pairing (section 5),Noise_KK_25519_AESGCM_SHA256for every later connection (section 6).
AES-GCM rather than ChaCha20-Poly1305, because WebCrypto has AES-GCM and no ChaCha.
Its 96-bit nonce is, as Noise defines for AES-GCM, 32 zero bits followed by the
64-bit big-endian counter. An implementation MUST pass the published Noise test
vectors for both protocol names (the cacophony set has one of each) and every vector
in vectors/.
Keys:
| Key | Owner | Lifetime |
|---|---|---|
hs |
The host's static X25519 key pair. Its public half is in every pairing payload, so it authenticates the host and nothing else. | Created once per host. Rotating it ends every pairing. |
gs |
The guest's static X25519 key pair. | Created fresh for each pairing; a guest MUST NOT use one gs with two hosts or for two pairings. |
psk |
32 random bytes the host draws for one pairing. | Until the pairing ends (section 5.5). |
Every handshake draws a fresh ephemeral key pair, also when a guest retries the same handshake on another relay. Every handshake has no payload: each handshake message ends with the encryption of an empty payload, which is still a 16-byte tag once a key is set.
3.1 Prologue¶
Both handshakes use the same prologue:
prologue = "relay-channel/3" || u16(len(dev)) || dev || u16(len(channel)) || channel
where dev is the device id from the pairing payload and channel the channel id
the handshake runs on, both as the ASCII text of section 2.1 and 4, and u16 is a
2-byte big-endian length. A session is bound to that channel id: a relay cannot move
it to another channel. Channel ids are chosen by relays, so the prologue does not by
itself tell two relays apart; a host MUST treat a channel id equal to any channel id
it already holds, on any relay, as a failed creation.
3.2 L1 frames¶
Each L0 data frame carries exactly one L1 frame after its 0x01 byte. The L1 frame's
first byte is its type:
| Type | Name | Sender | Valid on | Body | Body length |
|---|---|---|---|---|---|
0x01 |
pair | guest | a pairing channel | IKpsk1 message 1 | exactly 96 |
0x02 |
connect | guest | a device channel, or a pairing channel (section 5.4) | KK message 1 | exactly 48 |
0x03 |
accept | host | either | Message 2 of the handshake the guest started | exactly 48 |
0x04 |
transport | either | either, after a handshake | One Noise transport message | 18 to 65,535 |
0x05 |
abort | either | either | empty | 0 |
A receiver MUST abort (section 9.2) on an unknown type, a body of the wrong length, a type its role does not receive, or a type the channel does not take. The type byte is not authenticated. A relay that changes it gets the same result as dropping the frame: the frame fails as what it now claims to be, and the stream behind it fails to decrypt.
4. Pairing payload¶
The host gives the guest everything it needs for the first contact in a pairing payload, a JSON object. It travels only out of band (a QR on a screen, copied text, an explicit intent), never through a relay.
| Field | Type | Meaning |
|---|---|---|
v |
integer | 3. |
hs |
string | The host's static public key, 64 lowercase hex characters. |
psk |
string | The pairing secret, 64 lowercase hex characters. |
dev |
string | The device id the host assigned, 1 to 64 characters from A-Z a-z 0-9 _ -. |
exp |
integer | Unix seconds after which the host refuses the pairing. A host SHOULD set it 5 minutes after it draws the payload. |
relays |
array | 1 to 16 relay entries, in the host's order of preference. |
name |
string, optional | A display name for the host, at most 100 characters. |
url |
string, optional | A host-defined URL, at most 2048 characters, for the host application's own use. |
name and url are host-supplied text that arrives in a scanned QR code; an
application that shows or opens them treats them as untrusted.
A relay entry, here and in the relays control message, is an object with
url (a relay base URL), channel (a channel id, section 2.1) and token (the
guest token for that channel, section 2.1). Unknown fields are ignored.
A guest MUST refuse a payload that is not a JSON object, whose v is not 3, that
misses a required field, or that has a field of the wrong type or form, a relay
entry included. It then drops relay entries whose URL fails the policy of section
8.1 and keeps the rest in order; if none remain, it refuses the payload. A guest
SHOULD refuse a payload more than 60 seconds past its exp by its own clock
(now > exp + 60). The host MUST enforce exp itself, whatever the guest does.
A payload carries psk and live guest tokens. A guest that took it from the
clipboard SHOULD clear the clipboard.
5. Pairing¶
5.1 State¶
The host keeps one pairing state per device (dev), shared by all its pairing
channels:
- the
psk,expand the pairing channels; - at most one candidate per pairing channel: a guest key, a session and a
handshake hash
hfrom apairorconnectthat has been answered but not yet proved (section 6.3); - up to 4 pending keys, each with the code derived from its
h.
5.2 The host prepares¶
For one device, the host draws dev and psk, creates one pairing channel on each
relay it uses, and builds the payload. The pairing is open.
5.3 First contact¶
The guest creates gs and connects to the relays of the payload in order (section
8.2). On the first one that answers ready, it runs Noise_IKpsk1 as initiator with
its gs, the host's hs as the known remote static, the psk, and the prologue of
section 3.1, and sends message 1 in a pair frame. In IKpsk1
(-> e, es, s, ss, psk) the guest's static key travels encrypted under es; the
psk is mixed in last, so the tag on the message's empty payload is what proves
knowledge of the psk.
The host, on a pair frame on a pairing channel of an open pairing before exp,
reads the message as responder with hs and the psk. It aborts, and changes
nothing in the pairing state, if decryption fails (the sender knows hs but not
the psk), if the guest key is already pending (it reconnects with connect,
section 5.4), or if 4 keys are already pending. Otherwise it holds the result as the
channel's candidate and answers with accept.
The guest, on accept, finishes the handshake, derives the pairing code (section
5.6), and sends a ping (section 9.1) as its first transport frame. It shows the
code only when the pong arrives, since the host sends that only after it promoted
the key, so a code the person types is never one the host does not yet hold.
The host promotes the candidate when a transport frame decrypts under it (section
6.3): its guest key becomes pending with its code. A key has at most one pending
entry and one code, the one from the pair that was promoted. Until the guest has
seen its pong, it sends a fresh pair (with a fresh ephemeral key) on every new
socket; once it has, it uses connect for every later socket, on any of the pairing
channels. The host aborts a pair from a key that is already pending. A relay that replays a recorded pair gets an accept it cannot use,
because it cannot produce the transport frame that follows.
5.4 While keys are pending¶
- The host accepts
connectframes on its pairing channels from a pending key only (after confirmation, from the bound key only), so a guest that lost its socket or moved to another relay reconnects with KK (section 6). Promoting such a candidate adds no pending entry and leaves the key's code as it was. - The host answers
pingand aborts on any application message or any control message other thanping,pongandclose. It sends norelaysand gives a pending guest no access of any kind.
5.5 Confirmation and the end of a pairing¶
The person who pairs reads the code on their own guest and types it into the host application. The host binds the pending key whose code equals the typed code. A host MUST NOT bind a key any other way, and MUST NOT show the codes of pending keys: a person who sees a code on the host could confirm a stranger's. If two pending keys have the typed code, the host binds neither and ends the pairing. After 3 typed codes that match no pending key, the host ends the pairing.
Someone who photographed the payload can race the real guest and hold pending keys of their own, but their codes are unknown to the person, so the typed code binds the real guest. With at most 3 stranger keys pending, the chance that a stranger's code equals the real one is below 3 in a million, and then the host binds neither and the person starts over.
A pairing ends:
- bound, on confirmation. The host binds the key to
dev, erases thepsk, forgets the other pending keys and candidates (aborting their sessions), and creates one fresh device channel per relay. It sendsrelayswith them on the bound key's current pairing session if that session's socket is open, and otherwise right after promoting the bound key's nextconnecton a pairing channel. The host deletes the pairing channels when a candidate of the bound key is promoted on a device channel. If that has not happened 10 minutes after confirmation, the guest never received its channels: the host unbinds the key, deletes the device channels and the pairing channels, and the pairing has failed. Until the pairing channels are deleted, the payload's guest tokens still open a socket on them, which gives a stranger nothing but the chance to displace the real guest there (4409). - ended, at
expwith no confirmation, on aclosefrom a pending key, on a collision or the third unmatched code, or when a bound guest does not reach its device channels within 10 minutes. The host deletes the pairing channels (and, in the last case, the device channels), unbinds any key, and forgets the pairing.
5.6 The pairing code¶
code = (u32(SHA-256("relay-channel/3 pairing code" || h)[0..4]) mod 1000000)
written as 6 decimal digits with leading zeros, where u32 reads 4 bytes as a
big-endian unsigned integer and h is the IKpsk1 handshake hash after message 2.
(The modulo bias is below 2^-12 and does not matter here.)
6. Connections¶
6.1 Handshake¶
Each time the guest opens a socket on a device channel, it runs Noise_KK as
initiator with its gs and the host's hs, and the prologue of section 3.1, and
sends message 1 in a connect frame. The host reads it as responder with hs and
the guest key bound to that channel's device (on a pairing channel: each pending
key in turn), answers with accept, and holds the result as the channel's
candidate. If the device is not bound, no key matches, or decryption fails, it
aborts.
Both sides contribute ephemeral keys, so every connection has fresh transport keys, and a later theft of either static key does not open earlier sessions.
The guest's first transport frame after accept MUST be a ping. A host that
promotes a candidate on a frame that is not a ping MUST abort.
6.2 Split¶
After message 2, both sides call Noise's Split(), which returns c1 and c2. On
both sides c1 carries guest-to-host frames and c2 host-to-guest frames: the guest
encrypts with c1 and decrypts with c2, the host the other way round.
6.3 Candidates and the current session¶
A relay can replay a handshake message 1. So:
- A host keeps per channel at most one current session and one candidate. A
connectorpairreplaces the candidate, never the current session, and a failed one discards only the candidate. - A transport frame from the guest is decrypted first under the candidate, if there is one, then under the current session. Noise does not advance a nonce on a failed decryption, so the second try sees the same state the first did. If the candidate succeeds, it is promoted: it becomes current and the old current session is discarded. If the current session succeeds, the candidate is discarded. If both fail, the host aborts and discards both.
- A host MUST NOT send a transport frame under a candidate, and MUST NOT record anything about a connection (that it is connected, a queued answer) before the candidate is promoted.
- Presence events never change session state. A new guest socket always means a new handshake, whatever the relay said.
The guest keeps one session per socket and discards it when the socket closes or on an abort.
7. Transport¶
7.1 Frames¶
After the handshake every L1 frame is transport or abort. A transport frame
carries one Noise transport message: the AES-GCM encryption, with empty associated
data and the cipher state's counter nonce, of the plaintext
| Byte | Meaning |
|---|---|
| 0 | kind: 0x00 application data, 0x01 control |
| 1 | flags: bit 0 set means more fragments of this message follow; other bits MUST be 0 |
| 2.. | payload, at most 65,517 bytes |
A frame replayed, dropped or reordered by the relay fails to decrypt, and the receiver aborts. A receiver aborts on an unknown kind or a flag bit other than bit 0.
7.2 Fragments¶
A message of either kind longer than 65,517 bytes is split into fragments sent in order, each in its own frame with the same kind, with bit 0 set on every fragment but the last. Fragments of two messages in one direction MUST NOT interleave: the frame after a fragment with bit 0 set belongs to the same message. A receiver MUST abort on a fragment whose kind differs from the previous fragment of the same message, and when the payloads of one message add up to more than 16 MiB (16,777,216 bytes). An empty message is one frame with an empty payload.
7.3 Rekey¶
The unit is the transport frame, each fragment counting as one. Each cipher state
calls Noise's Rekey() right after it encrypts or decrypts the frame whose nonce n
satisfies (n + 1) mod 2^20 = 0. Both ends of a direction do it at the same frame,
so no signal is needed; the nonce keeps counting. A session whose nonce would reach
2^64 - 1 MUST be closed.
8. Relays¶
8.1 URL policy¶
A relay is named by a base URL. This policy applies to every relay URL that crosses
between the sides: in a pairing payload, in a relays message, and in what a host
puts in them. A URL by which a host reaches a relay for itself is its own choice,
within section 2.3. A guest and a host MUST accept a relay URL only if it
is at most 2048 characters, is absolute, has no user info, query or fragment, has a
port (when present) from 1 to 65535, and either
- its scheme is
wss, or - its scheme is
wsand its host is a literal IPv4 address in 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16, or a literal IPv6 address, without a zone id, in fc00::/7.
Everything else is refused, including ws to a name (.local names too, since any
machine on the network can answer them), to loopback (127.0.0.0/8, ::1), to
link-local addresses (169.254.0.0/16, fe80::/10) and to IPv4-mapped IPv6 addresses.
Over ws, the network sees the guest token and the ciphertext; the guest token gives
it the guest's slot on that one relay and nothing more, which is why each relay has
its own channel and tokens. (The host's credentials must not cross such a network
at all, section 2.3.)
The socket URL is the base with one trailing / removed, then /v1/ws/ and the
channel id; the HTTP routes are built the same way.
8.2 Choosing a relay¶
A guest holds an ordered list of relays per host. It:
- Tries them in list order. Each gets 1.5 seconds, counted from starting the
connection to receiving
accept, and the guest keeps the first that gets there. - While on a relay other than the first, retries the ones before it every 60
seconds and moves up when one gets to
accept. A move opens the new socket and completes its handshake first, then closes the old socket. - On an abort, a socket that closes, or a frame that fails to decrypt, goes back to step 1. After a round in which every relay failed, it waits before the next round: 1 second, doubling each round, at most 60 seconds, and back to 1 second after a success.
The host keeps a socket on its side of every channel it holds, on every relay. When one closes without a code in the 4400 to 4499 range, it reconnects with the same backoff. On 4401 or 4410 it stops using the channel (section 8.4). On 4409 it keeps the newer socket if it opened one itself, and otherwise reconnects with the backoff. On 4429 it stops using the relay until 00:00 UTC. On 4400 or 4413 it stops using the relay and reports a fault.
8.3 The list¶
The list changes only through a relays control message inside an established
session with a bound key. A guest MUST ignore relay lists from anywhere else: a
relay, a presence event, another pairing payload.
The guest applies a relays message as a whole: it filters the entries through the
URL policy and, if any remain, replaces its list with them and starts over at step 1
of section 8.2. The host keeps the channels it took out of the list until it promotes a candidate
from the guest on a channel of the new list, then deletes them.
8.4 A channel that is gone¶
A relay answers 4401 or 4410 for a channel it no longer has. A guest whose every relay for a host answered 4401 or 4410, in three rounds in a row, SHOULD treat the pairing as ended: for a pairing in progress, it stops and says the pairing failed; for a bound key, it says the host revoked or forgot the device. A host that gets 4401 or 4410 on a channel stops using it.
9. Control messages and abort¶
9.1 Control messages¶
A control message is a JSON object with a string field op. Unknown fields are
ignored.
op |
Sender | Fields | Effect |
|---|---|---|---|
relays |
host | relays: 1 to 16 relay entries (section 4), the guest's tokens |
Replaces the guest's relay list for this host (section 8.3). |
close |
guest | The guest unpairs. The host unbinds the device (or forgets a pending key), sends closed, then deletes the device's channels on every relay. |
|
closed |
host | Answers close. |
|
revoked |
host | Sent before the host deletes a revoked device's channels, so the guest can forget its key and say why. | |
ping |
either | Answered with pong. |
|
pong |
either |
A guest that sends close keeps its key and its list until it receives closed,
sends close again as the first message after ping on every connection until then,
and treats a pairing that is gone (section 8.4) as closed.
A receiver ignores an op it does not know, and aborts on a message that is not a
JSON object, has no string op, or carries a known op from the wrong side, with a
field of the wrong type, or with a relay list out of range.
9.2 Abort¶
On any decryption failure, malformed frame or protocol violation, a side sends an
abort frame and discards its session state for that socket: for the host, the
channel's candidate, and its current session unless the failure was in a handshake
message (section 6.3). A guest that receives abort discards its session and goes
back to step 1 of section 8.2; a host that receives one discards the channel's
candidate and current session, and the guest reconnects. abort is not
authenticated, so a relay can forge it. That ends a connection, which a relay can
already do by closing the socket or dropping one frame. A relay can thereby make a
pairing fail, by keeping the guest from getting through, but it cannot make the host
bind any key other than the one whose code the person typed.
10. What the host application must do¶
L1 secures the stream between two keys. The parts below are outside it and decide
whether the whole system is safe; each application states in its own documents how
it meets them. SECURITY.md says how AInBox does.
- The code that holds the guest key. A guest's private key is only as safe as the code that can use it. The application MUST ensure that the code running the guest side is authentic, and SHOULD keep the private key where that code can use it without reading it (a hardware-backed key store).
- The host key at rest.
hsMUST be stored where only the host process can read it. - The confirmation. The application MUST take the typed code only from the person entitled to pair, over a connection as confidential as the rest of its administration, and MUST NOT show pending codes (section 5.5).
- What a session may do. L1 says who is on the other end, not what they may ask. The application's L2 decides what a bound guest can reach.
- Revocation. Revoking a device MUST send
revoked, delete its channels on every relay and unbind its key.
11. Versions¶
The L1 version is fixed for a device when it pairs (v in the payload, and the
prologue). Hosts and guests never negotiate it, so there is nothing to downgrade.
The L0 version is in the path (/v1).
12. Test vectors¶
vectors/ holds one JSON file per topic. Each file has a description and a list of
cases; hex strings are lowercase, and list indexes are 0-based. Where a vector gives
a frame by its SHA-256 and length instead of its bytes, the digest is normative.
vectors/tools/generate.py regenerates them from a small reference implementation
(vectors/tools/noise_ref.py), which passes the cacophony vectors for both protocol
names; every handshake and transport transcript and the rekey function are also
replayed through the noiseprotocol package. Fixed ephemeral keys appear in the
handshake vectors only so the transcripts are reproducible; an implementation MUST
draw fresh ones.
The vectors pin bytes and pure functions. The state machines (sections 5, 6.3, 8 and 9) are tested by the libraries' own suites, including a hostile relay that drops, duplicates, reorders, delays, relabels and alters frames.