# 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 = subject =
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": "", "signature": "" }`). 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. ## 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).