Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ revoked, possibly compromised) always fail. `--strict`, the gate CI runs on
every push, also fails on a signature that could not be checked and on a
signer never established: a key taken from the signature itself, a key not
held, a file not downloaded, a key whose published copy
(`tools/key-provenance.tsv`) could not be read that run. On a pull request
(`tools/key-provenance.tsv`) could not be read that run. A page that answers
with no OpenPGP data at all, as an error page served as 200, counts as not
read; one that serves another key, or data with no key in it such as a bare
revocation, is refused as a changed key. On a pull request
CI adds `--unread-warns`: a published copy that does not answer is a warning
there, and the key held decides, since a host being down is not the change's
fault; the push to main reads every copy again. A row there speaks
Expand Down
39 changes: 39 additions & 0 deletions tools/tests/verify-signatures.sh
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,45 @@ if [[ "$RC" -eq 0 && "$(klass_of "${W}/r9.tsv" good)" == "signature-keyring-key"
else
red "--strict --unread-warns checks it against the key held (exit ${RC}, got $(klass_of "${W}/r9.tsv" good))"; show
fi
# A page that answers with no key in it, as an error page served as 200, is unread too, not a changed key.
printf '<html><body>Service unavailable</body></html>\n' > "${PROV}/no-key.html"
fresh_root
write_manifest good
prov_table "${GOODFPR} korg file://${PROV}/no-key.html 2026-09-11 good good fixture, its page serving no key"
runprov --report="${W}/r11.tsv"
if [[ "$RC" -eq 0 && "$(klass_of "${W}/r11.tsv" good)" == "signature-keyring-key" ]] \
&& grep -qF "answered with no OpenPGP data in it" "$OUT" && ! grep -qF "REFUSING" "$OUT"; then
green "a page that answers with no key is unread, not a changed key"
else
red "a page that answers with no key is unread (exit ${RC}, got $(klass_of "${W}/r11.tsv" good))"; show
fi
fresh_root
runprov --strict
if [[ "$RC" -ne 0 ]] && grep -qF "published copy at file://${PROV}/no-key.html not read this run" "$OUT"; then
green "and --strict still counts its source unverifiable"
else
red "and --strict still counts its source unverifiable (exit ${RC})"; show
fi
fresh_root
runprov --strict --unread-warns --report="${W}/r12.tsv"
if [[ "$RC" -eq 0 && "$(klass_of "${W}/r12.tsv" good)" == "signature-keyring-key" ]] \
&& grep -qF " - good (file://${PROV}/no-key.html)" "$OUT"; then
green "and --strict --unread-warns checks it against the key held"
else
red "and --strict --unread-warns checks it against the key held (exit ${RC}, got $(klass_of "${W}/r12.tsv" good))"; show
fi
# OpenPGP data with no key gpg lists, as a bare revocation, is no error page: it stays refused.
sed 's/^://' "${FIXG}/openpgp-revocs.d/${GOODFPR}.rev" > "${PROV}/good-revocation-only.asc"
fresh_root
write_manifest good
prov_table "${GOODFPR} korg file://${PROV}/good-revocation-only.asc 2026-09-11 good good fixture, its page serving a bare revocation"
runprov --strict --unread-warns --report="${W}/r13.tsv"
if [[ "$RC" -ne 0 && "$(klass_of "${W}/r13.tsv" good)" == "published-key-changed" ]] \
&& ! grep -qF "answered with no OpenPGP data" "$OUT"; then
green "a page holding OpenPGP data but no key is refused, even under --unread-warns"
else
red "a page holding OpenPGP data but no key is refused (exit ${RC}, got $(klass_of "${W}/r13.tsv" good))"; show
fi
fresh_root
runprov --unread-warns
if [[ "$RC" -ne 0 ]] && grep -qF -- "--unread-warns eases --strict; give both" "$OUT"; then
Expand Down
24 changes: 22 additions & 2 deletions tools/verify-signatures.sh
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,15 @@ primaries_in() {
| awk -F: '$1 == "pub" { p = 1; next } p && $1 == "fpr" { print toupper($10); p = 0 }' || true
}

# Whether FILE holds any OpenPGP data: an armor header, or a binary packet,
# whose first byte has its top bit set.
has_openpgp_data() {
local b
grep -qa -- '-----BEGIN PGP' "$1" && return 0
b="$(head -c1 -- "$1" | od -An -tu1 | tr -d ' ')"
[[ -n "$b" && "$b" -ge 128 ]]
}

# anchored_import FPR FILE: merge key FPR alone from FILE and print FILE's primary fingerprints;
# returns 1 when FILE lacks FPR, and 2 when FPR is there but cannot be taken alone.
anchored_import() {
Expand Down Expand Up @@ -453,7 +462,7 @@ anchored_import() {
# that could not be read is remembered, and gives no class this run.
declare -A PROV_FETCHED=() PROV_REFUSED=() PROV_MISSED=()
import_provenance_keys_for() {
local name="$1" i fpr tmp home got rc try
local name="$1" i fpr tmp home got rc try nodata
for i in "${!PROV_FPR[@]}"; do
case "${PROV_SIGNS[$i]}" in *",${name},"*) ;; *) continue ;; esac
[[ -z "${PROV_FETCHED[$i]:-}" ]] || continue
Expand Down Expand Up @@ -491,16 +500,27 @@ import_provenance_keys_for() {
esac
rc=0
got="$(anchored_import "$fpr" "$tmp")" || rc=$?
nodata=0
if [[ "$rc" -eq 1 && -z "$got" ]] && ! has_openpgp_data "$tmp"; then nodata=1; fi
rm -f "$tmp"
if [[ "$rc" -eq 0 ]]; then
dim " ${name}: imported the key ${PROV_KIND[$i]} publishes (${fpr})"
continue
fi
# A page with no OpenPGP data at all (an error page served as 200) is
# unread, as a page that does not answer is: either one could hide a
# change, so --strict still fails it. Another key, or OpenPGP data
# with no key gpg can list (a bare revocation), is refused.
if [[ "$nodata" -eq 1 ]]; then
warn "${name}: ${PROV_LOC[$i]} answered with no OpenPGP data in it"
PROV_MISSED[$i]=1
continue
fi
if [[ "$rc" -eq 2 ]]; then
err "${name}: ${PROV_LOC[$i]} serves ${fpr}, but it could not be taken"
err " alone. REFUSING it: the recorded fingerprint is the anchor."
else
err "${name}: ${PROV_LOC[$i]} now publishes ${got:-no key},"
err "${name}: ${PROV_LOC[$i]} now publishes ${got:-no key gpg can list},"
err " not the recorded ${fpr}. REFUSING it: a key that"
err " changed at a published location is a finding, not an update."
fi
Expand Down
Loading