# 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 <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:
  - 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 <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

````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 <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:

````bash
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:

````bash
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:

```bash
# 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:

```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                          |
|--------------------|------------|--------------------|-------------------------------|
| `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
