niko_trust/docs/TRUST-MODEL.md
Niko Marmeladkov 79df689f7a refactor: drop PoW, checkpoint chain, gossip, light node and BFT; keep WS
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.
2026-08-26 00:49:25 +03:00

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

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.

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).