JuliusBrussee/caveman · critical

binary signing private key does not match committed public k

Error message

binary signing private key does not match committed public key

What it means

Thrown by scripts/sign-binary-checksums.mjs when the private key from CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM derives a different public key than the committed public-key.pem passed on the command line. Both are normalized to canonical SPKI PEM before comparing, so formatting differences cannot cause a false mismatch — a hit means the key pair genuinely differs. This prevents signing binaries with a key the published verification flow does not trust.

Source

Thrown at scripts/sign-binary-checksums.mjs:49

    );
}

function normalizePublicKey(value) {
  return createPublicKey(value).export({ type: "spki", format: "pem" }).toString();
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  try {
    const [checksumsPath, outputPath, publicKeyPath] = process.argv.slice(2);
    const privateKeyPEM = process.env.CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM;
    if (!checksumsPath || !outputPath || !publicKeyPath) {
      throw new Error("usage: sign-binary-checksums.mjs <checksums.txt> <output.keysig> <public-key.pem>");
    }
    if (!privateKeyPEM) throw new Error("CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM is required");
    const checksums = readFileSync(checksumsPath);
    const publicKeyPEM = readFileSync(publicKeyPath, "utf8");
    if (normalizePublicKey(privateKeyPEM) !== normalizePublicKey(publicKeyPEM)) {
      throw new Error("binary signing private key does not match committed public key");
    }
    const bundle = checksumSignatureBundle(checksums, privateKeyPEM);
    if (!verifyChecksumSignatureBundle(checksums, bundle, publicKeyPEM)) {
      throw new Error("generated checksum signature failed local verification");
    }
    writeFileSync(outputPath, `${JSON.stringify(bundle)}\n`, { mode: 0o600 });
  } catch (error) {
    process.stderr.write(`${error.message}\n`);
    process.exit(1);
  }
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Export the private key that corresponds to the committed public-key.pem and re-run.
  2. If the rotation is intentional, generate the new key pair, commit the new public-key.pem, and update the CI secret to match, then re-run.
  3. Verify the pair locally: `diff <(openssl pkey -in priv.pem -pubout) keys/binary-public.pem`.

Example fix

# before: env holds rotated private key, repo holds old public key
CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM="$(cat new-key.pem)" node scripts/sign-binary-checksums.mjs ...
# after: commit the matching public key first
git add keys/binary-public.pem && git commit -m 'rotate binary signing key'
CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM="$(cat new-key.pem)" node scripts/sign-binary-checksums.mjs dist/checksums.txt dist/checksums.keysig keys/binary-public.pem
Defensive patterns

Strategy: validation

Validate before calling

# Verify the key pair matches BEFORE running the signer.
if [ "$(openssl pkey -in "$PRIV_PEM" -pubout | openssl pkey -pubin -outform DER | sha256sum)" != \
     "$(openssl pkey -pubin -in "$PUB_PEM" -outform DER | sha256sum)" ]; then
  echo "signing key does not match committed public key" >&2; exit 1
fi

Try / catch

try { signRelease(); } catch (e) { if (/does not match committed public key/.test(e.message)) haltRotation(); else throw e; }

Prevention

When it happens

Trigger: Using a newly generated or wrong private key while the repo still carries the old committed public key; rotating the key pair but updating only one side; pointing the third argument at a stale copy of the public key.

Common situations: Post-rotation release where the new private key is in the environment but the old public-key.pem is still committed; multiple signing keys in an org and the wrong one configured; CI secret updated ahead of the repo.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/f2d98dda638010d8. Report an issue: GitHub.