Skip to content

Commit 22cf78d

Browse files
committed
docs: align trust documentation for router updates
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I1c2eeda1271671cac6cc69a8aaa12ddf6a6a6964
1 parent f0d457f commit 22cf78d

3 files changed

Lines changed: 31 additions & 42 deletions

File tree

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ further in the [architechture document].
116116
- `GET /metrics`: Prometheus metrics
117117
- `GET /health`: JSON health summary of configured upstreams
118118
- `GET /trust/<hash>.narinfo`: trust decision and matching claim count
119-
- `GET /provenance/<hash>.narinfo`: stored signer claims for a narinfo hash
120119

121120
### Notes
122121

@@ -148,7 +147,8 @@ further in the [architechture document].
148147
[trust documentation]: ./docs/trust.md
149148

150149
`trust.mode = "signed"` accepts only narinfos that verify against the selected
151-
upstream's configured `public_key`. `trust.mode = "quorum"` records signed
150+
upstream's configured signer keys (`public_key` and `public_keys`).
151+
`trust.mode = "quorum"` records signed
152152
claims and accepts a route only after enough distinct signer keys agree on the
153153
same `StorePath`, `NarHash`, `NarSize`, and `References`. With
154154
`mesh.gossip_trust_claims`, peers relay re-verified claims so a quorum can form
@@ -247,7 +247,6 @@ mode = "off" # off | signed | quorum
247247
threshold = 2 # signer agreement needed in quorum mode
248248
require_distinct_signers = true # count signer keys, not upstream URLs
249249
fail_closed = true # reject untrusted candidates when enabled
250-
# claim_ttl = "30d" # optional: ignore claims older than this in quorum
251250

252251
[logging]
253252
level = "info" # tracing filter directive, e.g. debug or ncro=debug,tower_http=warn
@@ -556,9 +555,9 @@ from unlisted senders or with invalid signatures are silently dropped.
556555

