veriskit 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 — 2026-07-10
4
+
5
+ ### Added
6
+ - Signed evidence. `veris evidence keygen` creates an Ed25519 keypair (via Node's built-in crypto, no new dependency); `veris evidence sign <evidence.json>` writes a detached signature; `veris evidence verify` checks a sibling signature automatically and can assert the signer with `--pubkey` or `--key-id`. Bundles carry the signature. Signing is opt-in; unsigned evidence still verifies for integrity. `VERISKIT_SIGNING_KEY` supplies the key in CI.
7
+
8
+ ### Changed
9
+ - `veris init` now gitignores `keys/`.
10
+
3
11
  ## 0.4.0 — 2026-07-10
4
12
 
5
13
  ### Added
package/README.md CHANGED
@@ -190,7 +190,7 @@ veris evidence verify .veris/runs/<run-id>/evidence.json
190
190
  This recomputes the digest and reports whether the record was edited or
191
191
  corrupted since it was written. An integrity digest is not forgery-proof on its
192
192
  own. To prove authorship, publish the digest separately (a CI log or PR) or sign
193
- it. Cryptographic signing is planned for a later release.
193
+ the record with an Ed25519 key (see Signing below).
194
194
 
195
195
  Package a run as a single portable file (the record, its report, and its logs,
196
196
  each with a digest, plus a bundle digest over everything):
@@ -202,6 +202,28 @@ veris evidence verify <bundle> # checks the record, every log, and the report
202
202
 
203
203
  `veris evidence show` prints the latest record's key facts.
204
204
 
205
+ ### Signing (optional)
206
+
207
+ An integrity digest proves a record was not edited. A signature proves a
208
+ specific key vouched for it. VerisKit signs with Ed25519 from Node's built-in
209
+ crypto, so signing adds no dependency and works offline.
210
+
211
+ ```bash
212
+ veris evidence keygen # writes .veris/keys/veriskit-signing.key(.pub)
213
+ veris evidence sign .veris/runs/<id>/evidence.json --key .veris/keys/veriskit-signing.key
214
+ veris evidence verify .veris/runs/<id>/evidence.json --pubkey .veris/keys/veriskit-signing.key.pub
215
+ ```
216
+
217
+ `sign` writes a detached `<evidence.json>.sig` next to the record. `verify`
218
+ picks it up automatically and checks it. In CI, pass the key through the
219
+ `VERISKIT_SIGNING_KEY` environment variable instead of a file.
220
+
221
+ A signature proves that whoever holds the private key signed the record. It
222
+ does not prove who that is. VerisKit prints the key fingerprint; to bind it to
223
+ a person or system, compare that fingerprint to one you already trust, or
224
+ assert it with `--pubkey` or `--key-id`. Keep the private key secret; `keygen`
225
+ writes it into `.veris/keys/`, which `veris init` gitignores.
226
+
205
227
  Commit `.veris/config.json` and `.veris/.gitignore`. `veris init` keeps `runs/`,
206
228
  `reports/`, `cache/`, `graph.json`, and `evidence/` out of your history.
207
229
 
@@ -213,7 +235,7 @@ VerisKit says what it cannot do as plainly as what it can:
213
235
  - **No test generation.** `plan` tells you what to test. Writing the tests is a later release.
214
236
  - **One project root.** A monorepo with several `tsconfig.json` files is not modeled yet. Resolution runs against the root project.
215
237
  - **Scanner fallback on plain-JS or TS 7.x-native projects.** The accurate resolver needs the classic TypeScript compiler API. Without it you get the labeled, relative-imports-only graph described above, and no dependency is added to paper over the gap.
216
- - **No cryptographic signing.** Evidence carries an integrity digest, not a signature. Keyless signing is planned.
238
+ - **No keyless or identity-bound signing.** Evidence can be signed with a local Ed25519 key (see Signing), but sigstore-style keyless signing that ties a signature to an identity is not built yet.
217
239
 
218
240
  ## Part of Baseframe Labs
219
241
 
package/dist/index.js CHANGED
@@ -849,6 +849,7 @@ var init_init = __esm({
849
849
  "cache/",
850
850
  "graph.json",
851
851
  "evidence/",
852
+ "keys/",
852
853
  ""
853
854
  ].join("\n");
854
855
  }
@@ -1897,7 +1898,84 @@ var init_plan = __esm({
1897
1898
  }
1898
1899
  });
