feat: sign the release checksum list

SHA256SUMS.txt sat beside the artifacts it vouches for, so whoever could
rewrite a release rewrote the list with it. The packs were already
reproducible, which answers corruption and lets a third party rebuild an
archive byte for byte; nothing answered a rewritten release.

The list is now signed with an ed25519 key kept for this alone, and the
public half is allowed_signers at the repository root, so verification
does not go through the release page: ssh-keygen -Y verify against the
committed file, then sha256sum --check. Rehearsed on all three outcomes:
a good signature, a tampered pack caught by the sums, a rewritten list
caught by the signature.

The release steps sign and upload the signature, the README points a
downloader at the procedure, and the reproducibility section says what
each half proves. Rotation keeps retired lines so past releases stay
verifiable. Three tests hold the trust root, the signing step and the
documented principal in agreement.
This commit is contained in:
Abdessamad Derraz committed 2026-09-04 14:01:05 +02:00
1 parent 5587c25675
commit 1a96853aee
7 files changed
+104 -3

No files matched your search

+46
View File
@@ -69,6 +69,52 @@ class ReadmeRegressions(unittest.TestCase):
self.assertEqual(unknown, set(), f"README advertises unknown flags: {unknown}")
class ReleaseSigningRegressions(unittest.TestCase):
"""A checksum list published beside its own artifacts proves nothing.
SHA256SUMS.txt answers corruption; whoever can rewrite a release rewrites
the list with it. The signature is what a third party checks, and it is
only checkable while allowed_signers, the signing step and the upload
list stay in agreement.
"""
PRINCIPAL = "releases@retrobios"
def test_allowed_signers_names_one_usable_key(self):
path = ROOT / "allowed_signers"
self.assertTrue(path.exists(), "allowed_signers is the published trust root")
lines = [
line for line in path.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.startswith("#")
]
self.assertTrue(lines, "allowed_signers carries no key")
for line in lines:
principal, keytype, blob = line.split()[:3]
self.assertEqual(principal, self.PRINCIPAL)
self.assertTrue(keytype.startswith(("ssh-", "sk-", "ecdsa-")), keytype)
self.assertTrue(len(blob) > 40, "key material looks truncated")
def test_the_release_signs_the_list_and_ships_the_signature(self):
process = (ROOT / "wiki" / "release-process.md").read_text(encoding="utf-8")
self.assertIn(
"ssh-keygen -Y sign", process, "the release no longer signs the list"
)
upload = next(
line for line in process.splitlines()
if line.startswith("for f in dist/SHA256SUMS.txt")
)
self.assertIn(
"dist/SHA256SUMS.txt.sig", upload,
"the signature is produced but never uploaded",
)
def test_the_documented_verification_matches_the_published_principal(self):
process = (ROOT / "wiki" / "release-process.md").read_text(encoding="utf-8")
self.assertIn("ssh-keygen -Y verify", process)
self.assertIn(f"-I {self.PRINCIPAL}", process)
self.assertIn("allowed_signers", process)
class WorkflowRegressions(unittest.TestCase):
"""The test suite must sit on every road into main.