Watch
1
0
Fork
You've already forked keys
0

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
This commit is contained in:
Alexander Hess 2026-09-11 17:00:11 +02:00
commit 30fee8de37
Signed by: alexander
GPG key ID: B46EDB4DFC26805D
6 changed files with 116 additions and 15 deletions

View file

@ -65,6 +65,8 @@ gpg --verify OPENPGP.md.asc OPENPGP.md
- `public-key.asc`, the armored export of the current PGP key, - `public-key.asc`, the armored export of the current PGP key,
for convenient usage, for convenient usage,
refreshed by `scripts/snapshot.sh` refreshed by `scripts/snapshot.sh`
- `security.txt`, clearsigned, expiry matches the key,
refreshed by `scripts/security-txt.sh`
- `ceremonies/`, signed records of exceptional events, - `ceremonies/`, signed records of exceptional events,
such as the 2026 supersession and the `age` key introduction such as the 2026 supersession and the `age` key introduction
- `history/`, armored exports of all retired keys, - `history/`, armored exports of all retired keys,
@ -75,7 +77,7 @@ gpg --verify OPENPGP.md.asc OPENPGP.md
written by `scripts/snapshot.sh` written by `scripts/snapshot.sh`
- One commit per ceremony to keep - One commit per ceremony to keep
snapshot, updated tables, 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: - Distribution:
- WKD on all three UID domains - WKD on all three UID domains
- Public keyservers (see section below) - Public keyservers (see section below)
@ -181,11 +183,12 @@ gpg --list-secret-keys <FPR>
Then, in this repo: Then, in this repo:
1. `./scripts/snapshot.sh` 1. `./scripts/snapshot.sh`
2. Update the dates in the two tables above 2. `./scripts/security-txt.sh`
3. `./scripts/sign.sh` 3. Update the dates in the two tables above
4. `./scripts/verify.sh` 4. `./scripts/sign.sh`
5. `./scripts/publish.sh` 5. `./scripts/verify.sh`
6. One commit 6. `./scripts/publish.sh`
7. One commit
Then, on every other machine: Then, on every other machine:

View file

@ -1,7 +1,7 @@
-----BEGIN PGP SIGNATURE----- -----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCarGC7QAKCRC0bttN/CaA iHUEABYKAB0WIQTW0Zid8fayP9d2KT20bttN/CaAXQUCarGDIgAKCRC0bttN/CaA
XRhgAP9JASLtDJi1M6m8uskF8IfxHw/4uUmP/bnSB5HeXfFNKwEAk17jzrsNYcfb XYaFAP92QJ2JOCVI5L0SgWwnE2p9wbr19jmwa4bLpe8jjDWM1AD+KmUPYusOcWu0
1HVZlJi3G9WRpA3NzEe9nP6QVxWuows= BtCjSOPVI5P0S390W67K12LOzI/vOAY=
=ot15 =q3oD
-----END PGP SIGNATURE----- -----END PGP SIGNATURE-----

View file

@ -39,6 +39,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 gpg --verify AGE.md.asc AGE.md
gpg --verify security.txt
``` ```
Each `gpg --verify` will print two things: 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 Detached signature, renewed at every ceremony
- [`public-key.asc`](public-key.asc): - [`public-key.asc`](public-key.asc):
Armored export of the current OpenPGP key 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/): - [`snapshots/`](snapshots/):
Yearly exports of the current OpenPGP key Yearly exports of the current OpenPGP key
- [`ceremonies/`](ceremonies/): - [`ceremonies/`](ceremonies/):
@ -125,8 +128,10 @@ Besides OpenPGP,
- [`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,
warns when the key is about to expire, and 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): - [`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):
Pushes the current key to the keyservers Pushes the current key to the keyservers
- [`security-txt.sh`](scripts/security-txt.sh):
Generates and clearsigns `security.txt`

60
scripts/security-txt.sh Executable file
View file

@ -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" <<EOT
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: $EXPIRES
Preferred-Languages: en, de
EOT
gpg \
--clearsign \
--digest-algo SHA512 \
--local-user "${SIGNING_KEY}!" \
--yes \
--output "$OUT" \
"$UNSIGNED"
gpg --verify "$OUT"
echo "Wrote security.txt (Expires: $EXPIRES)"

View file

@ -4,17 +4,18 @@
# #
# Usage: ./scripts/verify.sh [FILE...] # Usage: ./scripts/verify.sh [FILE...]
# #
# FILE defaults to KEYS.md OPENPGP.md ceremonies/*.md AGE.md # FILE defaults to KEYS.md OPENPGP.md ceremonies/*.md AGE.md security.txt
# #
# 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, and, # Also checks the key's expiry, and,
# if KEYS.md or AGE.md is among the FILEs, # if KEYS.md or AGE.md is among the FILEs,
# that age-recipient.txt matches the recipient in them # that age-recipient.txt matches the recipient in them, and,
# if security.txt is among the FILEs, that it expires with the key
# #
# 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 a cross-check fails # or if the key or security.txt has expired, or if a cross-check fails
set -eu set -eu
@ -36,6 +37,7 @@ if [ $# -eq 0 ]; then
"$REPO/OPENPGP.md" \ "$REPO/OPENPGP.md" \
"$REPO"/ceremonies/*.md \ "$REPO"/ceremonies/*.md \
"$REPO/AGE.md" \ "$REPO/AGE.md" \
"$REPO/security.txt" \
# Keep this list aligned with the documentation at the top # Keep this list aligned with the documentation at the top
fi fi
@ -104,10 +106,13 @@ fi
# age-recipient.txt is the only unsigned artifact, # age-recipient.txt is the only unsigned artifact,
# so it must match the recipient in the signed documents # so it must match the recipient in the signed documents;
# security.txt must expire together with the signing key,
# but only cross-check it when it was among the verified files
for FILE; do for FILE; do
case "$FILE" in case "$FILE" in
*/KEYS.md | KEYS.md | */AGE.md | AGE.md) */KEYS.md | KEYS.md | */AGE.md | AGE.md)
# shellcheck disable=SC2016 # The backticks are literal Markdown # shellcheck disable=SC2016 # The backticks are literal Markdown
EXPECTED=$(sed -n 's/^> `\(age1[0-9a-z]*\)`$/\1/p' "$FILE") EXPECTED=$(sed -n 's/^> `\(age1[0-9a-z]*\)`$/\1/p' "$FILE")
@ -120,6 +125,16 @@ for FILE; do
FAILED=1 FAILED=1
fi 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 esac
done done

18
security.txt Normal file
View file

@ -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-----