2026-07-08 19:10:11 +02:00
|
|
|
#!/bin/sh
|
|
|
|
|
|
|
|
|
|
# Verify the signed files in this repo against the pinned [S] subkey
|
|
|
|
|
#
|
|
|
|
|
# Usage: ./scripts/verify.sh [FILE...]
|
|
|
|
|
#
|
2026-09-11 17:00:11 +02:00
|
|
|
# FILE defaults to KEYS.md OPENPGP.md ceremonies/*.md AGE.md security.txt
|
2026-07-08 19:10:11 +02:00
|
|
|
#
|
|
|
|
|
# A FILE with a FILE.asc next to it is checked as a detached signature,
|
|
|
|
|
# any other FILE as a clearsigned document
|
|
|
|
|
#
|
2026-09-11 15:48:18 +02:00
|
|
|
# Also checks the key's expiry, and,
|
|
|
|
|
# if KEYS.md or AGE.md is among the FILEs,
|
2026-09-11 17:00:11 +02:00
|
|
|
# that age-recipient.txt matches the recipient in them, and,
|
|
|
|
|
# if security.txt is among the FILEs, that it expires with the key
|
2026-07-08 19:10:11 +02:00
|
|
|
#
|
|
|
|
|
# Exits non-zero if any file lacks a valid signature from the pinned key,
|
2026-09-11 17:00:11 +02:00
|
|
|
# or if the key or security.txt has expired, or if a cross-check fails
|
2026-07-08 19:10:11 +02:00
|
|
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Internal [S] subkey; every file must carry a valid signature from this key
|
|
|
|
|
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Warn when the key expires within this many days
|
|
|
|
|
WARN_DAYS=60
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if [ $# -eq 0 ]; then
|
|
|
|
|
set -- \
|
|
|
|
|
"$REPO/KEYS.md" \
|
|
|
|
|
"$REPO/OPENPGP.md" \
|
|
|
|
|
"$REPO"/ceremonies/*.md \
|
2026-09-11 15:48:18 +02:00
|
|
|
"$REPO/AGE.md" \
|
2026-09-11 17:00:11 +02:00
|
|
|
"$REPO/security.txt" \
|
2026-07-08 19:10:11 +02:00
|
|
|
# Keep this list aligned with the documentation at the top
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Print the fingerprints of all keys that made a valid signature, one per line
|
|
|
|
|
signers() {
|
|
|
|
|
gpg \
|
|
|
|
|
--status-fd 1 \
|
|
|
|
|
--verify "$@" \
|
|
|
|
|
2>/dev/null \
|
|
|
|
|
| awk '$2 == "VALIDSIG" { print $3 }'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FAILED=0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for FILE; do
|
|
|
|
|
|
|
|
|
|
if [ ! -f "$FILE" ]; then
|
|
|
|
|
echo "Error: $FILE not found" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -f "$FILE.asc" ]; then
|
|
|
|
|
SIGNERS=$(signers "$FILE.asc" "$FILE")
|
|
|
|
|
else
|
|
|
|
|
SIGNERS=$(signers "$FILE")
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if printf '%s\n' "$SIGNERS" | grep -qx "$SIGNING_KEY"; then
|
|
|
|
|
echo "OK $FILE"
|
|
|
|
|
else
|
|
|
|
|
echo "FAIL $FILE (signed by: $(printf '%s' "${SIGNERS:-nobody}" | tr '\n' ' '))" >&2
|
|
|
|
|
FAILED=1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Notes:
|
|
|
|
|
# - The 7th field of the pub line is the primary key's expiry in epoch seconds
|
|
|
|
|
# - A subkey resolves to the primary key
|
|
|
|
|
|
|
|
|
|
EXPIRY=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
|
|
|
|
| awk -F: '$1 == "pub" { print $7; exit }')
|
|
|
|
|
|
|
|
|
|
if [ -z "$EXPIRY" ]; then
|
|
|
|
|
echo "Error: No expiry found for key $SIGNING_KEY" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
NOW=$(date -u +%s)
|
|
|
|
|
|
|
|
|
|
DAYS_LEFT=$(( (EXPIRY - NOW) / 86400 ))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$EXPIRY" -le "$NOW" ]; then
|
|
|
|
|
echo "FAIL Key expired $(( (NOW - EXPIRY) / 86400 )) days ago" >&2
|
|
|
|
|
FAILED=1
|
|
|
|
|
elif [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
|
|
|
|
|
echo "WARN Key expires in $DAYS_LEFT days" >&2
|
|
|
|
|
else
|
|
|
|
|
echo "OK Key expires in $DAYS_LEFT days"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
2026-09-11 15:48:18 +02:00
|
|
|
# age-recipient.txt is the only unsigned artifact,
|
2026-09-11 17:00:11 +02:00
|
|
|
# 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
|
2026-09-11 15:48:18 +02:00
|
|
|
for FILE; do
|
|
|
|
|
|
|
|
|
|
case "$FILE" in
|
2026-09-11 17:00:11 +02:00
|
|
|
|
2026-09-11 15:48:18 +02:00
|
|
|
*/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
|
|
|
|
|
;;
|
2026-09-11 17:00:11 +02:00
|
|
|
|
|
|
|
|
*/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
|
|
|
|
|
;;
|
|
|
|
|
|
2026-09-11 15:48:18 +02:00
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
2026-07-08 19:10:11 +02:00
|
|
|
exit "$FAILED"
|