Docs Menu

Specs

spec/07-security.md · shown exactly as it appears in the repository

07 — Security

Status: Draft, v0.1. License: CC-BY-4.0 — see LICENSE-DOCS.

Overview

This chapter specifies the cryptographic primitives behind the guarantees asserted elsewhere in this document — 00-overview.md's non-linkability guarantee and 02-connections.md's key-exchange requirements both defer full detail here — and consolidates the protocol's trust boundaries into one place. It also documents a known gap in the current handshake that narrows confidentiality during a specific window, rather than leaving it unstated.

Cryptographic primitives

02-connections.md requires each party to generate a fresh X25519 key pair and compute a shared secret via Diffie-Hellman; this section specifies what happens to that secret afterward.

An implementation MUST derive the actual encryption key from the shared secret using HKDF-SHA256, with the message's domain as the HKDF info parameter and no salt. This serves two purposes: it never uses the raw DH output directly as a key, and it gives a single connection's one shared secret a distinct derived key per domain — so extending a connection to a new domain (03-cross-domain.md) never reuses the same key material across domains, even though it reuses the same underlying secret.

Payloads MUST be encrypted with AES-256-GCM, using a freshly generated random 12-byte IV on every single encryption call — never a static or counter-based IV. This is what makes the non-linkability guarantee in 00-overview.md hold in practice: encrypting the same plaintext (for example, a routing token repeatedly embedding the same dbHost/dbName) under the same key produces different ciphertext every time, so nothing about the ciphertext itself ever reveals that two messages, or two connections, share an underlying value. The IV and the GCM authentication tag MUST travel alongside the ciphertext — prepended, in that order — so the receiving party can decrypt and verify in one pass.

Not everything encrypted in this protocol has a DH-derived secret behind it. A separate, static key — provisioned once per deployment and shared by every deployment in the ecosystem, not tied to any one connection — covers the cases where no connection-specific secret exists or applies. The next two sections cover each of those cases: a routingToken (02-connections.md) is always encrypted with this deployment key, since the router — not a connection peer — stores and echoes it back, and the router is never a party to any connection's DH exchange; and the handshake messages that precede a shared secret's existence fall back to it as well, which is the subject of its own section below given the trust implications involved.

Trust boundaries

The router is the only party other than the two ends of a connection that ever touches a Bonafide message, and its visibility is deliberately minimal. It stores and looks up rows keyed on (sendKey, domain) (02-connections.md) — none of which identifies a person — and forwards whatever encryptedPayload it's given, without inspecting, validating, or acting on anything inside it. It MUST NOT hold, and never needs, any decryption key: not the per-connection shared secret derived above, and not the deployment key introduced above. A routingToken is opaque ciphertext to the router for the same reason — it's encrypted with a key only agent-7 deployments hold, never the router.

Beyond the router, trust is strictly pairwise: a connection's shared secret, and the domain content encrypted with it, is known only to the two parties of that connection. No other connection, no other domain sharing the same connection (03-cross-domain.md's per-domain key derivation above keeps this true even when one connection spans several domains), and no third party — including another Bonafide deployment unrelated to either party — can decrypt it, because X25519 Diffie-Hellman never makes the shared secret visible to anyone but the two parties who independently compute it.

The handshake bootstrap key

Two messages in the handshake (02-connections.md) are encrypted with the deployment key introduced above rather than a per-connection shared secret: the joiner's cxnEstablished (step 3) and the inviter's complete (step 5). Neither can use the DH-derived secret, because it doesn't yet exist on both sides at the point each is sent — complete, in particular, carries the very DH public key the joiner needs in order to compute that secret for the first time, so encrypting it with a secret that depends on itself isn't possible.

This is a real, necessary gap, not an oversight, and the router still cannot read either message — it never holds the deployment key, confirmed by bonafide-router-7 never referencing it anywhere. But the deployment key is not scoped to one connection, or even one pair of deployments: it is the same value shared by every agent-7 deployment in the ecosystem, which is what lets any inviter's deployment and any joiner's deployment complete a handshake without having coordinated with each other in advance. That breadth is exactly what makes it weaker than the per-connection secret described under Trust boundaries above: any deployment that holds the shared value could, in principle, decrypt another pair's cxnEstablished or complete message if it were able to observe the traffic — something no deployment can do to a connection's post-handshake messages, which are confidential even against other deployments. This is an open gap in the current design, not a resolved one; this document records it rather than asserting a guarantee the reference implementation doesn't actually provide.

Planned extension (non-normative preview): expected to ship by the end of 2026, in conjunction with the router federation extension previewed in 02-connections.md. The gap exists because the joiner has no way to know the inviter's DH public key until complete (step 5) — the connection code shared out-of-band in step 1 carries only routing information today, not key material. A future version is expected to embed the inviter's DH public key directly in the connection code itself. This would let the joiner compute a one-sided shared secret immediately and encrypt cxnEstablished with it instead of the deployment key, and since the inviter already holds the full shared secret by step 4, complete could then be encrypted with it too — closing this gap for both messages without a new round trip or a new primitive.

The two extensions solve complementary halves of the same scenario — a connection established with someone on a different host, reached by routing across independently operated routers. Federation (02-connections.md) gets the handshake envelope to the right receiving backend across router boundaries; this extension means that backend no longer needs the shared ecosystem-wide key to decrypt what arrives, once it does.