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 guarantees asserted elsewhere (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 in 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 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 one connection's shared secret a distinct derived key per domain, so extending to a new domain (03-cross-domain.md) never reuses 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 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 (e.g. a routing token repeatedly embedding the same dbHost/dbName) under the same key produces different ciphertext every time, so nothing about the ciphertext itself reveals that two messages, or two connections, share an underlying value. The IV and 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 connection) covers the cases where no connection-specific secret exists or applies. The next two sections cover each case: 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 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 too, 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, and not the deployment key. A routingToken is opaque ciphertext to the router for the same reason: it's encrypted with a key only bonafide-agent 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 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: 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 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 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 can't read either message: it never holds the deployment key. But the deployment key isn't scoped to one connection, or even one pair of deployments: it's the same value shared by every bonafide-agent deployment in the ecosystem, letting any inviter's and any joiner's deployment complete a handshake without prior coordination. That breadth makes it weaker than the per-connection secret under Trust boundaries: any deployment holding the shared value could, in principle, decrypt another pair's cxnEstablished or complete message if it could observe the traffic, unlike a connection's post-handshake messages, which stay confidential even against other deployments. This is an open gap, not a resolved one; this document records it rather than asserting a guarantee the reference implementation doesn't provide.

Planned extension (non-normative preview): expected to ship by the end of 2026, alongside 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. This would let the joiner compute a one-sided shared secret immediately and encrypt cxnEstablished with it instead of the deployment key; since the inviter already holds the full shared secret by step 4, complete could then be encrypted with it too, closing the gap for both messages without a new round trip or primitive.

The two extensions solve complementary halves of the same scenario: a connection established across hosts, routed through independently operated routers. Federation (02-connections.md) gets the handshake envelope to the right backend across router boundaries; this extension means that backend no longer needs the shared ecosystem-wide key to decrypt what arrives.