publish / publish (push) Failing after 18s
The bootstrap key generated during implementation was lost with its scratch directory. Nothing shipped pinning it, so this is a straight replacement of slot 0 rather than a rotation — the second slot stays reserved for a real one. The private half now lives only in the INDEX_SIGNING_KEY secret, so CI is the only thing that can sign. This push is what makes it do so: the committed signature is still the old key's, and the verify self-check will fail loudly if the secret does not match the public key pinned here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
/**
|
|
* Shared ed25519 key helpers.
|
|
*
|
|
* The host pins public keys in the exact string form `ed25519:<base64>`, where
|
|
* the base64 payload is the RAW 32-byte ed25519 public key -- NOT a DER/SPKI
|
|
* blob and NOT a PEM. node:crypto only exports SPKI, so we slice the raw key
|
|
* out of it (and splice it back in on the way in).
|
|
*
|
|
* An ed25519 SPKI DER is always exactly 44 bytes:
|
|
* 30 2a 30 05 06 03 2b 65 70 03 21 00 || <32-byte raw key>
|
|
* so the prefix is a fixed 12-byte constant.
|
|
*/
|
|
import { createPublicKey, type KeyObject } from "node:crypto";
|
|
|
|
/** Fixed 12-byte DER prefix of an ed25519 SubjectPublicKeyInfo. */
|
|
const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
|
|
export const PUBKEY_PREFIX = "ed25519:";
|
|
|
|
/**
|
|
* The public key currently compiled into the Punktfunk host (slot 1).
|
|
* `verify` defaults to this so a signed index can be checked with no arguments.
|
|
*/
|
|
export const DEFAULT_PUBLIC_KEY =
|
|
"ed25519:qK18TE2qygIyObtMlHxUI/G1gKby8tPxuieAfnEKgYE=";
|
|
|
|
/** Encode a node KeyObject public key as the host's `ed25519:<base64>` form. */
|
|
export function encodePublicKey(key: KeyObject): string {
|
|
const spki = key.export({ type: "spki", format: "der" });
|
|
if (spki.length !== 44 || !spki.subarray(0, 12).equals(SPKI_PREFIX)) {
|
|
throw new Error(
|
|
`not an ed25519 public key (unexpected SPKI: ${spki.length} bytes)`,
|
|
);
|
|
}
|
|
return PUBKEY_PREFIX + spki.subarray(12).toString("base64");
|
|
}
|
|
|
|
/** Parse the host's `ed25519:<base64>` form into a usable KeyObject. */
|
|
export function decodePublicKey(text: string): KeyObject {
|
|
const trimmed = text.trim();
|
|
if (!trimmed.startsWith(PUBKEY_PREFIX)) {
|
|
throw new Error(`public key must start with "${PUBKEY_PREFIX}"`);
|
|
}
|
|
const b64 = trimmed.slice(PUBKEY_PREFIX.length);
|
|
if (!/^[A-Za-z0-9+/]{43}=$/.test(b64)) {
|
|
throw new Error(
|
|
"public key payload must be base64 of exactly 32 raw bytes",
|
|
);
|
|
}
|
|
const raw = Buffer.from(b64, "base64");
|
|
if (raw.length !== 32) {
|
|
throw new Error(`public key must be 32 raw bytes, got ${raw.length}`);
|
|
}
|
|
return createPublicKey({
|
|
key: Buffer.concat([SPKI_PREFIX, raw]),
|
|
format: "der",
|
|
type: "spki",
|
|
});
|
|
}
|