Skip to content

feat(release): self-signed code signing for macOS + Windows binaries #60

Description

@mxaddict

Problem

Release artifacts for macOS (x86_64-apple-darwin + aarch64-apple-darwin) and Windows (x86_64-pc-windows-msvc) ship unsigned. On user machines this triggers:

  • macOS Gatekeeper"<binary>" cannot be opened because the developer cannot be verified. Users must right-click → Open or run xattr -d com.apple.quarantine.
  • Windows SmartScreenWindows protected your PC blue dialog on first run. Users must click "More info → Run anyway".

This is the #1 friction point reported by non-technical users trying our binaries.

Why self-signed (not paid cert)

Apple Developer ID = $99/yr per org. Windows EV Code Signing = $300+/yr. Self-signed certs are a viable interim:

  • macOS: a self-signed Developer ID Application-style cert (security create-keychain + codesign --sign) signs the binary. Gatekeeper still shows a warning, but the binary is signature-bound — distinguishable from unsigned, immune to mid-flight tampering, and the warning text shifts from "developer cannot be verified" to a self-signing variant.
  • Windows: a self-signed cert via New-SelfSignedCertificate + signtool.exe sign /fd sha256 ... signs the binary. SmartScreen still warns on first run but the signature is checkable, and users who trust the publisher cert (one-click install into the local cert store) skip the warning thereafter.

Path forward when we can afford real certs: swap the cert source in CI, no other workflow change.

Scope

  1. Generate self-signed certs offline; store the cert + private key as GitHub Actions org secrets (SELF_SIGNED_MAC_CERT_P12, SELF_SIGNED_MAC_CERT_PASS, SELF_SIGNED_WIN_CERT_PFX, SELF_SIGNED_WIN_CERT_PASS).
  2. macOS CI step — after the release binary builds, import the cert into a keychain (security import + security set-key-partition-list) and codesign --force --options runtime --sign "<cert name>" <binary>. Verify with codesign -dv --verbose=4 <binary>. Re-tar / re-dmg after signing.
  3. Windows CI step — base64-decode the .pfx, signtool sign /f cert.pfx /p $PASS /fd sha256 /tr http://timestamp.digicert.com /td sha256 <binary>.exe. Verify with signtool verify /pa <binary>.exe. Re-zip / re-msi after signing.
  4. Document the user-facing warning + bypass in README (or release-notes template) so users know what to expect.

Acceptance

  • macOS arm64 + x86_64 release binaries pass codesign -dv showing our cert as the signer
  • Windows release binary passes signtool verify /pa (allowing for the self-signed CA → expect "no trusted root" but valid signature chain)
  • First-run UX captured in README: what dialog appears, how to bypass

Non-goals

  • Notarization (requires Apple Developer membership)
  • EV SmartScreen reputation (requires paid EV cert)
  • Reproducible signing (out of scope; signing happens on CI runners)

Effort

M — straightforward Actions integration, ~50 lines of YAML per platform + cert-generation runbook + secret setup. Bulk of the work is testing the round-trip on a real CI run.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions