Skip to main content

Binary & Package Integrity

Trust is the product. Arcan manages secrets — if users can't trust the binary itself, nothing else matters. Every release is signed, notarized, checksummed, and provenance-tracked. This is not optional. This is not a post-launch task. This ships with v0.1.0.

Three Layers of Trust

┌─────────────────────────────────────────────────────────────────┐
│ TRUST CHAIN │
│ │
│ Layer 1: Build Pipeline Integrity │
│ CI/CD builds all binaries — never a human laptop │
│ Source → GitHub Actions → signed artifact │
│ │
│ Layer 2: OS-Native Signing │
│ macOS: Apple Developer ID + notarization │
│ Windows: Authenticode EV certificate │
│ Linux: GPG detached signatures │
│ │
│ Layer 3: Universal Verification │
│ SHA-256 checksums (GPG-signed) │
│ SLSA provenance (supply chain attestation) │
│ Sigstore/cosign (keyless signing, transparency log) │
│ │
│ Applies to BOTH: │
│ • Arcan core binary (what users install) │
│ • Plugin packages (.arcanpkg files in the registry) │
└─────────────────────────────────────────────────────────────────┘

Why This Matters

Most OSS projects ship unsigned binaries with a checksum file. Users are expected to manually verify — and almost nobody does. This creates a gap:

  • A compromised GitHub release replaces the binary → users download malware
  • A MITM on the download CDN injects a trojan → users trust it because "it's from GitHub"
  • A contributor's credentials are stolen → attacker pushes a poisoned release

For a secrets manager, this is existential. If the binary is compromised, every secret it manages is compromised. Our trust model must be airtight.

Our standard: A user installing Arcan should have the same level of trust as installing software from the Mac App Store or apt install from an official repository.


Layer 1: Build Pipeline Integrity

Rule: No Human Builds Release Binaries

All release artifacts are built in GitHub Actions runners — clean, ephemeral environments. No developer machine ever produces a release binary.

Developer pushes git tag (v0.1.0)
→ GitHub Actions triggers release workflow
→ Clean runner checks out exact tagged commit
→ Builds binaries for all OS/arch combinations
→ Signs each binary (OS-specific + checksums)
→ Generates SLSA provenance attestation
→ Publishes to GitHub Releases
→ Updates Homebrew tap
→ Publishes Docker images
→ Publishes plugin packages to registry

Signing Key Storage

KeyStorageAccess
Apple Developer ID certificateGitHub Actions encrypted secretRelease workflow only
Apple notarization credentialsGitHub Actions encrypted secretRelease workflow only
Windows Authenticode EV certificateGitHub Actions encrypted secretRelease workflow only
GPG signing key (release)GitHub Actions encrypted secretRelease workflow only
Registry Ed25519 signing keyAWS KMSRegistry publish workflow only
Cosign identityGitHub OIDC (keyless)Automatic via GitHub identity

No signing key exists on any developer's machine. Keys are provisioned directly into GitHub Actions secrets and never exported.

GoReleaser

GoReleaser automates the entire release pipeline:

  • Cross-compilation for all OS/arch (linux, darwin, windows × amd64, arm64)
  • macOS universal binaries
  • Checksum generation and GPG signing
  • Docker image building and pushing
  • Homebrew tap formula updates
  • GitHub Release creation with auto-generated changelog
  • SBOM (Software Bill of Materials) generation
# .goreleaser.yaml (in repo root)
project_name: arcan
builds:
- main: ./cmd/arcan
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.built={{.Date}}

signs:
- cmd: gpg
args: ["--armor", "--detach-sign", "${artifact}"]
artifacts: checksum

brews:
- repository:
owner: getarcan
name: homebrew-tap
homepage: "https://getarcan.dev"
description: "Secrets management platform"
install: |
bin.install "arcan"

dockers:
- image_templates:
- "ghcr.io/getarcan/arcan:{{.Version}}"
- "ghcr.io/getarcan/arcan:latest"
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'

permissions:
contents: write
packages: write
id-token: write # for Sigstore OIDC

jobs:
release:
runs-on: macos-latest # macOS runner for notarization
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Import Apple certificates
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
# Import signing certificate into keychain
echo "$APPLE_CERTIFICATE" | base64 --decode > cert.p12
security create-keychain -p "" build.keychain
security import cert.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -k "" build.keychain

