feat(ci): verify the kernel hash under two checked-in kernel.org PGP keys - #43
Open
mcfbytes wants to merge 2 commits into
Open
feat(ci): verify the kernel hash under two checked-in kernel.org PGP keys#43mcfbytes wants to merge 2 commits into
mcfbytes wants to merge 2 commits into
Conversation
…keys 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-<ver>.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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens the Renovate kernel-hash refresh workflow by requiring cryptographic provenance: it now verifies kernel.org-provided signatures using two checked-in OpenPGP public keys before updating Buildroot’s linux.hash, and documents the threat model and rationale.
Changes:
- Update
renovate-hash-sync.ymlto verify both the kernel tarball’s developer signature and the series checksum manifest signature, requiring both paths to validate and agree on the sha256. - Add checked-in kernel.org public keys under
.github/keys/and document their provenance/trust characteristics. - Document the decision and rationale in ADR 0021 and update Renovate documentation accordingly.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/renovate-hash-sync.yml |
Implements dual-signature verification and fingerprint pinning before rewriting linux.hash. |
docs/renovate.md |
Updates the Renovate workflow documentation to reflect the new signature verification model and ADR reference. |
docs/decisions/0021-kernel-hash-gpg-verification.md |
New ADR describing the threat model, decision, and operational consequences. |
.github/keys/README.md |
Documents the checked-in PGP keys as trust roots and how to verify/extend them. |
.github/keys/kernel.org-gregkh.asc |
Adds the kernel stable maintainer public key used for path A verification. |
.github/keys/kernel.org-autosigner.asc |
Adds the kernel.org checksum autosigner public key used for path B verification. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
renovate-hash-sync.ymlfetched kernel.org's PGP-clearsignedsha256sums.ascand grepped it as plain text, never verifying the signature. The.ascextension was doing no work — we'd 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 exposure is narrow but real: anything able to serve us a manifest — a compromised mirror, a mis-issued certificate, or a bug in our own URL construction (exactly what #42 fixed) — could hand us a hash we'd 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.Note: GPG is not a replacement for
linux.hashWorth stating explicitly, since it was the natural first instinct.
linux.hashis Buildroot's own file format, consumed by its download infrastructure — Buildroot has no PGP support to defer to, so the hash file is its integrity mechanism. It's also the human-reviewable artifact that makes a bump PR auditable.So this isn't an alternative to the stored hash. It's 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.
What this does
Two independent signatures, verified against keys committed in
.github/keys/and never fetched at run time — a key pulled from the network at the moment of use is not a trust root, it's just another thing the attacker serves you.linux-<ver>.tar.signsha256sums.asc.tarsha256sumof the verified.tar.xzBoth must verify and agree, or the step fails hard and refuses to touch
linux.hash. A signature failure is a supply-chain event, never downgraded to the warn-and-skip path used for network errors.Three load-bearing details:
.signcovers the uncompressed tar but Buildroot hashes the.tar.xz, so we decompress the.xzwe actually fetched and verify that stream — a tampered.xzcan't decompress to signed content — then hash the.xz..asc. Grepping the signed file directly is the classic way this control gets implemented and defeated simultaneously.VALIDSIGline on--status-fd, not gpg's exit status (can be 0 for a good-but-expired key) or its human-readable output (influenceable via filename).The two keys are not equally trustworthy
ADR 0021 §4 records this honestly rather than implying uniform assurance:
647F2865 4894E3BD 457199BE 38DBBDC8 6092693Eis published on https://www.kernel.org/signature.html, so the committed key is verifiable against an authoritative kernel.org document.signature.htmldescribes the system but lists no fingerprint; noautosigner.ascunder/pub/linux/kernel/; keys.openpgp.org 404s it). It was obtainable only fromkeyserver.ubuntu.com, which accepts unverified uploads.Pinning it still buys something — it detects a future change of signer or manifest provenance, which an unverified fetch cannot — but it isn't an independently-rooted anchor, and that's precisely why path A exists rather than path B alone. kernel.org itself states the checksums are "NOT intended to replace developer signatures."
Verification
Ran the real step extracted from the final YAML, not a paraphrase:
ac26e508..., matching the hash already checked in — which cross-validates the existing pin under two signatures. Idempotent:PATCH_HASH_CHANGED=0, file untouched.a7a7e3d2...; only thesha256line rewritten, header comment preserved;PATCH_HASH_CHANGED=1.Negative tests — verification that can't reject bad input is theater:
The third confirms the fingerprint pinning is doing real work, not just accepting any good signature.
shellcheck clean on the step body.
Out of scope, but noticed
actionlint flags
github.event.pull_request.head.refused directly in the inline script of the pre-existing commit-and-push step — a script-injection vector in a workflow withcontents: write. It predates this PR (present onmaster) and is unrelated, so I left it alone, but it's worth a follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01UFdgJXQKvrucXeKDami499