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)
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-----
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcTMgAKCRC0bttN/CaA
XQsIAQCbVwRUzKeoM3OMJlSbGq4bpuSDmWXGPo/s5KbJlMEkNAEA1gVHfRpomGwL
coNc4VC9qDwk5wdpis+NDRNtpIOIqgs=
=quB1
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcTZwAKCRC0bttN/CaA
XXb9AQCw+qK35dxdRK6s/HWGMBPYiR8yB416TKU0kabgOYcfngEA4wFrDQLEmcla
1e86A8GM2CiFGKIYUJ6rts/V6Oxt9Qk=
=GQGh
-----END PGP SIGNATURE-----

View file

@ -66,7 +66,7 @@ gpg --verify OPENPGP.md.asc OPENPGP.md
for convenient usage,
refreshed by `scripts/snapshot.sh`
- `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,
see [Key History](#key-history) below
- `snapshots/<KEYID>-public-YYYY.asc`,

View file

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

View file

@ -38,6 +38,7 @@ gpg --import public-key.asc
gpg --verify KEYS.md.asc KEYS.md
gpg --verify OPENPGP.md.asc OPENPGP.md
gpg --verify AGE.md.asc AGE.md
```
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
### `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
`sh` scripts for my own use:
@ -107,7 +124,8 @@ How I implement [OpenPGP](https://www.openpgp.org/):
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
warns when the key is about to expire, and
cross-checks `age-recipient.txt`
- [`snapshot.sh`](scripts/snapshot.sh):
Exports the current key to `snapshots/` and `public-key.asc`
- [`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...]
#
# 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
@ -22,6 +22,7 @@ if [ $# -eq 0 ]; then
set -- \
"$REPO/KEYS.md" \
"$REPO/OPENPGP.md" \
"$REPO/AGE.md" \
# Keep this list aligned with the documentation at the top
fi

View file

@ -4,15 +4,17 @@
#
# 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,
# 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,
# or if the key has expired
# or if the key has expired, or if a cross-check fails
set -eu
@ -33,6 +35,7 @@ if [ $# -eq 0 ]; then
"$REPO/KEYS.md" \
"$REPO/OPENPGP.md" \
"$REPO"/ceremonies/*.md \
"$REPO/AGE.md" \
# Keep this list aligned with the documentation at the top
fi
@ -100,4 +103,26 @@ else
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"