How Vouchsafe Could Have Prevented the npm Debug/Chalk Compromise in 200 lines of Javascript

By Jay Kuri
2025-09-10

Yesterday, the npm ecosystem was rocked by news that the debug, chalk (and several other) packages were compromised and shipped with malicious code. Developers who pulled those versions into their projects unknowingly imported malware into their applications.

The attack was caught relatively quickly and corrected, but not before countless applications pulled in the bad code.

This kind of attack, where a trusted package suddenly ships bad code, isn’t new and isn’t unique to npm. It happens because the security model is effectively ‘if it’s on npm, it’s safe to use.’

This makes package registries like npm act as middlemen of trust. They assure us that you can trust the package because you trust them. And most of the time that’s true. But yesterday we saw what happens when that model breaks down.

Put another way, when running npm install you can trust that the package came from npm, but that doesn’t mean it got there legitimately.

There’s no guarantee that this package version really came from the maintainer you trust because there’s no portable, verifiable way to bind an author’s identity to the actual code being shipped.

Vouchsafe changes that.

Today I’m going to explain how and outline a way this trust issue could be fixed.

The Problem With Today’s Publishing

When you run npm publish, the registry verifies your account access and accepts your files and associates them with your account. Anyone who installs that version is trusting:

There’s no way for the end user (or their CI/CD pipeline, etc.) to check offline, independently that the maintainer actually signed off on the exact version they received.

How Vouchsafe Fixes This

With Vouchsafe, the maintainer creates a cryptographic identity easily, with one command:

[bob@dev] ~> create_vouchsafe_id -l bob_the_dev
Created identity: urn:vouchsafe:bob_the_dev.3gh2mjvkmb5cvt3ywqzr3pradn72ds4we5fhrvw4bm7blw6zwo5a
Saved to: bob_the_dev.json

That file now contains the developers vouchsafe ID and keys. (so treat it with the same care as you would your password)

The URN you see is cryptographically derived from their public key, and anyone can verify it matches the public key directly.

In other words it’s portable, self-verifying, and requires no registry lookups.

When they author is ready to publish a new package version, they generate a Vouchsafe attestation token:

That token is signed with their private key. It’s small, just a JWT with some application-specific claims-but it carries everything needed to verify it.

NOTE: no key, public or otherwise, needs to be distributed. Sign the token, you’re done.

How Users Verify the Package

Every package published to npm (or any registry) could include the attestation token inside the tarball (or alongside it).

When you install:

  1. Your tool obtains the package and the identity (or identities) associated with the package.
  2. It validates the token using vouchsafe.validateVouchToken(token) and ensuring the identity matches.
  3. It checks the package’s files against the entries in checksums.

If the token validates, you know the author produced that package and you can use the checksums to verify the files have not been tampered with. All of this can happen offline, without talking to npm or any other service.

If anything had been swapped, tampered, or re-signed, verification would fail immediately.

Why this works

Vouchsafe tokens are JWTs-but with critical enhancements:

Why didn’t someone do this already?

Part of the reason something like this hasn’t been done is the key management headache that goes along with it. Pushing keys around, revoking them, etc. is a headache. Key blobs are opaque and easy to swap without anyone easily noticing. AND they all have to be stored somewhere and accessible when you need them.

This is where Vouchsafe changes the game.

Because Vouchsafe tokens are entirely self-verifying, no key distribution is needed. Instead you simply need the human-readable URNs of the people you trust and what they are trusted for.

The token contains the public key needed to verify it. And the identity urn proves the public key hasn’t been tampered with. If anything is changed, the entire verification immediately fails. This process happens entirely offline, no online dependency whatsoever.

Why This Matters

In short: malicious packages like yesterday’s would never pass validation.

The attacker could publish them, but they couldn’t produce a valid Vouchsafe attestation token, and package managers would refuse to install.

Progressive enhancement

This wouldn’t require a ground-up rewrite of npm. It’s a drop-in upgrade that could be done even independently of npm. (but if you npm folks want to incorporate it directly, I’m all for it.)

In other words, your CI/CD pipeline keeps working untouched, but when your package manager is Vouchsafe-aware, you get protection for free.

The Bigger Picture

This isn’t just about npm. Any software distribution channel-pip, crates.io, Docker Hub, even system updates-faces the same problem. Vouchsafe gives us a universal, decentralized way to bind authors to code and make that binding portable and provable everywhere.

Instead of asking: “Do I trust the registry?” You can ask: “Do I trust this maintainer’s URN?”

And then verify it yourself, in milliseconds.

Trust can be chained as well so npm could publish authors it trusts. You’re team could have it’s own list of authors you trust, flagging any packages that get new authors for review. A security firm could issue attestations that certain packages/versions have been reviewed, etc.

Let me demonstrate

Ok. So talk is cheap. I promised some code.

Lets imagine that we want to do this. Seems like a lot of work. It’s not. As I mentioned, it doesn’t even require participation by npm.

The process would look like this. Prior to publish, the package maintainer runs a script, we’ll call it make-publish-vch.mjs. This script determines what files would be published by npm and creates a vouch token including the package version and a sha256 checksum for each file. It places this in the root of the package as publish.vch. Then the package maintainer publishes the package.

On the other side, you can recognize that a publish.vch is there and you can run another script, say verify-publish-vch.mjs to validate the files. If the script passes, all files have been checked and have not been tampered with and it’s safe to install. If the vouch doesn’t come from who it should, the script fails. If any file has been changed, the script fails.

Sounds pretty straightforward, right? I’m not speaking hypothetically… the scripts are below. Less than 200 lines. Built in about an hour. Cryptographically provable origin, offline verifiable, no key distribution, drop-in simple.

