# 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: ```bash 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`](https://keys.openpgp.org/vks/v1/by-fingerprint/8B1140ADB5185334E9FA513B75681C8167312680) 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 ` | | Other UID 1 | `Alexander Hess ` | | Other UID 2 | `Alexander Hess ` | ### 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 (`!`) 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/-public-YYYY.asc`, committed at every ceremony - One commit per ceremony to keep snapshot, changelog, and re-signed `KEYS.md` together - Distribution: - WKD on all three UID domains - public keyservers (see section below) - public websites because this repo is the archive and not the channel: - [alexander-hess.com](https://www.alexander-hess.com/KEYS.md) ([Signature](https://www.alexander-hess.com/KEYS.md.asc)) - [webartifex.biz](https://www.webartifex.biz/KEYS.md) ([Signature](https://www.webartifex.biz/KEYS.md.asc)) - [webartifex.de](https://www.webartifex.de/KEYS.md) ([Signature](https://www.webartifex.de/KEYS.md.asc)) - Machines: subkeys only via `gpg --export-secret-subkeys !` => 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 ````bash 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: ```bash # 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. ````bash 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 # passphrase: 32 random chars from Vaultwarden gpg --edit-key adduid # Alexander Hess adduid # Alexander Hess 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 # => Vaultwarden login "Key Info" with all key IDs gpg --gen-revoke # reason 1 = compromised # => Vaultwarden note "Revocation Certificate" gpg --armor --export-secret-keys # => 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: ````bash gpg --armor --export-secret-subkeys > /tmp/subkeys.asc gpg --delete-secret-keys gpg --import /tmp/subkeys.asc gpg --list-secret-keys # Must show sec# and two ssb! ```` Re-passphrase the deployed subkeys with a memorable phrase for daily and headless signing: ````bash gpg --edit-key passwd quit gpg --armor --export-secret-subkeys > /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: ```bash # On each machine: gpgconf --kill gpg-agent gpg --import subkeys-deploy.asc echo ":6:" | gpg --import-ownertrust gpg --list-secret-keys # Must show sec# and two ssb! echo test | gpg --pinentry-mode loopback -u ! --sign -o /dev/null shred -u subkeys-deploy.asc ``` Finally, clean up the temporary files: ```bash shred -u /tmp/subkeys.asc /tmp/subkeys-deploy.asc ``` ## Key History All keys I have ever published live in the [`history`](history/) folder as armored exports in their final state. Each file is self-proving: ```bash gpg --show-keys history/*.asc # shows the revocations and expiries ``` | Key ID | Created | Terminated | Note | |--------------------|------------|--------------------|-------------------------------| | `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