Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1e92d685f | |||
| 43dade4488 |
@@ -37,6 +37,41 @@ if(ENABLE_UNITY_BUILD)
|
|||||||
set(CMAKE_UNITY_BUILD_BATCH_SIZE 8)
|
set(CMAKE_UNITY_BUILD_BATCH_SIZE 8)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# ── Reproducible-build support ─────────────────────────────────────────────
|
||||||
|
# REPRODUCIBLE_BUILD=ON strips absolute source paths from the final binary
|
||||||
|
# via -ffile-prefix-map. Two builds of the same commit with the same
|
||||||
|
# toolchain then produce byte-identical binaries (modulo any source paths
|
||||||
|
# that aren't routed through the macro — see scripts/verify-reproducible-build.sh
|
||||||
|
# for the full verification protocol).
|
||||||
|
#
|
||||||
|
# Default ON: this is a security property we want by default. Disable if
|
||||||
|
# you need stack traces with absolute paths (e.g. debugging a post-mortem).
|
||||||
|
option(REPRODUCIBLE_BUILD "Strip absolute source paths from binaries for reproducibility" ON)
|
||||||
|
if(REPRODUCIBLE_BUILD)
|
||||||
|
add_compile_options(
|
||||||
|
"-ffile-prefix-map=${CMAKE_SOURCE_DIR}=."
|
||||||
|
"-ffile-prefix-map=${CMAKE_BINARY_DIR}=."
|
||||||
|
)
|
||||||
|
# SOURCE_DATE_EPOCH is the canonical reproducible-build env var
|
||||||
|
# (https://reproducible-builds.org/docs/source-date-epoch/). If the
|
||||||
|
# user hasn't set it explicitly, fall back to the commit timestamp from
|
||||||
|
# git. This means binaries built without SOURCE_DATE_EPOCH still embed
|
||||||
|
# a deterministic timestamp (the commit time, not wall-clock).
|
||||||
|
if(NOT DEFINED ENV{SOURCE_DATE_EPOCH})
|
||||||
|
execute_process(
|
||||||
|
COMMAND git log -n 1 --format=%ct
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
OUTPUT_VARIABLE SOURCE_DATE_EPOCH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
)
|
||||||
|
if(NOT SOURCE_DATE_EPOCH)
|
||||||
|
set(SOURCE_DATE_EPOCH "1700000000") # 2023-11-14 fallback
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
message(STATUS "Reproducible build: ON (SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH})")
|
||||||
|
endif()
|
||||||
|
|
||||||
# ── Output directories ──
|
# ── Output directories ──
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# Triangles Release Process
|
||||||
|
|
||||||
|
> Canonical release pipeline for `SamiAhmed7777/triangles_v5`. This document
|
||||||
|
> is the source of truth for *how* a release is cut. The implementation lives
|
||||||
|
> in `scripts/verify-reproducible-build.sh` and `scripts/sign-release.sh`.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. **Reproducible** — any two builders with the same source tree, same
|
||||||
|
toolchain, and same flags produce byte-identical binaries.
|
||||||
|
2. **Signed** — every release artifact has a detached PGP signature that
|
||||||
|
verifiers can check against a known public key.
|
||||||
|
3. **Verifiable end-to-end** — a third party can confirm a release is
|
||||||
|
legitimate using only `gpg` and `sha256sum`, both installed by default
|
||||||
|
on every Linux distribution.
|
||||||
|
|
||||||
|
## Pipeline overview
|
||||||
|
|
||||||
|
```
|
||||||
|
source tag (e.g. v6.1.4)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ CI builds all 4 │ .github/workflows/build-all.yml
|
||||||
|
│ targets on each │ (ubuntu / windows / macos)
|
||||||
|
│ platform │
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│ produces: daemon.tar.gz, qt.tar.gz, .deb, .dmg, .exe, ...
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Local maintainer │ scripts/sign-release.sh <release-dir>
|
||||||
|
│ signs artifacts │ (uses release signing key in local keyring)
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│ produces: SHA256SUMS, *.asc detached signatures
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Push to GitHub │ .github/workflows/distribute.yml
|
||||||
|
│ release + Docker │ (uploads artifacts, builds Docker image,
|
||||||
|
│ + Homebrew tap + │ updates Homebrew formula, submits
|
||||||
|
│ WinGet + Snap │ WinGet + Snap PRs)
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Verifier │ scripts/sign-release.sh --verify <dir>
|
||||||
|
│ independently │ + gpg --import <release-pubkey>
|
||||||
|
│ confirms │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reproducibility — how it works today
|
||||||
|
|
||||||
|
The Triangles build is already reproducible for Release builds with the
|
||||||
|
following properties:
|
||||||
|
|
||||||
|
| Property | Implementation |
|
||||||
|
|---|---|
|
||||||
|
| `BUILD_DESC` | Git describe output, written to `build.h` at build time |
|
||||||
|
| `BUILD_DATE` | **Commit timestamp** (NOT wall-clock), from `git log -n 1 --format=%ci` |
|
||||||
|
| `__DATE__`/`__TIME__` fallback | Dead code in practice — `build.h` always defines `BUILD_DATE` |
|
||||||
|
| Build paths in binaries | Mapped with `-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.` so absolute source paths do not leak into debug info |
|
||||||
|
|
||||||
|
### Verifying reproducibility
|
||||||
|
|
||||||
|
Run on a clean checkout:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/verify-reproducible-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds `trianglesd` twice into two separate build directories and
|
||||||
|
compares SHA256 hashes. Exits 0 on success.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `BUILD_TYPE=Debug scripts/verify-reproducible-build.sh`
|
||||||
|
- `TARGET=triangles-qt scripts/verify-reproducible-build.sh`
|
||||||
|
- `BUILD_DIR_A=/tmp/A BUILD_DIR_B=/tmp/B scripts/verify-reproducible-build.sh`
|
||||||
|
|
||||||
|
## Signing — how it works
|
||||||
|
|
||||||
|
### Generate (or import) a release signing key
|
||||||
|
|
||||||
|
**One-time setup** (the maintainer's machine):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a fresh Ed25519 signing subkey under your existing PGP master.
|
||||||
|
# Ed25519 is preferred over RSA-4096: smaller signatures, faster, quantum-resistant
|
||||||
|
# at the security level we need for code-signing.
|
||||||
|
gpg --quick-generate-key 'Sami Ahmed <sami@cryptographic-triangles.org>' ed25519 sign never
|
||||||
|
|
||||||
|
# Print the public key block to publish on the website / GitHub.
|
||||||
|
gpg --armor --export 'sami@cryptographic-triangles.org' > release-pubkey.asc
|
||||||
|
|
||||||
|
# Export your secret key BACKUP. Store this on airgapped / offline media.
|
||||||
|
# Without this backup, lost local keyring = lost ability to sign new releases.
|
||||||
|
gpg --export-secret-keys 'sami@cryptographic-triangles.org' > release-seckey-BACKUP.asc
|
||||||
|
chmod 600 release-seckey-BACKUP.asc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import an existing key** (e.g. on a new maintainer machine):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --import release-seckey-BACKUP.asc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sign a release directory
|
||||||
|
|
||||||
|
After CI has produced the artifacts in a known directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/sign-release.sh /path/to/release-dir
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Generate `SHA256SUMS` for every release artifact (.tar.gz, .deb, .dmg,
|
||||||
|
.exe, .zip, .AppImage)
|
||||||
|
2. Write a detached PGP signature (`<artifact>.asc`) for each artifact
|
||||||
|
3. Write a detached PGP signature over `SHA256SUMS` itself
|
||||||
|
4. Refuse to run if the signing key isn't in the local keyring (safety)
|
||||||
|
|
||||||
|
### Verify a release
|
||||||
|
|
||||||
|
A third party (user, exchange, package maintainer) verifies with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Import the public key (one-time).
|
||||||
|
gpg --import release-pubkey.asc
|
||||||
|
|
||||||
|
# 2. Verify everything in the release directory.
|
||||||
|
scripts/sign-release.sh --verify /path/to/release-dir
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks:
|
||||||
|
- `SHA256SUMS.asc` against `SHA256SUMS` (the master signature)
|
||||||
|
- Each `<artifact>.asc` against its `<artifact>` (belt-and-suspenders)
|
||||||
|
- Each artifact's SHA256 against `SHA256SUMS` (integrity)
|
||||||
|
|
||||||
|
## Why both per-artifact signatures AND a SHA256SUMS signature?
|
||||||
|
|
||||||
|
- **SHA256SUMS + signature**: small, fast to verify, single point of trust.
|
||||||
|
If the SHA256SUMS.asc checks out and a file's SHA256 matches an entry,
|
||||||
|
you're done — you trust that entry.
|
||||||
|
- **Per-artifact signatures**: defense against a hypothetical attack where
|
||||||
|
someone modifies `SHA256SUMS` but not the artifacts (or vice versa).
|
||||||
|
Two independent signature chains.
|
||||||
|
|
||||||
|
For most verifiers, checking `SHA256SUMS.asc` + `sha256sum -c SHA256SUMS`
|
||||||
|
is sufficient. The per-artifact .asc files are insurance.
|
||||||
|
|
||||||
|
## CI integration
|
||||||
|
|
||||||
|
`.github/workflows/build-all.yml` already produces the artifacts. The
|
||||||
|
remaining work (separate PR) is to add a "sign" job that runs
|
||||||
|
`scripts/sign-release.sh` against the assembled release directory using a
|
||||||
|
key stored as a GitHub Actions secret.
|
||||||
|
|
||||||
|
**Required secrets (one-time setup in repo Settings → Secrets):**
|
||||||
|
- `GPG_PRIVATE_KEY` — base64-encoded `release-seckey-BACKUP.asc`
|
||||||
|
(see [GitHub docs on encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets))
|
||||||
|
- `GPG_PASSPHRASE` — passphrase for the signing key (if any)
|
||||||
|
- `GITHUB_TOKEN` — already provided by Actions
|
||||||
|
|
||||||
|
**Suggested job sketch** (in `.github/workflows/build-all.yml` after all
|
||||||
|
build jobs complete):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sign:
|
||||||
|
name: Sign release artifacts
|
||||||
|
needs: [build-linux-daemon, build-linux-qt, build-windows-daemon, build-windows-qt, build-macos]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Import signing key
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 -d | gpg --import
|
||||||
|
|
||||||
|
- name: Sign artifacts
|
||||||
|
run: scripts/sign-release.sh release-artifacts/
|
||||||
|
env:
|
||||||
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public key distribution
|
||||||
|
|
||||||
|
The release public key MUST be published in **at least three independent
|
||||||
|
places** so a keyserver takedown or DNS hijack cannot prevent verification:
|
||||||
|
|
||||||
|
1. **This repository** — `release-pubkey.asc` at the repo root, committed
|
||||||
|
on every release tag.
|
||||||
|
2. **The website** — `https://cryptographic-triangles.org/release-pubkey.asc`
|
||||||
|
3. **Public keyservers** — submit to `keys.openpgp.org`, `keyserver.ubuntu.com`,
|
||||||
|
`pgp.mit.edu`. Each is independently operated.
|
||||||
|
|
||||||
|
Distribution list refreshed with every key rotation (rare; treat as
|
||||||
|
multi-year commitment).
|
||||||
|
|
||||||
|
## Failure modes & recovery
|
||||||
|
|
||||||
|
| Scenario | Recovery |
|
||||||
|
|---|---|
|
||||||
|
| Signing key compromised | Revoke via pre-published revocation certificate. Re-cut release. Document incident. |
|
||||||
|
| Signing key lost (no backup) | Cannot sign new releases. Existing artifacts still verify against the published public key. Treat as catastrophic; re-mint a new key and treat the chain as fork-vulnerable until community updates. |
|
||||||
|
| Public key not yet distributed | User gets `gpg: Can't check signature: No public key`. Provide clear "first verify the key fingerprint out-of-band" instructions on the website. |
|
||||||
|
| CI secret leaked | Rotate the signing key immediately; treat all artifacts signed with the old key as suspect. |
|
||||||
|
| `SHA256SUMS` signed but artifacts don't match | `sha256sum -c` fails. Either an artifact was corrupted in transit, or someone tampered. Re-download from GitHub and re-verify. |
|
||||||
|
|
||||||
|
## Checklist for cutting a release
|
||||||
|
|
||||||
|
- [ ] Source tree is clean (no uncommitted changes)
|
||||||
|
- [ ] `scripts/verify-reproducible-build.sh` passes (builds are reproducible)
|
||||||
|
- [ ] All CI jobs on the release tag are green
|
||||||
|
- [ ] Release artifacts are in a single directory (`release-artifacts/`)
|
||||||
|
- [ ] `scripts/sign-release.sh release-artifacts/` runs without error
|
||||||
|
- [ ] `scripts/sign-release.sh --verify release-artifacts/` passes
|
||||||
|
- [ ] `release-pubkey.asc` is current and committed to the repo
|
||||||
|
- [ ] GitHub release created with all artifacts + SHA256SUMS + SHA256SUMS.asc
|
||||||
|
- [ ] `distribute.yml` workflow ran (Docker Hub, Homebrew, WinGet, Snap)
|
||||||
|
- [ ] Announcement posted (Twitter/Mastodon, Discord/Telegram, mailing list if any)
|
||||||
|
|
||||||
|
## Future work
|
||||||
|
|
||||||
|
- **Reproducibility hardening**: add `-ffile-prefix-map` to compile flags so
|
||||||
|
absolute source paths don't leak into the binary (would also fix the
|
||||||
|
simd.c:265 UBSan build-id drift).
|
||||||
|
- **Gitian-style deterministic builds**: containerized build environment
|
||||||
|
pinned to a specific GCC/binutils version, so multiple independent
|
||||||
|
verifiers can rebuild from source and get identical hashes.
|
||||||
|
- **Transparency log**: publish each release artifact hash to a Sigstore /
|
||||||
|
sigsum / Certificate Transparency-style log so any tampering is publicly
|
||||||
|
auditable.
|
||||||
|
- **Key rotation policy**: document how/when the signing key gets rotated
|
||||||
|
(probably never, but state the policy).
|
||||||
+29
-22
@@ -1,29 +1,36 @@
|
|||||||
# Version Bump Script
|
# Scripts
|
||||||
|
|
||||||
Updates the version number across all files in the repo from a single command.
|
Operational scripts for the Triangles project. See also `doc/release-process.md`
|
||||||
|
for the canonical release pipeline documentation.
|
||||||
|
|
||||||
## Usage
|
## Build verification
|
||||||
|
|
||||||
**Set a specific version:**
|
- **`verify-reproducible-build.sh`** — builds the daemon (or another target)
|
||||||
```bash
|
twice from the same source tree and verifies the SHA256 hashes match.
|
||||||
bash scripts/bump-version.sh 5.7.0
|
Catches accidental introduction of non-determinism (e.g. `__DATE__`/`__TIME__`
|
||||||
```
|
regressions, dirty git state, PIE base-address drift).
|
||||||
|
|
||||||
**Or edit `src/clientversion.h` first, then sync everything else:**
|
## Release signing
|
||||||
```bash
|
|
||||||
bash scripts/bump-version.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## What it updates
|
- **`sign-release.sh`** — generates `SHA256SUMS`, writes detached PGP
|
||||||
|
signatures (`.asc`) over each release artifact and over `SHA256SUMS`.
|
||||||
|
Supports `--verify` for independent third-party verification.
|
||||||
|
Uses `TRIANGLES_RELEASE_KEY` env var (defaults to
|
||||||
|
`sami@cryptographic-triangles.org`).
|
||||||
|
|
||||||
- `src/clientversion.h` (source of truth)
|
## Existing infrastructure
|
||||||
- `src/version.h`
|
|
||||||
- `triangles-qt.pro`
|
|
||||||
- `Dockerfile`
|
|
||||||
- All packaging manifests (Docker, Snap, Scoop, WinGet, RPM, Flatpak, Debian, AppImage)
|
|
||||||
|
|
||||||
## What still needs manual review after running
|
- **`bump-version.sh`** — sync version numbers across all manifests from
|
||||||
|
`src/clientversion.h`.
|
||||||
- `packaging/appstream/...metainfo.xml` — add a new `<release>` entry
|
- **`sign-snapshot.sh`** — sign a UTXO snapshot file with the wallet's
|
||||||
- `README.md` — update header version if desired
|
signing address (not a PGP key; this is a chain-level signature, not a
|
||||||
- Any documentation with download URLs
|
release signature).
|
||||||
|
- **`validate_onion_seeds.py`** — validate every `.onion` address in
|
||||||
|
`triangles.conf` against the v3 hidden-service checksum.
|
||||||
|
- **`ibd-smoke-test.sh`** — fresh-datadir IBD smoke test for catching the
|
||||||
|
classic "stalls early / loops around 570" failure mode.
|
||||||
|
- **`ci/build-rocksdb.sh`** — build and install a pinned RocksDB version
|
||||||
|
for CI.
|
||||||
|
- **`ci/package-linux-daemon.sh`** — Linux packaging step (.deb).
|
||||||
|
- **`ci/package-windows-daemon.sh`** — Windows packaging step.
|
||||||
|
- **`tri/`** — operator-facing CLI for node administration.
|
||||||
|
|||||||
Executable
+222
@@ -0,0 +1,222 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# sign-release.sh
|
||||||
|
#
|
||||||
|
# Sign Triangles release artifacts (the binaries/.debs/.dmgs/.exes built
|
||||||
|
# by the GitHub Actions release pipeline) with a long-term PGP key, and
|
||||||
|
# write SHA256SUMS + detached .asc signatures alongside each artifact.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/sign-release.sh /path/to/release-dir
|
||||||
|
# scripts/sign-release.sh /path/to/release-dir --key 0xDEADBEEF
|
||||||
|
# scripts/sign-release.sh --verify /path/to/release-dir
|
||||||
|
#
|
||||||
|
# Inputs (in the release directory):
|
||||||
|
# - *.tar.gz, *.deb, *.dmg, *.exe, *.zip, *.AppImage (any release artifact)
|
||||||
|
# - SHA256SUMS file (if present, re-signed; if absent, generated)
|
||||||
|
#
|
||||||
|
# Outputs (written next to each artifact):
|
||||||
|
# - <artifact>.asc - detached PGP signature (binary or clearsigned)
|
||||||
|
# - SHA256SUMS - canonical checksum list (overwrites any existing)
|
||||||
|
# - SHA256SUMS.asc - detached PGP signature over SHA256SUMS
|
||||||
|
#
|
||||||
|
# Verification mode (--verify):
|
||||||
|
# For each *.asc, runs `gpg --verify` against the artifact.
|
||||||
|
# Then runs `sha256sum -c SHA256SUMS` if present.
|
||||||
|
# Exits 0 if all artifacts verify; non-zero on any failure.
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - gpg2 or gpg on PATH
|
||||||
|
# - Signing key already in the local keyring (or use --key to select)
|
||||||
|
# - For verification: the signer's public key must be importable
|
||||||
|
# (either already in the keyring, or fetched from a keyserver)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEFAULT_KEY="${TRIANGLES_RELEASE_KEY:-sami@cryptographic-triangles.org}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '2,30p' "$0"
|
||||||
|
exit "${1:-1}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Parse args ─────────────────────────────────────────────────────────────
|
||||||
|
MODE="sign"
|
||||||
|
RELEASE_DIR=""
|
||||||
|
SIGN_KEY="$DEFAULT_KEY"
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--verify)
|
||||||
|
MODE="verify"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--key)
|
||||||
|
SIGN_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
RELEASE_DIR="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_DIR" ]; then
|
||||||
|
echo "ERROR: release directory required" >&2
|
||||||
|
usage 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$RELEASE_DIR" ]; then
|
||||||
|
echo "ERROR: not a directory: $RELEASE_DIR" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$RELEASE_DIR"
|
||||||
|
|
||||||
|
# ── Sign mode ──────────────────────────────────────────────────────────────
|
||||||
|
if [ "$MODE" = "sign" ]; then
|
||||||
|
command -v gpg >/dev/null || { echo "ERROR: gpg not found" >&2; exit 3; }
|
||||||
|
|
||||||
|
# Verify the signing key actually exists in the keyring (don't want to
|
||||||
|
# silently create a new key with the same email).
|
||||||
|
if ! gpg --list-secret-keys "$SIGN_KEY" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: signing key '$SIGN_KEY' not found in local keyring" >&2
|
||||||
|
echo " import it first: gpg --import <keyfile>" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Signing artifacts in $RELEASE_DIR with key $SIGN_KEY..."
|
||||||
|
|
||||||
|
# Generate (or regenerate) SHA256SUMS for every release artifact in the dir.
|
||||||
|
# Recognized extensions: .tar.gz, .deb, .dmg, .exe, .zip, .AppImage, .dmg.blockmap
|
||||||
|
# Excludes: .asc files, SHA256SUMS itself, README/notes text files.
|
||||||
|
ARTIFACTS=()
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
case "$f" in
|
||||||
|
*.asc|SHA256SUMS|SHA256SUMS.asc|*.txt|*.md) continue ;;
|
||||||
|
esac
|
||||||
|
ARTIFACTS+=("$f")
|
||||||
|
done < <(find . -maxdepth 1 -type f -print0 | sort -z)
|
||||||
|
|
||||||
|
if [ ${#ARTIFACTS[@]} -eq 0 ]; then
|
||||||
|
echo "ERROR: no release artifacts found in $RELEASE_DIR" >&2
|
||||||
|
echo " expected: .tar.gz, .deb, .dmg, .exe, .zip, .AppImage" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Found ${#ARTIFACTS[@]} artifact(s):"
|
||||||
|
for a in "${ARTIFACTS[@]}"; do echo " - $a"; done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Regenerate SHA256SUMS from scratch (deterministic sort).
|
||||||
|
: > SHA256SUMS
|
||||||
|
for a in "${ARTIFACTS[@]}"; do
|
||||||
|
sha256sum "$a" >> SHA256SUMS
|
||||||
|
done
|
||||||
|
echo "✓ Wrote SHA256SUMS"
|
||||||
|
|
||||||
|
# Detached signature over each artifact.
|
||||||
|
for a in "${ARTIFACTS[@]}"; do
|
||||||
|
rm -f "${a}.asc"
|
||||||
|
if gpg --batch --yes \
|
||||||
|
--local-user "$SIGN_KEY" \
|
||||||
|
--armor --detach-sign \
|
||||||
|
--output "${a}.asc" \
|
||||||
|
"$a" 2>/dev/null; then
|
||||||
|
echo "✓ Signed ${a}"
|
||||||
|
else
|
||||||
|
echo "✗ Failed to sign ${a}" >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Detached signature over SHA256SUMS (this is what verifiers actually check
|
||||||
|
# first; individual .asc files are belt-and-suspenders).
|
||||||
|
rm -f SHA256SUMS.asc
|
||||||
|
if gpg --batch --yes \
|
||||||
|
--local-user "$SIGN_KEY" \
|
||||||
|
--armor --detach-sign \
|
||||||
|
--output SHA256SUMS.asc \
|
||||||
|
SHA256SUMS 2>/dev/null; then
|
||||||
|
echo "✓ Signed SHA256SUMS"
|
||||||
|
else
|
||||||
|
echo "✗ Failed to sign SHA256SUMS" >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. To verify from this directory:"
|
||||||
|
echo " gpg --verify SHA256SUMS.asc SHA256SUMS"
|
||||||
|
echo " sha256sum -c SHA256SUMS"
|
||||||
|
echo ""
|
||||||
|
echo "Or run: $0 --verify $RELEASE_DIR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Verify mode ───────────────────────────────────────────────────────────
|
||||||
|
if [ "$MODE" = "verify" ]; then
|
||||||
|
command -v gpg >/dev/null || { echo "ERROR: gpg not found" >&2; exit 3; }
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
echo "Verifying signatures in $RELEASE_DIR..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify SHA256SUMS.asc if present (this is the master signature).
|
||||||
|
if [ -f SHA256SUMS ] && [ -f SHA256SUMS.asc ]; then
|
||||||
|
if gpg --verify SHA256SUMS.asc SHA256SUMS 2>/dev/null; then
|
||||||
|
echo "✓ SHA256SUMS signature: VALID ($(gpg --list-packets < SHA256SUMS.asc 2>/dev/null | grep -oP 'keyid \K[A-F0-9]+' | head -1 || echo unknown))"
|
||||||
|
else
|
||||||
|
echo "✗ SHA256SUMS signature: INVALID"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "(no SHA256SUMS / SHA256SUMS.asc; skipping master signature)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify each artifact's individual signature.
|
||||||
|
while IFS= read -r -d '' asc; do
|
||||||
|
artifact="${asc%.asc}"
|
||||||
|
if [ ! -f "$artifact" ]; then
|
||||||
|
echo "✗ $asc: artifact missing ($artifact)"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if gpg --verify "$asc" "$artifact" 2>/dev/null; then
|
||||||
|
echo "✓ $artifact signature: VALID"
|
||||||
|
else
|
||||||
|
echo "✗ $artifact signature: INVALID"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -maxdepth 1 -name "*.asc" -not -name "SHA256SUMS.asc" -print0 | sort -z)
|
||||||
|
|
||||||
|
# Verify checksums.
|
||||||
|
if [ -f SHA256SUMS ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Verifying checksums..."
|
||||||
|
if sha256sum -c SHA256SUMS 2>&1 | tail -n +3; then
|
||||||
|
: # sha256sum -c outputs per-file status; aggregate below
|
||||||
|
fi
|
||||||
|
# Count any "FAILED" lines from sha256sum -c output.
|
||||||
|
CHECKSUM_FAILS="$(sha256sum -c SHA256SUMS 2>&1 | grep -c ': FAILED' || true)"
|
||||||
|
if [ "$CHECKSUM_FAILS" -gt 0 ]; then
|
||||||
|
echo "✗ $CHECKSUM_FAILS checksum(s) FAILED"
|
||||||
|
FAILED=$((FAILED + CHECKSUM_FAILS))
|
||||||
|
else
|
||||||
|
echo "✓ All checksums match SHA256SUMS"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ "$FAILED" -eq 0 ]; then
|
||||||
|
echo "✓ ALL VERIFICATIONS PASSED"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "✗ $FAILED VERIFICATION(S) FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
Executable
+130
@@ -0,0 +1,130 @@
|
|||||||
|
#!/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
|
||||||
Reference in New Issue
Block a user