Restructure the repository
- Make OpenPGP only one of many protocols + KEYS.md only holds the current keys + OPENPGP.md describes all OpenPGP details - Make the current public key available at the root - Add scripts/ to automate the various steps: + bootstrap.sh + publish.sh + sign.sh + snapshot.sh + verify.sh
This commit is contained in:
parent
a4f48a61a3
commit
8ece401276
11 changed files with 861 additions and 239 deletions
72
scripts/bootstrap.sh
Executable file
72
scripts/bootstrap.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Set up a machine's keyring to verify everything in this repo
|
||||
#
|
||||
# Usage: ./scripts/bootstrap.sh
|
||||
#
|
||||
# Imports the current public key and all historical keys,
|
||||
# trusts them ultimately, and runs ./scripts/verify.sh
|
||||
#
|
||||
# Notes:
|
||||
# - Idempotent => Safe to re-run at any time
|
||||
# - Touches public material only; the deployed secret subkeys
|
||||
# come from Vaultwarden, see OPENPGP.md
|
||||
|
||||
set -eu
|
||||
|
||||
|
||||
# Internal [S] subkey; only used to look up the key
|
||||
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||
|
||||
|
||||
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||
|
||||
|
||||
|
||||
gpg --quiet \
|
||||
--import "$REPO/public-key.asc"
|
||||
echo "Imported public-key.asc"
|
||||
|
||||
gpg --quiet \
|
||||
--import "$REPO"/history/*.asc
|
||||
echo "Imported history/*.asc"
|
||||
|
||||
|
||||
|
||||
# Notes:
|
||||
# - The first "fpr" line is the primary key's fingerprint
|
||||
# - A subkey resolves to it
|
||||
FPR=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
||||
| awk -F: '$1 == "fpr" { print $10; exit }')
|
||||
if [ -z "$FPR" ]; then
|
||||
echo "Error: Key $SIGNING_KEY not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# Notes:
|
||||
# - One "--import-ownertrust" call
|
||||
# => `gpg` checks the trustdb only once
|
||||
# - "--show-keys" reads a file without importing
|
||||
# => Its warnings about revoked keys are expected and silenced
|
||||
|
||||
TRUST=$(mktemp)
|
||||
trap 'rm -f "$TRUST"' EXIT
|
||||
|
||||
echo "$FPR:6:" > "$TRUST"
|
||||
for FILE in "$REPO"/history/*.asc; do
|
||||
gpg --show-keys --with-colons "$FILE" 2>/dev/null \
|
||||
| awk -F: '$1 == "fpr" { print $10 ":6:"; exit }' >> "$TRUST"
|
||||
done
|
||||
|
||||
gpg --quiet --import-ownertrust "$TRUST"
|
||||
sed 's/^/Trusted /; s/:6:$/ ultimately/' "$TRUST"
|
||||
|
||||
|
||||
|
||||
gpg --check-signatures "$FPR" >&2
|
||||
|
||||
echo ""
|
||||
|
||||
"$REPO/scripts/verify.sh"
|
||||
59
scripts/publish.sh
Executable file
59
scripts/publish.sh
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Publish the current OpenPGP public key to the keyservers
|
||||
#
|
||||
# Usage: ./scripts/publish.sh
|
||||
#
|
||||
# Run after every change to the key
|
||||
#
|
||||
# Notes:
|
||||
# - The SKS-style servers merge and never delete
|
||||
# => Every change must be pushed
|
||||
# - keys.openpgp.org strips UIDs until each address is verified
|
||||
# => Click the links mailed to all addresses after new or changed UIDs
|
||||
#
|
||||
# Exits non-zero if any keyserver rejected the upload
|
||||
|
||||
set -eu
|
||||
|
||||
|
||||
# Internal [S] subkey; only used to look up the key
|
||||
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||
|
||||
|
||||
KEYSERVERS='
|
||||
hkps://keys.openpgp.org
|
||||
hkps://keyserver.ubuntu.com
|
||||
hkps://pgpkeys.eu
|
||||
hkps://pgp.mit.edu
|
||||
hkps://keyserver.escomposlinux.org
|
||||
'
|
||||
|
||||
|
||||
# Notes:
|
||||
# - the first fingerprint line is the primary key's fingerprint
|
||||
# - a subkey resolves to it
|
||||
FPR=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
||||
| awk -F: '$1 == "fpr" { print $10; exit }')
|
||||
if [ -z "$FPR" ]; then
|
||||
echo "Error: Key $SIGNING_KEY not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
FAILED=0
|
||||
|
||||
|
||||
for server in $KEYSERVERS; do
|
||||
|
||||
if gpg --keyserver "$server" --send-keys "$FPR"; then
|
||||
echo "OK $server"
|
||||
else
|
||||
echo "FAIL $server" >&2
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
|
||||
exit "$FAILED"
|
||||
49
scripts/sign.sh
Executable file
49
scripts/sign.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Create detached signatures for the signed documents in this repo
|
||||
#
|
||||
# Usage: ./scripts/sign.sh [FILE...]
|
||||
#
|
||||
# FILE defaults to KEYS.md OPENPGP.md
|
||||
#
|
||||
# Writes FILE.asc next to each FILE and verifies it
|
||||
|
||||
set -eu
|
||||
|
||||
|
||||
# Internal [S] subkey; pinned with "!" below so `gpg` never picks another subkey
|
||||
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||
|
||||
|
||||
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
set -- \
|
||||
"$REPO/KEYS.md" \
|
||||
"$REPO/OPENPGP.md" \
|
||||
# Keep this list aligned with the documentation at the top
|
||||
fi
|
||||
|
||||
|
||||
for FILE; do
|
||||
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "Error: $FILE not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gpg --armor \
|
||||
--detach-sign \
|
||||
--digest-algo SHA512 \
|
||||
--local-user "${SIGNING_KEY}!" \
|
||||
--yes \
|
||||
--output "$FILE.asc" \
|
||||
"$FILE"
|
||||
|
||||
gpg --verify "$FILE.asc" "$FILE"
|
||||
|
||||
echo "Signed $FILE"
|
||||
echo ""
|
||||
|
||||
done
|
||||
72
scripts/snapshot.sh
Executable file
72
scripts/snapshot.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Export the current OpenPGP public key as a yearly snapshot
|
||||
#
|
||||
# Usage: ./scripts/snapshot.sh [YEAR]
|
||||
#
|
||||
# YEAR defaults to the current UTC year
|
||||
#
|
||||
# Writes snapshots/<KEYID>-public-<YEAR>.asc and refreshes public-key.asc
|
||||
|
||||
set -eu
|
||||
|
||||
|
||||
# Internal [S] subkey; only used to look up the key
|
||||
SIGNING_KEY='D6D1989DF1F6B23FD776293DB46EDB4DFC26805D'
|
||||
|
||||
|
||||
REPO=$(cd "$(dirname "$0")/.." && pwd)
|
||||
YEAR=${1:-$(date -u +%Y)}
|
||||
|
||||
# Notes:
|
||||
# - the first "fpr" line is the primary key's fingerprint
|
||||
# - a subkey resolves to it
|
||||
FPR=$(gpg --list-keys --with-colons "$SIGNING_KEY" \
|
||||
| awk -F: '$1 == "fpr" { print $10; exit }')
|
||||
if [ -z "$FPR" ]; then
|
||||
echo "Error: Key $SIGNING_KEY not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
# The last 16 hex chars, as used in OPENPGP.md
|
||||
KEYID=${FPR#"${FPR%????????????????}"}
|
||||
|
||||
OUT="$REPO/snapshots/$KEYID-public-$YEAR.asc"
|
||||
|
||||
|
||||
mkdir -p "$REPO/snapshots"
|
||||
|
||||
|
||||
# Merge the repo copy in first, so a lagging keyring cannot shrink the export
|
||||
gpg --quiet \
|
||||
--import "$REPO/public-key.asc"
|
||||
|
||||
# The retired keys must be present,
|
||||
# or "export-clean" below drops their certifications as unverifiable
|
||||
gpg --quiet \
|
||||
--import "$REPO"/history/*.asc
|
||||
|
||||
|
||||
gpg --armor \
|
||||
--export-options export-clean \
|
||||
--export "$FPR" \
|
||||
> "$OUT" \
|
||||
|| {
|
||||
rm -f "$OUT"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
if [ ! -s "$OUT" ]; then
|
||||
rm -f "$OUT"
|
||||
echo "Error: Nothing exported for $FPR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
cp "$OUT" "$REPO/public-key.asc"
|
||||
|
||||
|
||||
gpg --show-keys "$OUT" >&2
|
||||
|
||||
|
||||
echo "Wrote snapshots/$KEYID-public-$YEAR.asc and public-key.asc"
|
||||
103
scripts/verify.sh
Executable file
103
scripts/verify.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Verify the signed files in this repo against the pinned [S] subkey
|
||||
#
|
||||
# Usage: ./scripts/verify.sh [FILE...]
|
||||
#
|
||||
# FILE defaults to KEYS.md OPENPGP.md ceremonies/*.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
|
||||
#
|
||||
# Exits non-zero if any file lacks a valid signature from the pinned key,
|
||||
# or if the key has expired
|
||||
|
||||
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 \
|
||||
# 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
|
||||
|
||||
|
||||
exit "$FAILED"
|
||||
Loading…
Reference in a new issue