From 91e715223a8c68aaebc716ab760c35d8486df4ec Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:57:14 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(provenance):=20resolveKeeperTrustKey?= =?UTF-8?q?=20=E2=80=94=20operator-supplied=20keeper=20trust=20key,=20fail?= =?UTF-8?q?-closed=20(Phase=20B.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/prx/src/provenance/keeper-trust.ts | 47 +++++++++++++++++++ .../prx/test/provenance/keeper-trust.test.ts | 42 +++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 packages/prx/src/provenance/keeper-trust.ts create mode 100644 packages/prx/test/provenance/keeper-trust.test.ts diff --git a/packages/prx/src/provenance/keeper-trust.ts b/packages/prx/src/provenance/keeper-trust.ts new file mode 100644 index 00000000..68315c33 --- /dev/null +++ b/packages/prx/src/provenance/keeper-trust.ts @@ -0,0 +1,47 @@ +/** + * Resolve the **keeper trust key** — the public key prx verifies door-keeper's L3 + * attestation against (Phase B.2). Per the recorded hardening decision, the + * anchor is **operator-supplied and NEVER derived from the actor**: no + * `getPublicKey` (the actor asserting its own key is circular), no image-bind + * (still trusts the build). The operator sets it out-of-band; if it's absent the + * caller fails closed. + * + * `PRX_KEEPER_PUBKEY` is either a PEM literal or a path to a PEM file (mirrors + * `provenance/config.ts`'s env + injected-`read` pattern, so it stays seam-safe + * and offline-testable). Image-bind / TOFU-pin, if ever added, are explicit + * opt-ins layered ABOVE this — never a silent fallback. + */ + +import { readFileSync } from "node:fs"; + +import { getEnv } from "@bounded-systems/env"; + +/** Env var carrying the operator's keeper trust key (a PEM, or a path to one). */ +export const KEEPER_PUBKEY_ENV = "PRX_KEEPER_PUBKEY"; + +type EnvReader = (key: string) => string | undefined; + +function looksLikePem(s: string): boolean { + return s.trimStart().startsWith("-----BEGIN"); +} + +/** + * The operator-supplied keeper public key (PEM), or `null` when none is + * configured or it can't be read — the caller fails closed on `null` under + * `requireSigned`. The key is NEVER fetched from the actor or its image. + */ +export function resolveKeeperTrustKey( + env: EnvReader = getEnv, + read: (path: string) => string = (p) => readFileSync(p, "utf8"), +): string | null { + const raw = env(KEEPER_PUBKEY_ENV); + if (raw === undefined || raw.trim() === "") return null; // unset → fail closed + if (looksLikePem(raw)) return raw; // a PEM literal + // Otherwise treat it as a path to a PEM file. + try { + const contents = read(raw); + return looksLikePem(contents) ? contents : null; + } catch { + return null; // configured but unreadable / not a PEM → fail closed + } +} diff --git a/packages/prx/test/provenance/keeper-trust.test.ts b/packages/prx/test/provenance/keeper-trust.test.ts new file mode 100644 index 00000000..bde1b80e --- /dev/null +++ b/packages/prx/test/provenance/keeper-trust.test.ts @@ -0,0 +1,42 @@ +// Checks for resolveKeeperTrustKey (Phase B.2): the keeper trust key is +// operator-supplied and fail-closed — never derived from the actor. +import { describe, test, expect } from "bun:test"; + +import { resolveKeeperTrustKey } from "../../src/provenance/keeper-trust.ts"; + +const PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n"; +const envOf = + (vals: Record) => + (k: string): string | undefined => + vals[k]; + +describe("resolveKeeperTrustKey", () => { + test("returns null when PRX_KEEPER_PUBKEY is unset (fail closed)", () => { + expect(resolveKeeperTrustKey(envOf({}))).toBeNull(); + }); + + test("returns null for an empty value (fail closed)", () => { + expect(resolveKeeperTrustKey(envOf({ PRX_KEEPER_PUBKEY: " " }))).toBeNull(); + }); + + test("accepts a PEM literal", () => { + expect(resolveKeeperTrustKey(envOf({ PRX_KEEPER_PUBKEY: PEM }))).toBe(PEM); + }); + + test("reads a PEM from a path", () => { + const read = (p: string) => (p === "/keys/keeper.pem" ? PEM : "nope"); + expect(resolveKeeperTrustKey(envOf({ PRX_KEEPER_PUBKEY: "/keys/keeper.pem" }), read)).toBe(PEM); + }); + + test("fails closed when the path holds a non-PEM", () => { + const read = () => "not a pem"; + expect(resolveKeeperTrustKey(envOf({ PRX_KEEPER_PUBKEY: "/keys/bad" }), read)).toBeNull(); + }); + + test("fails closed when the path is unreadable", () => { + const read = () => { + throw new Error("ENOENT"); + }; + expect(resolveKeeperTrustKey(envOf({ PRX_KEEPER_PUBKEY: "/missing" }), read)).toBeNull(); + }); +}); From 21fb8fefb7fae2bb3fbfaa735934f68833cf2705 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:57:42 -0400 Subject: [PATCH 2/2] docs(changeset): resolveKeeperTrustKey (no release) --- .changeset/keeper-trust-key.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/keeper-trust-key.md diff --git a/.changeset/keeper-trust-key.md b/.changeset/keeper-trust-key.md new file mode 100644 index 00000000..8310cda8 --- /dev/null +++ b/.changeset/keeper-trust-key.md @@ -0,0 +1,9 @@ +--- +--- + +Add `resolveKeeperTrustKey` (`provenance/keeper-trust.ts`): resolve the +operator-supplied keeper trust public key from `PRX_KEEPER_PUBKEY` (PEM literal +or path), fail-closed when absent/unreadable. Per the hardening decision the +anchor is never derived from the actor (no `getPublicKey` / image-bind). The +verify primitive (`verifyL3Attestation`, #734) pairs with this. Not yet wired +into the gate. No API or behavior change, no release.