Spec: docs/superpowers/specs/2026-05-05-pack4-registry-signing.md.
These four scripts implement the offline + online operator workflow for the community registry's TUF-Light signing scheme.
| Script | Where it runs | Purpose |
|---|---|---|
generate_root_key.py |
Offline machine, once at marketplace launch | Mint the long-lived Root keypair. Public key gets pinned in source. |
generate_targets_key.py |
Offline machine, every rotation | Mint a fresh Targets keypair. Public key goes into a new root.json. |
sign_root.py |
Offline machine, every rotation | Sign root.json with the Root key, delegating to a Targets key. |
sign_payload.py |
GitHub Actions (online) | Sign registry.json / recalls/active.json / publishers/*.json with the Targets key. |
# All on an offline machine.
python scripts/registry_signing/generate_root_key.py --out-dir ./keys/root
python scripts/registry_signing/generate_targets_key.py --out-dir ./keys/targets
# Sign the initial root.json delegating to the brand-new Targets key.
python scripts/registry_signing/sign_root.py \
--root-key ./keys/root/root_private.pem \
--targets-pubkey ./keys/targets/targets_public.b64 \
--version 1 \
--valid-days 365 \
--min-client-version 0.97.0 \
--out ./registry/root.json
# Move the keys offline. Then:
# 1. Edit src/cognithor/skills/community/_pinned_keys.py:
# ROOT_PUBLIC_KEY_B64 = "<contents of keys/root/root_public.b64>"
# 2. Cut a Cognithor release including that change.
# 3. Push registry/root.json to the registry repo.
# 4. Set GitHub-Actions secret REGISTRY_TARGETS_PRIVATE_KEY = contents of
# keys/targets/targets_private.pem (in the registry repo).
# 5. Move keys/targets/targets_private.pem to encrypted offline backup;
# its only live copy now lives in the GitHub-Actions secret.
# 6. Move keys/root/* to a hardware token / encrypted air-gapped drive.Run this from a GitHub Actions workflow on every push to the registry repo:
- name: Sign registry payloads
env:
REGISTRY_TARGETS_PRIVATE_KEY: ${{ secrets.REGISTRY_TARGETS_PRIVATE_KEY }}
run: |
echo "$REGISTRY_TARGETS_PRIVATE_KEY" > /tmp/targets.pem
python scripts/registry_signing/sign_payload.py \
--in unsigned/registry.json \
--key /tmp/targets.pem \
--out signed/registry.json
python scripts/registry_signing/sign_payload.py \
--in unsigned/recalls/active.json \
--key /tmp/targets.pem \
--out signed/recalls/active.json
rm /tmp/targets.pemEach unsigned input must include _type, version, issued_at, valid_until, plus the type-specific body. sign_payload.py validates these before signing.
# Offline machine.
python scripts/registry_signing/generate_targets_key.py --out-dir ./keys/targets-v2
# Sign root.json v2 delegating to the new Targets key.
# The version MUST be > the current root.json version.
python scripts/registry_signing/sign_root.py \
--root-key ./keys/root/root_private.pem \
--targets-pubkey ./keys/targets-v2/targets_public.b64 \
--version 2 \
--valid-days 365 \
--min-client-version 0.97.0 \
--out ./registry/root.jsonThen:
- Push the new
root.jsonto the registry repo. - Update the GitHub-Actions secret
REGISTRY_TARGETS_PRIVATE_KEYto the new private key. - Wait for clients to pick up the new
root.json(next sync). - Securely destroy the old Targets private key from GitHub-Actions and any local copies.
There is no code recovery path — this is by design. The Root key lives on a hardware token / air-gapped drive precisely so this column has no online recovery option.
The only response is:
- Generate a new Root keypair on a clean offline machine.
- Cut a new Cognithor release with
_pinned_keys.pyupdated. - Sign a fresh
root.jsonwith the new Root key (and a new Targets key). - Burn the old Root key. Old clients without the new release will continue trusting the old Root indefinitely — they need to update.
The corresponding owner runbook is in docs/runbooks/registry_key_rotation.md.
- Commit
*_private.pemto any git repo. - Run
generate_root_key.pyon a network-connected machine. - Set
REGISTRY_TARGETS_PRIVATE_KEYto anything other than the freshly minted Targets PEM. - Add a CLI flag that disables signature verification on the client. The build-time
REQUIRE_SIGNED_REGISTRYconstant in_pinned_keys.pyis intentionally not togglable from the CLI — that would be a downgrade vector.