1899
1900
 
1901
+ // src/evidence/signing.ts
1902
+ import {
1903
+ createHash as createHash2,
1904
+ createPrivateKey,
1905
+ createPublicKey,
1906
+ sign as cryptoSign,
1907
+ verify as cryptoVerify,
1908
+ generateKeyPairSync
1909
+ } from "crypto";
1910
+ function derOf(publicKey) {
1911
+ return publicKey.export({ type: "spki", format: "der" });
1912
+ }
1913
+ function keyId(publicKey) {
1914
+ const key = typeof publicKey === "string" ? createPublicKey(publicKey) : publicKey;
1915
+ return createHash2("sha256").update(derOf(key)).digest("hex").slice(0, 8);
1916
+ }
1917
+ function signatureKeyId(sig) {
1918
+ const key = createPublicKey({
1919
+ key: Buffer.from(sig.publicKey, "base64"),
1920
+ format: "der",
1921
+ type: "spki"
1922
+ });
1923
+ return keyId(key);
1924
+ }
1925
+ function generateKeyPair() {
1926
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
1927
+ return {
1928
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }),
1929
+ privateKeyPem: privateKey.export({
1930
+ type: "pkcs8",
1931
+ format: "pem"
1932
+ }),
1933
+ keyId: keyId(publicKey)
1934
+ };
1935
+ }
1936
+ function signDigest(digest, privateKeyPem) {
1937
+ const privateKey = createPrivateKey(privateKeyPem);
1938
+ const publicKey = createPublicKey(
1939
+ privateKey
1940
+ );
1941
+ const sig = cryptoSign(null, Buffer.from(digest, "utf8"), privateKey);
1942
+ return {
1943
+ schema: SIGNATURE_SCHEMA,
1944
+ alg: "ed25519",
1945
+ keyId: keyId(publicKey),
1946
+ publicKey: derOf(publicKey).toString("base64"),
1947
+ digest,
1948
+ signature: sig.toString("base64"),
1949
+ signedAt: (/* @__PURE__ */ new Date()).toISOString()
1950
+ };
1951
+ }
1952
+ function verifySignature(sig) {
1953
+ try {
1954
+ const publicKey = createPublicKey({
1955
+ key: Buffer.from(sig.publicKey, "base64"),
1956
+ format: "der",
1957
+ type: "spki"
1958
+ });
1959
+ return cryptoVerify(
1960
+ null,
1961
+ Buffer.from(sig.digest, "utf8"),
1962
+ publicKey,
1963
+ Buffer.from(sig.signature, "base64")
1964
+ );
1965
+ } catch {
1966
+ return false;
1967
+ }
1968
+ }
1969
+ var SIGNATURE_SCHEMA;
1970
+ var init_signing = __esm({
1971
+ "src/evidence/signing.ts"() {
1972
+ "use strict";
1973
+ SIGNATURE_SCHEMA = "veriskit/signature@1";
1974
+ }
1975
+ });
1976
+
1900
1977
  // src/evidence/verify-evidence.ts
1978
+ import { existsSync as existsSync5 } from "fs";
1901
1979
  import { readFile as readFile3 } from "fs/promises";