557556
> [!TIP]
558557
> Setting `mesh.gossip_trust_claims = true` additionally gossips the trust
559-
> claims this node has verified locally and accepts claims relayed by peers,
558+
> claims this node has verified and accepts claims relayed by peers,
560559
> letting a `quorum` policy form across the mesh. A relayed claim only counts if
561-
> its signer key is trusted (a configured upstream key or one listed in
560+
> its signer key is trusted (a configured upstream or enabled fallback key, or one listed in
562561
> `trust.trusted_keys`) **and** its narinfo re-verifies against that key, so a
563562
> peer can only relay real signatures from trusted signers, never fabricate a
564563
> quorum with throwaway keys.

config.example.toml

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ upstream_cooldown = "15s"
6060

6161
[trust]
6262
# off | signed | quorum. In signed mode, accepted narinfos must verify against
63-
# the selected upstream's public_key. In quorum mode, at least threshold
63+
# one of the selected upstream's configured signer keys (`public_key` or
64+
# `public_keys`). In quorum mode, at least threshold
6465
# distinct configured signer keys must agree on StorePath, NarHash, NarSize,
6566
# and References before ncro stores a route. See docs/trust.md for the full
6667
# model.
@@ -69,14 +70,11 @@ mode = "off"
6970
require_distinct_signers = true
7071
threshold = 2
7172

72-
# Optional maximum age for a claim to count toward a quorum. Unset = never
73-
# expires. Prevents a long-dead signer from propping up a quorum forever.
74-
# claim_ttl = "30d"
7573
# Signer keys trusted to vouch for content in quorum mode, in addition to the
76-
# public_key of every configured upstream. A quorum counts only claims signed
77-
# by a key in this set; without it an attacker could self-sign forged content
78-
# under throwaway keys and forge agreement. In a mesh, list the other nodes'
79-
# upstream signer keys here so their relayed claims count.
74+
# keys configured on regular upstreams and an enabled fallback cache. A quorum
75+
# counts only claims signed by a key in this set; without it an attacker could
76+
# self-sign forged content under throwaway keys and forge agreement. In a mesh,
77+
# list the other nodes' upstream signer keys here so their relayed claims count.
8078
# trusted_keys = ["cache-b-1:base64key=", "cache-c-1:base64key="]
8179

8280
[discovery]
@@ -93,10 +91,10 @@ gossip_interval = "30s"
9391
peers = [ ]
9492
private_key = "/etc/ncro/node.key"
9593

96-
# When true, this node also gossips locally-verified trust claims to peers and
97-
# accepts claims relayed by peers (each re-verified against its original Nix
98-
# signer key before being trusted). Lets a quorum form across a mesh where each
99-
# node sees only one upstream. Only meaningful with trust.mode = "quorum".
94+
# When true, this node gossips trust claims it has verified and accepts claims
95+
# relayed by peers (each re-verified against its original Nix signer key before
96+
# being trusted). Lets a quorum form across a mesh where each node sees only one
97+
# upstream. Only meaningful with trust.mode = "quorum".
10098
gossip_trust_claims = false
10199

102100
[logging]

docs/trust.md

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ is cached.
6262
| Mode | Requirement to accept a candidate |
6363
| -------- | ------------------------------------------------------------------------------- |
6464
| `off` | No verification. Route-only behavior (default). |
65-
| `signed` | The narinfo must verify against the serving upstream's configured `public_key`. |
65+
| `signed` | The narinfo must verify against one of the serving upstream's configured signer keys. |
6666
| `quorum` | A verified signature **plus** at least `threshold` matching claims (below). |
6767

6868
<!--markdownlint-enable MD013-->
@@ -77,16 +77,18 @@ Every time ncro verifies a signature it records a **trust claim** in SQLite (see
7777
vouched for what:
7878

7979
- `signer_key` / `signer_name` -> the Nix signing key that verified.
80-
- `upstream_url` -> which upstream served this observation.
80+
- `upstream_url` -> the serving upstream for a local claim, or the
81+
`mesh://<relay-address>` that supplied a relayed claim.
8182
- the signed content tuple: `nar_hash`, `nar_size`, `references` (and the
8283
`store_path` / `narinfo_hash`).
8384
- provenance-only fields recorded for audit but **not** covered by the
8485
signature: `deriver`, `ca`, `file_hash`, `file_size`, and the raw `narinfo`
8586
bytes.
8687

8788
Claims are what make `quorum` and the mesh integration possible: they are a
88-
durable, queryable record of "signer X, via upstream Y, attests that path H has
89-
this NAR hash."
89+
durable, queryable record that signer X attests that path H has this NAR hash.
90+
For relayed claims, the relay address is recorded rather than an unverified
91+
assertion about the original upstream.
9092

9193
## Quorum counting
9294

@@ -114,7 +116,7 @@ The signature check alone would be theatre.
114116
ncro therefore counts a claim toward a quorum **only if its `signer_key` is in
115117
the trusted set**:
116118

117-
$$\text{trusted set} = \left\{ \text{upstream}_i.\text{public\_key} \right\} \cup
119+
$$\text{trusted set} = \left\{ \text{all configured upstream and enabled fallback signing keys} \right\} \cup
118120
\text{trust.trusted\_keys}$$
119121

120122
- Locally-recorded claims always use a configured upstream key, so a single node
@@ -124,11 +126,6 @@ $$\text{trusted set} = \left\{ \text{upstream}_i.\text{public\_key} \right\} \cu
124126
signed by an unknown key is dropped (and never even stored). This is what
125127
bounds a quorum to _trusted_ signers rather than _any_ signer.
126128

127-
`trust.claim_ttl` (optional, e.g. `"30d"`) bounds how old a claim may be and
128-
still count. Claims whose `last_seen` is older than the TTL are ignored when
129-
evaluating the quorum, so a signer that has long stopped serving a path cannot
130-
prop up a quorum forever. Unset means claims never expire.
131-
132129
> [!WARNING]
133130
> Quorum needs more than one independent signer to ever see the same path. On a
134131
> single node that only races caches sharing one signing key, a `threshold > 1`
@@ -166,33 +163,29 @@ With `mesh.gossip_trust_claims = true`, a node:
166163
set (above). A claim from any other key is dropped immediately, this is
167164
what stops an attacker from minting throwaway keys to forge a quorum, and
168165
it also bounds how many claims a hostile peer can write to the database.
169-
- **Valid signature.** The claim carries the original narinfo bytes, and the
170-
receiver re-verifies that narinfo against the claim's `signer_key`. The
171-
peer's packet signature only proves _which peer relayed it_, not that the
172-
content was signed; re-verifying means a peer cannot forge a claim under a
173-
trusted key it does not actually hold.
166+
- **Valid signature and canonical fields.** The claim carries the original
167+
narinfo bytes. The receiver re-verifies that narinfo against the claim's
168+
`signer_key` and rebuilds every signed field from it, so a peer cannot
169+
substitute a different path or content tuple.
174170

175171
This means **a peer becomes a witness, never a substitute for a real Nix
176172
signature.** A malicious or compromised peer cannot inflate a quorum: a claim
177173
signed by an untrusted key is dropped, and a claim claiming a trusted key but
178174
without a valid signature fails re-verification. A quorum therefore counts
179175
distinct _trusted Nix signers_, whether those signatures were observed locally
180-
or relayed by a peer. Peers are also allowlisted by their mesh public key
181-
(`mesh.peers[].public_key`), so unknown senders are rejected before any claim is
182-
even parsed.
176+
or relayed by a peer. Configure `mesh.peers[].public_key` to also allowlist mesh
177+
senders; peers without configured keys are accepted at the transport layer but
178+
still cannot alter the signed claim tuple.
183179

184180
## Inspecting Trust (At Runtime)
185181

186-
Two read endpoints expose the recorded claims:
182+
The trust endpoint exposes the recorded claims:
187183

188184
- `GET /trust/<hash>.narinfo` - the current trust decision for a path: the mode,
189185
threshold, the count of matching claims, whether the path is currently
190186
trusted, and the full list of claims.
191-
- `GET /provenance/<hash>.narinfo` - the same claim list, framed as an audit of
192-
who has vouched for the path (including the unsigned provenance fields).
193-
194-
Both are useful for confirming that claims are propagating across a mesh: after
195-
a path is resolved on one node, it should appear in `/trust` on a peer within a
187+
It is useful for confirming that claims are propagating across a mesh: after a
188+
path is resolved on one node, it should appear in `/trust` on a peer within a
196189
gossip interval or two.
197190

198191
## Configuration Summary
@@ -203,7 +196,6 @@ mode = "quorum" # off | signed | quorum
203196
threshold = 2 # matching claims required in quorum mode
204197
require_distinct_signers = true # count distinct signer keys, not mirrors
205198
fail_closed = true # reject (true) vs serve+warn (false)
206-
claim_ttl = "30d" # optional: ignore claims older than this
207199

208200
# Signer keys trusted to vouch in quorum mode, in addition to upstream keys.
209201
# In a mesh, list the OTHER nodes' upstream signer keys here.

0 commit comments

Comments
 (0)