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 verificationhugin-ui/src/cli/mod.rs— thehugin verifyCLI commandhugin-web/src/pages/verify.rs— website display +/signing-keyendpoint
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:
- Downloads all build artifacts from the build job
- Compiles
tools/sign/(standalone Rust binary, excluded from the workspace) - Signs every distributable file:
.tar.gz,.zip,.dmg,.deb,.AppImage - Uploads each
.sigalongside 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:
- Fetch update manifest from
https://hugin.nu/updates/manifest.json - Download the new binary +
.sig - Verify signature against the embedded public key (not a fetched key — prevents downgrade attacks)
- Verify version is greater than current
- 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:
- Generate a new 32-byte seed:
openssl rand -base64 32 - Derive the public key
- Update the
RELEASE_SIGNING_KEYGitHub Actions secret with the new base64 seed - Update the hardcoded public key in all three Rust source locations
- 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-keyover 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+.sigtogether, transfer over SneakerNet, verify withhugin verifyon the destination. - Auto-update on critical machines — leave on by default; the verification chain protects against drive-by malicious updates.