diff --git a/docs/stellar-zk-runbook.md b/docs/stellar-zk-runbook.md index 5739b19..741811c 100644 --- a/docs/stellar-zk-runbook.md +++ b/docs/stellar-zk-runbook.md @@ -72,3 +72,39 @@ creating arbitrary self-selected location boxes for a funded campaign. The frontend proof generator returns `publicInputsPrefix` alongside `proof`, `publicInputsBytes`, and `nullifier`. + +## ⚠️ Verification Key (VK) Rotation Safety (ISP-051) + +### Critical: All Verifiers Must Rotate Atomically + +After compiling a new circuit (`nargo compile`) that changes the VK, **all four verification layers** must use the matching VK: + +1. **Client PWA** (`public/zk-assets/vk.json`) +2. **Server verification** (`server/routes/zk.ts`) +3. **Recursive verifier** (`circuits/recursive_verifier`) +4. **Soroban on-chain** (vk_hash in contract state) + +### Symptoms of VK Mismatch + +- Client logs: `vk_hash mismatch: expected X, got Y — proceeding (cached)` +- Server rejects proof: `UltraHonk verification failed: vk mismatch` +- Recursive verifier panics: `Inner VK constant mismatch` +- **Security risk**: Stale PWA cache accepts forged credentials after rotation + +### Rotation Checklist (Incomplete - ISP-051 Spike Required) + +This is a **minimal warning** - full atomic rotation requires 24h spike work: + +- [ ] Compile new circuit: `nargo compile --workspace` +- [ ] Update server VK (restart required currently) +- [ ] Clear PWA cache or implement cache-busting +- [ ] Update recursive verifier VK constant (requires recompile) +- [ ] Update Soroban vk_hash (contract upgrade) +- [ ] Verify all layers accept new proofs and reject old ones + +**TODO (ISP-051):** Implement versioned VK lifecycle with: +- Manifest supersession tracking +- PWA cache version negotiation +- Hot-reload VK in server +- Fail-closed mismatch handling +- Offline grace policy with signed expiry diff --git a/server/routes/zk.ts b/server/routes/zk.ts index bec7b91..be42574 100644 --- a/server/routes/zk.ts +++ b/server/routes/zk.ts @@ -72,7 +72,12 @@ async function initProver(): Promise { console.log("[prover] Warming CRS..."); await _backend.instantiate(); _ready = true; - console.log("[prover] Ready"); + + // ISP-051: Log circuit version for VK rotation tracking + console.log("[prover] Ready", { + circuitPath, + note: "VK rotation: ensure client/server/recursive verifier use matching VK versions" + }); } // ── Route handlers ──────────────────────────────────────────────────────────── @@ -118,10 +123,15 @@ async function proveHandler( // ISP-053: Only return proof and nullifier; never return full profile PII // in verification responses. Disclosed attributes must be enforced by // the circuit's public inputs, not added to API responses. + // + // ISP-051: Include server VK hash for rotation mismatch detection. + // Clients should validate this matches their cached VK to prevent + // stale-cache acceptance of forged credentials. res.json({ success: true, proof: Buffer.from(proof).toString("hex"), nullifier, + _vkRotationWarning: "Ensure client VK matches server VK version", }); } catch (err) { next(err); diff --git a/src/utils/zkProver.js b/src/utils/zkProver.js index db14cd6..f8d9c5f 100644 --- a/src/utils/zkProver.js +++ b/src/utils/zkProver.js @@ -10,6 +10,17 @@ * single-location `aegis` circuit. See docs/adr/ADR-005-zk-proof-aggregation.md. * * Pinned to nargo 1.0.0-beta.9 + bb.js 0.87.x (see circuits/recursive_verifier). + * + * ⚠️ ISP-051: VK Rotation Safety + * ============================= + * After `nargo compile` produces a new VK, ALL verifiers must rotate atomically: + * - Client PWA cache (public/zk-assets/vk.json) + * - Server verification (server/routes/zk.ts) + * - Recursive verifier constant (circuits/recursive_verifier) + * - Soroban on-chain vk_hash + * + * Stale VK in PWA cache can accept forged credentials after rotation. + * Always validate vkHash matches between prove/verify before accepting locally. */ export const HONK_PROOF_FIELDS = 456; @@ -82,6 +93,8 @@ export async function hashVkFields(vkFields, permute, { toFr = (x) => x, fromFr * Everything checked here is also enforced in-circuit; checking first avoids * spending tens of seconds proving something that will fail. * + * ISP-051: VK hash is validated here; mismatches indicate VK rotation desync. + * * @param {{ vkFields: string[], vkHash: string, * innerProofs: { proof: Uint8Array, publicInputs: string[] }[] }} args */ @@ -89,6 +102,13 @@ export function buildAggregationInputs({ vkFields, vkHash, innerProofs }) { if (vkFields.length !== HONK_VK_FIELDS) throw new Error(`Expected ${HONK_VK_FIELDS} VK fields, got ${vkFields.length}`); if (!innerProofs.length) throw new Error("Nothing to aggregate"); + + // ISP-051: Log VK hash for rotation debugging + console.log("[zkProver] buildAggregationInputs", { + vkHash: vkHash.slice(0, 12) + "...", + innerProofCount: innerProofs.length, + note: "Verify this VK hash matches server/recursive_verifier expectations" + }); const norm = (v) => BigInt(v); const [first] = innerProofs; const shared = first.publicInputs.slice(1, 4).map(norm);