1
0
Fork 0
keys/KEYS.md
Alexander Hess a4f48a61a3
Supersede all old keys with 75681C8167312680
Executed the full supersession ceremony:

- ceremonies/: signed record of the supersession
  (signed by both old keys plus the new one)
- history/: added final states of
  344EA5AB10D868E0 (main key from 2020 to 2026) and
  6D11C1E91D3B0265 (main key for 5 months in 2026),
  both revoked as "superseded" and cross-certifying the new key
- snapshots/: first export of 75681C8167312680 as published,
  certified by both predecessors, verified on keys.openpgp.org
- the UID alexander@hess.software is revoked and withdrawn

All five keyservers, WKD (three domains), and the `git` forges
serve only the new key from now on.
2026-07-08 17:34:00 +02:00

8.4 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
    • ceremonies/<date>_<event>.md with own signatures for exceptional events (e.g., key supersessions); routine ceremonies go into the changelog below instead
  • 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

Key History

All keys I have ever published live in the history folder as armored exports in their final state.

Each file is self-proving:

gpg --show-keys history/*.asc  # shows the revocations and expiries
Key ID Created Terminated Note
6D11C1E91D3B0265 2026-02-16 revoked 2026-07-08 main key for 5 months in 2026
344EA5AB10D868E0 2020-07-17 revoked 2026-07-08 main key from 2020 to 2026
6F75AAA4E8C99523 2020-06-16 revoked 2020-07-17 discarded
9EE068C095A840A0 2018-10-16 revoked same day botched
77D28D388024AE85 2018-06-17 revoked same day botched
887603E7D66B8AD4 2018-06-17 revoked same day botched
CD9A863C023DB05B 2018-06-17 revoked same day never expires (see below)
12325FF0C2459FA0 2017-02-21 expired 2018-01-02 first key ever
  • All private keys are destroyed; so, none of the keys listed here can ever sign or decrypt again
  • These exports are the union of four public keyservers plus local archives, making them more complete than any single public source
  • The expiry policy above is a lesson from CD9A863C023DB05B: A revocation must reach every copy whereas an expiry travels inside every copy