Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/keeper-trust-key.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions packages/prx/src/provenance/keeper-trust.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
42 changes: 42 additions & 0 deletions packages/prx/test/provenance/keeper-trust.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined>) =>
(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();
});
});
Loading