The decentralization stack overcomplicated the project. Removed:
internal/pow, internal/smt, internal/checkpoint, internal/bft,
internal/lightnode, cmd/lightnode, relay gossip/checkpoint/proof/BFT
endpoints, their docs, vectors and the blake3 dependency.
Kept: WebSocket streaming on the relay, the full protocol v1 object set
including DelegationClaim (0x07) and KeyRotation request/confirm
(0x08/0x09) with chain resolution in verify.Graph, TrustedIssuers,
batch fetch, stable cursor pagination, per-type metrics.
INV-1 reverts to its original form: the relay holds no keys again.
Everything removed remains reachable at commit 20cc52c.
4.5 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.
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).