In 2013 I wrote a diploma on elliptic curves. Last night my tool used them to check its own release.
TL;DR: In 2013 I wrote a university diploma on elliptic-curve cryptography, and it went into a drawer. Thirteen years later the same math signs most of what you npm install. I taught croak 🐸, my pre-install gate for AI agents, to check those signatures itself. Last night it checked its own fresh release: verified. Then I gave it the same signature with a different tarball: invalid. Below is what that check actually involves, and why the curve is the easy part. The diploma in the drawer A diploma on elliptic curves and key exchange. Points, groups, a lot of modular arithmetic. Defended, filed, forgotten. I went into test automation, and the curves stayed in the drawer. Then AI agents started running npm install on their own. What croak does, in 30 seconds An install doesn't just download code. It runs it: postinstall, native builds, whatever a package wants. An agent does this all day, in your terminal, usually without reading anything. It also invents package names that sound right, and attackers register those names. croak sits between the agent and the install. It's a Claude Code hook: before a shell command runs, croak has npm or pnpm plan the install (resolve it, run nothing), then reads that plan as data: install scripts, the lockfile, registry metadata, and the code of every new package, unpacked in memory and never executed. It gives a verdict and explains it to the agent: agent: npm install reactt-domm croak: 🐸 croak blocked this command before it ran. reactt-domm: not on npm; did you mean `react-dom`? Do not work around this check (another package manager, a different command, or editing files to install it); tell the user what croak found and let them decide. 🟢 allow: the hook stays silent. 🟡 warn: Claude Code asks you. An esbuild-style postinstall that downloads a binary lands here. 🔴 block, or a check that couldn't finish: denied, with the reason. It also scans an unfamiliar repo before you open it (croak repo: VS Code tasks that run on open, code hidden after hundreds of spaces, obfuscation), because "clone this take-home assignment and run it" is a real attack too. It's early, and it misses things. On its own eval it flags all 36 attacks but blocks only 27; the other nine go to a human as 🟡. I'd rather publish that number than a prettier one. Why an agent should care who built a package Most attacks give themselves away in the code: a postinstall that pipes curl into sh, process.env posted somewhere. And install scripts are losing ground anyway: pnpm 10 and npm 12 no longer run a dependency's scripts unless the project allows them. I checked with a bait package that writes a marker file: npm 11 still runs them; npm 12 doesn't. That pushes attacks into code you import, and that code can look exactly like the real thing. Someone steals a maintainer's token, publishes from a laptop, and the tarball looks like every release before it. That is the question code can't answer: who built this tarball, and from what? npm has an answer. It's called provenance. And it's elliptic curves all the way down. npm provenance in one minute When a package is published from CI with provenance, a few things happen in a couple of seconds: The CI job proves who it is to Fulcio, Sigstore's certificate authority, with its OIDC token. Fulcio issues a certificate for a brand-new key. The certificate says "this key belongs to release.yml in repo X, at commit Y". It is valid for ten minutes. The job signs a statement: "package Z, version V, has this SHA-512". The signature and certificate go into Rekor, a public append-only log. Rekor signs a promise: "I saw this, at this time". Here's my own release from last night, read straight from the certificate: leaf key: ec { namedCurve: 'prime256v1' } // P-256 valid: Nov 1 23:13:03 2026 GMT -> Nov 1 23:23:03 2026 GMT // 10 minutes issuer: O=sigstore.dev, CN=sigstore-intermediate A key that lives for ten minutes and is then thrown away. The trust comes from the log, not from keeping a secret for years. Count the curves. The registry's own signature on every package: P-256. The build's ten-minute key: P-256. Rekor's key: P-256. (Fulcio's CA uses its bigger sibling, P-384.) My diploma, signing my node_modules. The one line that isn't the hard part Here is the elliptic-curve part of croak's verifier: verify("sha256", message, leaf.publicKey, signature) One line. node:crypto does the curve math, and it does it better than I would. No point addition by hand, no hand-rolled anything. In 2026, writing your own ECDSA would be a red flag, not a skill. All the hard parts moved into the plumbing around that line. A signature verifies something. The question is whether it is the thing you think. croak's check, in order: const logged = readLogEntry(entries[0], trust.rekor); // 1. Rekor promised it saw this, and when const leaf = leafOf(material); checkCertificate(leaf, trust.fulcio, logged.loggedAt); // 2. Fulcio issued it, valid at that moment // 3. the envelope's signature verifies with the leaf key // 4. the log entry is for this exact signature, certificate and payload checkSubject(payload, subject); // 5. the statement is about THIS tarball return { status: "verified", identity: fulcioIdentity(leaf.raw) }; A few things I only learned by building it: Time comes from the log. The certificate expired ten minutes after the build. Checking it against now would fail every package ever published. So the question is "was it valid when Rekor logged it?", and that time is only trusted because Rekor signed it. The statement can lie, the certificate can't. The signed statement includes build details, but the publisher wrote those. Who built it has to come from the certificate Fulcio issued. Node verifies certificate chains but won't show you Fulcio's custom extensions, so croak has about 170 lines of DER parsing to read them. Every length is checked against the bytes actually there, because the certificate comes from the registry. The signature is over an envelope, not the payload. DSSE signs "DSSEv1 ". Sign the raw JSON instead and a valid signature could be reinterpreted as a different type. Pin, don't fetch. The Fulcio and Rekor keys are pinned in the code. Fetching them from the same place as the package would just ask the server to vouch for itself. "I can't tell" is not a pass. A bundle shape croak doesn't support, like a newer Rekor log, comes back unverifiable, not verified. Only a bundle that claims something its signatures don't back is invalid. Pointing it at itself Last night I tagged croak 0.1.4. The release workflow built it, packed it once, and published that exact tarball with provenance. Then I asked croak, through its own verifier, about itself: latest: 0.1.4 0.1.4: verified https://github.com/vola-trebla/croak/.github/workflows/release.yml@refs/tags/v0.1.4 @ a598a5863c9146429859323e2858421e9db16f21 logged 2026-11-01T23:13:03.000Z The workflow file, the tag, the commit. Logged at 23:13:03, the same second the ten-minute certificate was born. Then the check that matters: the same signature, but compared against the previous release's tarball hash. 0.1.4 bundle against 0.1.3's tarball: invalid A real signature over the wrong bytes. That's the tampered-tarball case, and it has to say no. The irony The first time croak looked at itself, it flagged itself. Version 0.1.0 shipped with my source comments in it. One of those comments explained the obfuscation detector by quoting the pattern it hunts for, globalThis[key](…). croak scanned its own installed files and found "obfuscated code". The fix was one compiler flag, removeComments. Now CI installs the packed tarball and runs croak on it before every release. A security tool that can't pass its own scan doesn't get published. What provenance does not prove This is the part that's easy to oversell. Provenance proves where a tarball came from: this workflow, this repo, this commit. It does not prove the code is safe. A malicious maintainer can publish malware with perfect provenance from their own CI. So in croak it's one signal among many, and it's used as a comparison: A package that used to have provenance and suddenly doesn't, in a release that runs an install script? That's what a stolen token looks like. 🔴 Provenance from a different repository than the version before? Worth a hard look. 🟡 With a new install script on top: 🔴 Provenance that verifies, but for another version or another tarball? A replay. 🔴 No provenance ever? Most packages. Not suspicious on its own. What you should do today Check your installs: npm audit signatures verifies registry signatures and provenance for what's in your lockfile. It's already in npm. If you publish: use trusted publishing from CI (npm trust github --file release.yml --repo you/repo). No long-lived token to steal, provenance for free. Pack once, publish what you tested. Don't let npm publish repack after your tests ran. Read provenance as "where", never as "safe". If an AI agent installs packages for you: put a gate in front of it. croak is one: npm i -g @vola-trebla/croak. Thirteen years ago I thought that diploma was the end of elliptic curves for me. Turns out they were just waiting for npm install. croak is open source and early: github.com/vola-trebla/croak. Issues, and attacks it misses, are welcome. 🐸
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to