1902
1980
  function verifyRecord(record) {
1903
1981
  const recomputed = computeDigest(
@@ -1911,20 +1989,71 @@ function verifyRecord(record) {
1911
1989
  detail: ok ? "matches the canonical record" : `recomputed ${recomputed}, record claims ${record.digest}`
1912
1990
  }
1913
1991
  ];
1914
- return { ok, kind: "record", record, checks };
1992
+ return { ok, kind: "record", record, checks, signed: false };
1993
+ }
1994
+ function signatureChecks(recordDigest, sig, opts = {}) {
1995
+ const checks = [];
1996
+ const kid = signatureKeyId(sig);
1997
+ const validSig = verifySignature(sig) && sig.digest === recordDigest;
1998
+ checks.push({
1999
+ name: "signature",
2000
+ ok: validSig,
2001
+ detail: validSig ? `valid ed25519 signature by key ${kid}` : "signature does not match this record"
2002
+ });
2003
+ if (opts.expectedKeyId || opts.expectedPubKeyPem) {
2004
+ let matches = false;
2005
+ if (opts.expectedPubKeyPem) {
2006
+ try {
2007
+ matches = keyId(opts.expectedPubKeyPem) === kid;
2008
+ } catch {
2009
+ matches = false;
2010
+ }
2011
+ } else if (opts.expectedKeyId) {
2012
+ matches = opts.expectedKeyId.toLowerCase() === kid.toLowerCase();
2013
+ }
2014
+ checks.push({
2015
+ name: "signer",
2016
+ ok: matches,
2017
+ detail: matches ? `signer matches the expected key ${kid}` : `signer ${kid} does not match the expected key`
2018
+ });
2019
+ }
2020
+ return checks;
1915
2021
  }
