From ae768285b624b50f83399fca71522de3d60a4c75 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Fri, 11 Sep 2026 15:48:18 +0200 Subject: [PATCH] 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 --- AGE.md | 61 ++++++++++++++++++++++++++++ AGE.md.asc | 7 ++++ KEYS.md | 11 +++++ KEYS.md.asc | 8 ++-- OPENPGP.md | 2 +- OPENPGP.md.asc | 8 ++-- README.md | 20 ++++++++- age-recipient.txt | 1 + ceremonies/2026-09-13_age_key.md | 25 ++++++++++++ ceremonies/2026-09-13_age_key.md.asc | 7 ++++ scripts/sign.sh | 3 +- scripts/verify.sh | 31 ++++++++++++-- 12 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 AGE.md create mode 100644 AGE.md.asc create mode 100644 age-recipient.txt create mode 100644 ceremonies/2026-09-13_age_key.md create mode 100644 ceremonies/2026-09-13_age_key.md.asc diff --git a/AGE.md b/AGE.md new file mode 100644 index 0000000..70f0010 --- /dev/null +++ b/AGE.md @@ -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 diff --git a/AGE.md.asc b/AGE.md.asc new file mode 100644 index 0000000..7fbd523 --- /dev/null +++ b/AGE.md.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcVTQAKCRC0bttN/CaA +XVAsAP9aX22LadD4EXK7i9Oe+omWg2Yn3+WBtQT6WSL+17rD+gD+O7xVv25MZR4d +/AlJLAha+YG4705KWrRSZEbSm/9zkwQ= +=GjG6 +-----END PGP SIGNATURE----- diff --git a/KEYS.md b/KEYS.md index 8bca793..d5dd9a6 100644 --- a/KEYS.md +++ b/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) diff --git a/KEYS.md.asc b/KEYS.md.asc index 3592cb8..90f9ec9 100644 --- a/KEYS.md.asc +++ b/KEYS.md.asc @@ -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----- diff --git a/OPENPGP.md b/OPENPGP.md index ba90f9f..ebada0e 100644 --- a/OPENPGP.md +++ b/OPENPGP.md @@ -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/-public-YYYY.asc`, diff --git a/OPENPGP.md.asc b/OPENPGP.md.asc index f51ef8c..2d2c45d 100644 --- a/OPENPGP.md.asc +++ b/OPENPGP.md.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----- diff --git a/README.md b/README.md index 01f6f3c..7eb7036 100644 --- a/README.md +++ b/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): diff --git a/age-recipient.txt b/age-recipient.txt new file mode 100644 index 0000000..a680734 --- /dev/null +++ b/age-recipient.txt @@ -0,0 +1 @@ +age1gzy6tj8y93ljuelcedrkcsv3p6gygk7ng3386u32av9a4ujh737qef8crp diff --git a/ceremonies/2026-09-13_age_key.md b/ceremonies/2026-09-13_age_key.md new file mode 100644 index 0000000..d41e286 --- /dev/null +++ b/ceremonies/2026-09-13_age_key.md @@ -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. diff --git a/ceremonies/2026-09-13_age_key.md.asc b/ceremonies/2026-09-13_age_key.md.asc new file mode 100644 index 0000000..6335fba --- /dev/null +++ b/ceremonies/2026-09-13_age_key.md.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcOrAAKCRC0bttN/CaA +Xaj9AQD+Wwuxhlserdi+HGO5XJabm4xCEcVqCQid+zQctG97hQEAxBBSzzL6yNwG +4pHqSmaMUvJkIkSSBH4ERxSsGvvjGw4= +=h7yN +-----END PGP SIGNATURE----- diff --git a/scripts/sign.sh b/scripts/sign.sh index ad3fae8..6fc5389 100755 --- a/scripts/sign.sh +++ b/scripts/sign.sh @@ -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 diff --git a/scripts/verify.sh b/scripts/verify.sh index 8381517..20c52b1 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -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"