Skip to content

Supply-chain hardening: minimal runtime deps, locked-down CI, install-time guarantees - #634

Open
OBrezhniev wants to merge 14 commits into
feature/memory-scopingfrom
feature/supply-chain-hardening
Open

Supply-chain hardening: minimal runtime deps, locked-down CI, install-time guarantees#634
OBrezhniev wants to merge 14 commits into
feature/memory-scopingfrom
feature/supply-chain-hardening

Conversation

@OBrezhniev

Copy link
Copy Markdown
Member

Reduces the supply-chain attack surface across the dependency tree, the install pipeline, CI, and the template renderer. Stacked on #628.

Runtime dependency tree: 16 → 10 packages

Every removed package is replaced by a small, tested, zero-dependency module with byte-identical output:

removed replacement verification
ejs (recurring CVE history) src/template_render.js (~70 lines, exact ejs-subset semantics) verifiers byte-identical for groth16/plonk/fflonk; hardhat contract suite 7/7; additionally rendered the templates of PRs #221 #227 #345 #468 #553 #615 byte-identically vs ejs
bfj + check-types + hoopy + tryer src/json_writer.js (streaming, backpressured) byte-identical to JSON.stringify(v, null, 1) (10 pinned cases); on a 1.1 GB zkey → 2.59 GB JSON: byte-identical and 3.2× faster (12:53 → 3:58), same peak RSS
logplease (unmaintained since 2017) src/cli_logger.js (~60 lines) identical line format/colors; all library call sites now use the universal debug/info/warn/error set

What remains at runtime: five iden3 repos pinned to git commit SHAs, three iden3-owned registry packages, @noble/hashes, and the web-worker shim.

Install-time guarantees

  • .npmrc: ignore-scripts=true (the whole tree is verified script-free — a future postinstall becomes an explicit decision, not silent code execution) and save-exact=true
  • registry runtime deps exact-pinned
  • Dependabot: 7-day cooldown for npm and actions (registry-takeover malware typically lives for hours), plus a github-actions ecosystem entry to maintain the SHA pins

CI lockdown

  • actions pinned to commit SHAs (tags are mutable — the tj-actions compromise moved tags)
  • permissions: contents: read on all workflows; persist-credentials: false on every checkout (no job pushes, so the GITHUB_TOKEN never lands in .git/config where a compromised dev dep running under npm test could read it; workflows reference no other secrets)
  • root installs run npm ci --ignore-scripts; npm audit signatures verifies registry signatures + provenance on every run
  • tutorial workflow: checkout@v2 → pinned v6, npm installnpm ci, circom binary release-pinned and sha256-verified

Renderer security review (applied findings)

  • render() rejects data keys shadowing its internals (a __out key silently produced an empty document), throws on unterminated <% tags and non-string templates (fail closed, as ejs did)
  • both verifier exporters assert every interpolated vk leaf is a plain decimal number, so a poisoned vk object from an untrusted vkey JSON cannot smuggle source text into a generated contract (zkey-derived vks always satisfy this; validation is deliberately in the exporters, not the renderer — upstream PR templates legitimately interpolate non-decimal shapes)
  • verified empirically: data values can never reach the compiled source; template chunks are JSON.stringify-embedded (quote/backslash/U+2028-safe); escape table matches ejs byte-for-byte

Templates themselves remain trusted code by contract, exactly as with ejs.

Verification

Full suite 284 passed + 1 skipped (16 new tests), lint clean, bundles rebuilt, hardhat verifier contracts green, all byte-compat comparisons above.

Two follow-ups deliberately not included: repo Settings can't be set from files (default workflow token → read-only; require approval for outside-collaborator runs), and the same CI/npmrc treatment for the sibling repos.

🤖 Generated with Claude Code

https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp

OBrezhniev and others added 14 commits August 30, 2026 22:17
…alls

- actions/checkout and actions/setup-node pinned to the commit SHAs the
  v6 tags currently resolve to (tags are mutable; SHAs are not)
- permissions: contents: read on both workflows
- root npm ci runs --ignore-scripts (the dependency tree is verified to
  contain zero install scripts; the hardhat/browser_tests subprojects
  keep scripts for their native toolchains)
- npm audit signatures step verifies registry signatures + provenance
- tutorial: checkout@v2 -> pinned v6, npm install -> npm ci, and the
  circom binary download is now release-pinned and sha256-verified

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
- .npmrc: ignore-scripts=true (tree is verified script-free; a future
  postinstall becomes an explicit decision) and save-exact=true. The
  pretest hook is folded into the test script since ignore-scripts also
  disables pre/post hooks.
- registry runtime deps exact-pinned (@noble/hashes, bfj, ejs,
  logplease; circom_runtime already was)
- renovate.json with minimumReleaseAge 7 days: registry-takeover
  malware is typically live for hours, a cooldown makes it unreachable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
Same line format and colors ([LEVEL]  snarkJS: message), same level
gating, log() kept as the info alias the ceremony progress messages
use. logplease was unmaintained since 2017.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
All 13 CLI call sites were bfj.write(file, value, {space: 1}). The new
src/json_writer.js streams the value with an explicit stack and
backpressured 1 MiB chunks (the exported r1cs/ptau/witness JSONs reach
hundreds of MB), producing output byte-identical to
JSON.stringify(value, null, space) -- verified byte-for-byte against
bfj on wtns/zkey/vkey/ptau exports of real artifacts; r1cs export
differs only in empty-object formatting ("{}" instead of bfj's
"{\n   }"), which converges with how proofs were already written.
Removes bfj + check-types + hoopy + tryer from the runtime tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
The Solidity verifier templates use exactly four constructs: <% %>
scriptlets, <%= %> escaped output, <%- %> raw output, and the -%>
newline slurp. src/template_render.js implements that subset with
ejs's semantics (with-scoping of the data object, null/undefined as
empty, the same XML escape table). Rendered verifiers verified
byte-identical to ejs output for all three protocols, and the hardhat
verifier-contract suite passes (7/7). Removes a runtime dependency
with a recurring CVE history; also shrinks the browser bundles, which
inlined all of ejs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
logger.log was a logplease-ism: console-style loggers treat it as info
but winston-style loggers read the first argument as a level, so the
four ceremony progress calls were the one place a foreign logger could
misbehave. Every library call site now sticks to debug/info/warn/error
-- the interface every logging library agrees on -- and the CLI logger
sheds the now-unused alias.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
The update-delay protection intended by the removed renovate.json,
expressed in the tool this repo actually uses. Also lets Dependabot
maintain the commit-pinned actions (it bumps the SHA and the version
comment together).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
The workflows reference no secrets, so the only credential in a job is
the automatic GITHUB_TOKEN -- which actions/checkout persists into
.git/config by default, readable by every later step (including
anything a compromised dev dependency executes under npm test). No job
pushes, so no checkout needs credentials at all:
persist-credentials: false everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
Findings from the ejs-replacement security review, all fail-closed:
- render() rejects data keys starting with __ (with-scoping would let
  them shadow the renderer's internals: a __out key silently produced
  an empty document, __esc/__raw a confusing TypeError)
- render() throws on an unterminated <% tag instead of emitting the raw
  tag text (ejs threw here too), and on a non-string template
- both verifier exporters assert every interpolated vk leaf is a plain
  decimal number (protocol/curve tags exempt), so a poisoned vk object
  from an untrusted vkey JSON cannot smuggle source text into the
  generated contract; zkey-derived vks always satisfy this

Rendered verifiers for all three protocols remain byte-identical to the
ejs output. Templates themselves remain trusted code by contract,
unchanged from ejs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
The direct bfj import survived the bfj removal unnoticed because the
nested worktree resolved it from the parent checkout's node_modules;
CI (npm ci from the lockfile) correctly had no bfj to import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
Root cause of the one hung windows job on this PR: npm ci stalled in a
git-dependency clone (npm applies no timeout to those, unlike registry
fetches) and silently consumed the whole 30-minute job budget. The
lockfile is https-only -- npm's 'ssh://' integrity warnings are its
cosmetic hosted-git wording, not the fetch protocol. A capped install
step turns a stalled clone into a fast, clearly-attributed failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
The hung windows job ran with a cold cache (this PR changes
package-lock.json, so every OS's first run re-clones the five git
deps); the rerun passed precisely because the sibling windows job had
saved a warm cache by then. Close the remaining gaps: the tutorial
workflow had no cache at all, the contracts job now keys on both
lockfiles it installs, and the browser-bundles job keys on the only
lockfile it uses instead of the root one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01833VaUEJmrFZ7bVprrWwpp
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