@veritasacta/verify
Advanced tools
+50
-11
@@ -17,4 +17,5 @@ #!/usr/bin/env node | ||
| * Exit codes: | ||
| * 0 = valid | ||
| * 1 = invalid or error | ||
| * 0 = signature valid (proven authentic) | ||
| * 1 = signature invalid (proven tampered) | ||
| * 2 = verifier error (malformed input, missing key, parse failure — undecidable) | ||
| */ | ||
@@ -114,4 +115,5 @@ | ||
| ${bold('Exit Codes:')} | ||
| 0 All artifacts valid | ||
| 1 Invalid signature, error, or missing input | ||
| 0 Signature valid — proven authentic | ||
| 1 Signature invalid — proven tampered | ||
| 2 Verifier error — malformed input, missing key, or parse failure | ||
| `); | ||
@@ -429,3 +431,3 @@ } | ||
| console.log(''); | ||
| process.exit(allPassed ? 0 : 1); | ||
| process.exit(allPassed ? 0 : 1); // self-test: 0=pass, 1=fail (not a verification result) | ||
| } | ||
@@ -447,7 +449,7 @@ | ||
| console.error('Usage: npx @veritasacta/verify <file.json> [--key <hex>]'); | ||
| process.exit(1); | ||
| process.exit(2); | ||
| } | ||
| } catch (err) { | ||
| console.error(red(`Error reading input: ${err.message}`)); | ||
| process.exit(1); | ||
| process.exit(2); | ||
| } | ||
@@ -460,3 +462,3 @@ | ||
| console.error(red(`Error: Invalid JSON: ${err.message}`)); | ||
| process.exit(1); | ||
| process.exit(2); | ||
| } | ||
@@ -468,3 +470,3 @@ | ||
| printBundleResult(result, opts); | ||
| process.exit(result.valid ? 0 : 1); | ||
| process.exit(exitCodeForResult(result)); | ||
| } | ||
@@ -475,8 +477,45 @@ | ||
| printResult(result, opts); | ||
| process.exit(result.valid ? 0 : 1); | ||
| process.exit(exitCodeForResult(result)); | ||
| } | ||
| /** | ||
| * Derive exit code from a verification result. | ||
| * | ||
| * 0 = signature valid (proven authentic) | ||
| * 1 = signature invalid (proven tampered — the math confirms modification) | ||
| * 2 = verifier error (undecidable — missing key, malformed input, etc.) | ||
| * | ||
| * The distinction matters: exit 1 is a security event (evidence of tampering). | ||
| * Exit 2 is an operational error (the check could not be performed). | ||
| */ | ||
| function exitCodeForResult(result) { | ||
| if (result.valid) return 0; | ||
| // These errors mean the verifier could not perform the check at all. | ||
| // The signature was not tested — the result is undecidable. | ||
| const undecidableErrors = [ | ||
| 'no_public_key', | ||
| 'missing_signature', | ||
| 'missing_payload', | ||
| 'unsupported_algorithm', | ||
| ]; | ||
| if (result.error && undecidableErrors.includes(result.error)) return 2; | ||
| if (result.error && result.error.startsWith('JWKS')) return 2; | ||
| // For bundles: if any receipt had an undecidable error, the bundle is undecidable | ||
| if (result.errors && Array.isArray(result.errors)) { | ||
| const hasUndecidable = result.errors.some(e => | ||
| undecidableErrors.some(u => e.includes(u)) | ||
| ); | ||
| if (hasUndecidable) return 2; | ||
| } | ||
| // Default: the signature was tested and failed. This is proven tampering. | ||
| return 1; | ||
| } | ||
| main().catch((err) => { | ||
| console.error(red(`Fatal error: ${err.message}`)); | ||
| process.exit(1); | ||
| process.exit(2); | ||
| }); |
+1
-1
| { | ||
| "name": "@veritasacta/verify", | ||
| "version": "0.2.4", | ||
| "version": "0.2.5", | ||
| "mcpName": "io.github.tomjwxf/veritasacta-verify", | ||
@@ -5,0 +5,0 @@ "description": "CLI tool to verify signed artifacts (receipts, manifests, tickets). Works offline.", |
+8
-2
@@ -79,5 +79,11 @@ # @veritasacta/verify | ||
| - `0` — signature valid (all artifacts valid for bundles) | ||
| - `1` — signature invalid, missing key, or error | ||
| | Code | Meaning | Semantics | | ||
| |------|---------|-----------| | ||
| | `0` | Signature valid | **Proven authentic** — the Ed25519 math confirms this artifact has not been modified since signing | | ||
| | `1` | Signature invalid | **Proven tampered** — the signature does not match the payload. This is a security event. | | ||
| | `2` | Verifier error | **Undecidable** — malformed input, missing key, unparseable JSON. The check could not be performed. | | ||
| The distinction matters: exit 1 is evidence of tampering (the math was tested and failed). | ||
| Exit 2 is an operational error (the math was never tested). These demand different responses. | ||
| ## Where Do Receipts Come From? | ||
@@ -84,0 +90,0 @@ |
+64
-1
@@ -12,5 +12,6 @@ #!/usr/bin/env node | ||
| import { readFileSync } from 'node:fs'; | ||
| import { readFileSync, writeFileSync, unlinkSync } from 'node:fs'; | ||
| import { join, dirname } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { verifyArtifact } from '@veritasacta/artifacts'; | ||
@@ -110,2 +111,64 @@ | ||
| // ── Exit Code Contract (3-way split) ──────────────────────────── | ||
| console.log('\n⬢ Exit code contract (0=valid, 1=invalid, 2=error)\n'); | ||
| const cliPath = join(pkgRoot, 'cli.js'); | ||
| function runCli(args) { | ||
| try { | ||
| execFileSync('node', [cliPath, ...args], { | ||
| stdio: 'pipe', | ||
| timeout: 10000, | ||
| }); | ||
| return 0; | ||
| } catch (err) { | ||
| return err.status; | ||
| } | ||
| } | ||
| // Exit 0: valid receipt with correct key | ||
| assert( | ||
| runCli([join(pkgRoot, 'samples', 'sample-receipt.json'), '--key', sampleKey]) === 0, | ||
| 'Exit 0 for valid receipt' | ||
| ); | ||
| // Exit 1: tampered artifact (wrong key forces signature mismatch) | ||
| assert( | ||
| runCli([join(pkgRoot, 'samples', 'sample-receipt.json'), '--key', '0000000000000000000000000000000000000000000000000000000000000000']) === 1, | ||
| 'Exit 1 for invalid signature (wrong key)' | ||
| ); | ||
| // Exit 2: no input file specified | ||
| assert( | ||
| runCli([]) === 2, | ||
| 'Exit 2 for missing input' | ||
| ); | ||
| // Exit 2: non-existent file | ||
| assert( | ||
| runCli(['/tmp/nonexistent_file_for_verify_test.json']) === 2, | ||
| 'Exit 2 for unreadable file' | ||
| ); | ||
| // Exit 2: malformed JSON (test with a temp file containing invalid JSON) | ||
| const badJsonPath = join(pkgRoot, 'test', '_tmp_bad.json'); | ||
| writeFileSync(badJsonPath, 'not-valid-json{{{'); | ||
| assert( | ||
| runCli([badJsonPath]) === 2, | ||
| 'Exit 2 for invalid JSON' | ||
| ); | ||
| // Exit 2: valid JSON but missing required fields (no key) | ||
| const malformedPath = join(pkgRoot, 'test', '_tmp_malformed.json'); | ||
| writeFileSync(malformedPath, '{"type": "protectmcp:decision", "tool_name": "something"}'); | ||
| assert( | ||
| runCli([malformedPath]) === 2, | ||
| 'Exit 2 for missing public key (undecidable)' | ||
| ); | ||
| // Clean up temp files | ||
| try { unlinkSync(badJsonPath); } catch {} | ||
| try { unlinkSync(malformedPath); } catch {} | ||
| // ── Results ───────────────────────────────────────────────────── | ||
@@ -112,0 +175,0 @@ |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
32866
13.57%680
14.48%127
4.96%2
100%