Skip to content

Latest commit

 

History

History
147 lines (114 loc) · 7.15 KB

File metadata and controls

147 lines (114 loc) · 7.15 KB

Security Model

Reminder: this is a hobby project. It has not had a security audit. The model below is what's intended; treat it as design notes, not a guarantee.

The mechanics of how a connection forms are in architecture.md; this doc is about the trust decisions at each step.

Identity

Identity is anchored to a Dilithium (ML-DSA-65, FIPS-204) keypair. The address is Base58Check(ripemd160(sha256(dilithium_pub))). A Dilithium-bound Ed25519 operational key does per-envelope signing and carries the libp2p transport, authorised by a Dilithium-signed IdentityBinding.

The honest post-quantum boundary: the identity layer is PQ-safe, but the transport key exchange (libp2p Noise, Tor v3) is still Curve25519. The full accounting of what that does and doesn't buy you is ADR-0001.

The connection lifecycle, trust-first

Authenticated DHT records

Peer records carry an address hash, onion, timestamp, the announcer's Dilithium pubkey, and a signature. Recipients verify the pubkey hashes to the claimed address and the signature checks out — so records can't be spoofed or stale-replayed.

This trades the old "hash-only, least-information" posture for authenticity: a full DHT scrape now yields a global address↔Dilithium-pubkey table. Because the pubkey is post-quantum-safe, that's a correlation trade-off, not a confidentiality break. The DHT remains a routing layer, not an authorization layer — identity is re-verified on the wire at handshake time.

Verified-before-prompt signaling

An unknown dialer's CONN_REQUEST carries its Dilithium pubkey and a signature over the request body. The responder verifies the dialer owns the claimed address before the accept prompt fires — so you never get prompted to accept a peer who can't prove who they are. The Dilithium pubkey is PQ-safe, so revealing it early to an unknown peer costs nothing; the Ed25519 operational key is exchanged only after both sides accept.

3-step handshake

  1. Dialer sends address + Dilithium proof. Responder verifies before prompting.
  2. Responder proves its own identity + binding.
  3. Initiator proves its binding.

Operational Ed25519 keys cross only after mutual accept (in CONN_CONFIRM, inside the IdentityBinding). Real IPs are encrypted to each side's key and exchanged over Tor — your IP is revealed only to a peer you accepted.

Service connection authorization (ADR-0002)

Ensemble hosts many services, each under its own per-service keypair / E-address. The model for who may connect to a hosted service is deliberately conservative — locked down by default, services opt into their own leniency. Canonical rationale: ADR-0002.

Two primitives every service gets

1. A service dials as itself. When a hosted service opens a connection, the daemon signs the signaling handshake with that service's keypair — the responder sees the service's E-address as the connecting identity, not the host daemon's node address. The service keypair never leaves the daemon; the client just asks (over its registration stream) to dial. The libp2p transport underneath stays node-to-node; only the signaling/ACL identity is the service.

2. A service decides its own inbound policy. Every service declares an ACL in its manifest, and the gate is default-deny:

ACL Inbound unknown peer
public accepted
allowlist accepted iff the address is listed
contacts / unset rejected

A service that needs per-peer decisions sets the manifest flag decides_connections. Every unknown-peer request is then delivered to the service's own code as a connection_request event (carrying the dialer's service address); the service replies accept or reject. A service that doesn't set the flag keeps the synchronous default-deny gate. If a service sets the flag but registers no handler, the gate fails closed.

The invariant that makes it safe

Introductions are information, never authorization. A service (e.g. a matchmaker) can introduce two peers, but an introduction creates no grant — it's delivered as an event, and the receiving service decides for itself whether to accept the resulting connection. There are no automatic grants: nothing a third party can send you punches a hole in your gate.

Worked example (matchmaker → players): a matchmaker introduces player A to player B by service address. Each player's service sets decides_connections and, on the connection_request, accepts iff the dialer is a peer the verified matchmaker just introduced it to. Player A dials player B as its own player service, so B's gate sees A's player address and matches it against the introduction. All matchmaking-specific policy lives in the game service; Ensemble's core stays generic and default-deny.

Rejected alternatives (reasoning in ADR-0002): auto-grant on introduction turns the introduction primitive into a general ACL-bypass for every hosted service; node-address gating drags node identity into the per-service ACL layer and is coarser-grained.

Install & admin gating

The service-install and registry publish family is gated by where the call arrives: free over the daemon's local Unix socket (filesystem permission is the auth), admin-signature-required on any network transport. A daemon with no admin key disables the install family on the network entirely — an open install RPC is remote code execution as a feature, so there is no soft default. See ADR-0009 §10.

Web UI auth

The browser SPA holds a non-extractable WebCrypto Ed25519 key and signs each gRPC-Web request with the admin seed — the same auth that protects the TCP/UDS gRPC endpoints protects the browser. The seed-handling SPA refuses to serve over plaintext on a non-loopback bind unless you explicitly assert a TLS terminator (--trust-proxy-tls) or accept the risk on a trusted LAN (--unsafe-plaintext-ui).

The Tor bundle TOFU pin

The daemon SHA-256-verifies the downloaded Tor Expert Bundle against a hash pinned in source. That pin is trust-on-first-use — set by whoever last bumped the bundle. make verify-tor-bundle adds an out-of-band PGP check to close the mirror-substitution gap; run it before any hash bump (setup.md).

Summary table

Concern Mechanism
Identity forgery Dilithium-anchored address, signature-verified at handshake
DHT record spoofing Signed PeerRecords (pubkey hashes to address)
Accepting an impostor Verified-before-prompt; ownership proven before any prompt
IP exposure Real IPs encrypted to peer's key, swapped only after mutual accept
Unwanted service connections Per-service default-deny ACL; opt-in decides_connections
Privilege via introduction None — introductions carry no grant
Remote service install Off-socket calls require admin signature; disabled with no admin key
Tor supply chain SHA-256 pin + out-of-band PGP verification on bump