From 30fee8de37a647c96debe54f6ee8f7c91e3a7df3 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Fri, 11 Sep 2026 17:00:11 +0200 Subject: [PATCH] Add security.txt - Add scripts/security-txt.sh to generate a security.txt file - Add the current security.txt - Extend verify.sh to verify security.txt by default and cross-check that it expires together with the signing key - Describe security.txt in OPENPGP.md and README.md --- OPENPGP.md | 15 ++++++----- OPENPGP.md.asc | 8 +++--- README.md | 7 ++++- scripts/security-txt.sh | 60 +++++++++++++++++++++++++++++++++++++++++ scripts/verify.sh | 23 +++++++++++++--- security.txt | 18 +++++++++++++ 6 files changed, 116 insertions(+), 15 deletions(-) create mode 100755 scripts/security-txt.sh create mode 100644 security.txt diff --git a/OPENPGP.md b/OPENPGP.md index ebada0e..7ab9798 100644 --- a/OPENPGP.md +++ b/OPENPGP.md @@ -65,6 +65,8 @@ gpg --verify OPENPGP.md.asc OPENPGP.md - `public-key.asc`, the armored export of the current PGP key, for convenient usage, refreshed by `scripts/snapshot.sh` + - `security.txt`, clearsigned, expiry matches the key, + refreshed by `scripts/security-txt.sh` - `ceremonies/`, signed records of exceptional events, such as the 2026 supersession and the `age` key introduction - `history/`, armored exports of all retired keys, @@ -75,7 +77,7 @@ gpg --verify OPENPGP.md.asc OPENPGP.md 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 + and re-signed `KEYS.md`, `OPENPGP.md`, `AGE.md`, and `security.txt` together - Distribution: - WKD on all three UID domains - Public keyservers (see section below) @@ -181,11 +183,12 @@ gpg --list-secret-keys 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 +2. `./scripts/security-txt.sh` +3. Update the dates in the two tables above +4. `./scripts/sign.sh` +5. `./scripts/verify.sh` +6. `./scripts/publish.sh` +7. One commit Then, on every other machine: diff --git a/OPENPGP.md.asc b/OPENPGP.md.asc index 2d2c45d..0bc61f9 100644 --- a/OPENPGP.md.asc +++ b/OPENPGP.md.asc @@ -1,7 +1,7 @@ -----BEGIN PGP SIGNATURE----- -iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCarGC7QAKCRC0bttN/CaA -XRhgAP9JASLtDJi1M6m8uskF8IfxHw/4uUmP/bnSB5HeXfFNKwEAk17jzrsNYcfb -1HVZlJi3G9WRpA3NzEe9nP6QVxWuows= -=ot15 +iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCarGDIgAKCRC0bttN/CaA +XYaFAP92QJ2JOCVI5L0SgWwnE2p9wbr19jmwa4bLpe8jjDWM1AD+KmUPYusOcWu0 +BtCjSOPVI5P0S390W67K12LOzI/vOAY= +=q3oD -----END PGP SIGNATURE----- diff --git a/README.md b/README.md index 7eb7036..3bf6784 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,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 +gpg --verify security.txt ``` Each `gpg --verify` will print two things: @@ -89,6 +90,8 @@ How I implement [OpenPGP](https://www.openpgp.org/): Detached signature, renewed at every ceremony - [`public-key.asc`](public-key.asc): Armored export of the current OpenPGP key +- [`security.txt`](security.txt): [RFC 9116](https://www.rfc-editor.org/info/rfc9116/) + contact file, clearsigned, renewed at every ceremony - [`snapshots/`](snapshots/): Yearly exports of the current OpenPGP key - [`ceremonies/`](ceremonies/): @@ -125,8 +128,10 @@ Besides OpenPGP, - [`verify.sh`](scripts/verify.sh): Verifies all signed files against the pinned signing subkey, warns when the key is about to expire, and - cross-checks `age-recipient.txt` + cross-checks `age-recipient.txt` and `security.txt` - [`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 +- [`security-txt.sh`](scripts/security-txt.sh): + Generates and clearsigns `security.txt` diff --git a/scripts/security-txt.sh b/scripts/security-txt.sh new file mode 100755 index 0000000..e272220 --- /dev/null +++ b/scripts/security-txt.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# Generate security.txt (RFC 9116), clearsigned with the [S] subkey +# +# Usage: ./scripts/security-txt.sh +# +# Expires is read from the key, so the January ceremony only re-runs this +# after the key's expiry was extended + +set -eu + + +# Internal [S] subkey; pinned with "!" below so `gpg` never picks another subkey +SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D' + + +REPO=$(cd "$(dirname "$0")/.." && pwd) +OUT="$REPO/security.txt" + + +# Notes: +# - the 7th field of the pub line is the primary key's expiry in epoch seconds +# - a subkey resolves to the primary key +EPOCH=$(gpg --list-keys --with-colons "$SIGNING_KEY" \ + | awk -F: '$1 == "pub" { print $7; exit }') +if [ -z "$EPOCH" ]; then + echo "Error: No expiry found for key $SIGNING_KEY" >&2 + exit 1 +fi +EXPIRES=$(date -u -d "@$EPOCH" +%Y-%m-%dT%H:%M:%SZ) + + +UNSIGNED=$(mktemp) +trap 'rm -f "$UNSIGNED"' EXIT + + +cat > "$UNSIGNED" < `\(age1[0-9a-z]*\)`$/\1/p' "$FILE") @@ -120,6 +125,16 @@ for FILE; do FAILED=1 fi ;; + + */security.txt | security.txt) + EXPECTED=$(date -u -d "@$EXPIRY" +%Y-%m-%dT%H:%M:%SZ) + ACTUAL=$(sed -n 's/^Expires: //p' "$FILE") + if [ "$ACTUAL" != "$EXPECTED" ]; then + echo "FAIL security.txt expires $ACTUAL, key expires $EXPECTED" >&2 + FAILED=1 + fi + ;; + esac done diff --git a/security.txt b/security.txt new file mode 100644 index 0000000..960cff5 --- /dev/null +++ b/security.txt @@ -0,0 +1,18 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Canonical: https://www.alexander-hess.com/.well-known/security.txt +Canonical: https://www.webartifex.de/.well-known/security.txt +Contact: mailto:ah@alexander-hess.com +Contact: mailto:alexander@webartifex.de +Encryption: https://www.alexander-hess.com/public-key.asc +Encryption: https://www.webartifex.de/public-key.asc +Expires: 2027-01-31T11:00:29Z +Preferred-Languages: en, de +-----BEGIN PGP SIGNATURE----- + +iHUEARYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCaqcTnAAKCRC0bttN/CaA +XXX/AP0c4y7eMZmGQc5GwCWQDAgTELVBZc6zBNyOjoThEH0qdgEAmahp+3716stJ +iIosJWusNiz1ctVkEdS1WpzH3OfyEwQ= +=joRB +-----END PGP SIGNATURE-----