Sign release checksums with Ed25519 (#63) #64
Reference in New Issue
Block a user
Delete Branch "feat/63-signed-checksums"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #63.
What / why
Paid code signing is out of budget (macOS Developer ID ~$99/yr, Windows Authenticode ~$200-400/yr) and Sigstore keyless needs a Fulcio-trusted OIDC issuer, which self-hosted Gitea isn't. So this does the free half: publish
SHA256SUMSfor every release archive and sign it with a detached Ed25519 signature (SHA256SUMS.sig).This makes tampering detectable by anyone who checks. It does not remove Gatekeeper/SmartScreen warnings — the README and release notes say so explicitly, no overstatement.
Changes
scripts/sign_checksums.py(new) — dependency-light (cryptographyonly) helper:generate <dir> --out SHA256SUMS— hashes every file in a directory into asha256sum(1)-compatible manifest, sorted by filename.sign --sums ... --out ...— detached-signs the manifest. Signed message is domain-separated:b"bcc-release-v1|" + raw_bytes_of_SHA256SUMS, so a signature can't be replayed against an unrelated message under the same key. Key comes from$RELEASE_SIGNING_KEY(base64 raw 32-byte Ed25519 seed) or--key-b64.verify --sums ... --sig ... --pubkey-b64 ...— the same code path checks a signature (this satisfies the "give it a --verify mode" ask as a subcommand).tests/test_checksums.py(new) — 15 tests: hashing, manifest formatting/sorting/exclusion, sign→verify roundtrip, tamper detection, wrong-key rejection, domain-separation (a signature over the raw unprefixed bytes must NOT verify), bad key-length errors, and the CLI end-to-end via subprocess — including thatsignwith no key errors loudly and never writes a signature file..github/workflows/release.yml— Publish Release job now:release-files/.release-files/SHA256SUMS.secrets.RELEASE_SIGNING_KEYis set (via a step output, not a direct secret reference inif:).cryptography, signs →release-files/SHA256SUMS.sig.::warning::and continues — the release always publishes, it just ships without a.sigrather than failing.artifacts/**/*andrelease-files/SHA256SUMS*(the glob only picks up.sigwhen it exists, so a missing signature never breaks the release action).generate_release_notes, artifact actions stay on v3.README.md— new "Verifying your download" section: placeholder for the public key,sha256sum -c SHA256SUMS/Get-FileHashcommands, an explicit "what this does/doesn't prove" statement, the macOS right-click→Open workaround, and an optionalscripts/sign_checksums.py verifycommand for checking the signature itself.requirements-dev.txt/.github/workflows/ci.yml— addedcryptographyas a dev/test dependency (only place it's needed; the release job installs it separately, only when signing).Scope discipline
Deliberately touched no files from
bcc_core.py,tests/test_core.py,pyproject.toml, orbcc.spec— all new Python lives inscripts/sign_checksums.py+tests/test_checksums.pyto avoid colliding with concurrent work on those files on another branch.Verification
ruff check .— clean.ruff format --check .— clean.pytest -q— 212 passed, 1 skipped (pre-existing skip, unrelated), including the new 15 intest_checksums.py.Manual steps required before the next release tag
RELEASE_SIGNING_KEYrepo secret in Gitea (Settings → Actions → Secrets) — base64-encoding of the raw 32-byte Ed25519 seed. The key itself should be generated by the Catalog Console (#62), not by this PR or by hand — I did not generate or commit a key.README.md(## Verifying your downloadsection) with the corresponding base64 public key once the Console generates the pair.workflow_dispatchtest run ofrelease.ymlto confirm the signing step behaves as expected once the secret is set (it degrades gracefully — loud warning, no.sig— if you tag before doing this).