Introduction
cargo-codesign is a cross-platform binary signing CLI for Rust projects. It replaces the collection of per-platform shell scripts that most teams maintain for code signing, notarization, and update integrity verification.
What cargo-codesign does
- Signs macOS
.appbundles and bare binaries withcodesign - Creates DMG installers and codesigns them
- Submits to Apple notarization and staples the ticket
- Signs release archives with Ed25519 for in-app update verification
- Generates Ed25519 keypairs for update signing
- Validates your credentials and tool setup before signing (
cargo codesign status) - Uses a single config file (
sign.toml) that maps env var names to credentials
What cargo-codesign does NOT do
- Not a build tool. It operates on already-built artifacts. It does not call
cargo build. - Not a bundler. It does not create
.appbundles from raw binaries. That’s cargo-bundle, cargo-packager, or your own script. However, when given a.appvia--app, it handles the full chain: sign, DMG, codesign DMG, notarize, staple. - Not a release orchestrator. It does not bump versions, create tags, or publish to crates.io. That’s cargo-release or release-plz.
- Not a lipo/universal binary tool. Universal binary creation (
lipo -create) is a build step, not a signing step. However, this book documents how to do it as part of the end-to-end flow.
The three signing layers
Binary signing has three distinct concerns:
| Layer | What | Purpose | Tool |
|---|---|---|---|
| OS Trust | macOS codesign + notarize, Windows signtool | OS runs the binary without warning/blocking | cargo codesign macos, cargo codesign windows |
| Update Integrity | Ed25519 signature over the release archive | In-app updater verifies authenticity | cargo codesign update |
| Store Signing | App Store, Microsoft Store submission | Store distribution | Out of scope for v1 |
A typical release pipeline needs Layer 1 + Layer 2. Layer 3 is separate and not covered by cargo-codesign.
How to read this book
- New to signing? Start with Installation, then follow the macOS Signing Guide end-to-end.
- Setting up CI? Jump to GitHub Actions Walkthrough.
- Looking up a flag? See the CLI Reference or sign.toml Reference.
Installation
From source (recommended during early development)
cargo install --git https://github.com/sassman/cargo-codesign-rs
Or if cargo-codesign has been published:
cargo install cargo-codesign
Verify installation
cargo codesign --version
Prerequisites
cargo-codesign orchestrates platform-specific signing tools. You need the tools for your target platform:
| Platform | Required tools | How to get them |
|---|---|---|
| macOS | codesign, xcrun, hdiutil | Xcode Command Line Tools: xcode-select --install |
| Windows | signtool.exe | Windows SDK |
| Linux | cosign or minisign | See Linux guide |
cargo-codesign itself is a single Rust binary with no runtime dependencies beyond these platform tools.
Creating sign.toml
Run cargo codesign init to generate a sign.toml with guided prompts:
cargo codesign init
The wizard asks:
- Which platforms? — macOS, Windows, Linux, Update signing
- Auth mode (macOS) —
apple-idfor local/indie,api-keyfor CI - Signing method (Linux) — cosign, minisign, or gpg
After generating the file, it checks which credentials are already set in your environment and shows how to obtain any that are missing — with links to the relevant guide.
Example output
✓ 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
Set missing credentials in .env or CI secrets, then run:
cargo codesign status
Manual creation
You can also create sign.toml by hand — see the sign.toml Reference for the full format.
Checking Your Setup
Before signing anything, verify that your credentials and tools are in order:
cargo codesign status
Example output when everything is configured:
Using config: /path/to/project/sign.toml
✓ env:APPLE_ID: set
✓ env:APPLE_TEAM_ID: set
✓ env:APPLE_APP_PASSWORD: set
✓ tool:codesign: /usr/bin/codesign
✓ tool:xcrun: /usr/bin/xcrun
✓ tool:hdiutil: /usr/bin/hdiutil
All checks passed.
Example output with missing credentials:
Using config: /path/to/project/sign.toml
✗ env:APPLE_ID: APPLE_ID: not set
✓ env:APPLE_TEAM_ID: set
✗ env:APPLE_APP_PASSWORD: APPLE_APP_PASSWORD: not set
✓ tool:codesign: /usr/bin/codesign
✓ tool:xcrun: /usr/bin/xcrun
✓ tool:hdiutil: /usr/bin/hdiutil
2 check(s) failed.
What it checks
- Environment variables: For each env var name listed in your
sign.toml, checks that it’s set and non-empty. - Platform tools: Checks that
codesign,xcrun, andhdiutilare on your PATH (macOS). Windows checks forsigntool.exe. - Only checks credentials relevant to your configured auth mode —
api-keymode checks different vars thanapple-idmode.
Exit codes
| Code | Meaning |
|---|---|
| 0 | All checks passed |
| 2 | Configuration error (missing sign.toml, bad format) |
| 4 | One or more checks failed |
Loading credentials from .env
cargo-codesign automatically loads .env from the current directory (via dotenvy). This means you can keep your credentials in .env for local development:
# .env — never commit this file
APPLE_ID=you@example.com
APPLE_TEAM_ID=ABCDE12345
APPLE_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
Make sure .env is in your .gitignore.
macOS Signing Overview
Shipping a macOS app to users outside the App Store requires three steps:
- Code signing — digitally sign your binary with a Developer ID certificate so macOS recognizes it as trusted
- Notarization — submit the signed artifact to Apple’s servers for automated malware scanning
- Stapling — attach the notarization ticket to the artifact so it works offline
Skip any of these and users see the dreaded “Apple could not verify” dialog — or worse, Gatekeeper blocks the app entirely.
What cargo-codesign handles
cargo codesign macos runs the full pipeline:
.app bundle ──► sign inner binaries ──► sign .app ──► create DMG
│
sign DMG ◄────┘
│
notarize DMG
│
staple DMG
│
✓ Ready to ship
Before you start
You need:
- A Developer ID Application certificate — see Setting Up Credentials
- A built
.appbundle — cargo-codesign does not build or bundle. See below. - Apple credentials for notarization — either an API key or Apple ID + app-specific password
Building a .app bundle (not a cargo-codesign concern)
Before you can sign, you need a .app bundle. Here’s what that looks like:
# Minimal .app structure
mkdir -p "MyApp.app/Contents/MacOS"
mkdir -p "MyApp.app/Contents/Resources"
# Copy your binary
cp target/release/myapp "MyApp.app/Contents/MacOS/myapp"
# Copy metadata
cp Info.plist "MyApp.app/Contents/"
cp AppIcon.icns "MyApp.app/Contents/Resources/"
Tools like cargo-bundle and cargo-packager automate this. Your project may also have a custom bundle script.
For universal binaries (Intel + Apple Silicon), create both architectures and combine them before bundling:
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create \
target/x86_64-apple-darwin/release/myapp \
target/aarch64-apple-darwin/release/myapp \
-output target/universal-apple-darwin/release/myapp
Once you have a .app, cargo-codesign takes over.
Two modes
| Mode | Command | What it does |
|---|---|---|
| App mode | cargo codesign macos --app "MyApp.app" | Full chain: sign app → create DMG → sign DMG → notarize → staple |
| Bare binary mode | cargo codesign macos | Discover binaries via cargo metadata, sign each, copy to target/signed/ |
Most GUI apps use app mode. CLI tools distributed as standalone binaries use bare binary mode.
Setting Up macOS Credentials
Developer ID Application Certificate
You need a Developer ID Application certificate (not a Mac App Store certificate — that’s for App Store distribution, which cargo-codesign does not handle).
Getting the certificate
-
Join the Apple Developer Program (99 EUR/year) at developer.apple.com/programs
-
Create a Certificate Signing Request (CSR):
- Open Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority
- Enter your Apple ID email, leave CA Email empty, select Saved to disk
- Save the
.certSigningRequestfile
-
Upload to Apple Developer Portal:
- Go to developer.apple.com/account/resources/certificates
- Click + → Select Developer ID Application
- Upload your
.certSigningRequestfile - Download the
.cerfile
-
Install the certificate:
- Double-click the
.cerfile to import into Keychain Access - Verify:
security find-identity -v -p codesigning
- Double-click the
For CI: exporting a .p12
CI runners don’t have your Keychain. Export the certificate as a .p12 file:
# In Keychain Access: right-click the certificate → Export Items → .p12
# Or use openssl:
openssl pkcs12 -export \
-out certificate.p12 \
-inkey private-key.key \
-in certificate.pem \
-password pass:YOUR_PASSWORD
Base64-encode it for a GitHub Secret:
base64 -i certificate.p12 | pbcopy
Store as APPLE_CERTIFICATE_BASE64 in your repo’s GitHub Secrets.
Notarization Credentials
cargo-codesign supports two auth modes for notarization. Choose based on your setup:
| Mode | Best for | Credentials needed |
|---|---|---|
apple-id | Local development, indie devs | Apple ID + app-specific password |
api-key | CI, teams, automation | App Store Connect API key (.p8) |
apple-id mode
- Go to account.apple.com → Sign-In and Security → App-Specific Passwords
- Generate a password labeled “cargo-codesign” (or similar)
- Store it:
# .env (local) or GitHub Secrets (CI)
APPLE_ID=you@example.com
APPLE_TEAM_ID=ABCDE12345
APPLE_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
api-key mode
- Go to appstoreconnect.apple.com/access/integrations/api
- Create a new key with Developer access
- Download the
.p8file (you can only download it once) - Note the Key ID and Issuer ID
- Store them:
# Base64-encode the .p8 for CI secrets
APPLE_NOTARIZATION_KEY=$(base64 -i AuthKey_XXXXXXXXXX.p8)
APPLE_NOTARIZATION_KEY_ID=XXXXXXXXXX
APPLE_NOTARIZATION_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
See Auth Modes for how these map to sign.toml.
Signing a GUI App (–app mode)
This is the most common mode for macOS GUI applications. It handles the full pipeline from .app bundle to signed, notarized DMG.
Quick start
cargo codesign macos --app "target/release/bundle/MyApp.app"
This runs:
[1/5] Codesigning .app bundle...
✓ App signed
[2/5] Creating DMG...
✓ DMG created: target/release/bundle/MyApp.dmg
[3/5] Codesigning DMG...
✓ DMG signed
[4/5] Notarizing DMG...
✓ Notarized
[5/5] Stapling...
✓ Stapled
✓ Done: target/release/bundle/MyApp.dmg
What happens at each step
Step 1: Sign inner binaries and the .app bundle
cargo-codesign walks the .app bundle and signs:
- Every binary in
Contents/MacOS/ - Every dylib and framework in
Contents/Frameworks/ - The
.appbundle itself (with entitlements if configured)
Each is signed with codesign --force --timestamp --options runtime, which enables the hardened runtime required for notarization.
Step 2: Create DMG
Creates a DMG from the .app using hdiutil. The DMG is placed next to the .app:
target/release/bundle/MyApp.app → target/release/bundle/MyApp.dmg
Step 3: Sign the DMG
The DMG itself must also be codesigned — Apple’s notarization service requires it.
Step 4: Notarize
Submits the DMG to Apple’s notarization service and waits for the result. This typically takes 1-5 minutes.
On failure, cargo-codesign prints the notarization log with specific issues (e.g., unsigned binary inside the bundle, forbidden entitlement).
Step 5: Staple
Attaches the notarization ticket to the DMG so it works offline — users don’t need to be online for Gatekeeper to verify.
Skipping steps
For development builds where you don’t need notarization:
# Sign only, skip notarization and stapling
cargo codesign macos --app "MyApp.app" --skip-notarize
Overriding config from the command line
# Override signing identity
cargo codesign macos --app "MyApp.app" --identity "Developer ID Application: Other Team"
# Override entitlements
cargo codesign macos --app "MyApp.app" --entitlements custom-entitlements.plist
# See subprocess commands
cargo codesign macos --app "MyApp.app" --verbose
Full example: build, bundle, sign
# 1. Build
cargo build --release --target aarch64-apple-darwin -p my-app
# 2. Bundle (your script or cargo-bundle)
./scripts/bundle-macos.sh
# 3. Sign (cargo-codesign handles the rest)
cargo codesign macos --app "target/release/bundle/MyApp.app"
Or if you use a Makefile:
dmg:
cargo build --release --target aarch64-apple-darwin -p my-app
./scripts/bundle-macos.sh
set -a && source .env && set +a && \
cargo codesign macos --app "target/release/bundle/MyApp.app"
Signing a CLI Tool (bare binary mode)
If you’re distributing a standalone CLI binary (not a .app bundle), use bare binary mode. cargo-codesign discovers your binaries via cargo metadata and signs each one.
Quick start
# Build first
cargo build --release
# Sign all binary targets in the workspace
cargo codesign macos
This runs:
Discovering binaries via cargo metadata...
Signing my-cli...
✓ my-cli → target/signed/release/my-cli
✓ Done
How it works
- Runs
cargo metadata --format-version 1 --no-depsto find all binary targets - Looks for each binary in
target/release/ - Signs each with
codesign --force --timestamp --options runtime - Copies signed binaries to
target/signed/release/
The original binaries in target/release/ are untouched.
Limitations
- Bare binaries cannot be stapled — stapling only works on
.appbundles and DMG files - Notarization of bare binaries requires zipping them first, which cargo-codesign does not currently automate in this mode
- For full notarization, consider wrapping your CLI in a
.appbundle or distributing as a signed DMG
When to use this mode
- Distributing CLI tools via Homebrew taps (Homebrew handles its own verification)
- Internal tools where Gatekeeper isn’t a concern
- As a pre-step before packaging into an installer
Auth Modes: api-key vs apple-id
cargo-codesign supports two authentication modes for macOS notarization. The auth mode is configured in sign.toml and determines which credentials cargo-codesign expects.
apple-id mode
Best for local development and indie developers.
[macos]
identity = "Developer ID Application"
entitlements = "entitlements.plist"
auth = "apple-id"
[macos.env]
apple-id = "APPLE_ID"
team-id = "APPLE_TEAM_ID"
app-password = "APPLE_APP_PASSWORD"
Under the hood, cargo-codesign calls:
xcrun notarytool submit artifact.dmg \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait
Credentials needed:
APPLE_ID— your Apple ID emailAPPLE_TEAM_ID— your 10-character team IDAPPLE_APP_PASSWORD— an app-specific password (not your Apple ID password)
api-key mode
Best for CI and team environments. Uses an App Store Connect API key (.p8 file).
[macos]
identity = "Developer ID Application"
entitlements = "entitlements.plist"
auth = "api-key"
[macos.env]
certificate = "MACOS_CERTIFICATE"
certificate-password = "MACOS_CERTIFICATE_PASSWORD"
notarization-key = "APPLE_NOTARIZATION_KEY"
notarization-key-id = "APPLE_NOTARIZATION_KEY_ID"
notarization-issuer = "APPLE_NOTARIZATION_ISSUER_ID"
Under the hood, cargo-codesign calls:
xcrun notarytool submit artifact.dmg \
--key "/tmp/AuthKey.p8" \
--key-id "$APPLE_NOTARIZATION_KEY_ID" \
--issuer "$APPLE_NOTARIZATION_ISSUER_ID" \
--wait
The APPLE_NOTARIZATION_KEY env var contains the .p8 file contents base64-encoded. cargo-codesign decodes it to a temp file, uses it, and deletes it.
Credentials needed:
MACOS_CERTIFICATE— base64-encoded.p12certificate (for CI keychain import)MACOS_CERTIFICATE_PASSWORD— password for the.p12APPLE_NOTARIZATION_KEY— base64-encoded.p8API keyAPPLE_NOTARIZATION_KEY_ID— the key ID from App Store ConnectAPPLE_NOTARIZATION_ISSUER_ID— the issuer ID from App Store Connect
Which mode should I use?
| Scenario | Recommended mode |
|---|---|
| Local dev on your Mac | apple-id |
| Solo developer, simple CI | apple-id |
| Team with shared CI | api-key |
| Rotating credentials | api-key (keys can be revoked individually) |
You can use different modes locally vs CI by maintaining separate .env files or overriding env vars in CI. The sign.toml only names the env vars — the values come from the environment.
Troubleshooting macOS Signing
“Apple could not verify” dialog
Users see this when the app is not notarized or the ticket is not stapled.
Fix: Make sure you’re running the full pipeline without --skip-notarize:
cargo codesign macos --app "MyApp.app"
If notarization succeeded but users still see the dialog, the DMG may not have been stapled. Re-run without --skip-staple.
“MyApp.app is damaged and can’t be opened”
This usually means the code signature is invalid — often because the .app was modified after signing (e.g., by xattr quarantine stripping, or by copying incorrectly).
Fix: Re-sign after any modification to the bundle.
Notarization fails with “The signature of the binary is invalid”
The binary inside the .app wasn’t signed with the hardened runtime.
Fix: cargo-codesign always uses --options runtime so this shouldn’t happen. If it does, check that:
- You’re not signing with a different tool before cargo-codesign
- The
.appbundle structure is correct (binary inContents/MacOS/)
Notarization fails with “The executable does not have the hardened runtime enabled”
Same as above — the hardened runtime flag is missing.
“errSecInternalComponent” or keychain errors
This typically happens in CI when the keychain isn’t properly configured.
Fix for CI:
- Make sure the keychain is unlocked before signing
- The
security set-key-partition-listcall must includecodesign:in the-Sflag - See the GitHub Actions walkthrough for the correct keychain setup
Notarization times out
Apple’s notarization service occasionally takes longer than usual.
Fix: Re-run the command. xcrun notarytool submit --wait has a built-in timeout. If it consistently times out, check Apple’s system status.
“No signing identity found”
cargo codesign status shows tool:codesign as available but signing fails.
Fix:
- Verify the certificate is installed:
security find-identity -v -p codesigning - Check the identity string in
sign.tomlmatches. The default"Developer ID Application"matches any Developer ID Application certificate. - In CI, make sure the certificate was imported into the correct keychain
Verbose output
When debugging, always use --verbose to see the exact subprocess commands:
cargo codesign macos --app "MyApp.app" --verbose
This prints every codesign, hdiutil, xcrun, and stapler invocation with its arguments and output.
Windows Signing
Sign Windows executables using Azure Trusted Signing via signtool.exe.
Usage
cargo codesign windows
This will:
- Load
sign.tomland read the[windows]section - Discover binaries via
cargo metadata - Generate
metadata.jsonfor Azure Trusted Signing - Sign each
.exewithsigntool.exeusing SHA-256 + timestamp - Clean up temporary metadata files
Install tools automatically
On a fresh CI runner, use --install-tools to download the Azure Code Signing DLib via NuGet:
cargo codesign windows --install-tools
Configuration
[windows]
timestamp-server = "http://timestamp.acs.microsoft.com"
[windows.env]
tenant-id = "AZURE_TENANT_ID"
client-id = "AZURE_CLIENT_ID"
client-secret = "AZURE_CLIENT_SECRET"
endpoint = "AZURE_SIGNING_ENDPOINT"
account-name = "AZURE_SIGNING_ACCOUNT_NAME"
cert-profile = "AZURE_SIGNING_CERT_PROFILE"
See the sign.toml Reference for full details, and Setting Up Credentials for how to obtain Azure Trusted Signing credentials.
Setting Up Windows Credentials
To sign Windows executables with Azure Trusted Signing, you need six credentials. All are set as environment variables (or in .env).
1. Azure AD tenant
Register an Azure AD application for Trusted Signing.
| Credential | Env var | Where to find it |
|---|---|---|
| Tenant ID | AZURE_TENANT_ID | Azure Portal > Azure Active Directory > Overview > Tenant ID |
| Client ID | AZURE_CLIENT_ID | Azure Portal > App registrations > your app > Application (client) ID |
| Client Secret | AZURE_CLIENT_SECRET | Azure Portal > App registrations > your app > Certificates & secrets > New client secret |
2. Trusted Signing account
Create a Trusted Signing account and certificate profile in the Azure Portal.
| Credential | Env var | Where to find it |
|---|---|---|
| Endpoint | AZURE_SIGNING_ENDPOINT | Azure Portal > Trusted Signing > your account > Overview > Endpoint |
| Account name | AZURE_SIGNING_ACCOUNT_NAME | The name you chose when creating the Trusted Signing account |
| Certificate profile | AZURE_SIGNING_CERT_PROFILE | Azure Portal > Trusted Signing > Certificate profiles > profile name |
3. Tools
On the CI runner (Windows):
- signtool.exe — part of the Windows SDK. Usually available on
windows-latestGitHub runners. - Azure.CodeSigning.Dlib.dll — install with
cargo codesign windows --install-toolsor manually vianuget install Microsoft.Trusted.Signing.Client.
Verify
After setting all six variables, run:
cargo codesign status
Linux Signing
Sign Linux release archives using one of three methods: cosign (keyless OIDC), minisign (self-managed keys), or gpg (detached signatures).
Usage
cargo codesign linux --archive target/release/myapp.tar.gz
The signing method is determined by [linux] method in sign.toml. Override it at the command line:
cargo codesign linux --archive release.tar.gz --method cosign
cargo codesign linux --archive release.tar.gz --method minisign
cargo codesign linux --archive release.tar.gz --method gpg
Specify a custom output path for the signature file:
cargo codesign linux --archive release.tar.gz --output release.tar.gz.cosign-bundle
Methods
cosign (keyless OIDC)
Recommended for GitHub Actions. Uses Sigstore keyless signing via OIDC — no private key management required.
Produces a .bundle file alongside the archive.
minisign
Self-managed key signing via minisign. The private key is read from the environment variable configured in [linux.env] key.
Produces a .minisig file alongside the archive.
gpg
Standard GPG detached signatures. Uses the default GPG key on the system.
Produces a .sig file alongside the archive.
Configuration
[linux]
method = "cosign"
[linux.env]
key = "COSIGN_PRIVATE_KEY"
See the sign.toml Reference for full details, and Setting Up Credentials for how to obtain signing credentials.
Setting Up Linux Credentials
The credentials you need depend on the signing method configured in sign.toml.
cosign (keyless OIDC)
Recommended for GitHub Actions — uses Sigstore keyless signing via OIDC identity. No long-lived keys required.
In CI, cosign automatically uses the GitHub Actions OIDC token. Locally, it opens a browser for authentication.
If you prefer key-based signing:
cosign generate-key-pair
# Writes cosign.key (private) and cosign.pub (public)
Set COSIGN_PRIVATE_KEY to the contents of cosign.key (or store in CI secrets).
Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/
minisign (self-managed keys)
Generate a keypair:
minisign -G -s minisign.key -p minisign.pub
Set MINISIGN_PRIVATE_KEY to the contents of minisign.key.
Install minisign: cargo install minisign or https://jedisct1.github.io/minisign/
gpg
Use your existing GPG key:
gpg --list-secret-keys --keyid-format LONG
# Export for CI:
gpg --armor --export-secret-keys YOUR_KEY_ID | base64
Set GPG_PRIVATE_KEY to the base64-encoded armor output.
Verify
After setting credentials, run:
cargo codesign status
Generating Keypairs
Update signing uses Ed25519 (via ed25519-dalek) to sign release archives. This is separate from OS-level code signing — it’s for your in-app updater to verify that an update is authentic.
Generate a keypair
cargo codesign keygen
Output:
✓ Keypair generated
Private key: ./update-signing.key
Public key: ./update-signing.pub
Store the private key as UPDATE_SIGNING_KEY in your CI secrets.
Embed the public key in your binary at compile time:
const UPDATE_PUBLIC_KEY: &str = include_str!("../update-signing.pub");
Custom output paths
cargo codesign keygen \
--output-private ./secrets/signing.key \
--output-public ./keys/signing.pub
Key format
Both keys are base64-encoded Ed25519 keys (32 bytes each, base64-encoded):
# update-signing.key (private — NEVER commit this)
dGhpcyBpcyBhIGZha2UgcHJpdmF0ZSBrZXk=
# update-signing.pub (public — safe to commit)
dGhpcyBpcyBhIGZha2UgcHVibGljIGtleQ==
Security
- The private key must be kept secret. Store it as a CI secret (
UPDATE_SIGNING_KEY), never in the repository. - The public key is embedded in your binary at compile time. It’s safe to commit.
- Keys are generated using
OsRng(operating system’s cryptographically secure random number generator).
Signing Release Archives
After building your release archive (.tar.gz, .zip, etc.), sign it with your Ed25519 private key:
cargo codesign update --archive target/release/myapp-v1.0.0.tar.gz
Output:
✓ Signed: target/release/myapp-v1.0.0.tar.gz.sig
How it works
- Reads the archive bytes
- Signs with the Ed25519 private key from the
UPDATE_SIGNING_KEYenv var - Writes the base64-encoded signature to
<archive>.sig
Options
# Custom output path
cargo codesign update --archive release.tar.gz --output release.sig
# Use a different env var for the key
cargo codesign update --archive release.tar.gz --key-env MY_SIGNING_KEY
# Sign and verify in one step
cargo codesign update --archive release.tar.gz --public-key update-signing.pub
The --public-key flag tells cargo-codesign to verify the signature immediately after signing — a good sanity check.
Providing the key
The private key is read from an environment variable (default: UPDATE_SIGNING_KEY). You can set it:
# Via .env file
echo "UPDATE_SIGNING_KEY=$(cat update-signing.key)" >> .env
# Or export directly
export UPDATE_SIGNING_KEY=$(cat update-signing.key)
# Then sign
cargo codesign update --archive release.tar.gz
Integrating with Your Updater
The update signing flow produces a .sig file alongside your release archive. Your in-app updater uses the public key (embedded at compile time) to verify the signature before applying the update.
Embed the public key
#![allow(unused)]
fn main() {
const UPDATE_PUBLIC_KEY: &str = include_str!("../update-signing.pub");
}
Verify in your updater
Using ed25519-dalek directly:
#![allow(unused)]
fn main() {
use base64::{engine::general_purpose::STANDARD, Engine};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
fn verify_update(archive_bytes: &[u8], signature_b64: &str) -> bool {
let pub_bytes = STANDARD.decode(UPDATE_PUBLIC_KEY.trim()).unwrap();
let pub_array: [u8; 32] = pub_bytes.try_into().unwrap();
let verifying_key = VerifyingKey::from_bytes(&pub_array).unwrap();
let sig_bytes = STANDARD.decode(signature_b64.trim()).unwrap();
let sig_array: [u8; 64] = sig_bytes.try_into().unwrap();
let signature = Signature::from_bytes(&sig_array);
verifying_key.verify(archive_bytes, &signature).is_ok()
}
}
Or use cargo-codesign’s library directly:
#![allow(unused)]
fn main() {
use cargo_codesign::update::verify_bytes;
let is_valid = verify_bytes(archive_bytes, signature_b64, UPDATE_PUBLIC_KEY.trim()).is_ok();
}
Release workflow
- Build release archive
cargo codesign update --archive release.tar.gz --public-key update-signing.pub- Upload both
release.tar.gzandrelease.tar.gz.sigto your release - Your updater downloads both, verifies the signature, then applies
GitHub Actions Walkthrough
This guide shows how to set up macOS code signing in GitHub Actions using cargo-codesign.
Prerequisites
Configure these GitHub Secrets in your repository:
| Secret | Description |
|---|---|
MACOS_CERTIFICATE_BASE64 | Base64-encoded .p12 certificate |
MACOS_CERTIFICATE_PASSWORD | Password for the .p12 |
APPLE_ID | Your Apple ID email (for apple-id auth) |
APPLE_TEAM_ID | Your 10-character team ID |
APPLE_APP_PASSWORD | App-specific password |
Workflow
name: Release macOS
on:
push:
tags: ["v*"]
jobs:
build-and-sign:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --release -p my-app
- name: Bundle .app
run: ./scripts/bundle-macos.sh
- name: Install cargo-codesign
run: cargo install cargo-codesign
- name: Import certificate
run: cargo codesign macos --ci-import-cert
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
- name: Sign, package, and notarize
run: cargo codesign macos --app "target/release/bundle/MyApp.app" --verbose
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
- name: Cleanup certificate
if: always()
run: cargo codesign macos --ci-cleanup-cert
- name: Upload DMG
uses: softprops/action-gh-release@v2
with:
files: target/release/bundle/MyApp.dmg
Key points
--ci-import-certreads the certificate env var names fromsign.toml, base64-decodes the certificate, creates an ephemeral keychain, and imports it. No shell needed.--ci-cleanup-certdeletes the ephemeral keychain created by--ci-import-cert. Runsif: always()so cleanup happens even if signing fails. Safe to call when no keychain exists (logs a warning, exits 0).cargo codesign macos --apphandles the full sign → DMG → notarize → staple chain.- The env var names (
MACOS_CERTIFICATE,MACOS_CERTIFICATE_PASSWORD) come from yoursign.toml. The GitHub secret names (e.g.MACOS_CERTIFICATE_BASE64) can be whatever you prefer.
Composing with cargo-dist
If you use cargo-dist for releases, add the signing steps after the build job:
sign:
needs: [build]
runs-on: macos-latest
steps:
- name: Import certificate
run: cargo codesign macos --ci-import-cert
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
- name: Sign macOS artifacts
run: cargo codesign macos --app "path/to/MyApp.app" --verbose
- name: Cleanup
if: always()
run: cargo codesign macos --ci-cleanup-cert
Workflow Generation
Generate a GitHub Actions workflow from your sign.toml:
cargo codesign ci
This reads the configured platforms and env var names from sign.toml and generates .github/workflows/release-sign.yml with the correct secrets mappings.
Options
| Flag | Default | Description |
|---|---|---|
--output <PATH> | .github/workflows/release-sign.yml | Output path for the generated YAML |
--config <PATH> | auto-discovered sign.toml | Path to sign.toml |
What gets generated
For each platform configured in sign.toml, the workflow creates a job on the appropriate runner:
- macOS →
macos-latest - Windows →
windows-latest - Linux →
ubuntu-latest
Each job:
- Installs
cargo-codesign - Runs
cargo codesign statusto verify credentials - Runs the platform-specific signing command
Secrets are mapped from the env var names in sign.toml to ${{ secrets.X }}.
Calling from another workflow
The generated workflow uses workflow_call, so you can invoke it from your release workflow:
sign:
needs: [build]
uses: ./.github/workflows/release-sign.yml
secrets: inherit
Secrets Management
cargo-codesign follows a strict separation: sign.toml stores env var names, never values. All secret values reach cargo-codesign exclusively through environment variables.
Local development
Use a .env file in your project root. cargo-codesign loads it automatically via dotenvy:
# .env — NEVER commit this
APPLE_ID=you@example.com
APPLE_TEAM_ID=ABCDE12345
APPLE_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
Make sure .env is in your .gitignore.
CI (GitHub Actions)
Store secrets in your repository settings: Settings → Secrets and variables → Actions.
Map them to env vars in your workflow:
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
The sign.toml contract
[macos.env]
apple-id = "APPLE_ID" # ← this is the env var NAME, not the value
team-id = "APPLE_TEAM_ID"
app-password = "APPLE_APP_PASSWORD"
If sign.toml contains what looks like an actual secret value instead of an env var name, cargo-codesign will reject it.
Rotation
- apple-id mode: Revoke and regenerate the app-specific password at account.apple.com. Update the
APPLE_APP_PASSWORDsecret. - api-key mode: Revoke the key in App Store Connect and create a new one. Update the
APPLE_NOTARIZATION_KEY,APPLE_NOTARIZATION_KEY_IDsecrets. (Issuer ID doesn’t change.) - Update signing keys: Generate a new keypair with
cargo codesign keygen, updateUPDATE_SIGNING_KEYin CI, and ship a new binary with the updated public key. Old signatures won’t verify against the new key — this is intentional.
CLI Reference
Global options
cargo codesign <COMMAND> [OPTIONS]
Options:
--config <PATH> Path to sign.toml [default: auto-discover]
--verbose Print subprocess commands and full output
--dry-run Validate and print what would be done, without executing
--json Machine-readable output
-h, --help Print help
-V, --version Print version
Commands
cargo codesign status
Validate credentials, certificates, and tool availability.
cargo codesign status
Checks all env vars and tools configured in sign.toml. Only checks credentials relevant to the configured auth mode. See Checking Your Setup.
cargo codesign macos
Sign, notarize, and staple macOS artifacts.
cargo codesign macos [OPTIONS]
Options:
--app <PATH> Path to .app bundle (full sign+DMG+notarize+staple chain)
--dmg <PATH> Path to existing DMG (codesign + notarize + staple)
--entitlements <PATH> Entitlements plist (overrides sign.toml)
--identity <STRING> Signing identity substring [default from config or "Developer ID Application"]
--skip-notarize Sign only, skip notarization
--skip-staple Skip stapling
--ci-import-cert CI: import base64 certificate into ephemeral keychain
--ci-cleanup-cert CI: delete ephemeral keychain from a previous import
Modes:
--app <PATH>: Sign app → create DMG → sign DMG → notarize → staple--dmg <PATH>: Sign existing DMG → notarize → staple- Neither: Discover binaries via
cargo metadata, sign each, copy totarget/signed/
cargo codesign windows
Sign Windows executables via Azure Trusted Signing.
cargo codesign windows [OPTIONS]
Options:
--install-tools Download Azure Trusted Signing tools via NuGet
cargo codesign linux
Sign a Linux artifact with cosign, minisign, or gpg.
cargo codesign linux [OPTIONS]
Options:
--archive <PATH> Archive to sign (required)
--method <METHOD> Override method from config: cosign, minisign, gpg
--output <PATH> Signature output path
See Linux Signing Guide.
cargo codesign keygen
Generate an Ed25519 keypair for update signing.
cargo codesign keygen [OPTIONS]
Options:
--output-private <PATH> [default: ./update-signing.key]
--output-public <PATH> [default: ./update-signing.pub]
cargo codesign update
Sign a release archive for in-app update verification (Ed25519).
cargo codesign update [OPTIONS]
Options:
--archive <PATH> Archive to sign (required)
--output <PATH> Signature output [default: <archive>.sig]
--key-env <STRING> Env var name for the signing key [default: UPDATE_SIGNING_KEY]
--public-key <PATH> Also verify with this public key after signing
cargo codesign verify
Verify a signed artifact or signature file.
cargo codesign verify <ARTIFACT> --method <METHOD> [OPTIONS]
Options:
--method <METHOD> Verification method: macos, windows, update, cosign, minisign, gpg
--signature <PATH> Explicit signature/bundle file (auto-detected if omitted)
--public-key <PATH> Public key for update/minisign verification
See Verifying Signatures.
cargo codesign ci
Generate CI workflow (GitHub Actions YAML) from sign.toml.
cargo codesign ci [OPTIONS]
Options:
--output <PATH> Output path [default: .github/workflows/release-sign.yml]
See Workflow Generation.
cargo codesign init
Create sign.toml with guided interactive prompts.
cargo codesign init
See Creating sign.toml.
Verifying Signatures
cargo codesign verify checks whether a signed artifact’s signature is valid.
Usage
cargo codesign verify <ARTIFACT> --method <METHOD> [OPTIONS]
Options
| Flag | Description |
|---|---|
--method <METHOD> | Verification method (required): macos, windows, update, cosign, minisign, gpg |
--signature <PATH> | Path to signature/bundle file. Auto-detected if omitted |
--public-key <PATH> | Public key file. Required for update and minisign methods |
Methods
macOS
Runs codesign --verify --deep --strict -vvv and spctl --assess on the artifact. No signature file needed — macOS code signatures are embedded. For .dmg files, Gatekeeper assessment uses --type open --context context:primary-signature; for .app bundles and binaries it uses --type execute.
cargo codesign verify MyApp.app --method macos
cargo codesign verify MyApp.dmg --method macos
Windows
Runs signtool verify /pa /v on the .exe. No signature file needed — Windows signatures are embedded.
cargo codesign verify myapp.exe --method windows
update (ed25519)
Verifies a detached ed25519 signature created by cargo codesign update.
cargo codesign verify release.tar.gz --method update --public-key update-signing.pub
Default signature file: <artifact>.sig
cosign
Verifies a Sigstore cosign bundle.
cargo codesign verify release.tar.gz --method cosign
Default signature file: <artifact>.bundle
minisign
Verifies a minisign signature.
cargo codesign verify release.tar.gz --method minisign --public-key minisign.pub
Default signature file: <artifact>.minisig
gpg
Verifies a GPG detached signature.
cargo codesign verify release.tar.gz --method gpg
Default signature file: <artifact>.sig
Auto-detection
When --signature is omitted, the signature path is derived from the artifact path:
| Method | Default signature path |
|---|---|
update, gpg | <artifact>.sig |
cosign | <artifact>.bundle |
minisign | <artifact>.minisig |
macos, windows | Not applicable (embedded signatures) |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Verification passed |
| 1 | Verification failed or file not found |
| 2 | Bad arguments (unknown method, missing required flag) |
| 3 | Platform mismatch (e.g. --method macos on Linux) |
sign.toml Reference
sign.toml is the configuration file for cargo-codesign. It maps platform signing settings to environment variable names.
File location
cargo-codesign looks for config in this order:
--config <PATH>flag (explicit)./sign.toml(project root)./.cargo/sign.toml(fallback)
If both ./sign.toml and ./.cargo/sign.toml exist, ./sign.toml wins and a warning is emitted.
Full example
# sign.toml — cargo-codesign configuration
# Generate with: cargo codesign init (coming soon)
[macos]
identity = "Developer ID Application"
entitlements = "entitlements.plist"
auth = "api-key" # "api-key" (CI) or "apple-id" (local/indie)
[macos.env]
# api-key mode
certificate = "MACOS_CERTIFICATE"
certificate-password = "MACOS_CERTIFICATE_PASSWORD"
notarization-key = "APPLE_NOTARIZATION_KEY"
notarization-key-id = "APPLE_NOTARIZATION_KEY_ID"
notarization-issuer = "APPLE_NOTARIZATION_ISSUER_ID"
# apple-id mode
apple-id = "APPLE_ID"
team-id = "APPLE_TEAM_ID"
app-password = "APPLE_APP_PASSWORD"
[windows]
timestamp-server = "http://timestamp.acs.microsoft.com"
[windows.env]
tenant-id = "AZURE_TENANT_ID"
client-id = "AZURE_CLIENT_ID"
client-secret = "AZURE_CLIENT_SECRET"
endpoint = "AZURE_SIGNING_ENDPOINT"
account-name = "AZURE_SIGNING_ACCOUNT_NAME"
cert-profile = "AZURE_SIGNING_CERT_PROFILE"
[linux]
method = "cosign" # cosign | minisign | gpg
[linux.env]
key = "COSIGN_PRIVATE_KEY"
[update]
public-key = "update-signing.pub"
[update.env]
signing-key = "UPDATE_SIGNING_KEY"
[status]
cert-warn-days = 60
cert-error-days = 7
Sections
[macos]
| Field | Type | Default | Description |
|---|---|---|---|
identity | string | "Developer ID Application" | Signing identity substring |
entitlements | path | none | Path to entitlements plist |
auth | "api-key" or "apple-id" | required | Notarization auth mode |
[macos.env]
Maps credential fields to environment variable names. Which fields are required depends on the auth mode:
apple-id mode:
| Field | Required | Description |
|---|---|---|
apple-id | yes | Env var for Apple ID email |
team-id | yes | Env var for team ID |
app-password | yes | Env var for app-specific password |
api-key mode:
| Field | Required | Description |
|---|---|---|
certificate | yes | Env var for base64 .p12 certificate |
certificate-password | yes | Env var for .p12 password |
notarization-key | yes | Env var for base64 .p8 API key |
notarization-key-id | yes | Env var for API key ID |
notarization-issuer | yes | Env var for issuer ID |
[windows]
| Field | Type | Default | Description |
|---|---|---|---|
timestamp-server | string | none | Timestamp server URL |
[windows.env]
| Field | Description |
|---|---|
tenant-id | Azure tenant ID |
client-id | Azure client ID |
client-secret | Azure client secret |
endpoint | Azure signing endpoint |
account-name | Azure signing account name |
cert-profile | Azure certificate profile |
[linux]
| Field | Type | Description |
|---|---|---|
method | "cosign", "minisign", or "gpg" | Signing method |
[linux.env]
| Field | Description |
|---|---|
key | Signing key env var |
[update]
| Field | Type | Description |
|---|---|---|
public-key | path | Path to public key file |
[update.env]
| Field | Description |
|---|---|
signing-key | Env var for base64 Ed25519 private key |
[status]
| Field | Type | Description |
|---|---|---|
cert-warn-days | integer | Warn when cert expires within N days |
cert-error-days | integer | Error when cert expires within N days |
Strict parsing
All sections use deny_unknown_fields — typos in field names cause a clear parse error rather than being silently ignored.
Environment Variables
cargo-codesign reads secret values exclusively from environment variables. The sign.toml file stores env var names, never values.
.env auto-loading
cargo-codesign uses dotenvy to auto-load a .env file from the current directory. This makes local development convenient:
# .env (never commit)
APPLE_ID=you@example.com
APPLE_TEAM_ID=ABCDE12345
APPLE_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
macOS — apple-id mode
| Env var (default name) | Description |
|---|---|
APPLE_ID | Apple ID email address |
APPLE_TEAM_ID | 10-character Apple Developer Team ID |
APPLE_APP_PASSWORD | App-specific password for notarization |
macOS — api-key mode
| Env var (default name) | Description |
|---|---|
MACOS_CERTIFICATE | Base64-encoded .p12 certificate |
MACOS_CERTIFICATE_PASSWORD | Password for the .p12 file |
APPLE_NOTARIZATION_KEY | Base64-encoded .p8 App Store Connect API key |
APPLE_NOTARIZATION_KEY_ID | API key ID (from App Store Connect) |
APPLE_NOTARIZATION_ISSUER_ID | Issuer ID (from App Store Connect) |
Windows
| Env var (default name) | Description |
|---|---|
AZURE_TENANT_ID | Azure AD tenant ID |
AZURE_CLIENT_ID | Azure AD client/application ID |
AZURE_CLIENT_SECRET | Azure AD client secret |
AZURE_SIGNING_ENDPOINT | Azure Trusted Signing endpoint URL |
AZURE_SIGNING_ACCOUNT_NAME | Trusted Signing account name |
AZURE_SIGNING_CERT_PROFILE | Certificate profile name |
Linux
| Env var (default name) | Description |
|---|---|
COSIGN_PRIVATE_KEY | Cosign private key (for cosign method) |
Update signing
| Env var (default name) | Description |
|---|---|
UPDATE_SIGNING_KEY | Base64-encoded Ed25519 private key |
Custom env var names
All env var names are configurable in sign.toml. The names above are conventions — you can use any name:
[macos.env]
apple-id = "MY_CUSTOM_APPLE_ID_VAR"
Exit Codes
| Code | Meaning | Example |
|---|---|---|
0 | Success | Signing completed, all checks passed |
1 | Signing failed | Credential rejected, tool error, Apple/Azure rejection |
2 | Configuration error | Missing sign.toml, bad TOML syntax, missing required section |
3 | Prerequisite missing | codesign not found, wrong platform (e.g., cargo codesign macos on Linux) |
4 | Validation failed | cargo codesign status found one or more failing checks |
Usage in CI
- name: Check signing setup
run: cargo codesign status
# Exits 4 if any check fails — CI step fails
- name: Sign
run: cargo codesign macos --app "MyApp.app"
# Exits 1 on signing failure
# Exits 2 if sign.toml is broken
Distinguishing errors
Exit code 2 (config error) means the problem is in your sign.toml or CLI flags — fix the config. Exit code 1 (signing failed) means the config is valid but the operation failed — check credentials, network, or Apple’s status page.