Files
triangles_v5/release-process.md
T
Krystie 59b2ff8e63 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 13:33:11 -07:00

9.7 KiB

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 Currently absolute; should add -ffile-prefix-map for full reproducibility

Verifying reproducibility

Run on a clean checkout:

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):

# 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):

gpg --import release-seckey-BACKUP.asc

Sign a release directory

After CI has produced the artifacts in a known directory:

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:

# 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)
  • 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):

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 repositoryrelease-pubkey.asc at the repo root, committed on every release tag.
  2. The websitehttps://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).