Run #4281 cleared every layer above the ISO9660 wrapper: SHA256 (squashfs payload) caed117ca72c6c1d9204c49dd749d5f7b372f3a19cac1b2a7e66bee452a8d501 /tmp/.../a.squashfs caed117ca72c6c1d9204c49dd749d5f7b372f3a19cac1b2a7e66bee452a8d501 /tmp/.../b.squashfs …squashfs is now byte-identical, ISO TOC is identical, file listing diff is empty, but ISO SHA still differs. The remaining drift is in the ISO9660 metadata region between the system area (first 32 KiB) and the file payload start. Two complementary changes: 1. xorriso post-process now sets *every* date field xorriso writes, not just the obvious two: -alter_date_r all — atime + mtime + btime on all nodes, not just mtime. ISO9660 directory records carry creation+modification timestamps. -volume_date c m x f u s — every volume-descriptor date: c=creation m=modification x=expiration f=effective u=system area s=path table Default for any unset volume_date is "now", which is what was leaking through despite us setting c+m. 2. diagnose-divergence.sh now does whole-file cmp -l (capped at 200 lines so 1 GiB of all-different doesn't drown the report) and on any divergence, dumps a 128-byte xxd window from each ISO around the first differing byte plus a unified diff between the two windows. This tells us in the next failure log "first byte differs at offset N (LBA M), bytes around it look like X" — pinpoints the ISO9660 region without needing artifact download. Workflow tail-into-log step wired up the two new files (iso-cmp-first-200.txt, iso-around-first-diff.diff). If iter34 still fails the gate, the new diagnostic tells us exactly which structure (volume descriptor, path table, directory record, boot catalog…) is still drifting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SilverMetal Linux — reproducible ISO build pipeline
Milestone: Phase 1 / M1.1 — Kicksecure fork builds reproducibly. Exit criterion: two clean builds of the same commit produce a byte-identical SHA256.
This directory holds everything that turns a SilverMetal commit into a SilverMetal Linux ISO. M1.1 ships only the base (un-hardened) Kicksecure derivative. Hardening overlay, kernel swap, AppArmor profiles, etc. land in M1.2+ and must not be added here in the M1.1 PR.
Layout
linux/build/
├── README.md (this file)
├── derivative-maker/ git submodule -> Kicksecure/derivative-maker
├── config/
│ ├── silvermetal-base.conf derivative selection + branding
│ ├── snapshot-pin.env pinned snapshot.debian.org timestamp
│ └── source-date-epoch.env optional SOURCE_DATE_EPOCH override
├── docker/
│ └── Dockerfile.builder pinned debian:bookworm-slim builder image
└── scripts/
├── build.sh wrapper: container run -> derivative-maker
├── verify-reproducibility.sh build twice, compare SHA256
└── diagnose-divergence.sh diffoscope on mismatch
How reproducibility is achieved
The same levers any deterministic Debian build relies on, stacked together:
| Lever | Where it lives |
|---|---|
Pinned snapshot.debian.org mirror |
config/snapshot-pin.env |
SOURCE_DATE_EPOCH from commit time |
scripts/build.sh (auto) |
| Pinned builder image (by digest) | docker/Dockerfile.builder + BUILDER_IMAGE |
Deterministic mksquashfs flags |
MKSQUASHFS_OPTIONS in base conf |
| Pinned upstream toolchain | derivative-maker/ submodule |
LC_ALL=C.UTF-8, TZ=UTC |
scripts/build.sh |
diffoscope is the diagnostic tool used by diagnose-divergence.sh; the gate itself is plain sha256sum.
Reproduce a release locally
Procedure mirrors
docs/trust-model.md§ Reproducible builds.
Prerequisites: a Linux host (or WSL2) with Docker, ~30 GB free disk, ~8 GB RAM.
# 1. Clone the repo at the release tag.
git clone --recurse-submodules https://git.silverlabs.uk/SilverLABS/SilverMetal.git
cd SilverMetal
git checkout v1.1.0 # whichever release you want to verify
# 2. Build twice and compare. ~60-90 minutes per build.
linux/build/scripts/verify-reproducibility.sh
# 3. Compare against the published release.
sha256sum -c <(curl -fsSL https://git.silverlabs.uk/SilverLABS/SilverMetal/releases/download/v1.1.0/SHA256SUMS)
Mismatch with the published artefact = supply-chain anomaly. Report channel: security@silverlabs.uk.
Build once (no reproducibility check)
linux/build/scripts/build.sh
# Output lands in linux/build/output/<short-sha>/
The wrapper requires BUILDER_IMAGE to be pinned by digest. Local dev that hasn't built and pushed an image yet should override:
BUILDER_IMAGE=docker-registry:5000/silvermetal-builder@sha256:<digest> \
linux/build/scripts/build.sh
Gitea Actions
The CI workflow (.gitea/workflows/build-iso-linux.yaml) is the authority for "did this commit build reproducibly?". It:
- Checks out the commit with submodules.
- Runs
build.shtwice in${GITHUB_WORKSPACE}/build-{a,b}. - Fails the run if the two ISO SHA256s differ, and uploads a
diffoscopereport as an artefact. - On a tag push, attaches the verified ISO +
SHA256SUMS+BUILD_INFOto a Gitea release.
Self-hosted runner setup
The workflow runs on runs-on: silvermetal-builder, a self-hosted, privileged-capable Gitea Actions runner. Create it before merging the workflow:
- Provision a Debian 12 VM on the cluster with ≥ 8 vCPU, ≥ 16 GB RAM, ≥ 100 GB disk.
- Install Docker (
apt install docker.io); ensure the runner user can rundocker run --privileged. - Register
act_runneragainstgit.silverlabs.ukwith labelsilvermetal-builder. - Pre-pull the builder image so the first reproducibility run isn't a cold start:
docker pull docker-registry:5000/silvermetal-builder:latest - Cache the apt snapshot in a Docker volume to avoid throttling:
docker volume create silvermetal-apt-cache
The runner host name must not leak into ISO content. LC_ALL=C.UTF-8 and a constant TZ in the wrapper guard against that, but spot-check with diagnose-divergence.sh.
Bumping pinned inputs
Each of these is a deliberate, reviewed action — never automate:
derivative-makersubmodule — bump in its own PR, with a verification log showing two clean builds match.snapshot-pin.env— same procedure.- Builder image (
Dockerfile.builder) — edit and commit. CI'sbuilder-imagejob rebuilds, pushes, and feeds the new digest tobuild-and-verifyautomatically; no manualdocker build/docker pushstep. The hardcodedBUILDER_IMAGEdigest fallback inbuild.shis for local/offline rebuilds only — bump it opportunistically after any merged Dockerfile change so non-CIbuild.shkeeps working at that commit.
What this milestone is not
- No hardening overlay (M1.2)
- No SilverBrowser/SilverVPN/SilverSync/SilverChat integration (M1.6–1.9)
- No installer branding (M1.5)
- No update server (M1.10)
- No SBOM publication (M1.11)
- No signing ceremony / MOK / Secure Boot wiring (separate milestone)
If a change to this directory expands its scope into one of those, push back — the M1.1 gate is intentionally narrow.