AgentDock

Documentation

Production signing and key rotation

The concrete procedure for creating production trust roots, signing releases and rotating or revoking keys, without using the development root.

Status: a procedure, not something done. The registry still runs on a development root key kept on one developer machine. Nothing below has been executed on production keys, because creating a trust root is an owner decision. Everything marked tested is covered by the automated suite (tests/trust.test.ts); the hardware steps are untested here.

Roles and keys#

KeySignsWhere it livesLifetime
Root keyThe registry catalogueHardware token or cloud KMS; never on a laptop disk or in CIYears; rotated deliberately
Publisher keysPackages (one or more per publisher)The publisher's own machine, token or KMSRotated per publisher policy
Grant key (entitlement gateway)Short-lived download grantsCloudflare secretRotated freely; gateway accepts old and new during rollover

Arcotheon never needs the root private key in its own process: registry build --signer-cmd "<cmd>" --key-id <id> hands the message to a signer command and reads back a base64 Ed25519 signature (tested).

Rehearsal#

tests/rotation-rehearsal.test.ts runs steps 2 to 6 below end to end on a copy of the real registry with throwaway keys and an external signer: dev to production rotation, the old key revoked and useless to an attacker, a new SDK pinning only production, recovery-root rotation, expiry refresh. It passes; no production key was created to do it. Run it again before the real ceremony.

1. Create the root#

Pick one:

  • Cloud KMS with Ed25519 support. AWS KMS supports ECC_NIST_EDWARDS25519. Create the key with SIGN_VERIFY usage, a key policy that allows only two named admins to sign, and CloudTrail logging. Export the public key.
  • Hardware token with Ed25519 (a YubiKey 5.7+ OpenPGP applet, or similar). Generate on the device so the private key never exists elsewhere. Keep a second token in a separate safe as a recovery root (see step 3).

The signer command receives the message on stdin and prints the base64 signature. For AWS KMS, a minimal wrapper is:

bash
#!/bin/sh
# sign.sh <kms-key-id>: Ed25519 over stdin, base64 on stdout
f=$(mktemp) && cat > "$f" && aws kms sign --key-id "$1" --message "fileb://$f" \
  --message-type RAW --signing-algorithm ED25519_SHA_512 --query Signature --output text; rm -f "$f"

The keyId is the SHA-256 of the raw 32-byte public key (hex); arcotheon keys show prints it for file keys, and node -e over the exported public key gives it for KMS.

2. Ceremony for the first production root#

  1. Two people, one machine, recorded. Generate the root (and the recovery root) as above.
  2. Record the public keys and keyIds in the repository through a reviewed pull request: registry/root-keys.json lists the production root active and the development root revoked.
  3. Build and sign the first production catalogue with both roots (--key <dev> --also-sign <prod> signed by the development root, which existing clients trust) so clients adopt the production root, then sign again with the production root alone, which makes the development root revoked everywhere it was ever adopted (tested as rotation).
  4. Release an SDK/CLI whose ARCOTHEON_ROOT_KEYS pins the production root (and the recovery root). From that release on, new installs never trust the development key. Old clients follow the rotation if they connect between steps 3 and 4.
  5. Destroy or archive the development key and record that it was done.

3. Day to day#

  • Releasing: arcotheon registry build --root registry --signer-cmd ./sign.sh --key-id <id>. Two-person rule: a second maintainer reviews the generated v1/ diff (registry verify first) before it is merged.
  • Catalogue expiry: catalogues expire after 60 days. Re-sign with registry build --refresh on a schedule (calendar it every 30 days). An expired catalogue makes online clients refuse the registry; that is the freeze-attack defence.
  • Recovery root: pinned in the SDK next to the primary root but kept offline. If the primary is lost, sign a rotation with the recovery root.

4. Rotation (planned)#

  1. Generate the new root. Add it to root-keys.json (registry root add --public-key <b64>).
  2. Build with the old key plus --also-sign <new>; publish; wait until clients have refreshed (at least one full catalogue lifetime for offline users).
  3. Mark the old key revoked (registry root revoke <keyId>), build signed by the new key only.
  4. Ship an SDK pinning the new root. Clients that never saw the new key must update: that is the one limit of this scheme.

5. Compromise response#

  • Publisher key: registry publisher revoke-key <id> <keyId> --reason … (or suspend), rebuild, release. Packages signed by the key stop installing and running (tested). The publisher registers a new key and republishes.
  • Root key: rotate immediately using the recovery root (step 4 with the recovery root as the signer), then ship an SDK that drops the compromised root. Until clients pin the replacement, keep co-signing every catalogue with the recovery root (--also-sign): a client that has not yet seen the replacement can only verify through a root it already pins. Treat all catalogues signed since the suspected compromise as untrusted.
  • A malicious package: registry revoke <id> <version> --reason … (or suspend the publisher); clients refuse it on their next catalogue fetch, and arcotheon doctor reports installed copies.

6. What the owner must do#

  • Decide the custody model (KMS or hardware) and create the keys: Arcotheon cannot do this for you.
  • Enable GitHub two-factor authentication for every maintainer and keep branch protection on (see below).
  • Put the grant signing key in a Cloudflare secret when the entitlement gateway is deployed.

Repository protection (configured)#

main requires a pull request with one approving review and a passing registry-review check, blocks force pushes and deletions, and requires conversations to be resolved. Administrators are not forced through these rules (enforce_admins is off), so the account owner can still merge their own work. Turn it on when more maintainers exist.