1916
- async function verifyEvidenceFile(path) {
2022
+ async function verifyEvidenceFile(path, opts = {}) {
1917
2023
  const parsed = JSON.parse(await readFile3(path, "utf8"));
1918
2024
  if (parsed?.schema === "veriskit/bundle@1") {
1919
2025
  const { verifyBundle: verifyBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
1920
- return verifyBundle2(parsed);
2026
+ return verifyBundle2(parsed, opts);
2027
+ }
2028
+ const result = verifyRecord(parsed);
2029
+ const assertedSigner = Boolean(
2030
+ opts.expectedKeyId || opts.expectedPubKeyPem || opts.sigPath
2031
+ );
2032
+ const sigPath = opts.sigPath ?? `${path}.sig`;
2033
+ if (existsSync5(sigPath)) {
2034
+ const sig = JSON.parse(await readFile3(sigPath, "utf8"));
2035
+ result.checks.push(
2036
+ ...signatureChecks(result.record.digest, sig, {
2037
+ expectedKeyId: opts.expectedKeyId,
2038
+ expectedPubKeyPem: opts.expectedPubKeyPem
2039
+ })
2040
+ );
2041
+ result.signed = true;
2042
+ } else if (assertedSigner) {
2043
+ result.checks.push({
2044
+ name: "signature",
2045
+ ok: false,
2046
+ detail: "a signature was required (--pubkey/--key-id/--sig) but none was found"
2047
+ });
1921
2048
  }
1922
- return verifyRecord(parsed);
2049
+ result.ok = result.checks.every((c) => c.ok);
2050
+ return result;
1923
2051
  }
1924
2052
  var init_verify_evidence = __esm({
1925
2053
  "src/evidence/verify-evidence.ts"() {
1926
2054
  "use strict";
1927
2055
  init_record();
2056
+ init_signing();
1928
2057
  }
1929
2058
  });
1930
2059
 
@@ -1935,7 +2064,7 @@ __export(bundle_exports, {
1935
2064
  buildBundle: () => buildBundle,
1936
2065
  verifyBundle: () => verifyBundle
1937
2066
  });
1938
- function buildBundle(record, report, logs) {
2067
+ function buildBundle(record, report, logs, signature) {
1939
2068
  const manifest = {
1940
2069
  record: record.digest,
1941
2070
  report: sha256(report),
@@ -1943,10 +2072,10 @@ function buildBundle(record, report, logs) {
1943
2072
  Object.entries(logs).map(([k, v]) => [k, sha256(v)])
1944
2073
  )
1945
2074
  };
1946
- const base = { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
2075
+ const base = signature ? { schema: BUNDLE_SCHEMA, record, report, logs, manifest, signature } : { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
1947
2076
  return { ...base, bundleDigest: sha256(canonicalize(base)) };
1948
2077
  }
1949
- function verifyBundle(bundle) {
2078
+ function verifyBundle(bundle, opts = {}) {
1950
2079
  const checks = [];
1951
2080
  const recordResult = verifyRecord(bundle.record);
1952
2081
  checks.push(...recordResult.checks);
@@ -1971,11 +2100,28 @@ function verifyBundle(bundle) {
1971
2100
  ok: recomputed === bundle.bundleDigest,
1972
2101
  detail: recomputed === bundle.bundleDigest ? "matches" : "mismatch"
1973
2102
  });
2103
+ let signed = false;
2104
+ if (bundle.signature) {
2105
+ signed = true;
2106
+ checks.push(
2107
+ ...signatureChecks(bundle.record.digest, bundle.signature, {
2108
+ expectedKeyId: opts.expectedKeyId,
2109
+ expectedPubKeyPem: opts.expectedPubKeyPem
2110
+ })
2111
+ );
2112
+ } else if (opts.expectedKeyId || opts.expectedPubKeyPem) {
2113
+ checks.push({
2114
+ name: "signature",
2115
+ ok: false,
2116
+ detail: "a signature was required (--pubkey/--key-id) but the bundle is unsigned"
2117
+ });
2118
+ }
1974
2119
  return {
1975
2120
  ok: checks.every((c) => c.ok),
1976
2121
  kind: "bundle",
1977
2122
  record: bundle.record,
1978
- checks
2123
+ checks,
2124
+ signed
1979
2125
  };
1980
2126
  }
1981
2127
  var BUNDLE_SCHEMA;
@@ -1992,17 +2138,33 @@ var init_bundle = __esm({
1992
2138
  var evidence_exports = {};
1993
2139
  __export(evidence_exports, {
1994
2140
  runEvidenceBundle: () => runEvidenceBundle,
2141
+ runEvidenceKeygen: () => runEvidenceKeygen,
1995
2142
  runEvidenceShow: () => runEvidenceShow,
2143
+ runEvidenceSign: () => runEvidenceSign,
1996
2144
  runEvidenceVerify: () => runEvidenceVerify
1997
2145
  });
1998
- import { readFileSync as readFileSync5 } from "fs";
2146
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync } from "fs";
1999
2147
  import { writeFile as writeFile4 } from "fs/promises";
2000
- import { basename as basename3, join as join12 } from "path";
2148
+ import { basename as basename3, dirname as dirname2, join as join12 } from "path";
2001
2149
  import pc5 from "picocolors";
2002
- async function runEvidenceVerify(path) {
2150
+ async function runEvidenceVerify(path, opts = {}) {
2151
+ let expectedPubKeyPem;
2152
+ if (opts.pubkey) {
2153
+ try {
2154
+ expectedPubKeyPem = readFileSync5(opts.pubkey, "utf8");
2155
+ } catch {
2156
+ process.stderr.write(`veris: cannot read public key at ${opts.pubkey}
2157
+ `);
2158
+ return 1;
2159
+ }
2160
+ }
2003
2161
  let result;
2004
2162
  try {
2005
- result = await verifyEvidenceFile(path);
2163
+ result = await verifyEvidenceFile(path, {
2164
+ sigPath: opts.sig,
2165
+ expectedKeyId: opts.keyId,
2166
+ expectedPubKeyPem
2167
+ });
2006
2168
  } catch (err) {
2007
2169
  const msg = err instanceof Error ? err.message : String(err);
2008
2170
  process.stderr.write(`veris: cannot read evidence at ${path}: ${msg}
@@ -2027,8 +2189,93 @@ ${result.ok ? "digest OK" : "TAMPERED"} \xB7 verdict ${result.record.verdict.sta
2027
2189
  process.stdout.write(`
2028
2190
  ${HONESTY}
2029
2191
  `);
2192
+ if (result.signed && !opts.pubkey && !opts.keyId) {
2193
+ process.stdout.write(
2194
+ "\nThis record is signed. Confirm you trust the signing key by comparing its\nkey id above to one you already trust, or re-run with --pubkey / --key-id.\n"
2195
+ );
2196
+ }
2030
2197
  return result.ok ? 0 : 1;
2031
2198
  }
2199
+ async function runEvidenceKeygen(root, opts = {}) {
2200
+ const out = opts.out ?? join12(root, ".veris", "keys", "veriskit-signing.key");
2201
+ const pub = `${out}.pub`;
2202
+ if (existsSync6(out) || existsSync6(pub)) {
2203
+ process.stderr.write(
2204
+ `veris: a key already exists at ${out}. Refusing to overwrite it.
2205
+ `
2206
+ );
2207
+ return 1;
2208
+ }
2209
+ const { mkdir: mkdir2 } = await import("fs/promises");
2210
+ await mkdir2(dirname2(out), { recursive: true });
2211
+ const kp = generateKeyPair();
2212
+ writeFileSync(out, kp.privateKeyPem, { mode: 384 });
2213
+ chmodSync(out, 384);
2214
+ writeFileSync(pub, kp.publicKeyPem, "utf8");
2215
+ process.stdout.write(
2216
+ [
2217
+ `Wrote signing key ${out} (keep this secret; do not commit it)`,
2218
+ `Wrote public key ${pub}`,
2219
+ `Key id ${kp.keyId}`,
2220
+ ""
2221
+ ].join("\n")
2222
+ );
2223
+ return 0;
2224
+ }
2225
+ async function runEvidenceSign(evidencePath, opts = {}) {
2226
+ let privateKeyPem;
2227
+ if (process.env.VERISKIT_SIGNING_KEY) {
2228
+ privateKeyPem = process.env.VERISKIT_SIGNING_KEY;
2229
+ } else if (opts.key) {
2230
+ try {
2231
+ privateKeyPem = readFileSync5(opts.key, "utf8");
2232
+ } catch {
2233
+ process.stderr.write(`veris: cannot read signing key at ${opts.key}
2234
+ `);
2235
+ return 1;
2236
+ }
2237
+ } else {
2238
+ process.stderr.write(
2239
+ "veris: no signing key. Pass --key <path> or set VERISKIT_SIGNING_KEY.\n"
2240
+ );
2241
+ return 1;
2242
+ }
2243
+ let record;
2244
+ try {
2245
+ record = JSON.parse(readFileSync5(evidencePath, "utf8"));
2246
+ } catch {
2247
+ process.stderr.write(`veris: cannot read evidence at ${evidencePath}
2248
+ `);
2249
+ return 1;
2250
+ }
2251
+ const recomputed = computeDigest(
2252
+ record
2253
+ );
2254
+ if (recomputed !== record.digest) {
2255
+ process.stderr.write(
2256
+ "veris: refusing to sign, the record digest does not match its contents.\n"
2257
+ );
2258
+ return 1;
2259
+ }
2260
+ let sig;
2261
+ try {
2262
+ sig = signDigest(record.digest, privateKeyPem);
2263
+ } catch (err) {
2264
+ const msg = err instanceof Error ? err.message : String(err);
2265
+ process.stderr.write(`veris: signing failed: ${msg}
2266
+ `);
2267
+ return 1;
2268
+ }
2269
+ const out = opts.out ?? `${evidencePath}.sig`;
2270
+ writeFileSync(out, `${JSON.stringify(sig, null, 2)}
2271
+ `, "utf8");
2272
+ process.stdout.write(
2273
+ `Signed ${evidencePath}
2274
+ Wrote ${out} (key ${sig.keyId})
2275
+ `
2276
+ );
2277
+ return 0;
2278
+ }
2032
2279
  async function runEvidenceBundle(root, opts = {}) {
2033
2280
  const runDir = latestRunDir(root);
2034
2281
  if (!runDir) {
@@ -2055,7 +2302,15 @@ async function runEvidenceBundle(root, opts = {}) {
2055
2302
  }
2056
2303
  const logIds = record.checks.filter((c) => c.logDigest).map((c) => c.id);
2057
2304
  const logs = await readRunLogs(runDir, logIds);
2058
- const bundle = buildBundle(record, report, logs);
2305
+ let signature;
2306
+ const sigPath = join12(runDir, "evidence.json.sig");
2307
+ if (existsSync6(sigPath)) {
2308
+ try {
2309
+ signature = JSON.parse(readFileSync5(sigPath, "utf8"));
2310
+ } catch {
2311
+ }
2312
+ }
2313
+ const bundle = buildBundle(record, report, logs, signature);
2059
2314
  const outDir = await ensureEvidenceDir(root);
2060
2315
  const out = opts.out ?? join12(outDir, `${record.id}.bundle.json`);
2061
2316
  await writeFile4(out, `${JSON.stringify(bundle, null, 2)}
@@ -2101,10 +2356,12 @@ var init_evidence = __esm({
2101
2356
  "src/cli/commands/evidence.ts"() {
2102
2357
  "use strict";
2103
2358
  init_bundle();
2359
+ init_record();
2360
+ init_signing();
2104
2361
  init_store();
2105
2362
  init_verify_evidence();
2106
2363
  init_tty();
2107
- HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (planned) to prove authorship.";
2364
+ HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (veris evidence sign) to prove authorship.";
2108
2365
  }
2109
2366
  });
2110
2367
 
@@ -2164,8 +2421,8 @@ function buildCli() {
2164
2421
  });
2165
2422
  cli.command(
2166
2423
  "evidence <action> [file]",
2167
- "Evidence tools: verify <file> | bundle | show [file]"
2168
- ).option("--out <file>", "For bundle: write to a specific path").action(
2424
+ "Evidence: verify <file> | bundle | show [file] | keygen | sign <file>"
2425
+ ).option("--out <file>", "For bundle/keygen/sign: output path").option("--key <file>", "For sign: the private signing key (PEM)").option("--pubkey <file>", "For verify: assert the signer's public key").option("--key-id <id>", "For verify: assert the signer's key id").option("--sig <file>", "For verify: signature path (default <file>.sig)").action(
2169
2426
  async (action, file, opts) => {
2170
2427
  const mod = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
2171
2428
  if (action === "verify") {
@@ -2174,16 +2431,34 @@ function buildCli() {
2174
2431
  process.exitCode = 1;
2175
2432
  return;
2176
2433
  }
2177
- process.exitCode = await mod.runEvidenceVerify(file);
2434
+ process.exitCode = await mod.runEvidenceVerify(file, {
2435
+ pubkey: opts.pubkey,
2436
+ keyId: opts.keyId,
2437
+ sig: opts.sig
2438
+ });
2178
2439
  } else if (action === "bundle") {
2179
2440
  process.exitCode = await mod.runEvidenceBundle(process.cwd(), {
2180
2441
  out: opts.out
2181
2442
  });
2182
2443
  } else if (action === "show") {
2183
2444
  process.exitCode = await mod.runEvidenceShow(process.cwd(), file);
2445
+ } else if (action === "keygen") {
2446
+ process.exitCode = await mod.runEvidenceKeygen(process.cwd(), {
2447
+ out: opts.out
2448
+ });
2449
+ } else if (action === "sign") {
2450
+ if (!file) {
2451
+ process.stderr.write("veris: evidence sign needs a <file>\n");
2452
+ process.exitCode = 1;
2453
+ return;
2454
+ }
2455
+ process.exitCode = await mod.runEvidenceSign(file, {
2456
+ key: opts.key,
2457
+ out: opts.out
2458
+ });
2184
2459
  } else {
2185
2460
  process.stderr.write(
2186
- `veris: unknown evidence action '${action}' (use verify | bundle | show)
2461
+ `veris: unknown evidence action '${action}' (use verify | bundle | show | keygen | sign)
2187
2462
  `
2188
2463
  );
2189
2464
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veriskit",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The fastest way to prove your software works — a zero-config verification CLI.",
5
5
  "scripts": {
6
6
  "test": "vitest run",