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:
parent
8ece401276
commit
ae768285b6
12 changed files with 170 additions and 14 deletions
61
AGE.md
Normal file
61
AGE.md
Normal 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
7
AGE.md.asc
Normal 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
11
KEYS.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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-----
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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-----
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -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
1
age-recipient.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
age1gzy6tj8y93ljuelcedrkcsv3p6gygk7ng3386u32av9a4ujh737qef8crp
|
||||
25
ceremonies/2026-09-13_age_key.md
Normal file
25
ceremonies/2026-09-13_age_key.md
Normal 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.
|
||||
7
ceremonies/2026-09-13_age_key.md.asc
Normal file
7
ceremonies/2026-09-13_age_key.md.asc
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcOrAAKCRC0bttN/CaA
|
||||
Xaj9AQD+Wwuxhlserdi+HGO5XJabm4xCEcVqCQid+zQctG97hQEAxBBSzzL6yNwG
|
||||
4pHqSmaMUvJkIkSSBH4ERxSsGvvjGw4=
|
||||
=h7yN
|
||||
-----END PGP SIGNATURE-----
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue