Reproducible builds + SBOM
Every Nexo release ships with two artefacts that let an operator verify provenance and exact composition:
- CycloneDX SBOM (
sbom-cyclonedx.json) — every cargo dependency at the exact version + hash that was compiled into the binary. - SPDX SBOM (
sbom-spdx.json) — full filesystem scan viasyft, captures anything that wasn't a cargo dep (bundled binaries, generated assets, vendored data files).
Both SBOMs are Cosign-signed (*.bundle) using the same keyless
OIDC chain documented in Verifying releases.
Reading the SBOMs
# Pretty-print the CycloneDX dep tree:
jq '.components | map({name, version, purl})' sbom-cyclonedx.json | less
# Find a specific crate:
jq '.components[] | select(.name == "tokio")' sbom-cyclonedx.json
# Audit the cargo deps with `cargo-audit` (run against the SBOM,
# without rebuilding):
cargo audit --db ~/.cargo/advisory-db --json | \
jq -r '.vulnerabilities.list[].advisory.id'
Reproducible build claim
The release workflow targets bit-identical binary between two
runs given the same git sha + rust-toolchain.toml + Cargo.lock.
The pipeline pins:
- Rust toolchain:
rust-toolchain.tomlfixes the channel + components. - Dependency versions:
Cargo.lockis committed and--lockedis used by every release build. - Build environment: GitHub Actions
ubuntu-latestrunner +cargo build --releasewith noRUSTFLAGSoverrides. - Build provenance: SLSA Level 2 attestation generated by
actions/attest-build-provenance(Phase 27.2 wiring). - Cosign signature: each binary + SBOM signed via OIDC (Phase 27.3).
Reproducing a release locally
# 1. Check out the exact tag.
git clone https://github.com/lordmacu/nexo-rs && cd nexo-rs
git checkout v0.1.1
# 2. Build with the locked deps.
rustup show # confirms the toolchain matches rust-toolchain.toml
cargo build --release --bin nexo --locked
# 3. Compare your binary's sha256 against the release asset:
sha256sum target/release/nexo
# Expected: same hash listed in `sha256sums.txt` on the GitHub release.
If the hashes don't match: the build is not reproducible on your host. Common reasons:
- Different glibc version → embedded
__VERSIONED_SYMBOLstrings drift. The release workflow runs onubuntu-latest(currently Ubuntu 24.04, glibc 2.39); building on Debian 12 (glibc 2.36) produces different bytes. - Different LLVM in your local rustc build (rare, mostly affects Mac users compiling with stable + nightly side-by-side).
- Local
~/.cargo/config.tomlinjectingRUSTFLAGS. - Build PROFILE-DEV vs PROFILE-RELEASE.
For a guaranteed bit-identical reproduction, build inside the same container the workflow uses:
docker run --rm -v $(pwd):/src -w /src \
rust:1.80-bookworm \
cargo build --release --bin nexo --locked
This reproduces what the GitHub Actions runner would do — same glibc, same toolchain version, same LLVM.
SLSA verification
The workflow attaches an attestation.intoto.jsonl (SLSA Level 2
provenance) per release. Verify with slsa-verifier:
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
slsa-verifier verify-artifact nexo \
--provenance-path attestation.intoto.jsonl \
--source-uri github.com/lordmacu/nexo-rs \
--source-tag v0.1.1
A green verification proves:
- The artefact came from the
lordmacu/nexo-rsrepo - It was built by a GitHub-hosted runner (not a fork or local box)
- The build inputs match what's recorded in the provenance
Auditing for known CVEs
The SBOM lets cargo-audit work without rebuilding:
# Convert CycloneDX → cargo-audit's format:
cyclonedx-cli convert --input-format json \
--output-format json sbom-cyclonedx.json | \
jq '...' > deps.json
# Or just feed it to grype (broader scope, multi-format):
grype sbom:./sbom-cyclonedx.json
grype sbom:./sbom-spdx.json
Grype catches CVEs across both Rust crates and any system-level deps captured by syft.
Out of scope (deferred)
apk/pkgSBOM for the Termux deb — Termux's package metadata doesn't speak SPDX yet. The release SBOMs cover the same artifact contents though.- Reproducible Docker image layers — the current Dockerfile
uses
apt-get update && apt-get installwhich pulls whatever's latest at build time. Pinning to specific Debian package versions is a follow-up (Phase 34 hardening).