Skip to content

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

@mxaddict

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions