diff --git a/docs/supply-chain.md b/docs/supply-chain.md index c7b0aeb5..610d26b0 100644 --- a/docs/supply-chain.md +++ b/docs/supply-chain.md @@ -73,7 +73,8 @@ signature that could not be checked or a signer never established: a key taken from the signature itself, a key not held, a file not downloaded. A key that no publisher states anywhere passes it only while `tools/source-notes.tsv` records the routes that were tried -(`no-usable-key`); such a note for a key that is held fails it as stale. A +(`no-usable-key`); such a note for a key that is held fails it as stale, +while a signature the run could not fetch leaves its note untried. A source that publishes no OpenPGP signature is not the gate's: the lock pins it, and `tools/verify-provenance.sh --strict` checks whatever else its publisher states. diff --git a/tools/tests/verify-signatures.sh b/tools/tests/verify-signatures.sh index 715c0883..4b5bb554 100755 --- a/tools/tests/verify-signatures.sh +++ b/tools/tests/verify-signatures.sh @@ -277,6 +277,17 @@ write_manifest good fresh_root; run --strict "--notes=${W}/notes-stale.tsv" expect_fail "a no-usable-key note for a source whose key is held fails --strict" "stale note" +# A noted source whose signature could not be fetched this run: unverifiable, +# and the note untried, not stale. +printf 'fixture payload for unreached\n' > "${SRC}/unreached.tar.gz" +rm -f "${SRC}/unreached.tar.gz.sig" "${SRC}/.signatures/unreached.tar.gz.sig" +printf 'unreached no-usable-key https://example.invalid/ No route to the key was found. Checked 2026-09-28.\n' > "${W}/notes-unreached.tsv" +write_manifest good; add_row unreached sig +fresh_root; run --strict "--notes=${W}/notes-unreached.tsv" +expect_fail "a signature that could not be fetched fails --strict as unverifiable" "unverifiable" +if grep -q "stale note" "$OUT"; then red "a note for a signature that could not be fetched was called stale"; show +else green "a note for a signature that could not be fetched is untried, not stale"; fi + # A manifest row whose file was never downloaded. write_manifest good notfetched rm -f "${SRC}/notfetched.tar.gz" diff --git a/tools/verify-signatures.sh b/tools/verify-signatures.sh index 1809441f..92e8983d 100755 --- a/tools/verify-signatures.sh +++ b/tools/verify-signatures.sh @@ -72,9 +72,17 @@ chmod 700 "$GNUPGHOME" IMPORTED_MARK="${GNUPGHOME}/.kryptik-imported" -quiet_fetch() { - curl -fL --no-progress-meter --connect-timeout 20 \ - --retry 2 --retry-delay 2 -o "$2" "$1" +# A host that throttles (freedesktop.org answers 418 to a busy runner, others +# 429 or 503) is asked again after a pause; a 404 is an answer. +quiet_fetch() { # quiet_fetch URL OUT + local code try + for try in 1 2 3; do + if code="$(curl -fsL --connect-timeout 20 --retry 2 --retry-delay 2 \ + -o "$2" -w '%{http_code}' "$1" 2>/dev/null)"; then return 0; fi + case "$code" in 418|429|503) sleep $(( try * 5 )) ;; *) break ;; esac + done + rm -f "$2" + return 1 } # --- keys ------------------------------------------------------------------ @@ -911,9 +919,16 @@ if [[ "$NOTED" -gt 0 ]]; then printf ' - %s\n' "${NOTED_LIST[@]}" fi # A note that no unheld key needed: the key is held now, or the source went. +# A signature that could not be checked this run tried no note. +untried_this_run() { # untried_this_run NAME + local u + for u in "${UNVERIFIABLE_LIST[@]}"; do [[ "$u" == "$1 ("* ]] && return 0; done + return 1 +} STALE_NOTES=() for n_pkg in "${!NOTED_NO_KEY[@]}"; do [[ -n "${NOTE_USED[$n_pkg]:-}" ]] && continue + untried_this_run "$n_pkg" && continue [[ -n "${SEEN_SOURCE[$n_pkg]:-}" ]] && STALE_NOTES+=("$n_pkg") done if [[ "${#STALE_NOTES[@]}" -gt 0 ]]; then