1
0
Fork 0
keys/KEYS.md
Alexander Hess 0211c311d4
Generate a new PGP key
Document how the new key was created
2026-07-08 15:29:55 +02:00

6.7 KiB

OpenPGP Key Documentation & Ceremonies

This document specifies how the PGP keys are maintained for Alexander Hess (i.e., "me" in the following). It is re-signed at every ceremony, like so:

gpg --detach-sign --armor KEYS.md  # to produce KEYS.md.asc

gpg --verify KEYS.md.asc KEYS.md

Current Key

8B11 40AD B518 5334 E9FA 513B 7568 1C81 6731 2680 on OpenPGP

Field Value
Fingerprint 8B11 40AD B518 5334 E9FA 513B 7568 1C81 6731 2680
Key ID 75681C8167312680 (fingerprint's last 16 chars)
Primary ed25519 [C] OFFLINE (i.e., sec# on all machines)
Subkey 1 cv25519 [E]
Subkey 2 ed25519 [S] internal
Created 2026-07-08
Expires always End of January (next: 2027-01-31)
Primary UID Alexander Hess <ah@alexander-hess.com>
Other UID 1 Alexander Hess <alexander@webartifex.biz>
Other UID 2 Alexander Hess <alexander@webartifex.de>

Subkeys

Key ID Type Purpose Expires
253A7FFDDF34BE5E E encryption 2027-01-31
B46EDB4DFC26805D S signing on my own machines 2027-01-31

Policies

  • All parts (primary and every subkey) expire at the end of January so the update ceremony in early January has a brief grace period
  • No [A] subkey: SSH stays separate in ~/.ssh because I prefer defaults over cleverness
  • Internal [S] subkey pinned everywhere with bang syntax (<FPR>!) because I never want to rely on GPG's newest-subkey auto-selection
  • External [S] subkeys per client engagement with shorter 6-month expiry living on client machines

Storage

  • Secret material: Vaultwarden as secure notes (because they are offline-synced text, unlike attachments):
    • Armored export of the primary secret key
    • Revocation certificate, generated at key creation
  • Public record: this repo:
    • KEYS.md and detached signature KEYS.md.asc
    • snapshots/<KEYID>-public-YYYY.asc, committed at every ceremony
    • One commit per ceremony to keep snapshot, changelog, and re-signed KEYS.md together
  • Distribution:
  • Machines: subkeys only via gpg --export-secret-subkeys <FPR>! => keyring shows sec#
  • Rationale: secrets and public record in independent systems; so, either one alone suffices to recover or verify the other

Publication to Keyservers

FPR=8B1140ADB5185334E9FA513B75681C8167312680

for KS in \
    hkps://keys.openpgp.org \
    hkps://keyserver.ubuntu.com \
    hkps://pgpkeys.eu \
    hkps://pgp.mit.edu \
    hkps://keyserver.escomposlinux.org; do
  echo "=== $KS ==="
  gpg --keyserver "$KS" --send-keys "$FPR"
done
  • keys.openpgp.org strips UIDs until each address is verified => Click the links mailed to all addresses
  • Hockeypuck servers (the other four) are append-only: They merge and never delete => Run after every change

Ceremonies

  • The gpg-agent caches passphrases => Run gpg-connect-agent reloadagent /bye before any step that must actually prompt
  • Over ssh, the import may hang, for example, if the agent tries to raise a graphical pinentry => gpgconf --kill gpg-agent and gpg ... --pinentry-mode loopback (Note: loopback asks for a new passphrase only ONCE without confirmation)
  • Scratch keyrings isolate any step from the main keyring:
  # Disposable $GNUPGHOME
  T=$(mktemp -d)
  chmod 700 "$T"

  # To scope any operation to "$T"
  gpg --homedir "$T" ...

  # To stop the scratch agent
  gpgconf --homedir "$T" --kill gpg-agent
  rm -rf "$T"

To generate a brand-new Key

This section documents how we created the current key in the first place.

gpg --expert --full-generate-key
# (11) ECC (set your own) => toggle S off => only Certify => (1) Curve 25519
# expiry: end of NEXT January
# UID: Alexander Hess <ah@alexander-hess.com>
# passphrase: 32 random chars from Vaultwarden

gpg --edit-key <FPR>
  adduid       # Alexander Hess <alexander@webartifex.biz>
  adduid       # Alexander Hess <alexander@webartifex.de>
  uid 1        # Select the UID to become the default...
  primary      # ...and flag it
  addkey       # (12) ECC (encrypt only) => Curve 25519 => same expiry
  addkey       # (10) ECC (sign only)    => Curve 25519 => same expiry
  save

gpg --list-secret-keys --with-subkey-fingerprints <FPR>
# => Vaultwarden login "Key Info" with all key IDs

gpg --gen-revoke <FPR>
# reason 1 = compromised
# => Vaultwarden note "Revocation Certificate"

gpg --armor --export-secret-keys <FPR>
# => Vaultwarden note "Full Secret Key"

Ensure everything goes into Vaultwarden. Verify the keys from Vaultwarden before the destructive part below.

Split the keys so that machines only ever hold the subkeys while the primary secret key stays in Vaultwarden only:

gpg --armor --export-secret-subkeys <FPR> > /tmp/subkeys.asc
gpg --delete-secret-keys <FPR>
gpg --import /tmp/subkeys.asc
gpg --list-secret-keys <FPR>  # Must show sec# and two ssb!

Re-passphrase the deployed subkeys with a memorable phrase for daily and headless signing:

gpg --edit-key <FPR>
  passwd
  quit

gpg --armor --export-secret-subkeys <FPR> > /tmp/subkeys-deploy.asc
# => Vaultwarden note "Deployed Secret Keys"

Verify the deploy keys from Vaultwarden with a scratch keyring, sign test with the memorable phrase, then distribute to the machines:

# On each machine:
gpgconf --kill gpg-agent
gpg --import subkeys-deploy.asc
echo "<FPR>:6:" | gpg --import-ownertrust

gpg --list-secret-keys <FPR>  # Must show sec# and two ssb!
echo test | gpg --pinentry-mode loopback -u <S_SUBKEY_FPR>! --sign -o /dev/null

shred -u subkeys-deploy.asc

Finally, clean up the temporary files:

shred -u /tmp/subkeys.asc /tmp/subkeys-deploy.asc