8 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 2680on 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~/.sshbecause 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.mdand detached signatureKEYS.md.ascsnapshots/<KEYID>-public-YYYY.asc, committed at every ceremony- One commit per ceremony to keep
snapshot, changelog, and re-signed
KEYS.mdtogether
- Distribution:
- WKD on all three UID domains
- public keyservers (see section below)
- public websites because this repo is the archive and not the channel:
- Machines: subkeys only
via
gpg --export-secret-subkeys <FPR>!=> keyring showssec# - 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.orgstrips 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-agentcaches passphrases => Rungpg-connect-agent reloadagent /byebefore 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-agentandgpg ... --pinentry-mode loopback(Note:loopbackasks 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 |
|---|---|---|---|
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