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:
parent
ae768285b6
commit
30fee8de37
6 changed files with 116 additions and 15 deletions
15
OPENPGP.md
15
OPENPGP.md
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-----
|
||||||
|
|
|
||||||
|
|
@ -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
60
scripts/security-txt.sh
Executable 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)"
|
||||||
|
|
@ -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
18
security.txt
Normal 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-----
|
||||||
Loading…
Reference in a new issue