Release Signing

All Hugin release binaries are signed with Ed25519 (same algorithm as Minisign, WireGuard, OpenBSD signify). This proves a download came from HuginSecurity and wasn’t tampered with on GitHub, a CDN mirror, or in transit.

SHA-256 checksums (.sha256 files) verify integrity only. An attacker who replaces a binary can replace its checksum too. Ed25519 signatures can’t be forged without the private key, which only exists in GitHub Actions secrets.

🔗The Public Key

Hex:    a61ff9262c4509a7879ddaa5a8d86345ef805f6ddced28b097bf58dae270618b
Base64: ph/5JixFCaeHndqlqNhjRe+AX23c7Siwl79Y2uJwYYs=

This key is hardcoded in the hugin binary in three places (kept in sync on rotation):

  • hugin-service/src/updater.rs — auto-updater verification
  • hugin-ui/src/cli/mod.rs — the hugin verify CLI command
  • hugin-web/src/pages/verify.rs — website display + /signing-key endpoint

The official public key is also published at https://hugin.nu/signing-key over HTTPS.

🔗Verifying a Download

Every release artifact has a .sig sidecar in the same directory.

🔗Using hugin verify

hugin verify hugin-darwin-aarch64.dmg

Hugin reads hugin-darwin-aarch64.dmg.sig, verifies the signature against the embedded public key, and prints OK or FAIL. Exit code reflects the result for scripting.

Custom sig path:

hugin verify hugin-darwin-aarch64.dmg --sig /custom/path.sig

🔗Manual Verification

If you don’t trust the binary itself (e.g., first-time install on an air-gapped system), verify with an independent tool:

# Using minisign (after stripping the hugin-sig-ed25519: prefix)
PUBKEY="ph/5JixFCaeHndqlqNhjRe+AX23c7Siwl79Y2uJwYYs="
SIG=$(awk -F: '{print $2}' < hugin-darwin-aarch64.dmg.sig)
# Pass to your favourite Ed25519 verifier with the file as the message

Or using openssl with the raw key — see docs/release-signing.md for the full openssl command.

🔗Signature Format

Each release artifact gets a .sig file containing a single line:

hugin-sig-ed25519:<base64(64-byte-Ed25519-signature)>

The signed message is the raw file bytes — no metadata, no nonces, no timestamps. The hugin-sig-ed25519: prefix makes the format self-describing and grep-able.

🔗CI Pipeline

build (5 matrix targets) → sign → release → homebrew

The sign job:

  1. Downloads all build artifacts from the build job
  2. Compiles tools/sign/ (standalone Rust binary, excluded from the workspace)
  3. Signs every distributable file: .tar.gz, .zip, .dmg, .deb, .AppImage
  4. Uploads each .sig alongside its artifact

The signing key never touches a developer’s machine — only the GitHub Actions runner has access via the RELEASE_SIGNING_KEY secret.

🔗Auto-Update Verification

The auto-updater in hugin update verifies signatures before applying any update. The flow:

  1. Fetch update manifest from https://hugin.nu/updates/manifest.json
  2. Download the new binary + .sig
  3. Verify signature against the embedded public key (not a fetched key — prevents downgrade attacks)
  4. Verify version is greater than current
  5. Atomic swap of the binary + restart

If signature verification fails, the update is aborted and a warning is logged. The user’s existing binary is untouched.

🔗Key Rotation

If the signing key is compromised or needs rotation:

  1. Generate a new 32-byte seed: openssl rand -base64 32
  2. Derive the public key
  3. Update the RELEASE_SIGNING_KEY GitHub Actions secret with the new base64 seed
  4. Update the hardcoded public key in all three Rust source locations
  5. Release a new version

Old releases retain their old signatures — they remain valid for users with the old binary, but users with the new binary will reject them. This is intentional.

🔗Threat Model

What signing protects against:

  • Tampered downloads from any source (GitHub mirrors, CDN cache poisoning, MITM)
  • Malicious updates (auto-updater rejects unsigned)
  • Supply chain attacks where someone uploads to GitHub Releases (they’d need the GitHub Actions secret to sign)

What signing does not protect against:

  • A compromise of the signing key itself (treat the GitHub Actions secret accordingly)
  • A compromise of the build pipeline (signing happens after build — a compromised build job can sign anything)
  • A user installing a forged binary that’s signed with a different key (always check the public key matches hugin.nu/signing-key)

🔗Security Best Practices

  • First install — verify against hugin.nu/signing-key over HTTPS independently, or compare to the value in our published release-signing docs.
  • In CI / scripts — pin to a specific version + sig, don’t rely on “latest”.
  • Air-gapped installs — download .dmg + .sig together, transfer over SneakerNet, verify with hugin verify on the destination.
  • Auto-update on critical machines — leave on by default; the verification chain protects against drive-by malicious updates.