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.