Watch
1
0
Fork
You've already forked keys
0

Add age key

- Add AGE.md with policies
- Add age-recipient.txt for `age -R`
- Add ceremonies/2026-09-13_age_key.md as the signed record
  of which recipient is valid from which date
- List the recipient in KEYS.md
- Describe AGE.md and age-recipient.txt in README.md
- Extend the scripts:
  + sign.sh signs AGE.md by default
  + verify.sh verifies AGE.md by default
    and cross-checks age-recipient.txt,
    the only unsigned artifact,
    against the recipient in the signed KEYS.md and AGE.md
This commit is contained in:
Alexander Hess 2026-09-11 15:48:18 +02:00
commit ae768285b6
Signed by: alexander
GPG key ID: B46EDB4DFC26805D
12 changed files with 170 additions and 14 deletions

61
AGE.md Normal file
View file

@ -0,0 +1,61 @@
# `age` Key Documentation
This document specifies how I maintain my [age](https://github.com/FiloSottile/age) key.
It is (re-)signed with the current OpenPGP key:
```bash
./scripts/sign.sh AGE.md # to produce AGE.md.asc
gpg --verify AGE.md.asc AGE.md
```
## Current Key
> `age1gzy6tj8y93ljuelcedrkcsv3p6gygk7ng3386u32av9a4ujh737qef8crp`
| Field | Value |
|----------|--------------------------------------------|
| Type | X25519 (native age identity) |
| Created | 2026-09-11 |
| Expires | never (because `age` has no expiry) |
| Deployed | all machines, `~/.config/age/identity.txt` |
### Policies
- One identity on all machines,
like the OpenPGP `[E]` subkey
- No expiry and no revocation:
Rotation means replacing the key
in this file, in [`KEYS.md`](KEYS.md), and in [`age-recipient.txt`](age-recipient.txt),
recording it in [`ceremonies/`](ceremonies/),
running `./scripts/sign.sh` to re-sign the signed documents, and
encrypting anything that matters again
- The OpenPGP signature on this file is the trust anchor;
[`age-recipient.txt`](age-recipient.txt) is a convenience copy for `age -R`
### Storage
- Secret key: In my **Vaultwarden** as secure note "age Identity"
(because they are offline-synced text, unlike attachments)
- Public record: **This repository**
- [`AGE.md`](AGE.md)
- [`age-recipient.txt`](age-recipient.txt)
- Distribution: My **public websites**
- [alexander-hess.com](https://www.alexander-hess.com/age-recipient.txt)
([Policies](https://www.alexander-hess.com/AGE.md),
[Signature](https://www.alexander-hess.com/AGE.md.asc))
- [webartifex.de](https://www.webartifex.de/age-recipient.txt)
([Policies](https://www.webartifex.de/AGE.md),
[Signature](https://www.webartifex.de/AGE.md.asc))
## Ceremonies
- Introduction of this key:
[`ceremonies/2026-09-13_age_key.md`](ceremonies/2026-09-13_age_key.md)
- A future rotation gets its own record in [`ceremonies/`](ceremonies/),
signed with the OpenPGP key of that day

7
AGE.md.asc Normal file
View file

@ -0,0 +1,7 @@
-----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcVTQAKCRC0bttN/CaA
XVAsAP9aX22LadD4EXK7i9Oe+omWg2Yn3+WBtQT6WSL+17rD+gD+O7xVv25MZR4d
/AlJLAha+YG4705KWrRSZEbSm/9zkwQ=
=GjG6
-----END PGP SIGNATURE-----

11
KEYS.md
View file

@ -23,3 +23,14 @@ Fingerprint:
Armored export: [`public-key.asc`](public-key.asc) Armored export: [`public-key.asc`](public-key.asc)
For policies, ceremonies, and history, see: [`OPENPGP.md`](OPENPGP.md) For policies, ceremonies, and history, see: [`OPENPGP.md`](OPENPGP.md)
## `age`
Recipient:
> `age1gzy6tj8y93ljuelcedrkcsv3p6gygk7ng3386u32av9a4ujh737qef8crp`
Recipient file: [`age-recipient.txt`](age-recipient.txt)
For policies, see: [`AGE.md`](AGE.md)

View file

@ -1,7 +1,7 @@
-----BEGIN PGP SIGNATURE----- -----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcTMgAKCRC0bttN/CaA iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcTZwAKCRC0bttN/CaA
XQsIAQCbVwRUzKeoM3OMJlSbGq4bpuSDmWXGPo/s5KbJlMEkNAEA1gVHfRpomGwL XXb9AQCw+qK35dxdRK6s/HWGMBPYiR8yB416TKU0kabgOYcfngEA4wFrDQLEmcla
coNc4VC9qDwk5wdpis+NDRNtpIOIqgs= 1e86A8GM2CiFGKIYUJ6rts/V6Oxt9Qk=
=quB1 =GQGh
-----END PGP SIGNATURE----- -----END PGP SIGNATURE-----

View file

@ -66,7 +66,7 @@ gpg --verify OPENPGP.md.asc OPENPGP.md
for convenient usage, for convenient usage,
refreshed by `scripts/snapshot.sh` refreshed by `scripts/snapshot.sh`
- `ceremonies/`, signed records of exceptional events, - `ceremonies/`, signed records of exceptional events,
such as the 2026 supersession such as the 2026 supersession and the `age` key introduction
- `history/`, armored exports of all retired keys, - `history/`, armored exports of all retired keys,
see [Key History](#key-history) below see [Key History](#key-history) below
- `snapshots/<KEYID>-public-YYYY.asc`, - `snapshots/<KEYID>-public-YYYY.asc`,

View file

@ -1,7 +1,7 @@
-----BEGIN PGP SIGNATURE----- -----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCarGC1AAKCRC0bttN/CaA iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCarGC7QAKCRC0bttN/CaA
XS5IAQDcDzL+O2P0LvrrDvTiLPme2mcyAifUCbCCNSnHXjzerQEA5TAwKK0ktW29 XRhgAP9JASLtDJi1M6m8uskF8IfxHw/4uUmP/bnSB5HeXfFNKwEAk17jzrsNYcfb
7yDzIi4T+uQ0kAttq91U6oIT8Wzb1QM= 1HVZlJi3G9WRpA3NzEe9nP6QVxWuows=
=VAgU =ot15
-----END PGP SIGNATURE----- -----END PGP SIGNATURE-----

View file

@ -38,6 +38,7 @@ 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 gpg --verify OPENPGP.md.asc OPENPGP.md
gpg --verify AGE.md.asc AGE.md
``` ```
Each `gpg --verify` will print two things: Each `gpg --verify` will print two things:
@ -96,6 +97,22 @@ How I implement [OpenPGP](https://www.openpgp.org/):
Armored public keys of all previous OpenPGP keys Armored public keys of all previous OpenPGP keys
### `age`
Besides OpenPGP,
I also use the modern [`age`](https://github.com/FiloSottile/age) encryption:
- [`AGE.md`](AGE.md):
`age` policies
- [`AGE.md.asc`](AGE.md.asc):
Detached signature, renewed at every ceremony
- [`age-recipient.txt`](age-recipient.txt):
`age` public key for `age -R`
- [`ceremonies/`](ceremonies/):
Signed record of when a recipient became valid or rotated
because `age` keys carry no metadata of their own
### Scripts ### Scripts
`sh` scripts for my own use: `sh` scripts for my own use:
@ -107,7 +124,8 @@ How I implement [OpenPGP](https://www.openpgp.org/):
Renews the detached signatures Renews the detached signatures
- [`verify.sh`](scripts/verify.sh): - [`verify.sh`](scripts/verify.sh):
Verifies all signed files against the pinned signing subkey, Verifies all signed files against the pinned signing subkey,
and warns when the key is about to expire warns when the key is about to expire, and
cross-checks `age-recipient.txt`
- [`snapshot.sh`](scripts/snapshot.sh): - [`snapshot.sh`](scripts/snapshot.sh):
Exports the current key to `snapshots/` and `public-key.asc` Exports the current key to `snapshots/` and `public-key.asc`
- [`publish.sh`](scripts/publish.sh): - [`publish.sh`](scripts/publish.sh):

1
age-recipient.txt Normal file
View file

@ -0,0 +1 @@
age1gzy6tj8y93ljuelcedrkcsv3p6gygk7ng3386u32av9a4ujh737qef8crp

View file

@ -0,0 +1,25 @@
# New `age` Key
Effective as of 2026-09-13,
I publish an `age` identity next to my OpenPGP key:
```
age1gzy6tj8y93ljuelcedrkcsv3p6gygk7ng3386u32av9a4ujh737qef8crp
```
Type X25519, created 2026-09-11, no expiry and no revocation.
No OpenPGP key material changed in this ceremony.
`AGE.md` was added, `KEYS.md` gained the recipient,
and all detached signatures were renewed.
Because `age` recipients carry no metadata,
this signed record is the only proof
of which recipient was valid from which date.
A future rotation gets its own record here,
superseding this one.
The policies are in [`AGE.md`](../AGE.md).
This file is signed with the current OpenPGP key.

View file

@ -0,0 +1,7 @@
-----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcOrAAKCRC0bttN/CaA
Xaj9AQD+Wwuxhlserdi+HGO5XJabm4xCEcVqCQid+zQctG97hQEAxBBSzzL6yNwG
4pHqSmaMUvJkIkSSBH4ERxSsGvvjGw4=
=h7yN
-----END PGP SIGNATURE-----

View file

@ -4,7 +4,7 @@
# #
# Usage: ./scripts/sign.sh [FILE...] # Usage: ./scripts/sign.sh [FILE...]
# #
# FILE defaults to KEYS.md OPENPGP.md # FILE defaults to KEYS.md OPENPGP.md AGE.md
# #
# Writes FILE.asc next to each FILE and verifies it # Writes FILE.asc next to each FILE and verifies it
@ -22,6 +22,7 @@ if [ $# -eq 0 ]; then
set -- \ set -- \
"$REPO/KEYS.md" \ "$REPO/KEYS.md" \
"$REPO/OPENPGP.md" \ "$REPO/OPENPGP.md" \
"$REPO/AGE.md" \
# Keep this list aligned with the documentation at the top # Keep this list aligned with the documentation at the top
fi fi

View file

@ -4,15 +4,17 @@
# #
# Usage: ./scripts/verify.sh [FILE...] # Usage: ./scripts/verify.sh [FILE...]
# #
# FILE defaults to KEYS.md OPENPGP.md ceremonies/*.md # FILE defaults to KEYS.md OPENPGP.md ceremonies/*.md AGE.md
# #
# A FILE with a FILE.asc next to it is checked as a detached signature, # A FILE with a FILE.asc next to it is checked as a detached signature,
# any other FILE as a clearsigned document # any other FILE as a clearsigned document
# #
# Also checks the key's expiry # Also checks the key's expiry, and,
# if KEYS.md or AGE.md is among the FILEs,
# that age-recipient.txt matches the recipient in them
# #
# Exits non-zero if any file lacks a valid signature from the pinned key, # Exits non-zero if any file lacks a valid signature from the pinned key,
# or if the key has expired # or if the key has expired, or if a cross-check fails
set -eu set -eu
@ -33,6 +35,7 @@ if [ $# -eq 0 ]; then
"$REPO/KEYS.md" \ "$REPO/KEYS.md" \
"$REPO/OPENPGP.md" \ "$REPO/OPENPGP.md" \
"$REPO"/ceremonies/*.md \ "$REPO"/ceremonies/*.md \
"$REPO/AGE.md" \
# Keep this list aligned with the documentation at the top # Keep this list aligned with the documentation at the top
fi fi
@ -100,4 +103,26 @@ else
fi fi
# age-recipient.txt is the only unsigned artifact,
# so it must match the recipient in the signed documents
for FILE; do
case "$FILE" in
*/KEYS.md | KEYS.md | */AGE.md | AGE.md)
# shellcheck disable=SC2016 # The backticks are literal Markdown
EXPECTED=$(sed -n 's/^> `\(age1[0-9a-z]*\)`$/\1/p' "$FILE")
ACTUAL=$(cat "$REPO/age-recipient.txt")
if [ -z "$EXPECTED" ]; then
echo "FAIL No age recipient found in $FILE" >&2
FAILED=1
elif [ "$ACTUAL" != "$EXPECTED" ]; then
echo "FAIL age-recipient.txt has $ACTUAL, $FILE has $EXPECTED" >&2
FAILED=1
fi
;;
esac
done
exit "$FAILED" exit "$FAILED"