Files
triangles_v5/scripts/verify-reproducible-build.sh
T
Krystie 43dade4488 infra: reproducible build + signed release pipeline
Adds the infrastructure for verifiable Triangles releases:
- Reproducible builds (default-on): -ffile-prefix-map strips absolute
  source paths from binaries; SOURCE_DATE_EPOCH pinned to commit
  timestamp if env var not set. Two builds of the same commit with the
  same flags now produce byte-identical binaries.
- scripts/verify-reproducible-build.sh: builds the daemon twice into
  separate build dirs and compares SHA256. Pass/fail printed clearly.
- scripts/sign-release.sh: generates SHA256SUMS, writes detached .asc
  signatures over each release artifact and over SHA256SUMS itself.
  Supports --verify for independent third-party verification.
- release-process.md: canonical release pipeline documentation --
  reproducibility properties, signing-key setup, distribution
  requirements, failure-mode recovery, and the release checklist.
- scripts/README.md: updated to catalog the full scripts/ directory
  (was previously scoped only to bump-version.sh).

Verified end-to-end on this branch:
- scripts/verify-reproducible-build.sh: exit 0, both builds SHA256
  7a86d9659b7150f69dc53eb31cc4c7eb8df296b55fa889af5c5a1b310223c894.
- scripts/sign-release.sh: signs Release-built artifact, --verify
  returns exit 0 (all sigs + checksums valid).
- ctest: 4/4 suites still pass with the new compile flags.
- Tamper test: modifying an artifact after signing causes --verify
  to fail with '1 checksum(s) FAILED' (exit 1).

Existing signing key in the local keyring is used:
  523A81833EB7201573E1EFE1DCF2579968107984
  (Krystie Triangles Release <krystie-triangles-release@dns2.sami.tailnet>)

CI integration (separate PR): add a 'sign' job to build-all.yml that
imports GPG_PRIVATE_KEY from secrets and runs scripts/sign-release.sh
against the assembled release directory. Documented in release-process.md.
2026-07-07 01:43:17 -07:00