Yes, these are simplified versions of what we would actually want, but they do actually accomplish the thing.

make-publish-vch.mjs ( download )


#!/usr/bin/env node
// make-publish-vch.mjs
//
// Usage: node make-publish-vch.mjs ./my-identity.json
//
//  - identity JSON created by `create_vouchsafe_id -l 

verify-publish-vch.mjs ( download )


#!/usr/bin/env node
// verify-publish-vch.mjs
//
// Usage: node verify-publish-vch.mjs  [tokenFile]
//
//   permitted_urn  = the ONLY issuer URN you accept for this package (receiver policy)
//   tokenFile      = defaults to ./publish.vch
//
// Exits nonzero (and shouts) on any mismatch or policy failure.

import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { createHash } from 'node:crypto';
import { validateVouchToken } from 'vouchsafe'; // per README

function usage(msg) {
  if (msg) console.error('Error:', msg);
  console.error('Usage: node verify-publish-vch.mjs  [tokenFile]');
  process.exit(2);
}

const permittedUrn = process.argv[2];
const tokenFile = process.argv[3] || 'publish.vch';
if (!permittedUrn) usage('missing permitted_urn');

// read the token file
const token = readFileSync(resolve(tokenFile), 'utf8').trim();

// 1) Validate the token with the vouchsafe 
let decoded;
try {
  const res = await validateVouchToken(token); // throws on invalid
  decoded = res?.decoded || res || {};
} catch (e) {
  console.error('❌ Token failed cryptographic validation:', e.message || e);
  process.exit(1);
}

// 2) Enforce receiver policy: token issuer must match permitted URN
//    For simplicity in this demo we use one urn, you could support 
//    multiple permitted publisher URNs

if (decoded.iss !== permittedUrn) {
  console.error('❌ Issuer URN not permitted.');
  console.error(`   expected: ${permittedUrn}`);
  console.error(`   got:      ${decoded.iss}`);
  process.exit(1);
}

// 3) look at files in publish.vch, hash, and compare

const expected = decoded.checksums || {};

const expKeys = new Set(Object.keys(expected));

const actual = {};
for (const rel of expKeys) {
  try {
      const data = readFileSync(resolve(rel));
      actual[rel] = createHash('sha256').update(data).digest('hex');
  } catch(e) {
      console.error(`❌ Unable to verify FILE: ${rel} - `, e.toString());
  }
}

let ok = true;
const actKeys = new Set(Object.keys(actual));

for (const k of expKeys) {
  if (!actKeys.has(k)) {
    console.error(`❌ MISSING FILE: ${k}`);
    ok = false;
  } else if (expected[k] !== actual[k]) {
    console.error(`❌ HASH MISMATCH: ${k}
   expected: ${expected[k]}
     actual: ${actual[k]}`);
    ok = false;
  }
}
for (const k of actKeys) {
  if (!expKeys.has(k)) {
    console.error(`❌ EXTRA FILE: ${k}`);
    ok = false;
  }
}

// This is a demo, so it's slightly simplified.  
// 
// If we were verifying for real, we'd add a check
// for any files that are present in the unpacked directory that
// we don't have a vouch for, indicating an attempted injection.
// but for simplicity of demonstration, so that you can run it
// in the working directory, I've omitted that to avoid
// complaining about your .git and node_modules folders etc.

if (!ok) {
  console.error('\n🚨🚨🚨  VERIFICATION FAILED — ABORT INSTALL  🚨🚨🚨');
  process.exit(1);
}

console.log(`✅ OK — token issuer permitted and all ${actKeys.size} checksums match.`);

How you use the above scripts:


# 0) Install the library and create an identity (one-time)
npm install -g vouchsafe

create_vouchsafe_id -l maint -o maint.json   # produces JSON with urn+keypair. Keep this file safe!

# cd to package folder
cd dev/my_package

# 1) Generate an attested manifest of the exact files npm would publish
node path/to/make-publish-vch.mjs location/of/maint.json

# 2) Receiver (or your CI) verifies BEFORE install:
#    (Pass the permitted maintainer URN shown in maint.json)
node path/to/verify-publish-vch.mjs "urn:vouchsafe:maint.……" 
# -> OK — token issuer permitted and all checksums match.

# 3) Tamper test: change something that would be published
echo "// sneaky change: Send me your money! " >> README.md

# 4) Verify again (simulating the receiver)
node path/to/verify-publish-vch.mjs "urn:vouchsafe:maint.……" 
# -> HASH MISMATCH …  !!! VERIFICATION FAILED — ABORT INSTALL

Summary

So… what just happened? Well, I showed you how easy it is to use Vouchsafe… Less than 200 lines of code to put some real identity and trust verification into the npm ecosystem.

One additional command prior to publish (and could be incorporated into the npm publish process or even added to the package.json as a prepublish step) and an additional step prior to install on the receiving-side.

Would the real deal need a bit more, sure… but not a ton and mostly related to how and who is actually integrating it. We’d need to settle on a way to record which identities should be trusted to publish a package, but a single additional field in the npm database for a package would do the job. Or an agreement to put them in a PUBLISHERS in the git repo, or whatever.

WTF did I just read.

Vouchsafe is a new primitive for identity and trust. It’s based on JWTs so it’s easy to understand and works anywhere JWTs work. It makes the things you do with JWTs now easier and adds some superpowers, like verification without key distribution headaches, and offline validation.

Check out the website, the npm module or, if you’re particularly technical minded, check out the open spec

If it seems interesting and you want to talk about it, Join us on the discord

Vouchsafe: identity you can prove, trust you can carry.

Read more at getvouchsafe.org.

Get the reference implementation package.

Read the open spec.

Or join us on Discord.