From be0c272eabfd06a72405b384bc21c94eec84c09c Mon Sep 17 00:00:00 2001 From: BrianCLong <6404035+BrianCLong@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:49:14 -0600 Subject: [PATCH] feat(cacert): externalize verification network surface --- docs/ga/CACERT_VERIFICATION_NETWORK.md | 29 ++++ docs/ga/MVP-4-GA-VERIFICATION.md | 1 + docs/ga/verification-map.json | 17 +++ packages/cacert/package.json | 7 +- packages/cacert/schema/cacert.schema.json | 58 +++++++- .../schema/verification_request.schema.json | 33 +++++ .../schema/verification_response.schema.json | 20 +++ packages/cacert/src/cacert.ts | 86 ++++++++++- packages/cacert/src/index.ts | 1 + packages/cacert/src/verification.ts | 112 ++++++++++++++ packages/cacert/tests/cacert.test.ts | 138 ++++++++++++++---- packages/cacert/tools/cacert-verify.mjs | 30 ++++ 12 files changed, 498 insertions(+), 34 deletions(-) create mode 100644 docs/ga/CACERT_VERIFICATION_NETWORK.md create mode 100644 packages/cacert/schema/verification_request.schema.json create mode 100644 packages/cacert/schema/verification_response.schema.json create mode 100644 packages/cacert/src/verification.ts create mode 100755 packages/cacert/tools/cacert-verify.mjs diff --git a/docs/ga/CACERT_VERIFICATION_NETWORK.md b/docs/ga/CACERT_VERIFICATION_NETWORK.md new file mode 100644 index 00000000000..286cd7cdd2f --- /dev/null +++ b/docs/ga/CACERT_VERIFICATION_NETWORK.md @@ -0,0 +1,29 @@ +# CACert Verification Network (CEN-V) + +This document defines the external CACert verification network for deterministic admissibility checks outside Summit internal systems. + +## Verification flow + +1. Input CACert and verification_request.json. +2. Validate signature against trust anchors (Cosign-compatible public key distribution). +3. Validate evidence hashes for report.json, metrics.json, stamp.json, and decision trace references. +4. Validate admissibility verdict consistency and validity window. +5. Emit verification_response.json with deterministic reasons. + +## Components + +- **Verification service** (read-only): `POST /api/v1/cacert/verify` and `POST /api/v1/cacert/verify/offline`. +- **Public key distribution**: static trust-anchor bundle keyed by `key_id`, with revocation list support. +- **Evidence reference validation**: URI-to-sha256 checks over portable artifacts, no internal API dependency. +- **Offline verifier**: `cacert-verify` CLI for air-gapped validation. + +## Trust model + +- Summit admissibility authority signs CACert using Ed25519 (`key_id` identifies trust anchor). +- External verifiers independently resolve public key trust anchors and revocation sets. +- Trust anchors are distributed via signed bundles and pinned by hash in verifier configuration. +- CACert validity depends only on signed certificate fields + referenced artifact digests. + +## Failure guarantees + +CEN-V never returns false PASS. Any signature, evidence, decision trace, key status, or validity-window anomaly returns FAIL with structured reason codes. diff --git a/docs/ga/MVP-4-GA-VERIFICATION.md b/docs/ga/MVP-4-GA-VERIFICATION.md index 860ee447d27..d939a03d8f4 100644 --- a/docs/ga/MVP-4-GA-VERIFICATION.md +++ b/docs/ga/MVP-4-GA-VERIFICATION.md @@ -19,6 +19,7 @@ This sweep captures the minimum credible verification for the GA-hardening surfa | Media Authenticity & Provenance | `scripts/ci/verify_media_provenance.ts` + `docs/governance/media_provenance.md` | B | `make ga-verify` | Blocks marketing/public media changes without deterministic provenance evidence. | | GA Exit Criteria Scorecard | `docs/ga/GA_EXIT_CRITERIA_v1.md` (binary release gate, weighted GA Readiness Index, and acceptance criteria) | C | `make ga-verify` | Anchors GA promotion to an explicit scorecard with auditable acceptance tests and status tracking. | | GA Release Artifact Convergence | `scripts/release/generate-release-artifacts.mjs` + `scripts/release/verify-release.mjs` + `.github/workflows/release-integrity.yml` | A | `node scripts/release/verify-release.mjs` | Produces deterministic release surface, manifest, SBOM, provenance, and fails CI on any integrity or reproducibility drift. | +| CACert Verification Network | `packages/cacert/src/verification.ts` + `packages/cacert/tools/cacert-verify.mjs` + `packages/cacert/schema/verification_response.schema.json` | B | `pnpm --filter @summit/cacert test` | Verifies portable CACert signature validation, evidence integrity, replay/key revocation detection, and deterministic FAIL responses for external auditors. | | MVP GA Pilot Gate Artifacts | `scripts/ga/verify-mvp-ga-pilot-gate.mjs` + `testing/ga-verification/mvp-ga-pilot-docs.ga.test.mjs` + `.github/workflows/mvp-ga-pilot-gate.yml` | B | `make ga-verify` | Enforces MVP pilot cut line/remediation/evidence docs, gate checklist JSON, runbook links, and demo script as non-optional release artifacts. | ## Deferred / To-Improve Items diff --git a/docs/ga/verification-map.json b/docs/ga/verification-map.json index 7f81e66c245..7eda64c6dce 100644 --- a/docs/ga/verification-map.json +++ b/docs/ga/verification-map.json @@ -230,5 +230,22 @@ ], "keyword": "rate limiting", "notes": "Documentation-driven contract while runtime harness is stabilized." + }, + { + "feature": "CACert Verification Network", + "tier": "B", + "artifact": "packages/cacert/src/verification.ts", + "ciStatus": "wired", + "evidence": [ + "docs/ga/CACERT_VERIFICATION_NETWORK.md", + "packages/cacert/schema/verification_response.schema.json", + "packages/cacert/tools/cacert-verify.mjs" + ], + "keywords": [ + "CACert", + "admissibility_status", + "verifyCACert" + ], + "notes": "Externalizes CACert into an independently verifiable network with offline CLI verification and deterministic structured failure codes." } ] diff --git a/packages/cacert/package.json b/packages/cacert/package.json index 38362c9ac65..403bfecf3a9 100644 --- a/packages/cacert/package.json +++ b/packages/cacert/package.json @@ -4,12 +4,17 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "bin": { + "cacert-verify": "tools/cacert-verify.mjs" + }, "scripts": { "build": "tsc", - "test": "npx tsx --test tests/*.test.ts" + "test": "npx tsx --test tests/*.test.ts", + "verify": "node ./tools/cacert-verify.mjs" }, "devDependencies": { "@types/node": "^22.0.0", + "tsx": "^4.20.6", "typescript": "^5.9.3" } } diff --git a/packages/cacert/schema/cacert.schema.json b/packages/cacert/schema/cacert.schema.json index 3de142ea255..2e2607cc536 100644 --- a/packages/cacert/schema/cacert.schema.json +++ b/packages/cacert/schema/cacert.schema.json @@ -3,11 +3,44 @@ "title": "CACert", "type": "object", "additionalProperties": false, - "required": ["cert_version", "verdict", "evidence_hash", "trace_ref", "policy_refs"], + "required": [ + "cert_version", + "cert_id", + "issuer", + "key_id", + "issued_at", + "expires_at", + "verdict", + "evidence_hash", + "trace_ref", + "policy_refs", + "evidence", + "signature" + ], "properties": { "cert_version": { "type": "string", - "const": "1.0" + "const": "2.0" + }, + "cert_id": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "issuer": { + "type": "string", + "minLength": 1 + }, + "key_id": { + "type": "string", + "minLength": 1 + }, + "issued_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time" }, "verdict": { "type": "string", @@ -26,6 +59,27 @@ "items": { "type": "string" } + }, + "evidence": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["uri", "sha256"], + "properties": { + "uri": { "type": "string", "minLength": 1 }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } + } + } + }, + "signature": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "value"], + "properties": { + "algorithm": { "type": "string", "const": "ed25519" }, + "value": { "type": "string" } + } } } } diff --git a/packages/cacert/schema/verification_request.schema.json b/packages/cacert/schema/verification_request.schema.json new file mode 100644 index 00000000000..d800e3d532c --- /dev/null +++ b/packages/cacert/schema/verification_request.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "verification_request", + "type": "object", + "additionalProperties": false, + "required": ["cert", "evidence_hashes", "trust_anchors"], + "properties": { + "cert": { "$ref": "./cacert.schema.json" }, + "evidence_hashes": { + "type": "object", + "additionalProperties": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "trace_hashes": { + "type": "object", + "additionalProperties": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "now": { "type": "string", "format": "date-time" }, + "revoked_keys": { + "type": "array", + "items": { "type": "string" } + }, + "trust_anchors": { + "type": "object", + "additionalProperties": { "type": "string", "minLength": 1 } + } + } +} diff --git a/packages/cacert/schema/verification_response.schema.json b/packages/cacert/schema/verification_response.schema.json new file mode 100644 index 00000000000..60bb6ad24b3 --- /dev/null +++ b/packages/cacert/schema/verification_response.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "verification_response", + "type": "object", + "additionalProperties": false, + "required": ["cert_id", "valid_signature", "evidence_integrity", "admissibility_status", "reasons"], + "properties": { + "cert_id": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "valid_signature": { "type": "boolean" }, + "evidence_integrity": { "type": "boolean" }, + "admissibility_status": { "type": "string", "enum": ["PASS", "FAIL"] }, + "reasons": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/packages/cacert/src/cacert.ts b/packages/cacert/src/cacert.ts index 73cab77218b..1da41dde918 100644 --- a/packages/cacert/src/cacert.ts +++ b/packages/cacert/src/cacert.ts @@ -1,3 +1,5 @@ +import { createHash } from 'node:crypto'; + export type CACertVerdict = 'PASS' | 'FAIL'; export interface CACertVerdictBundle { @@ -11,22 +13,96 @@ export interface CACertBundle { verdict: CACertVerdictBundle; hash: string; trace_ref?: string; + issuedAt?: string; + expiresAt?: string; + issuer?: string; + keyId?: string; + signature?: string; +} + +export interface CACertEvidenceRef { + uri: string; + sha256: string; } -export interface CACert { - cert_version: '1.0'; +export interface CACertUnsigned { + cert_version: '2.0'; + issuer: string; + key_id: string; + issued_at: string; + expires_at: string; verdict: CACertVerdict; evidence_hash: string; trace_ref: string; policy_refs: string[]; + evidence: CACertEvidenceRef[]; } -export function generateCACert(bundle: CACertBundle): CACert { +export interface CACert extends CACertUnsigned { + cert_id: string; + signature: { + algorithm: 'ed25519'; + value: string; + }; +} + +export function sha256Hex(value: string): string { + return createHash('sha256').update(value).digest('hex'); +} + +export function canonicalize(value: unknown): string { + if (value === null || typeof value !== 'object') { + return JSON.stringify(value); + } + + if (Array.isArray(value)) { + return `[${value.map((item) => canonicalize(item)).join(',')}]`; + } + + const entries = Object.entries(value as Record).sort(([a], [b]) => + a.localeCompare(b), + ); + + return `{${entries + .map(([key, val]) => `${JSON.stringify(key)}:${canonicalize(val)}`) + .join(',')}}`; +} + +export function buildUnsignedCACert(bundle: CACertBundle): CACertUnsigned { return { - cert_version: '1.0', + cert_version: '2.0', + issuer: bundle.issuer ?? 'summit://admissibility-authority', + key_id: bundle.keyId ?? 'summit-root-ed25519-v1', + issued_at: bundle.issuedAt ?? '2026-01-01T00:00:00.000Z', + expires_at: bundle.expiresAt ?? '2027-01-01T00:00:00.000Z', verdict: bundle.verdict.verdict, evidence_hash: bundle.hash, trace_ref: bundle.trace_ref ?? 'embedded', - policy_refs: [...bundle.verdict.policy_failures], + policy_refs: [...bundle.verdict.policy_failures].sort(), + evidence: [...bundle.verdict.evidence_refs] + .sort((a, b) => a.localeCompare(b)) + .map((uri) => ({ uri, sha256: bundle.hash })), + }; +} + +export function computeCACertId(unsignedCert: CACertUnsigned): string { + return sha256Hex(canonicalize(unsignedCert)); +} + +export function buildSigningPayload(cert: Pick & CACertUnsigned): string { + return canonicalize(cert); +} + +export function generateCACert(bundle: CACertBundle): CACert { + const unsigned = buildUnsignedCACert(bundle); + const cert_id = computeCACertId(unsigned); + + return { + cert_id, + ...unsigned, + signature: { + algorithm: 'ed25519', + value: bundle.signature ?? '', + }, }; } diff --git a/packages/cacert/src/index.ts b/packages/cacert/src/index.ts index 571d12ae40f..8236b2e8d4b 100644 --- a/packages/cacert/src/index.ts +++ b/packages/cacert/src/index.ts @@ -1 +1,2 @@ export * from './cacert.js'; +export * from './verification.js'; diff --git a/packages/cacert/src/verification.ts b/packages/cacert/src/verification.ts new file mode 100644 index 00000000000..857fcaa2838 --- /dev/null +++ b/packages/cacert/src/verification.ts @@ -0,0 +1,112 @@ +import { verify } from 'node:crypto'; + +import type { CACert, CACertVerdict } from './cacert.js'; +import { buildSigningPayload, computeCACertId } from './cacert.js'; + +export interface VerificationRequest { + cert: CACert; + evidence_hashes: Record; + trace_hashes?: Record; + now?: string; + revoked_keys?: string[]; + trust_anchors: Record; +} + +export interface VerificationResponse { + cert_id: string; + valid_signature: boolean; + evidence_integrity: boolean; + admissibility_status: CACertVerdict; + reasons: string[]; +} + +function verifyEd25519(payload: string, signatureValue: string, publicKeyPem: string): boolean { + if (!signatureValue || !publicKeyPem) { + return false; + } + + try { + return verify(null, Buffer.from(payload), publicKeyPem, Buffer.from(signatureValue, 'base64')); + } catch { + return false; + } +} + +export function verifyCACert(request: VerificationRequest): VerificationResponse { + const reasons: string[] = []; + const { cert } = request; + + const { signature, cert_id, ...unsigned } = cert; + const expectedCertId = computeCACertId(unsigned); + if (cert_id !== expectedCertId) { + reasons.push('CERT_ID_MISMATCH'); + } + + const trustKey = request.trust_anchors[cert.key_id]; + if (!trustKey) { + reasons.push('UNKNOWN_SIGNING_KEY'); + } + + if (request.revoked_keys?.includes(cert.key_id)) { + reasons.push('REVOKED_SIGNING_KEY'); + } + + const signatureOk = verifyEd25519( + buildSigningPayload({ cert_id, ...unsigned }), + signature?.value ?? '', + trustKey, + ); + if (!signatureOk) { + reasons.push('INVALID_SIGNATURE'); + } + + if (request.now && cert.expires_at < request.now) { + reasons.push('EXPIRED_CERT_REPLAY'); + } + + if (request.now && cert.issued_at > request.now) { + reasons.push('NOT_YET_VALID'); + } + + for (const evidence of cert.evidence) { + const providedHash = request.evidence_hashes[evidence.uri]; + if (!providedHash) { + reasons.push(`MISSING_EVIDENCE:${evidence.uri}`); + continue; + } + + if (providedHash !== evidence.sha256) { + reasons.push(`EVIDENCE_HASH_MISMATCH:${evidence.uri}`); + } + } + + if (request.trace_hashes) { + const traceHash = request.trace_hashes[cert.trace_ref]; + if (!traceHash) { + reasons.push('MISSING_DECISION_TRACE'); + } else if (traceHash !== cert.evidence_hash) { + reasons.push('DECISION_TRACE_MISMATCH'); + } + } + + const valid_signature = signatureOk && !reasons.some((reason) => reason.includes('SIGNING_KEY')); + const evidence_integrity = !reasons.some( + (reason) => + reason.startsWith('MISSING_EVIDENCE') || + reason.startsWith('EVIDENCE_HASH_MISMATCH') || + reason === 'MISSING_DECISION_TRACE' || + reason === 'DECISION_TRACE_MISMATCH' || + reason === 'CERT_ID_MISMATCH', + ); + + const admissibility_status: CACertVerdict = + valid_signature && evidence_integrity && cert.verdict === 'PASS' ? 'PASS' : 'FAIL'; + + return { + cert_id: cert.cert_id, + valid_signature, + evidence_integrity, + admissibility_status, + reasons, + }; +} diff --git a/packages/cacert/tests/cacert.test.ts b/packages/cacert/tests/cacert.test.ts index b3ccdbd5521..3c4f90ca0ab 100644 --- a/packages/cacert/tests/cacert.test.ts +++ b/packages/cacert/tests/cacert.test.ts @@ -1,14 +1,42 @@ import assert from 'node:assert/strict'; +import { generateKeyPairSync, sign } from 'node:crypto'; import test from 'node:test'; -import { generateCACert } from '../src/cacert.js'; +import { buildSigningPayload, generateCACert } from '../src/cacert.js'; +import { verifyCACert } from '../src/verification.js'; + +function buildSignedCert() { + const { publicKey, privateKey } = generateKeyPairSync('ed25519'); + const cert = generateCACert({ + trace_ref: 'trace://bundle-123', + hash: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + verdict: { + verdict: 'PASS', + reasons: [], + evidence_refs: ['artifact://report.json', 'artifact://metrics.json'], + policy_failures: [], + }, + keyId: 'summit-root-ed25519-v1', + }); + + const { signature: _signature, ...signable } = cert; + const signature = sign(null, Buffer.from(buildSigningPayload(signable)), privateKey); + cert.signature.value = signature.toString('base64'); + + return { + cert, + trustAnchors: { + 'summit-root-ed25519-v1': publicKey.export({ type: 'spki', format: 'pem' }).toString(), + }, + }; +} test('generateCACert returns deterministic certificate output', () => { const bundleA = { verdict: { verdict: 'FAIL' as const, reasons: ['NO_PROVENANCE'], - evidence_refs: [], + evidence_refs: ['artifact://report.json'], policy_failures: ['NO_PROVENANCE'], }, hash: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', @@ -18,7 +46,7 @@ test('generateCACert returns deterministic certificate output', () => { hash: bundleA.hash, verdict: { policy_failures: ['NO_PROVENANCE'], - evidence_refs: [], + evidence_refs: ['artifact://report.json'], reasons: ['NO_PROVENANCE'], verdict: 'FAIL' as const, }, @@ -27,34 +55,92 @@ test('generateCACert returns deterministic certificate output', () => { const certA = generateCACert(bundleA); const certB = generateCACert(bundleB); - assert.deepEqual(certA, { - cert_version: '1.0', - verdict: 'FAIL', - evidence_hash: bundleA.hash, - trace_ref: 'embedded', - policy_refs: ['NO_PROVENANCE'], + assert.equal(certA.cert_version, '2.0'); + assert.equal(certA.verdict, 'FAIL'); + assert.equal(certA.evidence_hash, bundleA.hash); + assert.equal(certA.trace_ref, 'embedded'); + assert.deepEqual(certA.policy_refs, ['NO_PROVENANCE']); + assert.deepEqual(certA, certB); +}); + +test('verifyCACert passes for valid signature + evidence', () => { + const { cert, trustAnchors } = buildSignedCert(); + const response = verifyCACert({ + cert, + trust_anchors: trustAnchors, + evidence_hashes: { + 'artifact://report.json': cert.evidence_hash, + 'artifact://metrics.json': cert.evidence_hash, + }, + trace_hashes: { + 'trace://bundle-123': cert.evidence_hash, + }, + now: '2026-05-01T00:00:00.000Z', }); - assert.deepEqual(certA, certB); - assert.equal( - JSON.stringify(certA), - '{"cert_version":"1.0","verdict":"FAIL","evidence_hash":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","trace_ref":"embedded","policy_refs":["NO_PROVENANCE"]}', - ); + assert.equal(response.valid_signature, true); + assert.equal(response.evidence_integrity, true); + assert.equal(response.admissibility_status, 'PASS'); + assert.deepEqual(response.reasons, []); }); -test('generateCACert preserves an explicit trace ref', () => { - const cert = generateCACert({ - trace_ref: 'trace://bundle-123', - hash: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - verdict: { - verdict: 'PASS', - reasons: [], - evidence_refs: [], - policy_failures: [], +test('verifyCACert detects forged CACert signature', () => { + const { cert, trustAnchors } = buildSignedCert(); + cert.verdict = 'FAIL'; + const response = verifyCACert({ cert, trust_anchors: trustAnchors, evidence_hashes: {} }); + + assert.equal(response.valid_signature, false); + assert.equal(response.admissibility_status, 'FAIL'); + assert.ok(response.reasons.includes('INVALID_SIGNATURE')); +}); + +test('verifyCACert detects evidence tampering', () => { + const { cert, trustAnchors } = buildSignedCert(); + const response = verifyCACert({ + cert, + trust_anchors: trustAnchors, + evidence_hashes: { + 'artifact://report.json': cert.evidence_hash, + 'artifact://metrics.json': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }, + }); + + assert.equal(response.evidence_integrity, false); + assert.ok(response.reasons.some((reason) => reason.startsWith('EVIDENCE_HASH_MISMATCH'))); +}); + +test('verifyCACert detects replay and revoked keys', () => { + const { cert, trustAnchors } = buildSignedCert(); + const response = verifyCACert({ + cert, + trust_anchors: trustAnchors, + evidence_hashes: { + 'artifact://report.json': cert.evidence_hash, + 'artifact://metrics.json': cert.evidence_hash, + }, + revoked_keys: ['summit-root-ed25519-v1'], + now: '2028-05-01T00:00:00.000Z', + }); + + assert.equal(response.admissibility_status, 'FAIL'); + assert.ok(response.reasons.includes('REVOKED_SIGNING_KEY')); + assert.ok(response.reasons.includes('EXPIRED_CERT_REPLAY')); +}); + +test('verifyCACert detects missing evidence and decision trace mismatch', () => { + const { cert, trustAnchors } = buildSignedCert(); + const response = verifyCACert({ + cert, + trust_anchors: trustAnchors, + evidence_hashes: { + 'artifact://report.json': cert.evidence_hash, + }, + trace_hashes: { + 'trace://bundle-123': 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', }, }); - assert.equal(cert.trace_ref, 'trace://bundle-123'); - assert.equal(cert.verdict, 'PASS'); - assert.deepEqual(cert.policy_refs, []); + assert.equal(response.admissibility_status, 'FAIL'); + assert.ok(response.reasons.some((reason) => reason.startsWith('MISSING_EVIDENCE'))); + assert.ok(response.reasons.includes('DECISION_TRACE_MISMATCH')); }); diff --git a/packages/cacert/tools/cacert-verify.mjs b/packages/cacert/tools/cacert-verify.mjs new file mode 100755 index 00000000000..2eb06e03ebd --- /dev/null +++ b/packages/cacert/tools/cacert-verify.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; + +import { verifyCACert } from '../dist/verification.js'; + +function getArg(name) { + const index = process.argv.indexOf(name); + if (index === -1) return undefined; + return process.argv[index + 1]; +} + +const certPath = getArg('--cert'); +const requestPath = getArg('--request'); +const outputPath = getArg('--output'); + +if (!certPath || !requestPath) { + console.error('Usage: cacert-verify --cert --request [--output ]'); + process.exit(2); +} + +const cert = JSON.parse(await readFile(certPath, 'utf8')); +const request = JSON.parse(await readFile(requestPath, 'utf8')); +const response = verifyCACert({ ...request, cert }); + +if (outputPath) { + await writeFile(outputPath, JSON.stringify(response, null, 2)); +} + +console.log(JSON.stringify(response, null, 2)); +process.exit(response.admissibility_status === 'PASS' ? 0 : 1);