Files
punktfunk-plugin-index/tools/keys.ts
T
enricobuehlerandClaude Fable 5 575ea1a371
publish / publish (push) Failing after 18s
chore(index): repin to the operator-held signing key
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>
2026-07-20 22:57:38 +02:00

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",
});
}