- uses: goreleaser/goreleaser-action@v5
with:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.APPLE_ID }}
AC_PASSWORD: ${{ secrets.APPLE_NOTARY_PASSWORD }}
AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}

- name: Sign with Sigstore
uses: sigstore/cosign-installer@v3
- run: |
for f in dist/arcan-*; do
cosign sign-blob --yes "$f" --bundle "$f.cosign.bundle"
done

- name: Generate SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]

Layer 2: OS-Native Signing

macOS — Apple Developer ID + Notarization (P0, required for launch)

Without notarization, macOS Gatekeeper blocks the binary:

"arcan" cannot be opened because the developer cannot be verified.

Process (automated in CI):

# 1. Code sign the binary
codesign --sign "Developer ID Application: GetArcan Inc." \
--options runtime \
--timestamp \
arcan-darwin-arm64

# 2. Create ZIP for notarization
ditto -c -k arcan-darwin-arm64 arcan-darwin-arm64.zip

# 3. Submit to Apple for malware scanning
xcrun notarytool submit arcan-darwin-arm64.zip \
--apple-id "$AC_USERNAME" \
--password "$AC_PASSWORD" \
--team-id "$AC_TEAM_ID" \
--wait

# 4. Staple the notarization ticket to the binary
xcrun stapler staple arcan-darwin-arm64

What Apple does: Scans the binary for known malware, records the hash in their transparency log, and issues a ticket that Gatekeeper checks on first run.

Cost: $99/year (Apple Developer Program)

Windows — Authenticode EV Code Signing (P2, when Windows users appear)

Without Authenticode, Windows SmartScreen warns:

Windows protected your PC — Microsoft Defender SmartScreen prevented an unrecognized app from starting.

EV (Extended Validation) vs standard certificates:

  • Standard certificates: SmartScreen still warns for new publishers until reputation builds
  • EV certificates: Immediate trust — bypasses SmartScreen from day one

Process (automated in CI):

# Sign with EV certificate
signtool sign \
/fd sha256 \
/tr http://timestamp.digicert.com \
/td sha256 \
/f "$EV_CERT_PATH" \
/p "$EV_CERT_PASSWORD" \
arcan-windows-amd64.exe

Cost: ~$300-500/year (DigiCert, Sectigo, or GlobalSign)

Deferred: Windows support is P2. We build Windows binaries from launch but defer Authenticode signing until we have Windows users. Checksums + GPG signing still protect integrity in the meantime.

Linux — GPG Detached Signatures (P0, required for launch)

Linux has no OS-level gatekeeper, but package managers and security-conscious users verify GPG signatures:

# In CI: sign the binary
gpg --armor --detach-sign arcan-linux-amd64

# Produces: arcan-linux-amd64.asc

# User verifies:
curl -fsSL https://getarcan.dev/.well-known/gpg-key.asc | gpg --import
gpg --verify arcan-linux-amd64.asc arcan-linux-amd64

GPG public key distribution:

  • https://getarcan.dev/.well-known/gpg-key.asc (our website)
  • GitHub repo SECURITY.md (includes key fingerprint)
  • Public key servers (keys.openpgp.org)

Layer 3: Universal Verification

SHA-256 Checksums (P0)

Every release publishes a checksums.txt file containing SHA-256 hashes of all artifacts:

# checksums.txt
e3b0c44298fc1c... arcan-0.1.0-linux-amd64.tar.gz
a7ffc6f8bf1ed7... arcan-0.1.0-linux-arm64.tar.gz
d7a8fbb307d780... arcan-0.1.0-darwin-amd64.tar.gz
ef2d127de37b94... arcan-0.1.0-darwin-arm64.tar.gz
4e07408562bedb... arcan-0.1.0-windows-amd64.zip

The checksums file itself is GPG-signed:

# In CI:
sha256sum arcan-0.1.0-* > checksums.txt
gpg --armor --detach-sign checksums.txt

# User verifies:
gpg --verify checksums.txt.sig checksums.txt
sha256sum -c checksums.txt

SLSA Provenance (P1)

