niko_trust/docs/TRUST-MODEL.md
Niko Marmeladkov 20cc52c3a5 feat: network layer — PoW, checkpoint chain, gossip, light node, WS, delegation, rotation, BFT
- BLAKE3 keyed proof-of-work on object storage and auth challenges,
  with frozen vectors cross-checked against an independent Python
  reference implementing the single-block hash it needs.
- Sparse Merkle trie over object IDs: order-independent roots,
  inclusion and absence proofs (internal/smt).
- Signed checkpoint chain per relay: transport key amendment to INV-1,
  /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe
  epoch continuity (internal/checkpoint).
- Head gossip with TOFU pinning and equivocation detection; light node
  (cmd/lightnode) that stores no history: quorum of pinned relays,
  every served object proven against the agreed root, LRU disk cache.
- WebSocket streaming on relay and light node (coder/websocket):
  scoped channels mirroring REST, raw envelopes verified client-side;
  light node marks streamed objects unproven until checkpoint coverage.
- Protocol v1 additions: DelegationClaim tag 0x07 with deterministic
  chain resolution in verify.Graph, KeyRotationRequest/Confirm tags
  0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge;
  spec sections, frozen vectors appended byte-identically, Python
  reference extended.
- Optional permissioned BFT finality over gossip (internal/bft):
  prevote/precommit with quorum certificates verifiable offline.
- Quick wins: Policy.TrustedIssuers, per-type stored metrics,
  batch fetch, lexicographic lists with stable cursor pagination.
- Security review of the network layer (docs/SECURITY-REVIEW.md) with
  findings F-01..F-09; hub send/close race and unstable pagination
  fixed under review.

12 packages green, vet/gofmt clean, protocol fuzzing stable.
2026-08-25 20:38:40 +03:00

5.2 KiB

Trust model

This document explains who is trusted to say what, and why the relay itself never makes a trust decision. Read this before assuming a missing signature check is a vulnerability.

Roles

Role What it is
Issuer Holds a signing key; makes a signed statement (a claim, a response).
Subject The address a claim is about.
Approver An address trusted to allow/deny an ApprovalRequest.
Relay The HTTP server (internal/server). Stores signed objects. No keys.
Verifier The consumer running internal/verify. Checks signatures, decides trust.

Objects are signed by their issuer

Every object on the wire is a transport.Envelope{TCE, Signature}. The TCE is a self-describing, content-addressed blob; the Signature is over the exact TCE bytes and is verified against the public key embedded inside the TCE (the issuer's key).

A claim therefore reads as:

issuer   = <key that signed it>
subject  = <address the claim is about>
claims   = { predicate: value }

The signature binds the statement to the issuer's key. There is no way to emit a record that appears to come from an issuer without holding that issuer's key.

Worked example

trust1abc says: trust1def has minecraft.op: true.

  1. trust1abc builds a Claim{Issuer: trust1abc, Subject: trust1def, Claims: {"minecraft.op": true}}, encodes it to TCE, signs it with trust1abc's key.

  2. PUT /v1/objects stores the envelope. The relay does not verify the signature — it only checks the envelope is well-formed and content-addressed.

  3. Later, a consumer queries GET /v1/claims?subject=trust1def and receives a list of envelopes ({ "tce": "<base64>", "signature": "<base64>" }). The consumer decodes the TCE to read issuer / subject / claims.

  4. The consumer runs verify.Graph.Add(env), which calls env.Verify() and confirms the signature matches trust1abc's key — i.e. trust1abc really said that.

An attacker cannot forge "from trust1abc": any record they produce carries their own key as issuer, so it shows up attributed to the attacker and is ignored by anyone who does not trust that key.

Why the relay does not verify signatures

The relay implements INV-5 ("who said what"), not "should I believe it". It is a content-addressed bulletin board:

  • It accepts any well-formed envelope (PUT returns 200 even for a bad signature). Storing unverified data is correct and intentional.
  • All cryptographic trust decisions live in internal/verify, which never talks to the network and never holds a key.

Consequence: signature verification is the verifier's job, not the server's. A PUT with a tampered signature is stored; verify.Graph.Add rejects it.

The relay's one key (INV-1 amendment)

The relay holds exactly one key, whose entire power is to describe its own storage: signing the checkpoint chain that commits to its object set (CHECKPOINT.md). That transport key can never mint a claim, approval or any other statement — statement signing lives in internal/identity/signer, which server code is structurally barred from importing, and both boundaries are enforced by source-scanning invariant tests. A hostile relay can therefore still lie about its own log; it remains unable to forge what anyone said. Light nodes and gossip exist precisely to make such lies detectable rather than merely suspected.

Who do you believe? (issuer anchoring)

Because any key can sign a claim about any subject, the verifier must decide which issuers it trusts. This is done out of band, by the consumer:

  • verify.Result.Issuer is always returned (verify.go). The consumer compares it to the set of issuers it trusts, e.g. result.Issuer == trust1abc.
  • For approvals, trust is anchored explicitly: Policy.Approvers lists the addresses whose Allow responses count, and Policy.Threshold is the k-of-n.

There is intentionally no Policy.Issuer filter: the model is "feed the verifier only the objects you retrieved, then trust based on Result.Issuer". If you need issuer restriction inside evaluation, add an Issuer/TrustedIssuers field to Policy — but that is a policy choice, not a relay concern.

Approval flow

  1. Issuer sends ApprovalRequest{Recipient: approver, Action: predicate, ...}.
  2. Approver sends ApprovalResponse{RequestHash, Decision: Allow, ...} signed by the approver. The response is bound to the request and verified together via protocol.VerifyApprovalResponse.
  3. The relay enforces at most one response per request (422 on a second).
  4. verify collects valid Allow responses from Policy.Approvers and checks the count against Policy.Threshold.

Revocation

  • A claim is revoked by a Revocation signed by the claim's issuer (protocol.VerifyRevocationOf).
  • An approval is withdrawn by a Revocation targeting the response's object id, signed by the responder (the approver revoking their own decision).