Skip to content

c0mpute edge: verify every byte from an untrusted gateway against the owner's signature - #35

Merged
ralyodio merged 1 commit into
mainfrom
feat/edge-verify-untrusted-gateways
Nov 9, 2026
Merged

ralyodio merged 1 commit into
mainfrom
feat/edge-verify-untrusted-gateways

Conversation

@ralyodio

@ralyodio ralyodio commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Any c0mpute node can now be a public gateway (nodes announce gateway on /api/nodes, and the edge's GatewayPool already discovers and load-balances them). This makes the edge refuse to trust a stranger's gateway.

Trust

  • Trusted, proxied as before: the C0MPUTE_GATEWAYS seeds (our dev2 gateways) and the new C0MPUTE_TRUSTED_GATEWAYS.
  • Everything discovered from /api/nodes is untrusted and gives verified reads only. C0MPUTE_VERIFY_ALL=1 verifies ours too.

Verified read (new lib/c0mpute-verify.ts, mirrors c0mpute hosting/site.rs)

  • GET {gw}/hosting/v1/sites/{site}/resolve?path= (c0mpute >= 0.3.5) gives the record, the manifest (and v2 parts) and the gateway's resolution.
  • Checks: ed25519 over the v1/v2 signing bytes; site_id = first 20 hex of blake3(c0mpute-site\n{owner}\n{name}) and equal to the requested site; blake3 of the manifest and each part; manifest name; candidate resolution recomputed locally (first present candidate, then 404.html as a 404; a gateway cannot skip a candidate that exists or leave out a part it needs); entry must equal the manifest's; chunk count, and each chunk's blake3 and length.
  • Chunks come from any gateway (/hosting/v1/sites/{site}/chunks/{hex}), are cached on disk by hash ($EDGE_CERT_DIR/chunks, re-hashed on read).
  • Rollback guard: highest verified seq per site, in memory and in $EDGE_CERT_DIR/site-seqs.json; a lower seq from any gateway is refused.
  • A gateway that answers 404 (no routes yet) sits out verified reads for 5 min, one that fails a check for 10 min. It is never proxied unverified. The first chunk is fetched before the status line so an unavailable file falls through to the next gateway and then the stale copy; a later chunk that cannot be had cuts the response short.
  • Verified responses carry x-c0mpute-verified: 1, the gateway's ETag (304 on If-None-Match), cache-control: public, max-age=60, nosniff.

blake3: @noble/hashes 2.4.0, vendored unmodified under vendor/noble-hashes (MIT) so the dev2 checkout keeps running with a plain git pull, no npm install (dev2 has no npm).

Tests: node --test tests/*.test.ts: 132 pass (18 new). Fixtures generated in TS in the Rust formats (ed25519 key, serde_json/BTreeMap manifests, v2 parts), plus golden records signed by the Rust node. Covers tampered chunk, wrong signature, forged record, wrong site id, rollback (pure and across gateways), v2 parted site (missing and tampered part), candidate skipping (foo.html claimed when foo exists, also across parts), 404.html, an old gateway without routes (never sent a /s/ request), and C0MPUTE_VERIFY_ALL. I also checked every record, manifest and chunk in a local Rust node's hosting catalog (4 v1 + 2 v2 records); all verify.

Smoke against the live network: with verifyAll, every current gateway (dev2 and the dev3/vienna nodes) answers 404 on the resolve route, so all of them are skipped and the edge answers 503 instead of serving unverified bytes. Without it, cloud.killer serves from the trusted dev2 gateway exactly as before.

🤖 Generated with Claude Code

… owner's signature

Any c0mpute node can now announce a public gateway, and the edge already
discovers and load-balances them from /api/nodes. It must not trust a
stranger's bytes. Only our own gateways (C0MPUTE_GATEWAYS seeds and the new
C0MPUTE_TRUSTED_GATEWAYS) are proxied as they answer; every discovered gateway
gives verified reads only (C0MPUTE_VERIFY_ALL=1 verifies ours too).

A verified read asks the gateway for /hosting/v1/sites/<site>/resolve?path=
(c0mpute >= 0.3.5) and checks, mirroring hosting/site.rs: the ed25519
signature over the v1/v2 signing bytes, site id = blake3(owner, name), the
manifest and every v2 part by blake3, the path resolution itself (first
present candidate, 404.html as a 404, no skipped candidate, entry equal to the
manifest's), and each chunk (from any gateway, /chunks/<hash>) by hash and
length. A record below the highest seq already verified for the site is
refused (rollback), persisted in $EDGE_CERT_DIR/site-seqs.json. Verified
chunks are cached on disk by hash ($EDGE_CERT_DIR/chunks).

A gateway without the routes (404) or that fails a check sits out verified
reads (5 / 10 minutes); the edge never falls back to its unverified /s/ bytes.
The first chunk is fetched before the status line so an unavailable file falls
through to the next gateway and the stale copy; a later chunk that cannot be
had cuts the response short rather than send anything unverified.

blake3 is @noble/hashes 2.4.0, vendored unmodified (vendor/noble-hashes) so the
dev2 checkout keeps running with no npm install. Cross-checked against records,
manifests and chunks produced by the Rust node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit 909e25e into main Nov 9, 2026
3 checks passed
@ralyodio
ralyodio deleted the feat/edge-verify-untrusted-gateways branch November 9, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant