diff --git a/.github/keys/README.md b/.github/keys/README.md new file mode 100644 index 0000000..d8ad39f --- /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 0022](../../docs/decisions/0022-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 0022. 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..96c3aab 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/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. # # 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/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 + 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/0022-kernel-hash-gpg-verification.md b/docs/decisions/0022-kernel-hash-gpg-verification.md new file mode 100644 index 0000000..bf31009 --- /dev/null +++ b/docs/decisions/0022-kernel-hash-gpg-verification.md @@ -0,0 +1,134 @@ +# 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`, +`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..cf4309f 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 0022](decisions/0022-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 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 + 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