- 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.
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
trust1abcsays:trust1defhasminecraft.op: true.
-
trust1abcbuilds aClaim{Issuer: trust1abc, Subject: trust1def, Claims: {"minecraft.op": true}}, encodes it toTCE, signs it withtrust1abc's key. -
PUT /v1/objectsstores the envelope. The relay does not verify the signature — it only checks the envelope is well-formed and content-addressed. -
Later, a consumer queries
GET /v1/claims?subject=trust1defand receives a list of envelopes ({ "tce": "<base64>", "signature": "<base64>" }). The consumer decodes theTCEto readissuer/subject/claims. -
The consumer runs
verify.Graph.Add(env), which callsenv.Verify()and confirms the signature matchestrust1abc's key — i.e.trust1abcreally 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 (
PUTreturns200even 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.Issueris 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.Approverslists the addresses whoseAllowresponses count, andPolicy.Thresholdis 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
- Issuer sends
ApprovalRequest{Recipient: approver, Action: predicate, ...}. - Approver sends
ApprovalResponse{RequestHash, Decision: Allow, ...}signed by the approver. The response is bound to the request and verified together viaprotocol.VerifyApprovalResponse. - The relay enforces at most one response per request (
422on a second). verifycollects validAllowresponses fromPolicy.Approversand checks the count againstPolicy.Threshold.
Revocation
- A claim is revoked by a
Revocationsigned by the claim's issuer (protocol.VerifyRevocationOf). - An approval is withdrawn by a
Revocationtargeting the response's object id, signed by the responder (the approver revoking their own decision).