#!/usr/bin/env bun /** * Verify v1/index.json.sig against v1/index.json. * * This is exactly what the host does before it will parse a single byte of the * catalog, so a green run here means a host that pins the same key will accept * the index. Used as a post-sign self-check in CI, and by anyone auditing. * * Public key, in precedence order: * --pub ed25519: (or a path to a file containing that string) * $INDEX_PUBLIC_KEY * the key currently pinned in the host (see tools/keys.ts) * * Usage: * bun tools/verify.ts [--pub ed25519:...] [path/to/index.json] */ import { verify } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { DEFAULT_PUBLIC_KEY, PUBKEY_PREFIX, decodePublicKey } from "./keys.ts"; function die(message: string): never { console.error(`error: ${message}`); process.exit(1); } const args = process.argv.slice(2); let pubArg: string | undefined; const positional: string[] = []; for (let i = 0; i < args.length; i++) { const arg = args[i]!; if (arg === "--pub") { pubArg = args[++i]; if (!pubArg) die("--pub needs a value"); } else if (arg.startsWith("--pub=")) { pubArg = arg.slice("--pub=".length); } else if (arg.startsWith("--")) { die(`unknown flag ${arg}`); } else { positional.push(arg); } } let source = "host-pinned default"; let pubText = DEFAULT_PUBLIC_KEY; if (pubArg) { // Accept either the literal ed25519:... string or a file containing it. if (!pubArg.startsWith(PUBKEY_PREFIX) && existsSync(pubArg)) { source = pubArg; pubText = readFileSync(pubArg, "utf8").trim(); } else { source = "--pub"; pubText = pubArg; } } else if (process.env.INDEX_PUBLIC_KEY) { source = "$INDEX_PUBLIC_KEY"; pubText = process.env.INDEX_PUBLIC_KEY; } let publicKey; try { publicKey = decodePublicKey(pubText); } catch (err) { die(`bad public key from ${source}: ${(err as Error).message}`); } const file = resolve(positional[0] ?? "v1/index.json"); const sigFile = `${file}.sig`; let data: Buffer; try { data = readFileSync(file); } catch (err) { die(`cannot read ${file}: ${(err as Error).message}`); } let sigText: string; try { sigText = readFileSync(sigFile, "utf8"); } catch (err) { die(`cannot read ${sigFile}: ${(err as Error).message}`); } // Whitespace-tolerant on read, matching the host. const b64 = sigText.replace(/\s+/g, ""); if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) { die(`${sigFile} is not valid base64`); } const signature = Buffer.from(b64, "base64"); if (signature.length !== 64) { die(`${sigFile} must decode to a 64-byte ed25519 signature, got ${signature.length} bytes`); } if (!verify(null, data, publicKey, signature)) { console.error(`FAILED: signature in ${sigFile} does not match ${file}`); console.error(` key used: ${pubText.trim()} (from ${source})`); console.error( "\n Either the index was modified after signing, or it was signed with a\n" + " different key than the one being checked. Re-run `bun run sign`, and\n" + " confirm the signing key matches a slot the host pins.", ); process.exit(1); } console.log(`OK: ${sigFile} is a valid signature over ${file} (${data.length} bytes)`); console.log(` key: ${pubText.trim()} (from ${source})`);