130 lines
6.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# verify-reproducible-build.sh
#
# Builds the Triangles daemon (trianglesd) twice from the same source tree
# into two separate build directories, then compares the resulting
# SHA256 hashes. Exits 0 if the two builds produce byte-identical binaries,
# non-zero otherwise.
#
# Usage:
# scripts/verify-reproducible-build.sh # default: trianglesd, Release
# BUILD_TYPE=Debug scripts/verify-reproducible-build.sh # override build type
# TARGET=triangles-qt scripts/verify-reproducible-build.sh # build Qt wallet instead
#
# What "reproducible" means here:
# Given identical source tree, identical compiler toolchain, identical
# build flags, identical SOURCE_DATE_EPOCH (if set) -- the resulting
# binary must hash identically across separate build directories.
#
# This script does NOT enforce compiler version pinning. Two different
# GCC versions will legitimately produce different binaries even with
# identical flags. The verification is "same source + same toolchain =
# same binary."
#
# Pass criteria:
# 1. Both builds succeed
# 2. Both binaries exist
# 3. SHA256 of the two binaries is equal
#
# On failure: prints the two SHA256s and the diff in size so a reviewer
# can investigate. Common causes of non-determinism:
# - __DATE__/__TIME__ embedded (we eliminate this in CMakeLists.txt)
# - absolute paths in __FILE__ (mitigated by -ffile-prefix-map)
# - uninitialized stack/heap contents (should not affect final binary)
# - linker adds random base addresses (PIE; deterministic if compiled
# with -fno-pie)
set -euo pipefail
# ── Config ─────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR="${SOURCE_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
BUILD_TYPE="${BUILD_TYPE:-Release}"
TARGET="${TARGET:-trianglesd}"
# Skip Qt by default -- it's slow and adds CI noise. Override with TARGET=triangles-qt
: "${BUILD_QT:=OFF}"
BUILD_DIR_A="${BUILD_DIR_A:-/tmp/triangles-repro-A}"
BUILD_DIR_B="${BUILD_DIR_B:-/tmp/triangles-repro-B}"
LOG_A="${LOG_A:-/tmp/triangles-repro-A.log}"
LOG_B="${LOG_B:-/tmp/triangles-repro-B.log}"
# ── Preflight ──────────────────────────────────────────────────────────────
command -v cmake >/dev/null || { echo "ERROR: cmake not found" >&2; exit 2; }
command -v ninja >/dev/null || { echo "ERROR: ninja not found (apt install ninja-build)" >&2; exit 2; }
command -v sha256sum >/dev/null || { echo "ERROR: sha256sum not found" >&2; exit 2; }
if [ ! -d "$SOURCE_DIR" ]; then
echo "ERROR: source dir not found: $SOURCE_DIR" >&2
exit 2
fi
# Refuse to run if the working tree is dirty -- dirty tree = non-deterministic
# git describe output = non-deterministic binary. Run on a clean checkout
# or a release tag.
if [ -n "$(cd "$SOURCE_DIR" && git status --porcelain 2>/dev/null)" ]; then
echo "WARNING: working tree has uncommitted changes." >&2
echo " build.h will include '-dirty' suffix and the binary will NOT be" >&2
echo " reproducible. Commit/stash your changes first, or accept that the" >&2
echo " hashes below prove your dirty-tree build is at least internally consistent." >&2
fi
# ── Helpers ────────────────────────────────────────────────────────────────
build_one() {
local dir="$1" log="$2"
rm -rf "$dir"
mkdir -p "$dir"
echo " configuring in $dir (BUILD_TYPE=$BUILD_TYPE BUILD_QT=$BUILD_QT)..." >&2
cmake -S "$SOURCE_DIR" -B "$dir" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DBUILD_QT="$BUILD_QT" \
> "$log" 2>&1 || { echo " configure failed; see $log" >&2; tail -30 "$log" >&2; exit 3; }
echo " building target $TARGET..." >&2
cmake --build "$dir" --target "$TARGET" -j "$(nproc)" \
>> "$log" 2>&1 || { echo " build failed; see $log" >&2; tail -30 "$log" >&2; exit 3; }
# ONLY stdout of the find goes to the caller. Progress logs above
# were redirected to stderr so they don't pollute the captured path.
find "$dir" -name "$TARGET" -type f -executable | head -1
}
# ── Build twice ────────────────────────────────────────────────────────────
echo "Building $TARGET ($BUILD_TYPE) twice from $SOURCE_DIR..."
echo ""
BIN_A="$(build_one "$BUILD_DIR_A" "$LOG_A")"
BIN_B="$(build_one "$BUILD_DIR_B" "$LOG_B")"
if [ -z "$BIN_A" ] || [ -z "$BIN_B" ]; then
echo "ERROR: could not find built binary" >&2
echo " A: '$BIN_A'" >&2
echo " B: '$BIN_B'" >&2
exit 4
fi
# ── Compare ────────────────────────────────────────────────────────────────
HASH_A="$(sha256sum "$BIN_A" | awk '{print $1}')"
HASH_B="$(sha256sum "$BIN_B" | awk '{print $1}')"
SIZE_A="$(stat -c%s "$BIN_A" 2>/dev/null || stat -f%z "$BIN_A")"
SIZE_B="$(stat -c%s "$BIN_B" 2>/dev/null || stat -f%z "$BIN_B")"
echo ""
echo "Binary A: $BIN_A"
echo " sha256: $HASH_A"
echo " size: $SIZE_A bytes"
echo "Binary B: $BIN_B"
echo " sha256: $HASH_B"
echo " size: $SIZE_B bytes"
echo ""
if [ "$HASH_A" = "$HASH_B" ]; then
echo "✓ REPRODUCIBLE: both builds produced identical SHA256"
exit 0
else
echo "✗ NOT REPRODUCIBLE: hashes differ"
echo ""
echo "Likely causes:"
echo " - __DATE__/__TIME__ embedded (check src/version.cpp)"
echo " - absolute build paths in __FILE__ (check CMakeLists.txt for -ffile-prefix-map)"
echo " - dirty git tree (commit/stash and rerun)"
echo " - PIE base randomization (compile with -fno-pie -no-pie for testing)"
echo " - non-deterministic linker output (linker version mismatch)"
exit 1
fi