feat(repro): content-addressed .deb hashing; per-package verdict #36
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/canonical-deb-hash"
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?
repro-checkcould not pass for any release, for four independent reasons. #35 removed two (the unrecorded CI run number inUTS_VERSION, the missingPACKAGING_REVISION). This removes the other two.3. The comparison hashed a repack
Both sides ran
strip-signatures.sh, which ends indpkg-deb -b— and that recompressesdata.tarwith the local dpkg/liblzma defaults. The build side ran it inside the pinned container;repro-check.shran it on the bare host. So a verifier on any other distro got a different hash for every package — including the ~800-byte meta packages where the strip is a complete no-op — with nothing explaining why.strip-signatures.sh's own header says "we never recompress: xz/zstd output isn't byte-stable across tool versions". True for.ko; equally true fordata.tar.New
tools/canonical-deb-hash.pynever repacks. Pure stdlib: parse thearcontainer, decompresscontrol/datatar, canonicalize each member's content, digest a sorted description of the tree. Identical output on Debian, Arch or macOS — and it drops thesbattachanddiffoscopehost dependencies entirely. The PE certificate table is parsed directly, which also makes the transform symmetric on both sides rather than relying onsbattachbeing a perfect inverse ofsbsign(a concern I could not otherwise settle).Deliberately not in the digest:
ar/tar member order, compression, and member mtimes — all packaging metadata, and mtime clamping varies by dpkg version, which is exactly the false-positive class being removed.SOURCE_DATE_EPOCHis recorded in the manifest and fed back into the rebuild, so it is checked by other means. In the digest: path, type, permission bits, uid/gid, size, link target, sha256 of canonicalized content.4. The verdict was one boolean, so "cannot check" read as "failed"
kbuild links the signing certificate's DER into
.builtin_trusted_keys, so a rebuild with a different key produces a differentvmlinuz— and since a different-length cert shifts what follows, a differentSystem.mapand-dbgpayload. Handing a verifier the published.dercannot fix this:sign-fileneeds the private half, so a cert+foreign-key PEM aborts onX509_check_private_key. That's inherent, so localize it rather than pretend otherwise.Two digests per package.
key_independent_sha256excludesboot/vmlinuz-*,boot/System.map-*,usr/lib/debug/**;canonical_sha256covers everything. A third party gets a hard pass/fail on the first — which still covers all ~5,000 modules,/boot/config-*and themodules.*indexes — and an operator withREPRO_SIGNING_KEY_PATHgets the second. The verdict is now a per-package table (OK exact/OK*/FAIL/SKIP) with a summary, and it refuses to report success if nothing was comparable.Removed: a fetch block that never ran
The released-
.debfetch was gated on a manifest fieldcanonical_paththatbuild-manifest.pynever wrote — so$work/released/was always empty, andrepro-check.yml's "upload diff report on failure" always uploaded nothing (the exact "no files found" warning its comment was written to avoid). A comment now records that comparing against published bytes is a genuinely stronger check that is still not attempted, so nobody reads its absence as coverage.Verification
The money test — three
.debs with identical logical content but different compression, compressor, member order, and mtimes:a.deb(xz -9)e05e9ca6…ff12e935…ff12e935…b.deb(xz -1, reversed order)a1051752…ff12e935…ff12e935…c.deb(gzip, different mtimes)759f162c…ff12e935…ff12e935…Raw hashes all differ; canonical hashes all match. That proves the strip-environment defect fixed without needing two distros.
Negative controls — the digest must still be sensitive to real differences:
0644→0600strip_module_signaturerecovers the body exactly; unsigned module untouched; a signed and unsigned.ko.xzhash the same ✓The key-independent split, with two packages built as if by different keys:
key_independentmatches,canonicaldiffers ✓key_independent✓/boot/config-*key_independent✓Integration: the 3-column TSV flows through
build-manifest.pyproducingschema_version: 3with correctrepro_classper package; the legacy 2-column form still parses.dep-list-paritystill passes — the review flagged that job's extraction anchors onrepro-check.sh's apt block and would fold any new apt block into the extracted set; the pure-Python approach adds no packages, and the job reports18 pkgs; repro-check omits: curl jq nodejs sbsigntoolas before.shellcheck,py_compile,yamllint -d relaxedall clean.Docs corrected where they overclaimed
verifying-packages.mdsaid exit 0 "confirmed the published binaries correspond to the public source". It does not — nothing compares published bytes. It now states what exit 0 does and does not establish, and names the gap explicitly rather than leaving a stronger claim standing.building-from-source.mddescribed the dead released-.debfetch, and claimed source tarballs are attached as release assets (they are not — only.debs,final.config,manifest.jsonand the.der). Replaced with what the manifest actually provides for the GPLv2 §3 obligation.🤖 Generated with Claude Code