Specs
spec/02-connections.md · shown exactly as it appears in the repository
02: Connections
Status: Draft, v0.1.
License: This text is CC-BY-4.0; the field names, enum values, and other machine-readable schema elements defined here are also MPL-2.0. See LICENSE-DOCS and LICENSE.
Overview
A connection is established between two parties through a short sequence of encrypted messages relayed by a router, combined with an out-of-band exchange of a single-use connection code (e.g. an alphanumeric code or a QR code). A connection code isn't a distinct primitive: it's a sendKey registered for one-time use with an expiry date, treated by the router exactly like any other sendKey row. The router's purpose is to route envelopes, period: it MUST NOT participate in, interpret, or hold any concept of connection semantics. It only ever registers keys and forwards envelopes, and that isn't a policy restriction, it's structural: every envelope it forwards, including the handshake messages below, is encrypted with a key the router never holds, so even a misbehaving router has nothing to peek at.
Key exchange
Each party MUST generate a fresh X25519 key pair for the connection, consistent with the key-material isolation requirement in 00-overview.md: no connection may reuse a key pair from another. Once both parties have exchanged public keys (see the handshake below), each independently computes the same shared secret via Diffie-Hellman.
The shared secret itself MUST NOT be transmitted between parties or made visible to the router. Diffie-Hellman satisfies this by construction: each side derives the same value from its own private key and the other party's public key, so there is nothing to transmit even in principle. Full cryptographic detail is in 07-security.md.
The shared secret derives the symmetric key that encrypts all content exchanged over the connection. A party's private key MUST be discarded immediately after computing the shared secret; only the shared secret is retained.
No addresses, only keys
There are no addresses in Bonafide. The router's data model (sendKey, domain, receiveUrl, routingToken) MUST NOT carry anything that identifies a person; all of it is routing information.
A sendKey is only meaningful as a lookup into the row stored under (sendKey, domain); it carries no information about who holds it. domain routes a key to the right receiveUrl when a connection spans multiple domains (see 03-cross-domain.md), nothing more. routingToken is opaque ciphertext: minted by the registering party's own deployment, encrypted with a key only it holds, and echoed back on every message for that row, letting a receiving agent recover which local tenant database a message belongs to without the router ever knowing.
The router, in short, never sees, stores, or needs anything that identifies a person: only keys, a domain, a server endpoint, and an encrypted blob it echoes back on delivery.
The router interface
The router exposes five operations. It stores rows keyed on (sendKey, domain), each holding a receiveUrl and an opaque routingToken; it MUST NOT inspect, validate, or act on anything else in a message.
A router deployment MUST restrict which callers may reach it to a configured IP allowlist, covering both the backends that use it and, once router federation exists (see below), any other router it forwards to or accepts traffic from. This is a network-level boundary, not a per-message access-control decision; Layer 1/Layer 2 access control (00-overview.md) governs what content flows once a caller can already reach the router.
| Operation | Request | Behavior |
|---|---|---|
| Register | POST /send-key/register {sendKey, receiveUrl, routingToken, expiryDate, domain} |
Creates a row. 409 if (sendKey, domain) already exists. |
| Replace | POST /send-key/replace {oldSendKey, newSendKey, domain} |
Atomically repoints an existing row at a new sendKey, keeping its receiveUrl/routingToken. 404 if not found, 409 if newSendKey is already taken. |
| Deregister | POST /send-key/deregister {sendKey, domain} |
Removes a row. |
| Check | GET /send-key/:cxnCode |
Returns {isCxnCode: boolean}: whether a row currently exists for this value, and has not expired. |
| Send | POST /a2a-messaging/send {version, messageId, sendKey, domain, encryptedPayload} |
Looks up the row for (sendKey, domain) and forwards {version, messageId, sendKey, domain, encryptedPayload, routingToken} to its receiveUrl. 404 if no row exists. |
Planned extension: router federation (non-normative preview)
Expected to ship by the end of 2026: a second table, keyed the same way as the one above (sendKey, domain), pointing at another router's address instead of a receiveUrl. When Send finds no row in the router's own table, it will consult this forward table; a match means forwarding to that router instead of returning 404. This lets independently operated routers interoperate without each needing to know every key directly. The trust model between routers (IP-allowlisting a forwarding router, loop prevention, and related questions) is still being designed and isn't specified here.
This solves delivery for a connection established across hosts; it doesn't by itself make that handshake decryptable without the ecosystem-wide bootstrap key (07-security.md), which is why the two extensions are planned together. See 07-security.md's own preview for the decryption half.
Establishing a connection
Call the party who generates and shares the connection code the inviter, and the party who scans or opens it the joiner. Either party may fill either role; nothing about these roles is persistent.
- Inviter MUST generate an X25519 key pair (per Key exchange, above) and a connection code with enough randomness to be unguessable. A guessable code would let an attacker complete the handshake in the joiner's place; since a code is single-use, the blast radius is bounded to that one connection and ends once either party notices and deletes it, but that doesn't make randomness optional: real content could be exchanged with the impersonator before it's noticed. The inviter MUST register the code with the router (
Register, with its ownreceiveUrl/routingToken) and save its connection record with statuscodeSentbefore sharing the code out-of-band: a joiner receiving the code before it's registered could fail an otherwise-valid attempt. - Joiner MUST confirm the code is live (
Check) before generating anything: an expired or non-existent code would never deliver the resulting envelope to the inviter, so checking first avoids wasting the attempt. They then generate their own X25519 key pair, independently per the key-material-isolation requirement in00-overview.md, and register that key with the router (Register): the key the inviter will use to reach them from now on. Their connection record is saved with statuscxnSent. - Joiner sends a
connection/cxnEstablishedmessage, addressed to the inviter's connection code, containing: a second freshly generated key (meant to become the key the inviter registers for itself; the joiner generates it on the inviter's behalf since the two are already exchanging keys here, and a separate round trip for the inviter to announce it would add chatter without adding anything), the key the joiner just registered (step 2, so the inviter knows how to reach them), the joiner's display name (entirely under their control, MAY be blank) and DH public key, and the joiner's alias (see04-access-control.md: in the current implementation a party shares one alias across every connection; a future implementation MAY generate one per connection, or more than one for the same connection). This is one-way: only the joiner's alias travels here. The inviter's alias follows later, once the connection completes (step 6). - Inviter receives this and MUST atomically replace its temporary code registration with the joiner's new key (
Replace): exactly one registration must ever be live on the inviter's side, never both the old code and the new key (which would leave the code usable by a second party after a joiner has already claimed it), and never neither (which would make the inviter briefly unreachable). The inviter adopts that key as the one it will use to reach the joiner, computes the shared secret from its own private key and the joiner's public key, discards its private key, and moves to statuscxnEstablished. - Inviter sends a
connection/completemessage back to the key the joiner registered, carrying its own display name (entirely under its control; MAY be blank, same as the joiner's in step 3) and DH public key. - Joiner receives this, computes the same shared secret from its own private key and the inviter's public key, discards its private key, and sets its status to
cxnEstablished. It then sends asubscription/refreshSubscriptionsmessage to re-synchronize any subscriptions established before the connection completed (see04-access-control.md). The inviter replies with its own alias, the other half of the one-way exchange from step 3. The shared secret now exists on both sides, so this alias is sent encrypted with it, rather than with the bootstrap key step 3 used.
Only the inviter's registered key changes during the handshake (via the atomic Replace in step 4); the key the joiner registers in step 2 is stable from the moment it's sent, which is why the inviter can already reach the joiner as soon as cxnEstablished arrives.
Connection states
| Status | Meaning |
|---|---|
codeSent |
Inviter has registered a code and is waiting for a joiner. |
unregistered |
Reserved for a connection record created but not yet registered with the router (not reached in the handshake above; present for implementation completeness). |
cxnSent |
Joiner has registered its own key and sent its cxnEstablished message; awaiting the inviter's complete reply. |
cxnEstablished |
Both parties have exchanged public keys, computed the shared secret, and may exchange content. |
A connection MUST be in cxnEstablished status before any content exchange occurs: this is the connection-existence precondition from 00-overview.md's two-layer model made concrete, no other status permits it.
Bulk invitations
A single connection code may also be registered for repeated use (a bulk invitation), letting many independent joiners each establish their own pairwise connection from the same shared code. Each resulting connection MUST be fully independent of every other one established from the same code: its own key material, its own non-linkability guarantee (00-overview.md), nothing that lets the inviter, another joiner, or an outside observer tie two of these connections back to the shared code or each other. Each is established exactly as described above, indistinguishable from a one-to-one connection; the inviter's side just tracks how many uses remain and stops accepting joiners once none are left.
Ending a connection
Either party MAY end a connection unilaterally; it needs no agreement from the other. Doing so MUST be more than deregistering the key with the router: the party ending it MUST notify its peer (connection / delete), and both parties MUST then delete all content they hold that was shared over that connection, along with the connection itself. Full propagation/deletion mechanics are in 06-propagation.md.