SLSA (Supply-chain Levels for Software Artifacts) provides a cryptographic attestation that proves:

  • What was built (exact source commit hash)
  • Where it was built (specific CI runner)
  • How it was built (exact workflow file)
  • Who triggered the build (tagged by developer, built by CI)
{
"buildType": "https://github.com/slsa-framework/slsa-github-generator",
"builder": {
"id": "https://github.com/getarcan/arcan/.github/workflows/release.yml"
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/getarcan/arcan@refs/tags/v0.1.0",
"digest": { "sha1": "abc123def456..." },
"entryPoint": ".github/workflows/release.yml"
}
},
"materials": [
{
"uri": "git+https://github.com/getarcan/arcan",
"digest": { "sha1": "abc123def456..." }
}
]
}

SLSA Level 3 (non-forgeable provenance) is achievable with GitHub Actions native support. This means even if a maintainer's credentials are stolen, the attacker cannot forge a provenance attestation — the CI platform generates it, not the developer.

User verification:

slsa-verifier verify-artifact arcan-linux-amd64 \
--provenance-path arcan-linux-amd64.intoto.jsonl \
--source-uri github.com/getarcan/arcan \
--source-tag v0.1.0

Sigstore / Cosign (P1)

Sigstore provides keyless signing using the CI runner's OIDC identity. No GPG keys to manage, rotate, or distribute:

# In CI: sign with GitHub OIDC identity (keyless — no private key needed)
cosign sign-blob --yes arcan-linux-amd64 \
--bundle arcan-linux-amd64.cosign.bundle

# User verifies:
cosign verify-blob arcan-linux-amd64 \
--bundle arcan-linux-amd64.cosign.bundle \
--certificate-identity \
"https://github.com/getarcan/arcan/.github/workflows/release.yml@refs/tags/v0.1.0" \
--certificate-oidc-issuer \
"https://token.actions.githubusercontent.com"

Why Sigstore is powerful:

  • Keyless: The CI runner's GitHub identity IS the signing identity. No keys to steal.
  • Transparency log: Every signature is recorded in Rekor (public, append-only). Anyone can audit all signatures ever made.
  • Non-repudiable: The signing certificate is tied to a specific workflow run at a specific time. Cannot be forged after the fact.

Plugin Package Integrity

The same integrity standards apply to .arcanpkg files distributed through the registry. See Registry for the plugin-specific flow.

VerificationArcan BinaryPlugin Package
Checksum (SHA-256)checksums.txt in GitHub Releasemanifest.json in registry metadata
GPG signaturechecksums.txt.sig— (Ed25519 instead)
Code signing (OS)macOS notarization, AuthenticodeN/A (not OS-installed)
Ed25519 signature— (GPG instead)signature file in .arcanpkg
SLSA provenanceGitHub Release attestationRegistry build attestation
CosignGitHub Release bundleRegistry package bundle

Key difference: OS-native signing (Apple, Microsoft) applies to the core binary because users install it via OS mechanisms. Plugin packages use Ed25519 because they're loaded by the core runtime, not the OS.


Priority & Cost Summary

ItemPriorityCostStatus
SHA-256 checksums (GPG-signed)P0 — launchFreeCI/CD setup
macOS Developer ID + notarizationP0 — launch$99/yearRequires Apple Developer account
GPG signing (Linux binaries + checksums)P0 — launchFreeCI/CD setup
GoReleaserP0 — launchFree (OSS)CI/CD setup
Homebrew tap with checksumsP0 — launchFreeCI/CD setup
Ed25519 plugin signingP0 — launchFreeRegistry + core implementation
Sigstore/cosignP1 — post-launchFreeCI/CD addition
SLSA provenanceP1 — post-launchFreeCI/CD addition
Windows Authenticode (EV)P2 — when needed~$400/yearDeferred

Contributor Guidance

If you're contributing to Arcan, you don't need to worry about signing. The CI pipeline handles everything. Your responsibilities:

  1. Never build release binaries locally. Push a tag → CI does the rest.
  2. Never commit signing keys. They live in GitHub Actions secrets only.
  3. Always tag releases from main. No release branches, no cherry-picks.
  4. Test before tagging. CI runs tests on the tag, but failures mean a yanked release.

The release process:

# Ensure all tests pass
go test ./...

# Tag the release
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0

# CI takes over:
# → builds all platforms
# → signs everything
# → publishes to GitHub Releases
# → updates Homebrew tap
# → pushes Docker images
# → done in ~10 minutes