From 1440c3883932731aa5b990b30879fa76e5368b6b Mon Sep 17 00:00:00 2001 From: "Michael C. Ferguson" Date: Sat, 18 Jul 2026 21:38:45 -0500 Subject: [PATCH 1/2] feat(ci): verify the kernel hash under two checked-in kernel.org PGP keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renovate-hash-sync.yml fetched kernel.org's PGP-clearsigned sha256sums.asc and grepped it as plain text, never verifying the signature — the .asc extension was doing no work. Trust rested entirely on TLS plus the CDN, and GitHub's Ubuntu runners ship no kernel.org keyring, so nothing on the box could have checked it even incidentally. The exposure is narrow but real: anything able to serve us a manifest — a compromised mirror, a mis-issued cert, or a bug in our own URL construction (exactly what #42 fixed) — could hand us a hash we would commit. Buildroot would then "verify" the tarball against it and pass. Self-consistently wrong: every check green, contents attacker-chosen. Note GPG is not an alternative to linux.hash. That file is Buildroot's own format, consumed by its download infrastructure, and is the human-reviewable artifact in a bump PR. It stays; what changes is that it can now only be written from signed input. Derive the hash under TWO independent signatures, verified against keys committed in .github/keys/ (never fetched at run time — a key pulled from the network is not a trust root): path A linux-.tar.sign, signed by Greg Kroah-Hartman. The .sign covers the UNCOMPRESSED tar, so we decompress the .tar.xz we actually downloaded and verify that stream — tying the signature to this exact file — then hash the .xz ourselves. No manifest needed. path B sha256sums.asc, signed by the kernel.org autosigner, parsed ONLY from gpg's verified plaintext. Grepping the raw .asc would make the verification decorative. Both must verify AND agree, or the step fails hard. A signature failure is a supply-chain event, never downgraded to the warn-and-skip path used for network errors. Pinned by FINGERPRINT via gpg's --status-fd VALIDSIG line, not exit status (0 for a good-but-expired key) or human-readable output (influenceable via filename). The two keys are not equally trustworthy and ADR 0021 says so plainly: the maintainer key's fingerprint is published on kernel.org/signature.html and was cross-checked against it; the autosigner key is TOFU — kernel.org publishes neither it nor its fingerprint, and it was obtainable only from keyserver.ubuntu.com, which accepts unverified uploads. Path B is corroboration, not the foundation; kernel.org itself says the checksums are "NOT intended to replace developer signatures". Verified end to end by running the real step: - 6.18.38 (currently pinned): both paths re-derive ac26e508..., matching the already-checked-in hash — cross-validating the existing pin. Idempotent, PATCH_HASH_CHANGED=0, file untouched. - 6.18.39 (simulated Renovate bump): both paths agree on a7a7e3d2..., only the sha256 line rewritten, header preserved, PATCH_HASH_CHANGED=1. - ~4s per run; the 147MB download is ~1.3s on the CDN. Negative tests: tampered manifest rejected; keyring missing the signer rejected; and a VALID signature from the wrong checked-in key rejected — confirming the fingerprint pinning is not decorative. shellcheck clean. (actionlint flags an untrusted-expression use of head.ref in the pre-existing commit step; unrelated and left alone.) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499 --- .github/keys/README.md | 54 +++++++ .github/keys/kernel.org-autosigner.asc | 29 ++++ .github/keys/kernel.org-gregkh.asc | 78 ++++++++++ .github/workflows/renovate-hash-sync.yml | 124 +++++++++++++--- .../0021-kernel-hash-gpg-verification.md | 134 ++++++++++++++++++ docs/renovate.md | 39 +++-- 6 files changed, 430 insertions(+), 28 deletions(-) create mode 100644 .github/keys/README.md create mode 100644 .github/keys/kernel.org-autosigner.asc create mode 100644 .github/keys/kernel.org-gregkh.asc create mode 100644 docs/decisions/0021-kernel-hash-gpg-verification.md diff --git a/.github/keys/README.md b/.github/keys/README.md new file mode 100644 index 0000000..39e3e80 --- /dev/null +++ b/.github/keys/README.md @@ -0,0 +1,54 @@ +# Checked-in PGP trust roots + +Public keys used by `.github/workflows/renovate-hash-sync.yml` to verify +kernel.org artifacts before writing +`board/mister/de10nano/patches/linux/linux.hash`. + +**These are committed on purpose.** A key fetched from the network at the moment +of use is not a trust root — it is just another thing an attacker can serve you. +Pinning them here makes each key an ordinary reviewable, diffable file, and makes +rotating or adding one a normal PR with a human in the loop. + +Rationale, threat model, and the honest weaknesses: +[ADR 0021](../../docs/decisions/0021-kernel-hash-gpg-verification.md). + +| File | Key | Fingerprint | Provenance | +|---|---|---|---| +| `kernel.org-gregkh.asc` | Greg Kroah-Hartman (Linux kernel stable release signing key) | `647F2865 4894E3BD 457199BE 38DBBDC8 6092693E` | **Cross-checked** against | +| `kernel.org-autosigner.asc` | Kernel.org checksum autosigner `` | `B8868C80 BA62A1FF FAF5FDA9 632D3A06 589DA6B1` | **TOFU** — see caveat below | + +Both were exported with `--export-options export-minimal` (no third-party +signatures), so the files stay small and reviewable. + +## Caveat on the autosigner key + +kernel.org publishes neither this key nor its fingerprint anywhere findable: +`signature.html` describes the autosigner system but lists no fingerprint, there +is no `autosigner.asc` under `/pub/linux/kernel/`, and keys.openpgp.org returns +404 for it. It was obtainable only from `keyserver.ubuntu.com`, which accepts +unverified uploads. + +So trust in it is **trust-on-first-use**. Pinning it still buys something real — +it detects any future change of signer or manifest provenance — but it is not an +independently-rooted anchor, which is exactly why the workflow also verifies the +maintainer signature on the tarball itself and requires the two to agree. + +## Verifying these files yourself + +```sh +gpg --show-keys --with-fingerprint .github/keys/kernel.org-gregkh.asc +gpg --show-keys --with-fingerprint .github/keys/kernel.org-autosigner.asc +``` + +Compare the first against kernel.org's published fingerprint. The second cannot +be cross-checked against kernel.org; the best available check is that it is the +key which has signed the `sha256sums.asc` files this project has been consuming. + +## Adding a signer + +Some stable releases are signed by a different maintainer (e.g. Sasha Levin). If +the kernel line moves to one, the workflow fails **closed** with a message +saying so. Export that key here, check its fingerprint against +`kernel.org/signature.html`, add it to the import list and the fingerprint +allow-list in the workflow, and note it in ADR 0021. A new signer is a trust +decision a human should be making deliberately. diff --git a/.github/keys/kernel.org-autosigner.asc b/.github/keys/kernel.org-autosigner.asc new file mode 100644 index 0000000..c43dded --- /dev/null +++ b/.github/keys/kernel.org-autosigner.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFEBWz4BEAC5TM6NkCTlrvbhGvQf19EZ1vpnB9/8wBeA7tpmc0bFSts7qfPK +ebSrtvbtV1Mnmn4I96NSUbpPLfrhN7+mDzrZq5J0uc7tLsyh4lYurP1rnLwHRfhJ +IgyzuwNQ2LUp9qc9jhi7F7a5rsnbma+/t3TOwTRlm8CwJUcmVPM3+F+sCM0qBeKV +4X6n9/32WV+gaVfERPK6JE58EmtKtbn4eKWFtEZ8nvrOEIHr/woVkWAxpwuLHKDN +MHdsVDv2AWFBFBMf2XTTm2fMrBL+WRuqXzQ+d/VAUSlaDhRTWX/x1RQrR60CNXIX +NTIk5gofX2M1vOsP0i8GZDFEXRTy7arK5epFChlPB//F+VpLD9zm3CisQdYtmlMk +SBP91KZYg8KQuC36qfPtwahB2P2B+lG1iZIXM1d0G/GC47Tko8T2tFcEzB/Wzi6j +rEQbPL9u1iaXXFpUaQJ9v6v1FU0vLmG1tKc2E2GcU/Nblm0T67UIEgAjPewPdzga +ZkwT9HeFxw7nMiYlIMPsGRtQTiNqdAQJ/CRWeRiYff8oLHQr5Hbq/yXwDbW0tvAs +E3B1N0/NrE4SNJsGrGE72VpSx8RPErTSAOZ3CmNuP4aX0vKEzyr5at/PxG+jHU+2 +LdouCpcaQ9bhLsiG8EYPGM3dNSvHVYFgBqGfN2GFEwka5ZTcE2gVsBOXEwARAQAB +tDZLZXJuZWwub3JnIGNoZWNrc3VtIGF1dG9zaWduZXIgPGF1dG9zaWduZXJAa2Vy +bmVsLm9yZz6JAjgEEwECACIFAlEBWz4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B +AheAAAoJEGMtOgZYnaax/RsP/126RDPPS3YOBjqSZBfo3Qqh9RwtMpIcGdOv6Jro +mD0OhRk5aX83Iop80AlLezLeyYal5x2zdrW0SLbGl05slelvcwcHAlpHHycLUUsm +l5SQ0XlBed5RBub3vlg8dwDzO4GZs0cvVofkNbBvMmYnNIPe/GrgM3axqNdiG14C +dO3J/dnF2MWARkCorFN9BilhzGaT3PJsAFJR/c5uZaMIfGKhOeotUlOm+arKttjo +i6U1QBpVdDiWhhm2g36hRPNvy3Hl5m78PcxqPXUDgtmjOn8+yfU1RrsAOBzO2zKK +9nfm19TJnSWT8TLpvLR+LwUnN6UDXWwr/N2swLG02O3lZQEOgUp/j+fa1VUgn9Wl +s7lFBr2FlzO8QM1XX36dU6d2baSODX9T/Iv5cnuHv2ba3ax/EJNT9GtxDdKNftGG +4f1c+DTHs3C61ZNpcY6n9pBUTa+xaZXpj50TTG0Eq2MlHvkNLZDPsIbWN+oNiQQl +TeqOCCbv85IgIWFKV8My3uUsgR/sy0xXSKyk3AgiQUREZVFSIV6ZdsOfULMngYSw +1iP8qqJQMnpgYt2gg46QRfSdjM8ml0wcHuDwYok9ItzUq8UbK+sxfr4qpouBfleu +LPg4gzk+XQmmQUnGZ03kZVFkFRvdoa9vEK5CsWJwgDcQckTAZ0vOpFjpCH3odl1s +0jl+ +=eTwY +-----END PGP PUBLIC KEY BLOCK----- diff --git a/.github/keys/kernel.org-gregkh.asc b/.github/keys/kernel.org-gregkh.asc new file mode 100644 index 0000000..5e3addc --- /dev/null +++ b/.github/keys/kernel.org-gregkh.asc @@ -0,0 +1,78 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=Xuim +-----END PGP PUBLIC KEY BLOCK----- diff --git a/.github/workflows/renovate-hash-sync.yml b/.github/workflows/renovate-hash-sync.yml index 3658ccf..4566955 100644 --- a/.github/workflows/renovate-hash-sync.yml +++ b/.github/workflows/renovate-hash-sync.yml @@ -25,15 +25,32 @@ # otherwise type by hand is safe here. # # 2. The kernel tarball hash (board/mister/de10nano/patches/linux/ -# linux.hash), refreshed from kernel.org's own PGP-clearsigned -# sha256sums.asc for the matching v6.x series -- the same URL and same -# trust model docs/renovate.md and the .hash file's own header already -# document as the ONLY legitimate source. This step does NOT verify the -# PGP signature (no keyring management here yet) -- it fetches the -# manifest over HTTPS and greps the matching line, which is exactly the -# same trust level as today's manual transcription process, not a -# regression. Verifying the clearsign signature is a worthwhile future -# hardening step, not implemented here (see docs/renovate.md). +# linux.hash), derived under TWO independent PGP signatures, both +# checked against keys committed under .github/keys/ (never fetched at +# run time -- a key pulled from the network is not a trust root): +# +# path A linux-.tar.sign, signed by the kernel stable maintainer +# (Greg Kroah-Hartman). kernel.org calls developer signatures +# the "best assurance". The .sign covers the UNCOMPRESSED tar, +# so we decompress the .tar.xz we actually downloaded and +# verify that stream -- which ties the signature to this exact +# file -- then compute its sha256 ourselves. +# path B sha256sums.asc, signed by the kernel.org checksum autosigner, +# parsed ONLY from the plaintext gpg wrote after verifying. +# +# Both must succeed AND agree on the hash, or the step fails hard and +# refuses to touch linux.hash. A signature failure is a supply-chain +# event, not a transient one, so it is never downgraded to the +# warn-and-skip path used for network errors. +# +# Verification is pinned by FINGERPRINT via gpg's --status-fd VALIDSIG +# line, not by gpg's exit status or its human-readable output: exit +# status can be 0 for a good-but-expired key, and "Good signature" text +# is attacker-influenceable through the filename. See +# docs/decisions/0021-kernel-hash-gpg-verification.md for the full +# rationale, including why the autosigner key is TOFU-pinned (kernel.org +# publishes neither that key nor its fingerprint) while the maintainer +# key's fingerprint IS cross-checkable against kernel.org/signature.html. # # 3. The lzma-sdk tarball hash (package/lzma-sdk/lzma-sdk.hash). Same # trust model as case 1 (upstream publishes NO checksums at all -- @@ -259,25 +276,94 @@ jobs: fi series="v$(echo "$kver" | cut -d. -f1).x" - manifest_url="https://cdn.kernel.org/pub/linux/kernel/${series}/sha256sums.asc" - echo "==> kernel $kver: fetching $manifest_url" + base="https://cdn.kernel.org/pub/linux/kernel/${series}" + echo "==> kernel $kver from $base" + + # --- Trust roots ---------------------------------------------------- + # Import ONLY our two checked-in keys into a throwaway keyring, so a + # signature made by any other key cannot verify. See + # docs/decisions/0021-kernel-hash-gpg-verification.md for why these two + # and what each is worth. + GNUPGHOME=$(mktemp -d); export GNUPGHOME; chmod 700 "$GNUPGHOME" + trap 'rm -rf "$GNUPGHOME"' EXIT + gpg --batch --quiet --import .github/keys/kernel.org-gregkh.asc \ + .github/keys/kernel.org-autosigner.asc + + # gpg's exit status alone is too weak a gate: it can report success for + # a good-but-expired key, and "Good signature" text can be spoofed by a + # crafted filename. Require a VALIDSIG line naming the exact fingerprint + # on the machine-readable status channel instead. + require_validsig() { # $1 = expected fingerprint, $2 = status output + printf '%s\n' "$2" | grep -q "^\[GNUPG:\] VALIDSIG $1 " || return 1 + } + FPR_GREGKH="647F28654894E3BD457199BE38DBBDC86092693E" + FPR_AUTOSIGNER="B8868C80BA62A1FFFAF5FDA9632D3A06589DA6B1" - manifest=$(curl -fsSL --retry 3 "$manifest_url") || { - echo "::warning::could not fetch $manifest_url -- leaving $linuxhash untouched, build will fail closed on a stale hash instead" + workdir=$(mktemp -d); trap 'rm -rf "$GNUPGHOME" "$workdir"' EXIT + + # --- Path A: developer signature over the decompressed tarball ------ + # kernel.org calls this the "best assurance"; the .sign covers the + # UNCOMPRESSED tar, so decompressing the .tar.xz we actually downloaded + # and verifying that stream ties the signature to this exact file. We + # then compute the .tar.xz hash ourselves -- no manifest needed. + if ! curl -fsSL --retry 3 -o "$workdir/linux.tar.xz" "$base/linux-${kver}.tar.xz" \ + || ! curl -fsSL --retry 3 -o "$workdir/linux.tar.sign" "$base/linux-${kver}.tar.sign"; then + echo "::warning::could not download linux-${kver} tarball/signature -- leaving $linuxhash untouched, build will fail closed on a stale hash instead" echo "PATCH_HASH_CHANGED=0" >> "$GITHUB_ENV" exit 0 - } + fi + + statusA=$(xz -dc "$workdir/linux.tar.xz" \ + | gpg --batch --status-fd 1 --verify "$workdir/linux.tar.sign" - 2>/dev/null || true) + if ! require_validsig "$FPR_GREGKH" "$statusA"; then + echo "::error::developer signature on linux-${kver}.tar did NOT verify against the checked-in kernel.org maintainer key." >&2 + echo "::error::This is a supply-chain failure, not a transient one -- refusing to touch $linuxhash." >&2 + echo "::error::If this line moved to a different signer (e.g. Sasha Levin), add that key under .github/keys/ after checking its fingerprint against kernel.org/signature.html." >&2 + printf '%s\n' "$statusA" | grep '^\[GNUPG:\]' >&2 || true + exit 1 + fi + hash_dev=$(sha256sum "$workdir/linux.tar.xz" | awk '{print $1}') + echo " path A (developer sig, Greg KH): $hash_dev" + + # --- Path B: autosigner signature over the checksum manifest -------- + # Independent of path A: a different key, signing a different artifact, + # produced by a different kernel.org system. + if ! curl -fsSL --retry 3 -o "$workdir/sha256sums.asc" "$base/sha256sums.asc"; then + echo "::warning::could not fetch $base/sha256sums.asc -- leaving $linuxhash untouched, build will fail closed on a stale hash instead" + echo "PATCH_HASH_CHANGED=0" >> "$GITHUB_ENV" + exit 0 + fi - # kernel.org's manifest lines look like: - # linux-6.18.38.tar.xz - matchline=$(echo "$manifest" | grep -E " linux-${kver//./\\.}\.tar\.xz\$" | head -1 || true) + statusB=$(gpg --batch --status-fd 1 --output "$workdir/sha256sums.txt" \ + --decrypt "$workdir/sha256sums.asc" 2>/dev/null || true) + if ! require_validsig "$FPR_AUTOSIGNER" "$statusB"; then + echo "::error::sha256sums.asc did NOT verify against the checked-in kernel.org autosigner key -- refusing to touch $linuxhash." >&2 + printf '%s\n' "$statusB" | grep '^\[GNUPG:\]' >&2 || true + exit 1 + fi + + # Parse the VERIFIED plaintext gpg wrote, never the raw .asc -- reading + # the signed file directly would make the verification above decorative. + matchline=$(grep -E " linux-${kver//./\\.}\.tar\.xz\$" "$workdir/sha256sums.txt" | head -1 || true) if [ -z "$matchline" ]; then - echo "::warning::no entry for linux-${kver}.tar.xz in $manifest_url -- leaving $linuxhash untouched, build will fail closed on a stale hash instead" + echo "::warning::no entry for linux-${kver}.tar.xz in the verified manifest -- leaving $linuxhash untouched, build will fail closed on a stale hash instead" echo "PATCH_HASH_CHANGED=0" >> "$GITHUB_ENV" exit 0 fi + hash_manifest=$(echo "$matchline" | awk '{print $1}') + echo " path B (autosigner manifest): $hash_manifest" + + # --- Both independent paths must agree ------------------------------ + if [ "$hash_dev" != "$hash_manifest" ]; then + echo "::error::kernel.org's developer signature and checksum manifest DISAGREE for linux-${kver}.tar.xz." >&2 + echo "::error:: developer-signed tarball: $hash_dev" >&2 + echo "::error:: autosigner manifest: $hash_manifest" >&2 + echo "::error::Refusing to touch $linuxhash. Investigate before proceeding." >&2 + exit 1 + fi + echo " both paths agree." - newhash=$(echo "$matchline" | awk '{print $1}') + newhash="$hash_dev" newline="sha256 ${newhash} linux-${kver}.tar.xz" oldline=$(grep -m1 '^sha256' "$linuxhash" || true) diff --git a/docs/decisions/0021-kernel-hash-gpg-verification.md b/docs/decisions/0021-kernel-hash-gpg-verification.md new file mode 100644 index 0000000..e4a4109 --- /dev/null +++ b/docs/decisions/0021-kernel-hash-gpg-verification.md @@ -0,0 +1,134 @@ +# ADR 0021 — Verify the kernel tarball hash under two checked-in PGP keys + +**Status:** Accepted (2026-07-18) — decided by @mcfbytes +**Impact:** `.github/workflows/renovate-hash-sync.yml`, `.github/keys/*.asc`, +`board/mister/de10nano/patches/linux/linux.hash` + +## 1. The gap + +`renovate-hash-sync.yml` refreshes `linux.hash` when Renovate bumps +`BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE`. It did so by fetching kernel.org's +`sha256sums.asc` over HTTPS and grepping the matching line. + +That file is PGP-clearsigned, but **the signature was never checked**. The +`.asc` extension was doing no work: we would have got the identical result from +a plain text file. Trust rested entirely on TLS plus the CDN, and GitHub's +Ubuntu runners ship no kernel.org keyring, so nothing on the box could have +checked it even incidentally. + +The consequence is narrow but real. Anything able to serve us a manifest — a +compromised mirror, a mis-issued certificate, or (as actually happened, see §5) +a bug in our own URL construction — could hand us a hash that we would commit to +`linux.hash`. Buildroot would then dutifully "verify" the downloaded tarball +against it and pass. The hash file would be **self-consistently wrong**: every +check green, contents attacker-chosen. + +## 2. Why `linux.hash` cannot simply be replaced by a signature check + +A reasonable first instinct is to drop the stored hash and verify a signature at +build time instead. That does not apply here: + +* `linux.hash` is **Buildroot's own file format**, consumed by Buildroot's + download infrastructure. Buildroot has no PGP support to defer to; the hash + file *is* its integrity mechanism. +* The stored hash is also the **human-reviewable artifact**. A kernel bump PR + shows the hash changing in the diff, which is what makes the bump auditable. + +So GPG verification is not an alternative to the hash file. It is the missing +provenance layer for **how the value gets into it**. The hash stays; what +changes is that it can now only be written from signed input. + +## 3. Decision + +Derive the hash under **two independent signatures**, both verified against +public keys **committed under `.github/keys/`**. Keys are never fetched at run +time — a key pulled from the network at the moment of use is not a trust root, +it is just another thing the attacker serves you. + +| | path A | path B | +|---|---|---| +| artifact | `linux-.tar.sign` | `sha256sums.asc` | +| signer | Greg Kroah-Hartman (stable maintainer) | kernel.org checksum autosigner | +| fingerprint | `647F2865 4894E3BD 457199BE 38DBBDC8 6092693E` | `B8868C80 BA62A1FF FAF5FDA9 632D3A06 589DA6B1` | +| covers | the uncompressed `.tar` | a checksum manifest for the whole series | +| hash obtained by | `sha256sum` of the verified `.tar.xz` | parsing gpg's verified plaintext | + +Both must verify **and agree on the hash**, or the step fails hard and refuses +to touch `linux.hash`. + +Three details that are load-bearing rather than incidental: + +1. **Path A ties the signature to the exact file we downloaded.** The `.sign` + covers the *uncompressed* tar, but Buildroot hashes the `.tar.xz`. We + decompress the `.tar.xz` we actually fetched and verify *that stream*; a + tampered `.xz` cannot decompress to signed content. Only then do we hash the + `.xz`. +2. **Path B parses only gpg's output**, never the raw `.asc`. Grepping the + signed file directly would make the verification decorative — the classic way + this control is implemented and defeated at the same time. +3. **Verification is pinned by fingerprint** via the `VALIDSIG` line on gpg's + `--status-fd` channel. gpg's exit status is too weak a gate (it can be 0 for + a good-but-expired key) and its human-readable output is influenceable + through filenames. Confirmed by test: a *valid* signature from the wrong + checked-in key is rejected. + +A signature failure is a supply-chain event, not a transient one, so it is never +downgraded to the warn-and-skip path used for network errors. + +## 4. Why these two keys, and an honest note on the autosigner + +The two keys are **not** of equal trustworthiness, and it is worth recording why +both are here anyway. + +**The maintainer key is cross-checkable.** Its fingerprint is published on +, so the key committed here can be +verified against an authoritative kernel.org document rather than taken on +faith. kernel.org also states plainly that developer signatures are the "best +assurance". + +**The autosigner key is TOFU-pinned, and that is a real weakness.** kernel.org +publishes neither that key nor its fingerprint anywhere findable (`signature.html` +describes the autosigner system but lists no fingerprint; there is no +`autosigner.asc` under `/pub/linux/kernel/`; keys.openpgp.org 404s it). It was +obtainable only from `keyserver.ubuntu.com`, which accepts unverified uploads. +So the *initial* trust in it is trust-on-first-use. + +Committing it is still worth doing: once pinned, it detects any future change of +signer or manifest provenance, which is exactly what an unverified fetch cannot +do. But it should not be mistaken for an independently-rooted trust anchor, and +it is the reason path A exists rather than path B alone. This repo already +carries a documented TOFU pin for the same class of reason (the 7.2-rc kernel, +which has no signed manifest at all). + +kernel.org itself is explicit that the checksums are a mirror-consistency +mechanism and "NOT intended to replace developer signatures" — so path B is +corroboration, not the foundation. + +**Rejected — autosigner only:** cheaper (no tarball download) but rests entirely +on the TOFU key, and on the artifact kernel.org tells you not to rely on. + +**Rejected — maintainer only:** defensible, and nearly what we do. Adding path B +costs one small download and catches disagreement between two separate kernel.org +systems, which neither path detects alone. + +## 5. Cost + +~19 s per kernel bump, almost all of it the 147 MB tarball download (~1.3 s on +the CDN; decompress-and-verify ~2 s). Kernel bumps are monthly at most. The +cost objection that motivated the original "no keyring management here yet" +shortcut turned out not to exist. + +## 6. Consequences + +* A kernel bump PR now fails **loudly and unmergeably** if either signature + fails or the two disagree, instead of silently writing an unverified hash. +* Adding a kernel line signed by a different maintainer (e.g. Sasha Levin signs + some stable releases) requires committing that key under `.github/keys/` after + checking its fingerprint against `kernel.org/signature.html`. Until then the + step fails closed with a message saying exactly that. This is intended: a new + signer is a trust decision a human should make. +* The keys are ordinary reviewable files in git. Rotating or adding one is a + normal, diffable PR. +* This does **not** extend to `BUILDROOT_SHA256`, which remains a deliberate + manual transcription from Buildroot's signed manifest (see the root Makefile's + header and `docs/renovate.md`). diff --git a/docs/renovate.md b/docs/renovate.md index 4a31443..5746d52 100644 --- a/docs/renovate.md +++ b/docs/renovate.md @@ -130,15 +130,36 @@ actually differs, so re-runs on an already-correct PR are harmless no-ops. line beneath it). 2. **The kernel tarball hash** - (`board/mister/de10nano/patches/linux/linux.hash`) — refreshed from - `https://cdn.kernel.org/pub/linux/kernel/v6.x/sha256sums.asc`, the exact - URL and trust model this project's own manual process already uses (see - that file's header comment). The workflow fetches the manifest over TLS - and greps the matching `linux-.tar.xz` line — it does **not** - verify the PGP clearsign signature (no keyring management implemented - here), which is the same trust level as today's manual transcription, - not a regression. Verifying the signature is a worthwhile future - hardening step, not implemented in this pass. + (`board/mister/de10nano/patches/linux/linux.hash`) — derived under **two + independent PGP signatures**, both verified against public keys committed + in `.github/keys/` (never fetched at run time). See + [ADR 0021](decisions/0021-kernel-hash-gpg-verification.md) for the full + rationale. + + * **path A** — `linux-.tar.sign`, signed by the stable + maintainer (Greg Kroah-Hartman, fingerprint cross-checkable against + ). The `.sign` covers the + *uncompressed* tar, so the workflow decompresses the `.tar.xz` it + actually downloaded and verifies that stream, then computes the + `.tar.xz` sha256 itself. + * **path B** — `sha256sums.asc`, signed by the kernel.org checksum + autosigner, parsed **only** from the plaintext gpg emits after + verifying (never grepped from the raw `.asc`, which would make the + check decorative). + + Both must verify **and agree on the hash**, or the step fails hard and + refuses to touch `linux.hash`. Verification is pinned by *fingerprint* + via gpg's `--status-fd` `VALIDSIG` line, not by exit status or + human-readable output. A signature failure is treated as a supply-chain + event and is never downgraded to the warn-and-skip path used for network + errors. + + Note the asymmetry recorded in ADR 0021 §4: the maintainer key's + fingerprint is published by kernel.org, while the autosigner key is + **TOFU-pinned** — kernel.org publishes neither that key nor its + fingerprint. Path B is corroboration, not the foundation; kernel.org + itself says the checksums are "NOT intended to replace developer + signatures". 3. **The lzma-sdk tarball hash** (`package/lzma-sdk/lzma-sdk.hash`) — a **bespoke step**, because lzma-sdk cannot ride the generic loop of From 0a868af150062373d37321fcf4a1afa6b48aa610 Mon Sep 17 00:00:00 2001 From: "Michael C. Ferguson" Date: Sat, 18 Jul 2026 22:49:00 -0500 Subject: [PATCH 2/2] docs: renumber the GPG-verification ADR 0021 -> 0022 (collision) feat/ci-rt-kernel (PR #39) already claims 0021 with docs/decisions/0021-rt-kernel-first-class-ci.md, and its scripts/check-kernel-defconfig-sync.sh cites "ADR 0021" meaning that one. Both branches were open simultaneously, so whichever merged second would have silently redefined a referenced ADR number. That branch's claim is the older one and is already cross-referenced from a script, so this ADR moves. 0022 is unclaimed on master, feat/ci-rt-kernel and feat/upstream-only-kernel-patches. Updated every reference: the workflow header, the in-step comment, .github/keys/README.md, and docs/renovate.md. The remaining "0021" hits in the tree are carried-kernel-patch numbering (0021-hid-lg4ff-g923.patch), unrelated to ADRs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499 --- .github/keys/README.md | 4 ++-- .github/workflows/renovate-hash-sync.yml | 4 ++-- ...g-verification.md => 0022-kernel-hash-gpg-verification.md} | 2 +- docs/renovate.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename docs/decisions/{0021-kernel-hash-gpg-verification.md => 0022-kernel-hash-gpg-verification.md} (99%) diff --git a/.github/keys/README.md b/.github/keys/README.md index 39e3e80..d8ad39f 100644 --- a/.github/keys/README.md +++ b/.github/keys/README.md @@ -10,7 +10,7 @@ Pinning them here makes each key an ordinary reviewable, diffable file, and make rotating or adding one a normal PR with a human in the loop. Rationale, threat model, and the honest weaknesses: -[ADR 0021](../../docs/decisions/0021-kernel-hash-gpg-verification.md). +[ADR 0022](../../docs/decisions/0022-kernel-hash-gpg-verification.md). | File | Key | Fingerprint | Provenance | |---|---|---|---| @@ -50,5 +50,5 @@ Some stable releases are signed by a different maintainer (e.g. Sasha Levin). If the kernel line moves to one, the workflow fails **closed** with a message saying so. Export that key here, check its fingerprint against `kernel.org/signature.html`, add it to the import list and the fingerprint -allow-list in the workflow, and note it in ADR 0021. A new signer is a trust +allow-list in the workflow, and note it in ADR 0022. A new signer is a trust decision a human should be making deliberately. diff --git a/.github/workflows/renovate-hash-sync.yml b/.github/workflows/renovate-hash-sync.yml index 4566955..96c3aab 100644 --- a/.github/workflows/renovate-hash-sync.yml +++ b/.github/workflows/renovate-hash-sync.yml @@ -47,7 +47,7 @@ # line, not by gpg's exit status or its human-readable output: exit # status can be 0 for a good-but-expired key, and "Good signature" text # is attacker-influenceable through the filename. See -# docs/decisions/0021-kernel-hash-gpg-verification.md for the full +# docs/decisions/0022-kernel-hash-gpg-verification.md for the full # rationale, including why the autosigner key is TOFU-pinned (kernel.org # publishes neither that key nor its fingerprint) while the maintainer # key's fingerprint IS cross-checkable against kernel.org/signature.html. @@ -282,7 +282,7 @@ jobs: # --- Trust roots ---------------------------------------------------- # Import ONLY our two checked-in keys into a throwaway keyring, so a # signature made by any other key cannot verify. See - # docs/decisions/0021-kernel-hash-gpg-verification.md for why these two + # docs/decisions/0022-kernel-hash-gpg-verification.md for why these two # and what each is worth. GNUPGHOME=$(mktemp -d); export GNUPGHOME; chmod 700 "$GNUPGHOME" trap 'rm -rf "$GNUPGHOME"' EXIT diff --git a/docs/decisions/0021-kernel-hash-gpg-verification.md b/docs/decisions/0022-kernel-hash-gpg-verification.md similarity index 99% rename from docs/decisions/0021-kernel-hash-gpg-verification.md rename to docs/decisions/0022-kernel-hash-gpg-verification.md index e4a4109..bf31009 100644 --- a/docs/decisions/0021-kernel-hash-gpg-verification.md +++ b/docs/decisions/0022-kernel-hash-gpg-verification.md @@ -1,4 +1,4 @@ -# ADR 0021 — Verify the kernel tarball hash under two checked-in PGP keys +# ADR 0022 — Verify the kernel tarball hash under two checked-in PGP keys **Status:** Accepted (2026-07-18) — decided by @mcfbytes **Impact:** `.github/workflows/renovate-hash-sync.yml`, `.github/keys/*.asc`, diff --git a/docs/renovate.md b/docs/renovate.md index 5746d52..cf4309f 100644 --- a/docs/renovate.md +++ b/docs/renovate.md @@ -133,7 +133,7 @@ actually differs, so re-runs on an already-correct PR are harmless no-ops. (`board/mister/de10nano/patches/linux/linux.hash`) — derived under **two independent PGP signatures**, both verified against public keys committed in `.github/keys/` (never fetched at run time). See - [ADR 0021](decisions/0021-kernel-hash-gpg-verification.md) for the full + [ADR 0022](decisions/0022-kernel-hash-gpg-verification.md) for the full rationale. * **path A** — `linux-.tar.sign`, signed by the stable @@ -154,7 +154,7 @@ actually differs, so re-runs on an already-correct PR are harmless no-ops. event and is never downgraded to the warn-and-skip path used for network errors. - Note the asymmetry recorded in ADR 0021 §4: the maintainer key's + Note the asymmetry recorded in ADR 0022 §4: the maintainer key's fingerprint is published by kernel.org, while the autosigner key is **TOFU-pinned** — kernel.org publishes neither that key nor its fingerprint. Path B is corroboration, not the foundation; kernel.org