Signing Rust Binaries Shouldn't Require Shell Scripts
You built a Rust GUI app. It compiles. It runs on your machine. You send the binary to a friend, they double-click it, and macOS says: "Apple could not verify this app." Right-click, Open, confirm the scary dialog — it works. But that's not a shipping experience.
The Wall
When you distribute a macOS application outside the App Store, three things must happen before a user can run it without warnings:
- Code signing — every binary inside the
.appbundle must be signed with a Developer ID certificate. - Notarization — the signed artifact must be submitted to Apple's servers for automated malware scanning.
- Stapling — Apple's approval ticket must be attached to the artifact so it can be verified offline.
Skip any of these and Gatekeeper blocks the app. The App Store handles all of this transparently. Outside the store, it's your problem.
The details are surprisingly tricky. It's not enough to sign the .app — you must sign every nested binary and framework inside it. The DMG that wraps your app needs its own signature. Notarization has two authentication modes (API key vs. Apple ID), each with different credential formats. And stapling only works on the outermost container.
What I Found
While building JPEG Locker, I hit all of these edges. The existing landscape looks like this:
rcodesign— pure Rust reimplementation of Apple code signing. If you need to sign macOS binaries from a non-Apple host (e.g. Linux CI), check it out. Notarization requires an App Store Connect API Key though — no local keychain orapple-idauth support. I wanted the full pipeline to work locally with my Developer ID certificate in the login keychain, then the same command on CI.cargo-bundle— creates.appbundles but doesn't sign them.cargo-packager— generates installers and.appbundles, but doesn't sign or notarize. Tauri's bundler does sign, but only within its own build pipeline.- Shell scripts — what most projects actually use.
I looked at how Zed, Lapce, and other Rust GUI projects handle this. Most of them maintain custom shell scripts that call codesign, xcrun notarytool, hdiutil, and stapler in the right order with the right flags. The scripts differ in small but important ways — credential handling, error recovery, which artifacts get signed at which stage.
This felt like a problem that should be solved once, in a cargo subcommand.
cargo-codesign
cargo-codesign replaces those scripts with a single command. After your build produces a .app bundle:
cargo codesign macos --app "target/release/bundle/My App.app"
Output:
[1/5] Codesigning .app bundle...
✓ App signed
[2/5] Creating DMG...
✓ DMG created: target/release/bundle/My App.dmg
[3/5] Codesigning DMG...
✓ DMG signed
[4/5] Notarizing DMG...
✓ Notarized
[5/5] Stapling...
✓ Stapled
✓ Done: target/release/bundle/My App.dmg
Five steps, one command. The full chain: sign inner binaries → sign .app → create DMG → sign DMG → notarize → staple.
For CLI tools without a .app bundle, leave out the --app flag. cargo-codesign discovers your workspace binaries via cargo metadata, signs each one, and copies them to target/signed/.
cargo-codesign is an orchestrator, not a reimplementation. It calls the platform's native tools (codesign, xcrun notarytool, signtool.exe, cosign) in the right order with the right flags. It doesn't build, bundle, or replace your release pipeline — it handles the signing step that comes after.
Setup
Configuration lives in a sign.toml at your project root. Generate one interactively:
cargo codesign init
The wizard asks which platforms you target and which auth mode to use. It generates the config and checks which credentials are already in your environment:
✓ Created sign.toml
Credential status (2 missing):
✓ APPLE_ID set
✗ APPLE_TEAM_ID Team ID from App Store Connect > Membership
✗ APPLE_APP_PASSWORD app-specific password for notarization
How to obtain missing credentials:
→ https://sassman.github.io/cargo-codesign-rs/macos/auth-modes.html
→ https://sassman.github.io/cargo-codesign-rs/macos/credentials.html
The cargo-codesign book covers the full setup: how to export your Developer ID certificate, how to create an app-specific password, which Apple Developer Program to enroll in, and how to choose between API key and Apple ID authentication.
From Local to CI
The same command that works on your machine works in GitHub Actions. cargo-codesign reads credentials from environment variables — the names are configured in sign.toml, so your CI just maps secrets:
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
There's also cargo codesign ci to generate a ready-made GitHub Actions workflow from your sign.toml.
Before any signing step, you can run cargo codesign status to validate that all tools and credentials are available. This fails fast with an actionable message instead of letting you wait 8 minutes for notarization to tell you a secret is missing.
Beyond macOS
The tool also covers Windows (Azure Trusted Signing via signtool.exe) and Linux (cosign, minisign, gpg). These are earlier in development than the macOS pipeline — there will be rough edges, and the developer experience is not where I want it yet. The macOS path is the one that's battle-tested: JPEG Locker ships with it today.
There's also an Ed25519 update signing feature (cargo codesign keygen + cargo codesign update) for projects that need in-app update verification — independent of OS-level trust, pure Rust, works on all platforms.
Install
cargo install cargo-codesign
Links
- Full documentation: sassman.github.io/cargo-codesign-rs
- Source: github.com/sassman/cargo-codesign-rs