Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .changeset/release-trust-226.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@cloudhal/release-verify": patch
"@cloudhal/core-contracts": patch
---

Signed key and yank lists for release bundles (RFC-0046 §9, #226).

`@cloudhal/core-contracts` gains the `release-keys` root (`keys.json`, signed by an offline root) and the `release-yank-list` root (`yanked.json`, signed by a yank key and expiring 30 days after it is issued).

`@cloudhal/release-verify` gains:
- `verifyKeys`, which checks a pinned root's signature and that the list is fresh (`expires` at most 13 months after `issuedAt`);
- `verifyYanked`, which checks a signature by an active yank key that `keys.json` lists, and refuses a stale list with `stale-yank-list`;
- `rememberYanks` and the `memory` option of `verifyKeys` and `verifyYanked`, for consumers that keep state: they refuse a rolled-back or pruned yank list, a replayed older `keys.json`, and a revoked or replaced yank key. A `keys.json` may list at most one active yank key;
- `mergeTrust`, which only reduces trust in the pinned keys: it revokes, marks retiring, and narrows a key's validity window, and it never adds a key.

`verifyRelease` now enforces each key's validity window. A statement's `createdAt` must lie within its key's `[notBefore, notAfter]`. After `notAfter`, a key verifies only while a fresh `keys.json` lists it as retiring; otherwise the release is refused with `key-expired`. `verifyRelease` also refuses a yanked version (`yanked`). `PINNED_KEYS` now carries each key's window.
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,13 @@ jobs:
// The active release key (docs/security/release-signing-keys.md; a test keeps them equal).
const PINNED_KEY_ID = "cloudhal-release-2026a";
const PINNED_PUBLIC_KEY = "7az3pPc4oT0AOfRcdfOZZQkM6XAGHo2jXEdDZ1qEVR8=";
const PINNED_NOT_BEFORE = "2026-11-12T00:00:00Z";
const PINNED_NOT_AFTER = "2027-11-12T00:00:00Z";
const fail = (m) => { console.error(`bundle-sign: ${m}`); process.exit(1); };
// RFC-0046 §9: no new signature within 90 days of the key's notAfter, so consumers get the
// next key in time. Dry runs check it too, so a dry run warns before a release fails.
const daysLeft = (Date.parse(PINNED_NOT_AFTER) - Date.now()) / 86_400_000;
if (!(daysLeft >= 90)) fail(`${PINNED_KEY_ID} expires on ${PINNED_NOT_AFTER.slice(0, 10)}, under 90 days away: rotate it first (docs/security/release-signing-keys.md, Rotation)`);
const dryRun = process.env.DRY_RUN === "true";
const keyId = dryRun ? `dryrun-${process.env.GITHUB_RUN_ID}` : PINNED_KEY_ID;
if (process.env.KEY_ID !== keyId) fail("KEY_ID is not the key this run signs with");
Expand All @@ -211,6 +217,9 @@ jobs:
const raw = pub.export({ format: "der", type: "spki" }).subarray(12).toString("base64");
if (!dryRun && raw !== PINNED_PUBLIC_KEY) fail(`the signing key is not ${PINNED_KEY_ID}`);
const statement = readFileSync("bundle/release-statement.json");
// RFC-0046 §9: a statement is valid only when its createdAt lies in the key's window.
const createdAt = Date.parse(JSON.parse(statement.toString("utf8")).createdAt);
if (!(createdAt >= Date.parse(PINNED_NOT_BEFORE) && createdAt <= Date.parse(PINNED_NOT_AFTER))) fail(`the statement's createdAt is outside ${PINNED_KEY_ID}'s window [${PINNED_NOT_BEFORE}, ${PINNED_NOT_AFTER}]`);
const sig = sign(null, statement, key);
if (sig.length !== 64 || !verify(null, statement, pub, sig)) fail("the signature does not verify");
writeFileSync("bundle/release-statement.sig", sig);
Expand Down
2 changes: 2 additions & 0 deletions crates/cloudhal-schema/src/generated/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ pub mod page;
pub mod project;
pub mod redirect;
pub mod release;
pub mod release_keys;
pub mod release_manifest;
pub mod release_statement;
pub mod release_yank_list;
pub mod schedule_run;
pub mod section;
pub mod site;
Expand Down
Loading
Loading