Restructure the repository
- Make OpenPGP only one of many protocols + KEYS.md only holds the current keys + OPENPGP.md describes all OpenPGP details - Make the current public key available at the root - Add scripts/ to automate the various steps: + bootstrap.sh + publish.sh + sign.sh + snapshot.sh + verify.sh
This commit is contained in:
parent
a4f48a61a3
commit
8ece401276
11 changed files with 861 additions and 239 deletions
240
KEYS.md
240
KEYS.md
|
|
@ -1,239 +1,25 @@
|
||||||
# OpenPGP Key Documentation & Ceremonies
|
# Keys
|
||||||
|
|
||||||
This document specifies how the PGP keys are maintained
|
The current public keys of Alexander Hess
|
||||||
for Alexander Hess (i.e., "me" in the following).
|
(i.e., "me" in this repository).
|
||||||
It is re-signed at every ceremony, like so:
|
|
||||||
|
This file is re-signed at every ceremony,
|
||||||
|
which can be verified:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gpg --detach-sign --armor KEYS.md # to produce KEYS.md.asc
|
./scripts/sign.sh KEYS.md # to produce KEYS.md.asc
|
||||||
|
|
||||||
gpg --verify KEYS.md.asc KEYS.md
|
gpg --verify KEYS.md.asc KEYS.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Current Key
|
## OpenPGP
|
||||||
|
|
||||||
|
Fingerprint:
|
||||||
|
|
||||||
> [`8B11 40AD B518 5334 E9FA 513B 7568 1C81 6731 2680`](https://keys.openpgp.org/vks/v1/by-fingerprint/8B1140ADB5185334E9FA513B75681C8167312680)
|
> [`8B11 40AD B518 5334 E9FA 513B 7568 1C81 6731 2680`](https://keys.openpgp.org/vks/v1/by-fingerprint/8B1140ADB5185334E9FA513B75681C8167312680)
|
||||||
on OpenPGP
|
on [OpenPGP](https://keys.openpgp.org)
|
||||||
|
|
||||||
|
Armored export: [`public-key.asc`](public-key.asc)
|
||||||
|
|
||||||
| Field | Value |
|
For policies, ceremonies, and history, see: [`OPENPGP.md`](OPENPGP.md)
|
||||||
|-------------|-------------------------------------------------------|
|
|
||||||
| 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
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
-----BEGIN PGP SIGNATURE-----
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCak5t4QAKCRC0bttN/CaA
|
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcTMgAKCRC0bttN/CaA
|
||||||
XUJ6AQC5D4Mq9Zg9LBxEsXBf4vXckBdHS0gNoDWElPngLHq73wEApM6GZC3RpWHm
|
XQsIAQCbVwRUzKeoM3OMJlSbGq4bpuSDmWXGPo/s5KbJlMEkNAEA1gVHfRpomGwL
|
||||||
6d/V4l1k5akWovyhJaH5AzQHMi/nGwM=
|
coNc4VC9qDwk5wdpis+NDRNtpIOIqgs=
|
||||||
=et51
|
=quB1
|
||||||
-----END PGP SIGNATURE-----
|
-----END PGP SIGNATURE-----
|
||||||
|
|
|
||||||
308
OPENPGP.md
Normal file
308
OPENPGP.md
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
# OpenPGP Key Documentation & Ceremonies
|
||||||
|
|
||||||
|
This document specifies how I maintain my PGP keys.
|
||||||
|
It is re-signed at every ceremony, like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/sign.sh OPENPGP.md # to produce OPENPGP.md.asc
|
||||||
|
|
||||||
|
gpg --verify OPENPGP.md.asc OPENPGP.md
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Current Key
|
||||||
|
|
||||||
|
> [`8B11 40AD B518 5334 E9FA 513B 7568 1C81 6731 2680`](https://keys.openpgp.org/vks/v1/by-fingerprint/8B1140ADB5185334E9FA513B75681C8167312680)
|
||||||
|
on [OpenPGP](https://keys.openpgp.org)
|
||||||
|
|
||||||
|
|
||||||
|
| 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
|
||||||
|
|
||||||
|
- Primary and internal subkeys 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 (`<S_SUBKEY_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: In my **Vaultwarden** as secure notes
|
||||||
|
(because they are offline-synced text, unlike attachments):
|
||||||
|
- "Full Secret Key": Armored export of primary and subkeys
|
||||||
|
- "Deployed Secret Keys": Armored export of the subkeys only
|
||||||
|
- "Revocation Certificate", generated at key creation
|
||||||
|
- "Key Info": All key IDs and fingerprints, as a login entry
|
||||||
|
- Public record: **This repository**:
|
||||||
|
- `KEYS.md` (with all current keys), this file, and `AGE.md`,
|
||||||
|
each with a detached `.asc` signature,
|
||||||
|
refreshed by `scripts/sign.sh`
|
||||||
|
- `public-key.asc`, the armored export of the current PGP key,
|
||||||
|
for convenient usage,
|
||||||
|
refreshed by `scripts/snapshot.sh`
|
||||||
|
- `ceremonies/`, signed records of exceptional events,
|
||||||
|
such as the 2026 supersession
|
||||||
|
- `history/`, armored exports of all retired keys,
|
||||||
|
see [Key History](#key-history) below
|
||||||
|
- `snapshots/<KEYID>-public-YYYY.asc`,
|
||||||
|
refreshed at every ceremony,
|
||||||
|
one file per year,
|
||||||
|
written by `scripts/snapshot.sh`
|
||||||
|
- One commit per ceremony to keep
|
||||||
|
snapshot, updated tables,
|
||||||
|
and re-signed `KEYS.md`, `OPENPGP.md`, and `AGE.md` together
|
||||||
|
- Distribution:
|
||||||
|
- WKD on all three UID domains
|
||||||
|
- Public keyservers (see section below)
|
||||||
|
- My public websites
|
||||||
|
because this repo is the archive and not the channel:
|
||||||
|
- [alexander-hess.com](https://www.alexander-hess.com/KEYS.md)
|
||||||
|
([Key](https://www.alexander-hess.com/public-key.asc),
|
||||||
|
[Signature](https://www.alexander-hess.com/KEYS.md.asc),
|
||||||
|
[security.txt](https://www.alexander-hess.com/.well-known/security.txt))
|
||||||
|
- [webartifex.de](https://www.webartifex.de/KEYS.md)
|
||||||
|
([Key](https://www.webartifex.de/public-key.asc),
|
||||||
|
[Signature](https://www.webartifex.de/KEYS.md.asc),
|
||||||
|
[security.txt](https://www.webartifex.de/.well-known/security.txt))
|
||||||
|
- 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
|
||||||
|
./scripts/publish.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script pushes to these five servers:
|
||||||
|
- `hkps://keys.openpgp.org`
|
||||||
|
- `hkps://keyserver.ubuntu.com`
|
||||||
|
- `hkps://pgpkeys.eu`
|
||||||
|
- `hkps://pgp.mit.edu`
|
||||||
|
- `hkps://keyserver.escomposlinux.org`
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- `keys.openpgp.org` strips UIDs until each address is verified
|
||||||
|
=> Click the links mailed to all addresses
|
||||||
|
- The other four run SKS-style software and 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Yearly Update
|
||||||
|
|
||||||
|
In early January,
|
||||||
|
*before* the primary and internal subkeys expire at the end of the month,
|
||||||
|
I conduct a yearly update ceremony.
|
||||||
|
|
||||||
|
Because extending an expiry needs the primary secret key,
|
||||||
|
I run it in a scratch keyring (i.e., `T` below),
|
||||||
|
never in the main one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
T=$(mktemp -d)
|
||||||
|
chmod 700 "$T"
|
||||||
|
|
||||||
|
gpg --homedir "$T" --import public-key.asc
|
||||||
|
gpg --homedir "$T" --import # Paste "Full Secret Key" from Vaultwarden, then Ctrl-D
|
||||||
|
|
||||||
|
gpg --homedir "$T" --edit-key <FPR>
|
||||||
|
expire # Primary => At the end of NEXT January
|
||||||
|
key 1
|
||||||
|
key 2
|
||||||
|
key ... # Any further internal subkeys; external ones keep their own 6-month expiry
|
||||||
|
expire # Selected subkeys => Same date
|
||||||
|
save
|
||||||
|
|
||||||
|
gpg --homedir "$T" --armor --export-secret-keys <FPR>
|
||||||
|
# => Vaultwarden note "Full Secret Key"
|
||||||
|
|
||||||
|
gpg --homedir "$T" --armor --export <FPR> | gpg --import
|
||||||
|
# => To make the main keyring carry the new expiry
|
||||||
|
|
||||||
|
gpgconf --homedir "$T" --kill gpg-agent
|
||||||
|
rm -rf "$T"
|
||||||
|
|
||||||
|
gpg --list-secret-keys <FPR>
|
||||||
|
# => Must still show sec# and two ssb (external subkeys, if any, as ssb#)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in this repo:
|
||||||
|
|
||||||
|
1. `./scripts/snapshot.sh`
|
||||||
|
2. Update the dates in the two tables above
|
||||||
|
3. `./scripts/sign.sh`
|
||||||
|
4. `./scripts/verify.sh`
|
||||||
|
5. `./scripts/publish.sh`
|
||||||
|
6. One commit
|
||||||
|
|
||||||
|
Then, on every other machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/bootstrap.sh # to merge the new expiry into the local keyrings
|
||||||
|
```
|
||||||
|
|
||||||
|
The deployed secret subkeys stay as they are;
|
||||||
|
only their self-signatures changed.
|
||||||
|
|
||||||
|
|
||||||
|
### 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 (external subkeys, if any, as 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
|
||||||
|
|
||||||
|
./scripts/bootstrap.sh # to configure public keys with ownertrust and a full verify
|
||||||
|
|
||||||
|
gpg --list-secret-keys <FPR> # Must show sec# and two ssb (external subkeys, if any, as 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, **never expires** |
|
||||||
|
| `77D28D388024AE85` | 2018-06-17 | revoked same day | botched, **never expires** |
|
||||||
|
| `887603E7D66B8AD4` | 2018-06-17 | revoked same day | botched, **never expires** |
|
||||||
|
| `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 the keyservers listed above 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
|
||||||
7
OPENPGP.md.asc
Normal file
7
OPENPGP.md.asc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCarGC1AAKCRC0bttN/CaA
|
||||||
|
XS5IAQDcDzL+O2P0LvrrDvTiLPme2mcyAifUCbCCNSnHXjzerQEA5TAwKK0ktW29
|
||||||
|
7yDzIi4T+uQ0kAttq91U6oIT8Wzb1QM=
|
||||||
|
=VAgU
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
111
README.md
111
README.md
|
|
@ -1,19 +1,114 @@
|
||||||
# OpenPGP Keys
|
# Keys
|
||||||
|
|
||||||
This repository documents my OpenPGP keys: Policies, ceremonies, and history.
|
Public record of the cryptographic keys of Alexander Hess
|
||||||
|
(i.e., "me" in this repository):
|
||||||
|
What they are, how they are handled, and how to verify them.
|
||||||
|
|
||||||
|
There are two audiences for this repository:
|
||||||
|
|
||||||
## Contents
|
- **I** want a how-to:
|
||||||
|
[`OPENPGP.md`](OPENPGP.md) and [`AGE.md`](AGE.md) hold the policies and ceremonies,
|
||||||
|
and the [`scripts/`](scripts/) run them.
|
||||||
|
- **Readers** want to confirm my keys and my identity:
|
||||||
|
Everything needed for that is in the [Verify](#verify) section below.
|
||||||
|
|
||||||
- [`KEYS.md`](KEYS.md): current key, policies, and all ceremonies
|
|
||||||
- [`KEYS.md.asc`](KEYS.md.asc): detached signature, renewed at every ceremony
|
|
||||||
- [`snapshots/`](snapshots/): yearly exports of the current key
|
|
||||||
- [`ceremonies/`](ceremonies/): signed records of exceptional events (e.g., key supersessions)
|
|
||||||
- [`history/`](history/): armored public keys of all keys ever published
|
|
||||||
|
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
|
The fingerprint of my [current PGP key](public-key.asc) is in [`KEYS.md`](KEYS.md),
|
||||||
|
which is signed.
|
||||||
|
|
||||||
|
Compare the fingerprint with at least one source outside this repository
|
||||||
|
to confirm my identity:
|
||||||
|
|
||||||
|
- [WKD](https://wiki.gnupg.org/WKD),
|
||||||
|
for example, via `gpg --locate-keys ah@alexander-hess.com`
|
||||||
|
- The keyservers listed in [`OPENPGP.md`](OPENPGP.md),
|
||||||
|
for example, `gpg --keyserver hkps://keys.openpgp.org --search-keys alexander@webartifex.de`
|
||||||
|
- [`KEYS.md`](KEYS.md) obtained from my websites:
|
||||||
|
- [alexander-hess.com](https://www.alexander-hess.com/KEYS.md)
|
||||||
|
- [webartifex.de](https://www.webartifex.de/KEYS.md)
|
||||||
|
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
gpg --import public-key.asc
|
||||||
|
|
||||||
gpg --verify KEYS.md.asc KEYS.md
|
gpg --verify KEYS.md.asc KEYS.md
|
||||||
|
gpg --verify OPENPGP.md.asc OPENPGP.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Each `gpg --verify` will print two things:
|
||||||
|
|
||||||
|
1. `Good signature from "Alexander Hess <...>"`:
|
||||||
|
The file is intact and was signed by this key.
|
||||||
|
2. `WARNING: This key is not certified with a trusted signature!`:
|
||||||
|
Because `gpg` likely has no record that this key is mine.
|
||||||
|
|
||||||
|
So, a good signature plus a matching `Primary key fingerprint` line is the full result.
|
||||||
|
|
||||||
|
To silence the warning on your machine,
|
||||||
|
tell `gpg` about your check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --lsign-key <FPR> # after comparing the fingerprint yourself
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
If you trusted one of the two keys I retired in 2026,
|
||||||
|
they certified the current one,
|
||||||
|
and the supersession is recorded in [`ceremonies/`](ceremonies/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --import history/344EA5AB10D868E0-public.asc history/6D11C1E91D3B0265-public.asc
|
||||||
|
gpg --check-signatures 8B1140ADB5185334E9FA513B75681C8167312680
|
||||||
|
gpg --verify ceremonies/2026-07-08_supersession.md.asc ceremonies/2026-07-08_supersession.md
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
The files in this repository are described below.
|
||||||
|
|
||||||
|
|
||||||
|
### OpenPGP
|
||||||
|
|
||||||
|
How I implement [OpenPGP](https://www.openpgp.org/):
|
||||||
|
|
||||||
|
- [`KEYS.md`](KEYS.md):
|
||||||
|
Current keys, signed
|
||||||
|
- [`KEYS.md.asc`](KEYS.md.asc):
|
||||||
|
Detached signature, renewed at every ceremony
|
||||||
|
- [`OPENPGP.md`](OPENPGP.md):
|
||||||
|
OpenPGP policies, ceremonies, and history
|
||||||
|
- [`OPENPGP.md.asc`](OPENPGP.md.asc):
|
||||||
|
Detached signature, renewed at every ceremony
|
||||||
|
- [`public-key.asc`](public-key.asc):
|
||||||
|
Armored export of the current OpenPGP key
|
||||||
|
- [`snapshots/`](snapshots/):
|
||||||
|
Yearly exports of the current OpenPGP key
|
||||||
|
- [`ceremonies/`](ceremonies/):
|
||||||
|
Signed records of exceptional events
|
||||||
|
- [`history/`](history/):
|
||||||
|
Armored public keys of all previous OpenPGP keys
|
||||||
|
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
`sh` scripts for my own use:
|
||||||
|
|
||||||
|
- [`bootstrap.sh`](scripts/bootstrap.sh):
|
||||||
|
Imports and trusts all keys on a new machine,
|
||||||
|
then verifies all files
|
||||||
|
- [`sign.sh`](scripts/sign.sh):
|
||||||
|
Renews the detached signatures
|
||||||
|
- [`verify.sh`](scripts/verify.sh):
|
||||||
|
Verifies all signed files against the pinned signing subkey,
|
||||||
|
and warns when the key is about to expire
|
||||||
|
- [`snapshot.sh`](scripts/snapshot.sh):
|
||||||
|
Exports the current key to `snapshots/` and `public-key.asc`
|
||||||
|
- [`publish.sh`](scripts/publish.sh):
|
||||||
|
Pushes the current key to the keyservers
|
||||||
|
|
|
||||||
71
public-key.asc
Normal file
71
public-key.asc
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
ktfVqN0JwAtZDQ==
|
||||||
|
=6Fh2
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
72
scripts/bootstrap.sh
Executable file
72
scripts/bootstrap.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Set up a machine's keyring to verify everything in this repo
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/bootstrap.sh
|
||||||
|
#
|
||||||
|
# Imports the current public key and all historical keys,
|
||||||
|
# trusts them ultimately, and runs ./scripts/verify.sh
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - Idempotent => Safe to re-run at any time
|
||||||
|
# - Touches public material only; the deployed secret subkeys
|
||||||
|
# come from Vaultwarden, see OPENPGP.md
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
|
||||||
|
# Internal [S] subkey; only used to look up the key
|
||||||
|
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||||
|
|
||||||
|
|
||||||
|
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
gpg --quiet \
|
||||||
|
--import "$REPO/public-key.asc"
|
||||||
|
echo "Imported public-key.asc"
|
||||||
|
|
||||||
|
gpg --quiet \
|
||||||
|
--import "$REPO"/history/*.asc
|
||||||
|
echo "Imported history/*.asc"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - The first "fpr" line is the primary key's fingerprint
|
||||||
|
# - A subkey resolves to it
|
||||||
|
FPR=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }')
|
||||||
|
if [ -z "$FPR" ]; then
|
||||||
|
echo "Error: Key $SIGNING_KEY not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - One "--import-ownertrust" call
|
||||||
|
# => `gpg` checks the trustdb only once
|
||||||
|
# - "--show-keys" reads a file without importing
|
||||||
|
# => Its warnings about revoked keys are expected and silenced
|
||||||
|
|
||||||
|
TRUST=$(mktemp)
|
||||||
|
trap 'rm -f "$TRUST"' EXIT
|
||||||
|
|
||||||
|
echo "$FPR:6:" > "$TRUST"
|
||||||
|
for FILE in "$REPO"/history/*.asc; do
|
||||||
|
gpg --show-keys --with-colons "$FILE" 2>/dev/null \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10 ":6:"; exit }' >> "$TRUST"
|
||||||
|
done
|
||||||
|
|
||||||
|
gpg --quiet --import-ownertrust "$TRUST"
|
||||||
|
sed 's/^/Trusted /; s/:6:$/ ultimately/' "$TRUST"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
gpg --check-signatures "$FPR" >&2
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
"$REPO/scripts/verify.sh"
|
||||||
59
scripts/publish.sh
Executable file
59
scripts/publish.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Publish the current OpenPGP public key to the keyservers
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/publish.sh
|
||||||
|
#
|
||||||
|
# Run after every change to the key
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - The SKS-style servers merge and never delete
|
||||||
|
# => Every change must be pushed
|
||||||
|
# - keys.openpgp.org strips UIDs until each address is verified
|
||||||
|
# => Click the links mailed to all addresses after new or changed UIDs
|
||||||
|
#
|
||||||
|
# Exits non-zero if any keyserver rejected the upload
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
|
||||||
|
# Internal [S] subkey; only used to look up the key
|
||||||
|
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||||
|
|
||||||
|
|
||||||
|
KEYSERVERS='
|
||||||
|
hkps://keys.openpgp.org
|
||||||
|
hkps://keyserver.ubuntu.com
|
||||||
|
hkps://pgpkeys.eu
|
||||||
|
hkps://pgp.mit.edu
|
||||||
|
hkps://keyserver.escomposlinux.org
|
||||||
|
'
|
||||||
|
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - the first fingerprint line is the primary key's fingerprint
|
||||||
|
# - a subkey resolves to it
|
||||||
|
FPR=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }')
|
||||||
|
if [ -z "$FPR" ]; then
|
||||||
|
echo "Error: Key $SIGNING_KEY not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
|
||||||
|
for server in $KEYSERVERS; do
|
||||||
|
|
||||||
|
if gpg --keyserver "$server" --send-keys "$FPR"; then
|
||||||
|
echo "OK $server"
|
||||||
|
else
|
||||||
|
echo "FAIL $server" >&2
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
exit "$FAILED"
|
||||||
49
scripts/sign.sh
Executable file
49
scripts/sign.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Create detached signatures for the signed documents in this repo
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/sign.sh [FILE...]
|
||||||
|
#
|
||||||
|
# FILE defaults to KEYS.md OPENPGP.md
|
||||||
|
#
|
||||||
|
# Writes FILE.asc next to each FILE and verifies it
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
|
||||||
|
# Internal [S] subkey; pinned with "!" below so `gpg` never picks another subkey
|
||||||
|
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||||
|
|
||||||
|
|
||||||
|
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
set -- \
|
||||||
|
"$REPO/KEYS.md" \
|
||||||
|
"$REPO/OPENPGP.md" \
|
||||||
|
# Keep this list aligned with the documentation at the top
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
for FILE; do
|
||||||
|
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "Error: $FILE not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gpg --armor \
|
||||||
|
--detach-sign \
|
||||||
|
--digest-algo SHA512 \
|
||||||
|
--local-user "${SIGNING_KEY}!" \
|
||||||
|
--yes \
|
||||||
|
--output "$FILE.asc" \
|
||||||
|
"$FILE"
|
||||||
|
|
||||||
|
gpg --verify "$FILE.asc" "$FILE"
|
||||||
|
|
||||||
|
echo "Signed $FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
done
|
||||||
72
scripts/snapshot.sh
Executable file
72
scripts/snapshot.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Export the current OpenPGP public key as a yearly snapshot
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/snapshot.sh [YEAR]
|
||||||
|
#
|
||||||
|
# YEAR defaults to the current UTC year
|
||||||
|
#
|
||||||
|
# Writes snapshots/<KEYID>-public-<YEAR>.asc and refreshes public-key.asc
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
|
||||||
|
# Internal [S] subkey; only used to look up the key
|
||||||
|
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||||
|
|
||||||
|
|
||||||
|
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
YEAR=${1:-$(date -u +%Y)}
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - the first "fpr" line is the primary key's fingerprint
|
||||||
|
# - a subkey resolves to it
|
||||||
|
FPR=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
||||||
|
| awk -F: '$1 == "fpr" { print $10; exit }')
|
||||||
|
if [ -z "$FPR" ]; then
|
||||||
|
echo "Error: Key $SIGNING_KEY not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# The last 16 hex chars, as used in OPENPGP.md
|
||||||
|
KEYID=${FPR#"${FPR%????????????????}"}
|
||||||
|
|
||||||
|
OUT="$REPO/snapshots/$KEYID-public-$YEAR.asc"
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p "$REPO/snapshots"
|
||||||
|
|
||||||
|
|
||||||
|
# Merge the repo copy in first, so a lagging keyring cannot shrink the export
|
||||||
|
gpg --quiet \
|
||||||
|
--import "$REPO/public-key.asc"
|
||||||
|
|
||||||
|
# The retired keys must be present,
|
||||||
|
# or "export-clean" below drops their certifications as unverifiable
|
||||||
|
gpg --quiet \
|
||||||
|
--import "$REPO"/history/*.asc
|
||||||
|
|
||||||
|
|
||||||
|
gpg --armor \
|
||||||
|
--export-options export-clean \
|
||||||
|
--export "$FPR" \
|
||||||
|
> "$OUT" \
|
||||||
|
|| {
|
||||||
|
rm -f "$OUT"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if [ ! -s "$OUT" ]; then
|
||||||
|
rm -f "$OUT"
|
||||||
|
echo "Error: Nothing exported for $FPR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
cp "$OUT" "$REPO/public-key.asc"
|
||||||
|
|
||||||
|
|
||||||
|
gpg --show-keys "$OUT" >&2
|
||||||
|
|
||||||
|
|
||||||
|
echo "Wrote snapshots/$KEYID-public-$YEAR.asc and public-key.asc"
|
||||||
103
scripts/verify.sh
Executable file
103
scripts/verify.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Verify the signed files in this repo against the pinned [S] subkey
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/verify.sh [FILE...]
|
||||||
|
#
|
||||||
|
# FILE defaults to KEYS.md OPENPGP.md ceremonies/*.md
|
||||||
|
#
|
||||||
|
# A FILE with a FILE.asc next to it is checked as a detached signature,
|
||||||
|
# any other FILE as a clearsigned document
|
||||||
|
#
|
||||||
|
# Also checks the key's expiry
|
||||||
|
#
|
||||||
|
# Exits non-zero if any file lacks a valid signature from the pinned key,
|
||||||
|
# or if the key has expired
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
|
||||||
|
# Internal [S] subkey; every file must carry a valid signature from this key
|
||||||
|
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||||
|
|
||||||
|
|
||||||
|
# Warn when the key expires within this many days
|
||||||
|
WARN_DAYS=60
|
||||||
|
|
||||||
|
|
||||||
|
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
set -- \
|
||||||
|
"$REPO/KEYS.md" \
|
||||||
|
"$REPO/OPENPGP.md" \
|
||||||
|
"$REPO"/ceremonies/*.md \
|
||||||
|
# Keep this list aligned with the documentation at the top
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Print the fingerprints of all keys that made a valid signature, one per line
|
||||||
|
signers() {
|
||||||
|
gpg \
|
||||||
|
--status-fd 1 \
|
||||||
|
--verify "$@" \
|
||||||
|
2>/dev/null \
|
||||||
|
| awk '$2 == "VALIDSIG" { print $3 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
|
||||||
|
for FILE; do
|
||||||
|
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "Error: $FILE not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$FILE.asc" ]; then
|
||||||
|
SIGNERS=$(signers "$FILE.asc" "$FILE")
|
||||||
|
else
|
||||||
|
SIGNERS=$(signers "$FILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s\n' "$SIGNERS" | grep -qx "$SIGNING_KEY"; then
|
||||||
|
echo "OK $FILE"
|
||||||
|
else
|
||||||
|
echo "FAIL $FILE (signed by: $(printf '%s' "${SIGNERS:-nobody}" | tr '\n' ' '))" >&2
|
||||||
|
FAILED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - The 7th field of the pub line is the primary key's expiry in epoch seconds
|
||||||
|
# - A subkey resolves to the primary key
|
||||||
|
|
||||||
|
EXPIRY=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
||||||
|
| awk -F: '$1 == "pub" { print $7; exit }')
|
||||||
|
|
||||||
|
if [ -z "$EXPIRY" ]; then
|
||||||
|
echo "Error: No expiry found for key $SIGNING_KEY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NOW=$(date -u +%s)
|
||||||
|
|
||||||
|
DAYS_LEFT=$(( (EXPIRY - NOW) / 86400 ))
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$EXPIRY" -le "$NOW" ]; then
|
||||||
|
echo "FAIL Key expired $(( (NOW - EXPIRY) / 86400 )) days ago" >&2
|
||||||
|
FAILED=1
|
||||||
|
elif [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
|
||||||
|
echo "WARN Key expires in $DAYS_LEFT days" >&2
|
||||||
|
else
|
||||||
|
echo "OK Key expires in $DAYS_LEFT days"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
exit "$FAILED"
|
||||||
Loading…
Reference in a new issue