From 128fd65ffff4a7e8dc91ce09ad10d740764161d0 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 15:55:11 -0400 Subject: [PATCH 01/11] feat: bind CAPPO request signatures to authority headers --- src/lib/covenant/http-message-signatures.ts | 38 +++++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/lib/covenant/http-message-signatures.ts b/src/lib/covenant/http-message-signatures.ts index 11a24bd..15335d9 100644 --- a/src/lib/covenant/http-message-signatures.ts +++ b/src/lib/covenant/http-message-signatures.ts @@ -1,4 +1,4 @@ -/** RFC 9421 request-signature helper for CAPPO's sole execution boundary. */ +/** RFC 9421 request-signature helpers for CAPPO's sole execution boundary. */ import { createHash, createPrivateKey, sign } from "crypto"; @@ -22,31 +22,55 @@ export function signCanonicalCapiEnvelope(payload: object, privateKeyB64: string .toString("base64url"); } +export interface CappoSignatureOptions { + coveredHeaders?: Record; + created?: number; +} + +/** + * Sign the exact CAPPO request bytes plus every trust-bearing identity header. + * Header names are normalized to lower-case because RFC 9421 component names + * are case-insensitive while the signature base is deterministic. + */ export function signCappoExecutionRequest( targetUri: string, body: string, privateKeyB64: string, keyId: string, - created = Math.floor(Date.now() / 1000), + options: CappoSignatureOptions = {}, ): Record { if (!privateKeyB64 || !keyId) { throw new Error("COVENANT_HTTP_SIGNING_PRIVATE_KEY and COVENANT_HTTP_SIGNING_KEY_ID are required"); } + if (!targetUri.startsWith("https://") && process.env.NODE_ENV === "production") { + throw new Error("CAPPO execution target must use HTTPS in production"); + } + + const created = options.created ?? Math.floor(Date.now() / 1000); const contentDigest = `sha-256=:${createHash("sha256").update(body).digest("base64")}:`; + const normalizedHeaders = Object.fromEntries( + Object.entries(options.coveredHeaders ?? {}).map(([name, value]) => [name.toLowerCase(), value]), + ); + const covered = ["@method", "@target-uri", "content-digest", ...Object.keys(normalizedHeaders).sort()]; + const componentList = covered.map((name) => `"${name}"`).join(" "); const params = `;created=${created};keyid="${keyId}"`; - const signatureInput = `sig1=("@method" "@target-uri" "content-digest")${params}`; - const signatureBase = [ + const signatureInput = `sig1=(${componentList})${params}`; + const signatureBaseLines = [ '"@method": POST', `"@target-uri": ${targetUri}`, `"content-digest": ${contentDigest}`, - `"@signature-params": ("@method" "@target-uri" "content-digest")${params}`, - ].join("\n"); + ...Object.keys(normalizedHeaders).sort().map((name) => `"${name}": ${normalizedHeaders[name]}`), + `"@signature-params": (${componentList})${params}`, + ]; + const privateKey = createPrivateKey({ key: Buffer.from(privateKeyB64, "base64"), format: "der", type: "pkcs8", }); - const signature = sign(null, Buffer.from(signatureBase, "utf8"), privateKey).toString("base64"); + const signature = sign(null, Buffer.from(signatureBaseLines.join("\n"), "utf8"), privateKey) + .toString("base64"); + return { "Content-Digest": contentDigest, "Signature-Input": signatureInput, From 6093ce229bcba44ba7571fcadb49639415567d2a Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 15:55:41 -0400 Subject: [PATCH 02/11] feat: prepare signed CAPPO execution envelopes --- src/lib/covenant/cappo-preparer.ts | 192 +++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 src/lib/covenant/cappo-preparer.ts diff --git a/src/lib/covenant/cappo-preparer.ts b/src/lib/covenant/cappo-preparer.ts new file mode 100644 index 0000000..17d3720 --- /dev/null +++ b/src/lib/covenant/cappo-preparer.ts @@ -0,0 +1,192 @@ +import { createHash, randomBytes, randomUUID } from "crypto"; + +import { + canonicalJson, + signCanonicalCapiEnvelope, + signCappoExecutionRequest, +} from "./http-message-signatures"; + +export interface PrepareCappoExecutionInput { + body: Record; + executionId: string; + workspaceId: string; + actorId: string; +} + +export interface PreparedCappoExecution { + targetUri: string; + body: string; + headers: Record; +} + +function sha256Hex(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + +/** Match Python json.dumps(..., sort_keys=True) for the ASCII identity records. */ +function pythonSortedJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(pythonSortedJson).join(", ")}]`; + if (value && typeof value === "object") { + const record = value as Record; + return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}: ${pythonSortedJson(record[key])}`).join(", ")}}`; + } + return JSON.stringify(value); +} + +function hashPythonSorted(value: unknown): string { + return sha256Hex(pythonSortedJson(value)); +} + +function safeWimseSegment(value: string): string { + const normalized = value.replace(/[^a-zA-Z0-9.-]/g, "-").replace(/^-+|-+$/g, ""); + if (!normalized) throw new Error("WIMSE identity segment is empty after normalization"); + return normalized.slice(0, 96); +} + +function base64Json(value: unknown): string { + return Buffer.from(JSON.stringify(value), "utf8").toString("base64"); +} + +function requireConfig(name: string): string { + const value = process.env[name]?.trim() || ""; + if (!value) throw new Error(`${name} is required`); + return value; +} + +export function prepareCappoExecution(input: PrepareCappoExecutionInput): PreparedCappoExecution { + const targetUri = requireConfig("CAPPO_EXECUTION_URL"); + const privateKey = requireConfig("COVENANT_HTTP_SIGNING_PRIVATE_KEY"); + const keyId = requireConfig("COVENANT_HTTP_SIGNING_KEY_ID"); + if (!targetUri.endsWith("/v1/exec")) { + throw new Error("CAPPO_EXECUTION_URL must terminate at /v1/exec"); + } + if (process.env.NODE_ENV === "production" && !targetUri.startsWith("https://")) { + throw new Error("CAPPO_EXECUTION_URL must use HTTPS in production"); + } + if (!input.executionId || !input.workspaceId || !input.actorId) { + throw new Error("executionId, workspaceId and actorId are required"); + } + + const action = typeof input.body.action === "string" ? input.body.action.trim() : ""; + if (!action) throw new Error("CAPPO request action is required"); + const lease = input.body.capability_lease; + if (!lease || typeof lease !== "object") throw new Error("capability_lease is required"); + + const unsignedBody: Record = { + ...input.body, + workspace_id: input.workspaceId, + pgl_id: input.actorId, + capability_lease: { + ...(lease as Record), + execution_id: input.executionId, + }, + }; + // A caller cannot self-declare CAPPO's decision. The kernel sets its internal + // ALLOW directive only after the lease-backed consequence evaluator succeeds. + delete unsignedBody.directive; + delete unsignedBody.security; + + const nonce = randomBytes(24).toString("base64url"); + const securityPayload = { + actor_id: input.actorId, + action, + data_hash: sha256Hex(canonicalJson(unsignedBody)), + nonce, + }; + const security = { + nonce, + signature: signCanonicalCapiEnvelope(securityPayload, privateKey), + }; + const finalObject = { ...unsignedBody, security }; + const body = JSON.stringify(finalObject); + const bodyHash = sha256Hex(body); + + const now = Math.floor(Date.now() / 1000); + const expires = now + 60; + const workload = `wimse://veklom/control-plane/${safeWimseSegment(input.workspaceId)}/execution/${safeWimseSegment(input.executionId)}`; + const confirmation = { method: "capi-http-signature", key_id: keyId }; + const candidateActHash = sha256Hex(canonicalJson({ + action, + execution_id: input.executionId, + workspace_id: input.workspaceId, + scope: unsignedBody.scope ?? {}, + })); + + const wit = { + iss: "https://capi.veklom.com", + sub: workload, + aud: "https://cappo.veklom.com", + exp: expires, + iat: now, + jti: randomUUID(), + cnf: confirmation, + trust_domain: "veklom.com", + profile_id: input.actorId, + }; + const ect = { + iss: "https://capi.veklom.com", + sub: workload, + aud: "https://cappo.veklom.com", + exp: expires, + iat: now, + jti: randomUUID(), + ephemeral_execution_id: input.executionId, + candidate_act_hash: candidateActHash, + cnf: confirmation, + intent_hash: sha256Hex(canonicalJson(unsignedBody)), + p5_operation_id: input.executionId, + }; + const authority = { + authority_id: `authority:${input.executionId}`, + ephemeral_execution_id: input.executionId, + scope_hash: sha256Hex(canonicalJson((unsignedBody.scope as Record) ?? {})), + policy_decision_hash: sha256Hex(canonicalJson({ decision: "candidate", source: "capi-gatekeeper" })), + candidate_act_hash: candidateActHash, + // CAPPO currently normalizes the canonical /v1/exec destination to this + // fixed digest-domain marker before the preauthorization check. + destination_hash: "target_hash", + rights: [action], + issued_at: now, + expires_at: expires, + proof_of_possession: sha256Hex(canonicalJson({ + execution_id: input.executionId, + workspace_id: input.workspaceId, + nonce, + })), + inbound_truth_state: "ADMISSIBLE", + required_truth_state: "ADMISSIBLE", + }; + const wpt = { + htm: "POST", + htu: "/v1/exec", + body_hash: bodyHash, + wit_hash: hashPythonSorted(wit), + ect_hash: hashPythonSorted(ect), + authority_hash: hashPythonSorted(authority), + jti: randomUUID(), + cnf: confirmation, + exp: expires, + }; + + const identityHeaders: Record = { + "workload-identity": base64Json(wit), + "execution-context": base64Json(ect), + "workload-proof": base64Json(wpt), + "veklom-authority": base64Json(authority), + "x-veklom-actor": input.actorId, + "x-veklom-nonce": nonce, + }; + const signatureHeaders = signCappoExecutionRequest(targetUri, body, privateKey, keyId, { + coveredHeaders: identityHeaders, + }); + + return { + targetUri, + body, + headers: { + "content-type": "application/json", + ...identityHeaders, + ...signatureHeaders, + }, + }; +} From f12d0f6ffbf2fa65ce6d76ca04b3d263c09aed3f Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 15:55:52 -0400 Subject: [PATCH 03/11] feat: expose protected CAPPO request preparation --- src/app/api/capi/v1/cappo/prepare/route.ts | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/app/api/capi/v1/cappo/prepare/route.ts diff --git a/src/app/api/capi/v1/cappo/prepare/route.ts b/src/app/api/capi/v1/cappo/prepare/route.ts new file mode 100644 index 0000000..d1469d0 --- /dev/null +++ b/src/app/api/capi/v1/cappo/prepare/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { requireAdminToken } from "@/lib/covenant/admin-auth"; +import { prepareCappoExecution } from "@/lib/covenant/cappo-preparer"; + +export const runtime = "nodejs"; + +export async function POST(request: NextRequest) { + const auth = requireAdminToken(request); + if (!auth.ok) { + return NextResponse.json({ error: auth.error }, { status: auth.status }); + } + + let input: unknown; + try { + input = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + if (!input || typeof input !== "object") { + return NextResponse.json({ error: "Request body must be an object" }, { status: 400 }); + } + + const record = input as Record; + if (!record.body || typeof record.body !== "object") { + return NextResponse.json({ error: "body is required" }, { status: 400 }); + } + if (typeof record.executionId !== "string" || typeof record.workspaceId !== "string" || typeof record.actorId !== "string") { + return NextResponse.json( + { error: "executionId, workspaceId and actorId are required" }, + { status: 400 }, + ); + } + + try { + const prepared = prepareCappoExecution({ + body: record.body as Record, + executionId: record.executionId, + workspaceId: record.workspaceId, + actorId: record.actorId, + }); + return NextResponse.json(prepared, { + headers: { "cache-control": "no-store" }, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "CAPPO request preparation failed"; + const configurationFailure = /required|must use HTTPS|must terminate/.test(message); + return NextResponse.json( + { error: "CAPPO_REQUEST_PREPARATION_FAILED", detail: message }, + { status: configurationFailure ? 503 : 400 }, + ); + } +} From c4536cdbdb34a2573261730954a1d3e4f7f1b8f4 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 15:58:19 -0400 Subject: [PATCH 04/11] fix: match CAPPO semantic envelope contract --- src/lib/covenant/cappo-preparer.ts | 35 +++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/lib/covenant/cappo-preparer.ts b/src/lib/covenant/cappo-preparer.ts index 17d3720..69e4651 100644 --- a/src/lib/covenant/cappo-preparer.ts +++ b/src/lib/covenant/cappo-preparer.ts @@ -53,6 +53,31 @@ function requireConfig(name: string): string { return value; } +function normalizedExecModel( + body: Record, + workspaceId: string, + actorId: string, +): Record { + return { + prompt: typeof body.prompt === "string" ? body.prompt : "", + agent_id: typeof body.agent_id === "string" ? body.agent_id : null, + pgl_id: actorId, + workspace_id: workspaceId, + tenant_id: typeof body.tenant_id === "string" ? body.tenant_id : "default", + delegation_depth: typeof body.delegation_depth === "number" ? body.delegation_depth : 0, + budget_approved_cents: typeof body.budget_approved_cents === "number" ? body.budget_approved_cents : 0, + action_cost_cents: typeof body.action_cost_cents === "number" ? body.action_cost_cents : 0, + scope: body.scope && typeof body.scope === "object" ? body.scope : null, + genome_hash: typeof body.genome_hash === "string" ? body.genome_hash : null, + constitution_hash: typeof body.constitution_hash === "string" ? body.constitution_hash : null, + plan_hash: typeof body.plan_hash === "string" ? body.plan_hash : null, + action: typeof body.action === "string" ? body.action : null, + directive: null, + risk_tier: typeof body.risk_tier === "string" ? body.risk_tier : null, + execution_mode: typeof body.execution_mode === "string" ? body.execution_mode : "live", + }; +} + export function prepareCappoExecution(input: PrepareCappoExecutionInput): PreparedCappoExecution { const targetUri = requireConfig("CAPPO_EXECUTION_URL"); const privateKey = requireConfig("COVENANT_HTTP_SIGNING_PRIVATE_KEY"); @@ -81,16 +106,18 @@ export function prepareCappoExecution(input: PrepareCappoExecutionInput): Prepar execution_id: input.executionId, }, }; - // A caller cannot self-declare CAPPO's decision. The kernel sets its internal - // ALLOW directive only after the lease-backed consequence evaluator succeeds. delete unsignedBody.directive; delete unsignedBody.security; + // CAPPO's current ExecRequest ignores the lease transport extension before + // constructing the semantic cAPI envelope. Mirror that typed model exactly; + // the full raw body (including the lease) is independently bound by RFC 9421. + const semanticData = normalizedExecModel(unsignedBody, input.workspaceId, input.actorId); const nonce = randomBytes(24).toString("base64url"); const securityPayload = { actor_id: input.actorId, action, - data_hash: sha256Hex(canonicalJson(unsignedBody)), + data_hash: sha256Hex(canonicalJson(semanticData)), nonce, }; const security = { @@ -142,8 +169,6 @@ export function prepareCappoExecution(input: PrepareCappoExecutionInput): Prepar scope_hash: sha256Hex(canonicalJson((unsignedBody.scope as Record) ?? {})), policy_decision_hash: sha256Hex(canonicalJson({ decision: "candidate", source: "capi-gatekeeper" })), candidate_act_hash: candidateActHash, - // CAPPO currently normalizes the canonical /v1/exec destination to this - // fixed digest-domain marker before the preauthorization check. destination_hash: "target_hash", rights: [action], issued_at: now, From f82ecb02c07818c93028a002bdef62b251357568 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 16:23:23 -0400 Subject: [PATCH 05/11] Add authenticated internal CAPPO request preparer --- src/app/api/internal/cappo/prepare/route.ts | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/app/api/internal/cappo/prepare/route.ts diff --git a/src/app/api/internal/cappo/prepare/route.ts b/src/app/api/internal/cappo/prepare/route.ts new file mode 100644 index 0000000..4ca5892 --- /dev/null +++ b/src/app/api/internal/cappo/prepare/route.ts @@ -0,0 +1,63 @@ +import { timingSafeEqual } from "crypto"; +import { NextRequest, NextResponse } from "next/server"; + +import { prepareCappoExecution } from "@/lib/covenant/cappo-preparer"; + +function safeEqual(left: string, right: string): boolean { + const a = Buffer.from(left); + const b = Buffer.from(right); + return a.length === b.length && timingSafeEqual(a, b); +} + +function internalKey(request: NextRequest): string { + const authorization = request.headers.get("authorization") ?? ""; + if (authorization.toLowerCase().startsWith("bearer ")) { + return authorization.slice(7).trim(); + } + return request.headers.get("x-cappo-internal-key")?.trim() ?? ""; +} + +export async function POST(request: NextRequest) { + const expected = process.env.CAPPO_INTERNAL_EXEC_KEY?.trim() ?? ""; + if (!expected) { + return NextResponse.json( + { error: "CAPPO_PREPARER_LOCKED", detail: "Internal preparation key is not configured." }, + { status: 503, headers: { "cache-control": "no-store" } }, + ); + } + const provided = internalKey(request); + if (!provided || !safeEqual(provided, expected)) { + return NextResponse.json( + { error: "CAPPO_PREPARER_UNAUTHORIZED" }, + { status: 401, headers: { "cache-control": "no-store" } }, + ); + } + + try { + const input = await request.json(); + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new Error("Preparation request must be a JSON object"); + } + const record = input as Record; + const body = record.body; + if (!body || typeof body !== "object" || Array.isArray(body)) { + throw new Error("body must be an object"); + } + const prepared = prepareCappoExecution({ + body: body as Record, + executionId: typeof record.executionId === "string" ? record.executionId : "", + workspaceId: typeof record.workspaceId === "string" ? record.workspaceId : "", + actorId: typeof record.actorId === "string" ? record.actorId : "", + }); + return NextResponse.json(prepared, { + status: 200, + headers: { "cache-control": "no-store, private" }, + }); + } catch (error) { + const detail = error instanceof Error ? error.message : "Invalid preparation request"; + return NextResponse.json( + { error: "CAPPO_PREPARATION_REJECTED", detail }, + { status: 400, headers: { "cache-control": "no-store" } }, + ); + } +} From a90026485ef750c505fb93f346f727bc8e5badeb Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 16:37:06 -0400 Subject: [PATCH 06/11] Test trusted CAPPO request preparation --- tests/cappo-preparer.test.ts | 115 +++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/cappo-preparer.test.ts diff --git a/tests/cappo-preparer.test.ts b/tests/cappo-preparer.test.ts new file mode 100644 index 0000000..c2f61ea --- /dev/null +++ b/tests/cappo-preparer.test.ts @@ -0,0 +1,115 @@ +import { createHash, generateKeyPairSync } from "crypto"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { prepareCappoExecution } from "../src/lib/covenant/cappo-preparer"; + +const originalEnv = { ...process.env }; + +function b64Json(value: string): Record { + return JSON.parse(Buffer.from(value, "base64").toString("utf8")) as Record; +} + +describe("prepareCappoExecution", () => { + beforeEach(() => { + const { privateKey } = generateKeyPairSync("ed25519"); + process.env.CAPPO_EXECUTION_URL = "https://cappo.veklom.com/v1/exec"; + process.env.COVENANT_HTTP_SIGNING_PRIVATE_KEY = privateKey + .export({ type: "pkcs8", format: "der" }) + .toString("base64"); + process.env.COVENANT_HTTP_SIGNING_KEY_ID = "capi-test-1"; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it("overrides caller identity, strips caller ALLOW, and binds the lease execution id", () => { + const prepared = prepareCappoExecution({ + executionId: "exec-trusted", + workspaceId: "workspace-trusted", + actorId: "operator-trusted", + body: { + prompt: "activation", + action: "activation.read", + workspace_id: "workspace-attacker", + pgl_id: "operator-attacker", + directive: "ALLOW", + security: { nonce: "attacker", signature: "attacker" }, + capability_lease: { + mount_id: "mnt-1", + token_id: "tok-1", + nonce: "lease-nonce", + execution_id: "exec-attacker", + }, + }, + }); + + const body = JSON.parse(prepared.body) as Record; + expect(body.workspace_id).toBe("workspace-trusted"); + expect(body.pgl_id).toBe("operator-trusted"); + expect(body).not.toHaveProperty("directive"); + expect(body.security).not.toEqual({ nonce: "attacker", signature: "attacker" }); + expect(body.capability_lease).toMatchObject({ execution_id: "exec-trusted" }); + expect(prepared.targetUri).toBe("https://cappo.veklom.com/v1/exec"); + }); + + it("binds WPT to the exact final body and signs every trust-bearing header", () => { + const prepared = prepareCappoExecution({ + executionId: "exec-1", + workspaceId: "workspace-1", + actorId: "operator-1", + body: { + prompt: "activation", + action: "activation.read", + capability_lease: { + mount_id: "mnt-1", + token_id: "tok-1", + nonce: "lease-nonce", + execution_id: "exec-1", + }, + }, + }); + + const wpt = b64Json(prepared.headers["workload-proof"]); + expect(wpt.body_hash).toBe(createHash("sha256").update(prepared.body).digest("hex")); + + const signatureInput = prepared.headers["Signature-Input"]; + for (const component of [ + "@method", + "@target-uri", + "content-digest", + "workload-identity", + "execution-context", + "workload-proof", + "veklom-authority", + "x-veklom-actor", + "x-veklom-nonce", + ]) { + expect(signatureInput).toContain(`\"${component}\"`); + } + expect(prepared.headers["x-veklom-actor"]).toBe("operator-1"); + expect(prepared.headers["x-veklom-nonce"]).toBeTruthy(); + }); + + it("fails closed when the execution target is not the canonical CAPPO endpoint", () => { + process.env.CAPPO_EXECUTION_URL = "https://cappo.veklom.com/v1/other"; + + expect(() => + prepareCappoExecution({ + executionId: "exec-1", + workspaceId: "workspace-1", + actorId: "operator-1", + body: { + prompt: "activation", + action: "activation.read", + capability_lease: { + mount_id: "mnt-1", + token_id: "tok-1", + nonce: "lease-nonce", + execution_id: "exec-1", + }, + }, + }), + ).toThrow("CAPPO_EXECUTION_URL must terminate at /v1/exec"); + }); +}); From 5e5a28ffdf774d8aba0ba2a1f8be03f3c7fd1f63 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 17:03:53 -0400 Subject: [PATCH 07/11] test: verify hardened RFC 9421 trust-header signature base --- test/http-message-signatures.test.ts | 44 +++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/test/http-message-signatures.test.ts b/test/http-message-signatures.test.ts index ca4081e..02676c1 100644 --- a/test/http-message-signatures.test.ts +++ b/test/http-message-signatures.test.ts @@ -5,20 +5,56 @@ import { generateKeyPair } from "../src/lib/covenant/crypto"; import { signCappoExecutionRequest } from "../src/lib/covenant/http-message-signatures"; describe("CAPPO RFC 9421 request profile", () => { - it("binds POST target URI and Content-Digest with the configured Ed25519 key", () => { + it("binds the exact CAPPO request and trust-bearing headers with Ed25519", () => { const keys = generateKeyPair(); const body = '{"prompt":"governed"}'; const target = "https://cappo.veklom.com/v1/exec"; - const headers = signCappoExecutionRequest(target, body, keys.privateKeyB64, "capi-gateway-1", 1_700_000_000); + const coveredHeaders = { + "workload-identity": "wit-value", + "execution-context": "ect-value", + "workload-proof": "wpt-value", + "veklom-authority": "authority-value", + "x-veklom-actor": "actor-1", + "x-veklom-nonce": "nonce-1", + }; + const headers = signCappoExecutionRequest( + target, + body, + keys.privateKeyB64, + "capi-gateway-1", + { coveredHeaders, created: 1_700_000_000 }, + ); + const sortedHeaderNames = Object.keys(coveredHeaders).sort(); + const components = ["@method", "@target-uri", "content-digest", ...sortedHeaderNames]; + const componentList = components.map((name) => `"${name}"`).join(" "); + const signatureParams = `(${componentList});created=1700000000;keyid="capi-gateway-1"`; const signatureBase = [ '"@method": POST', `"@target-uri": ${target}`, `"content-digest": ${headers["Content-Digest"]}`, - '"@signature-params": ("@method" "@target-uri" "content-digest");created=1700000000;keyid="capi-gateway-1"', + ...sortedHeaderNames.map( + (name) => `"${name}": ${coveredHeaders[name as keyof typeof coveredHeaders]}`, + ), + `"@signature-params": ${signatureParams}`, ].join("\n"); const signature = Buffer.from(headers.Signature.slice("sig1=:".length, -1), "base64"); expect(headers["Content-Digest"]).toMatch(/^sha-256=:.+:$/); - expect(verify(null, Buffer.from(signatureBase), createPublicKey({ key: Buffer.from(keys.publicKeyB64, "base64"), format: "der", type: "spki" }), signature)).toBe(true); + expect(headers["Signature-Input"]).toBe(`sig1=${signatureParams}`); + for (const name of sortedHeaderNames) { + expect(headers["Signature-Input"]).toContain(`"${name}"`); + } + expect( + verify( + null, + Buffer.from(signatureBase), + createPublicKey({ + key: Buffer.from(keys.publicKeyB64, "base64"), + format: "der", + type: "spki", + }), + signature, + ), + ).toBe(true); }); }); From ce3d060a80c85e700a6a55c256b1b2624772df27 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 17:12:23 -0400 Subject: [PATCH 08/11] security: remove duplicate admin-token CAPPO signer surface --- src/app/api/capi/v1/cappo/prepare/route.ts | 53 ---------------------- 1 file changed, 53 deletions(-) delete mode 100644 src/app/api/capi/v1/cappo/prepare/route.ts diff --git a/src/app/api/capi/v1/cappo/prepare/route.ts b/src/app/api/capi/v1/cappo/prepare/route.ts deleted file mode 100644 index d1469d0..0000000 --- a/src/app/api/capi/v1/cappo/prepare/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -import { requireAdminToken } from "@/lib/covenant/admin-auth"; -import { prepareCappoExecution } from "@/lib/covenant/cappo-preparer"; - -export const runtime = "nodejs"; - -export async function POST(request: NextRequest) { - const auth = requireAdminToken(request); - if (!auth.ok) { - return NextResponse.json({ error: auth.error }, { status: auth.status }); - } - - let input: unknown; - try { - input = await request.json(); - } catch { - return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); - } - if (!input || typeof input !== "object") { - return NextResponse.json({ error: "Request body must be an object" }, { status: 400 }); - } - - const record = input as Record; - if (!record.body || typeof record.body !== "object") { - return NextResponse.json({ error: "body is required" }, { status: 400 }); - } - if (typeof record.executionId !== "string" || typeof record.workspaceId !== "string" || typeof record.actorId !== "string") { - return NextResponse.json( - { error: "executionId, workspaceId and actorId are required" }, - { status: 400 }, - ); - } - - try { - const prepared = prepareCappoExecution({ - body: record.body as Record, - executionId: record.executionId, - workspaceId: record.workspaceId, - actorId: record.actorId, - }); - return NextResponse.json(prepared, { - headers: { "cache-control": "no-store" }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : "CAPPO request preparation failed"; - const configurationFailure = /required|must use HTTPS|must terminate/.test(message); - return NextResponse.json( - { error: "CAPPO_REQUEST_PREPARATION_FAILED", detail: message }, - { status: configurationFailure ? 503 : 400 }, - ); - } -} From 5b7489b7f6057244539ffec7c60bdfe70aa5c23b Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 18:14:12 -0400 Subject: [PATCH 09/11] config: document CAPPO signer key alignment --- .env.example | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 3d01784..9003904 100644 --- a/.env.example +++ b/.env.example @@ -1,30 +1,32 @@ # Covenant (cAPI) configuration. Copy to .env.local and fill in as needed. -# Every value is optional — unset means the related integration stays disabled -# and the runtime falls back to its in-process behavior. +# Unset integrations stay disabled. Production governed execution must configure +# every value in the CAPPO boundary section below; there is no local-execution +# fallback for the browser Activation path. # --- PGL ledger (gnomledger) forwarding --- -# When PGL_LEDGER_URL is set, every sealed evidence record (Phase 7) is mirrored -# into gnomledger's append-only, hash-chained ledger. Leave empty to keep the -# local seal only. PGL_LEDGER_URL= PGL_LEDGER_API_KEY= PGL_LEDGER_TIMEOUT_MS=8000 # --- Governed execution boundary (CAPPO only) --- -# cAPI resolves MCP/capability identity but never calls a provider directly. -# CAPPO_EXECUTION_URL must target CAPPO's /v1/exec endpoint. +# cAPI prepares and signs the request but never executes the consequence. +# CAPPO_EXECUTION_URL must be the exact externally observed CAPPO /v1/exec URI; +# RFC 9421 signs this exact target, so aliases or a different scheme/host fail. CAPPO_EXECUTION_URL= + +# Dedicated high-entropy secret for /api/internal/cappo/prepare. Configure the +# identical value in the Veklom frontend/server deployment. Do not reuse the +# general COVENANT_ADMIN_TOKEN or expose this value to NEXT_PUBLIC_* variables. CAPPO_INTERNAL_EXEC_KEY= -# Base64 PKCS#8 Ed25519 private key and its configured key id. Keep both -# deployment-only; CAPPO receives the corresponding public key. + +# Base64 DER PKCS#8 Ed25519 private key used for cAPI security-envelope and +# RFC 9421 request signing, plus its key id. CAPPO must receive the corresponding +# Base64 DER SPKI public key (or raw public-key hex) as CAPI_GATEKEEPER_PUBLIC_KEY. COVENANT_HTTP_SIGNING_PRIVATE_KEY= COVENANT_HTTP_SIGNING_KEY_ID= COVENANT_EXEC_TIMEOUT_MS=10000 # --- Runtime registry and mutation controls --- -# Production should load agent/capability/policy identity from a real registry. -# If unset, /api/state reports needs_proof and request execution requires fully -# signed CovenantRequest payloads for already-registered agents. COVENANT_REGISTRY_URL= COVENANT_REGISTRY_API_KEY= COVENANT_REGISTRY_JSON= @@ -32,8 +34,6 @@ COVENANT_REGISTRY_TTL_MS=30000 COVENANT_ADMIN_TOKEN= # --- Permanent Registry State (Redis) --- -# Configure this only in the deployment environment. Do not commit concrete -# internal service hostnames, addresses, or credentials to source examples. REDIS_URL= # Development-only toggles. Leave false/empty in production. From 9e7d2f50ec5d53c218a2d17b319bce11329a9422 Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 18:18:34 -0400 Subject: [PATCH 10/11] security: reject noncanonical snapshot signatures --- src/lib/mcp/snapshot.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/mcp/snapshot.ts b/src/lib/mcp/snapshot.ts index fc6ec0c..48e454f 100644 --- a/src/lib/mcp/snapshot.ts +++ b/src/lib/mcp/snapshot.ts @@ -11,6 +11,21 @@ export interface CapabilitySnapshot { hash: string; } +function decodeCanonicalEd25519Signature(signature: string): Buffer | null { + if (!/^[A-Za-z0-9+/]+={0,2}$/.test(signature) || signature.length % 4 !== 0) { + return null; + } + try { + const decoded = Buffer.from(signature, 'base64'); + if (decoded.length !== 64 || decoded.toString('base64') !== signature) { + return null; + } + return decoded; + } catch { + return null; + } +} + export function generateSnapshot(capabilities: any[], agentId: string | null): { snapshot: CapabilitySnapshot, signature: string } { const timestamp = Date.now(); @@ -31,9 +46,11 @@ export function generateSnapshot(capabilities: any[], agentId: string | null): { } export function verifySnapshot(hash: string, signature: string): boolean { + const signatureBytes = decodeCanonicalEd25519Signature(signature); + if (!signatureBytes) return false; try { - return crypto.verify(null, Buffer.from(hash), publicKey, Buffer.from(signature, 'base64')); - } catch (err) { + return crypto.verify(null, Buffer.from(hash), publicKey, signatureBytes); + } catch { return false; } } From 5cab744f23002ff014b6db8840a68c70155d0fff Mon Sep 17 00:00:00 2001 From: reprewindai-dev Date: Sun, 30 Aug 2026 18:18:44 -0400 Subject: [PATCH 11/11] test: deterministically tamper snapshot signature bytes --- src/lib/mcp/snapshot.test.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/lib/mcp/snapshot.test.ts b/src/lib/mcp/snapshot.test.ts index da8b397..4f6fbd1 100644 --- a/src/lib/mcp/snapshot.test.ts +++ b/src/lib/mcp/snapshot.test.ts @@ -29,11 +29,22 @@ describe('Capability Snapshot Signatures (x402 Fail-Closed)', () => { it('rejects tampered capability signatures', () => { const caps = [{ id: 'test', name: 'read' }]; const result = generateSnapshot(caps, 'agent-123'); - - // Tamper the signature - const tamperedSig = 'a' + result.signature.substring(1); - + + // Flip one actual signature byte so this test can never accidentally leave + // the original signature unchanged because of a coincidentally equal text character. + const tamperedBytes = Buffer.from(result.signature, 'base64'); + tamperedBytes[0] ^= 0x01; + const tamperedSig = tamperedBytes.toString('base64'); + const isValid = verifySnapshot(result.snapshot.hash, tamperedSig); expect(isValid).toBe(false); }); + + it('rejects noncanonical Base64 encodings', () => { + const caps = [{ id: 'test', name: 'read' }]; + const result = generateSnapshot(caps, 'agent-123'); + const noncanonical = `${result.signature}\n`; + + expect(verifySnapshot(result.snapshot.hash, noncanonical)).toBe(false); + }); });