Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .app bundles and bare binaries with codesign
  • 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 .app bundles from raw binaries. That’s cargo-bundle, cargo-packager, or your own script. However, when given a .app via --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:

LayerWhatPurposeTool
OS TrustmacOS codesign + notarize, Windows signtoolOS runs the binary without warning/blockingcargo codesign macos, cargo codesign windows
Update IntegrityEd25519 signature over the release archiveIn-app updater verifies authenticitycargo codesign update
Store SigningApp Store, Microsoft Store submissionStore distributionOut 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

Installation

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:

PlatformRequired toolsHow to get them
macOScodesign, xcrun, hdiutilXcode Command Line Tools: xcode-select --install
Windowssigntool.exeWindows SDK
Linuxcosign or minisignSee 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:

  1. Which platforms? — macOS, Windows, Linux, Update signing
  2. Auth mode (macOS) — apple-id for local/indie, api-key for CI
  3. 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, and hdiutil are on your PATH (macOS). Windows checks for signtool.exe.
  • Only checks credentials relevant to your configured auth mode — api-key mode checks different vars than apple-id mode.

Exit codes

CodeMeaning
0All checks passed
2Configuration error (missing sign.toml, bad format)
4One 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:

  1. Code signing — digitally sign your binary with a Developer ID certificate so macOS recognizes it as trusted
  2. Notarization — submit the signed artifact to Apple’s servers for automated malware scanning
  3. 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:

  1. A Developer ID Application certificate — see Setting Up Credentials
  2. A built .app bundle — cargo-codesign does not build or bundle. See below.
  3. 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

ModeCommandWhat it does
App modecargo codesign macos --app "MyApp.app"Full chain: sign app → create DMG → sign DMG → notarize → staple
Bare binary modecargo codesign macosDiscover 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

  1. Join the Apple Developer Program (99 EUR/year) at developer.apple.com/programs

  2. 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 .certSigningRequest file
  3. Upload to Apple Developer Portal:

  4. Install the certificate:

    • Double-click the .cer file to import into Keychain Access
    • Verify: security find-identity -v -p codesigning

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:

ModeBest forCredentials needed
apple-idLocal development, indie devsApple ID + app-specific password
api-keyCI, teams, automationApp Store Connect API key (.p8)

apple-id mode

  1. Go to account.apple.com → Sign-In and Security → App-Specific Passwords
  2. Generate a password labeled “cargo-codesign” (or similar)
  3. 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

  1. Go to appstoreconnect.apple.com/access/integrations/api
  2. Create a new key with Developer access
  3. Download the .p8 file (you can only download it once)
  4. Note the Key ID and Issuer ID
  5. 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:

  1. Every binary in Contents/MacOS/
  2. Every dylib and framework in Contents/Frameworks/
  3. The .app bundle 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

  1. Runs cargo metadata --format-version 1 --no-deps to find all binary targets
  2. Looks for each binary in target/release/
  3. Signs each with codesign --force --timestamp --options runtime
  4. 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 .app bundles 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 .app bundle 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 email
  • APPLE_TEAM_ID — your 10-character team ID
  • APPLE_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 .p12 certificate (for CI keychain import)
  • MACOS_CERTIFICATE_PASSWORD — password for the .p12
  • APPLE_NOTARIZATION_KEY — base64-encoded .p8 API key
  • APPLE_NOTARIZATION_KEY_ID — the key ID from App Store Connect
  • APPLE_NOTARIZATION_ISSUER_ID — the issuer ID from App Store Connect

Which mode should I use?

ScenarioRecommended mode
Local dev on your Macapple-id
Solo developer, simple CIapple-id
Team with shared CIapi-key
Rotating credentialsapi-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 .app bundle structure is correct (binary in Contents/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:

  1. Make sure the keychain is unlocked before signing
  2. The security set-key-partition-list call must include codesign: in the -S flag
  3. 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.toml matches. 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:

  1. Load sign.toml and read the [windows] section
  2. Discover binaries via cargo metadata
  3. Generate metadata.json for Azure Trusted Signing
  4. Sign each .exe with signtool.exe using SHA-256 + timestamp
  5. 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.

CredentialEnv varWhere to find it
Tenant IDAZURE_TENANT_IDAzure Portal > Azure Active Directory > Overview > Tenant ID
Client IDAZURE_CLIENT_IDAzure Portal > App registrations > your app > Application (client) ID
Client SecretAZURE_CLIENT_SECRETAzure 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.

CredentialEnv varWhere to find it
EndpointAZURE_SIGNING_ENDPOINTAzure Portal > Trusted Signing > your account > Overview > Endpoint
Account nameAZURE_SIGNING_ACCOUNT_NAMEThe name you chose when creating the Trusted Signing account
Certificate profileAZURE_SIGNING_CERT_PROFILEAzure 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-latest GitHub runners.
  • Azure.CodeSigning.Dlib.dll — install with cargo codesign windows --install-tools or manually via nuget 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

  1. Reads the archive bytes
  2. Signs with the Ed25519 private key from the UPDATE_SIGNING_KEY env var
  3. 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

  1. Build release archive
  2. cargo codesign update --archive release.tar.gz --public-key update-signing.pub
  3. Upload both release.tar.gz and release.tar.gz.sig to your release
  4. 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:

SecretDescription
MACOS_CERTIFICATE_BASE64Base64-encoded .p12 certificate
MACOS_CERTIFICATE_PASSWORDPassword for the .p12
APPLE_IDYour Apple ID email (for apple-id auth)
APPLE_TEAM_IDYour 10-character team ID
APPLE_APP_PASSWORDApp-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-cert reads the certificate env var names from sign.toml, base64-decodes the certificate, creates an ephemeral keychain, and imports it. No shell needed.
  • --ci-cleanup-cert deletes the ephemeral keychain created by --ci-import-cert. Runs if: always() so cleanup happens even if signing fails. Safe to call when no keychain exists (logs a warning, exits 0).
  • cargo codesign macos --app handles the full sign → DMG → notarize → staple chain.
  • The env var names (MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD) come from your sign.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

FlagDefaultDescription
--output <PATH>.github/workflows/release-sign.ymlOutput path for the generated YAML
--config <PATH>auto-discovered sign.tomlPath to sign.toml

What gets generated

For each platform configured in sign.toml, the workflow creates a job on the appropriate runner:

  • macOSmacos-latest
  • Windowswindows-latest
  • Linuxubuntu-latest

Each job:

  1. Installs cargo-codesign
  2. Runs cargo codesign status to verify credentials
  3. 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_PASSWORD secret.
  • api-key mode: Revoke the key in App Store Connect and create a new one. Update the APPLE_NOTARIZATION_KEY, APPLE_NOTARIZATION_KEY_ID secrets. (Issuer ID doesn’t change.)
  • Update signing keys: Generate a new keypair with cargo codesign keygen, update UPDATE_SIGNING_KEY in 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 to target/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

See Windows Signing Guide.

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

FlagDescription
--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:

MethodDefault signature path
update, gpg<artifact>.sig
cosign<artifact>.bundle
minisign<artifact>.minisig
macos, windowsNot applicable (embedded signatures)

Exit codes

CodeMeaning
0Verification passed
1Verification failed or file not found
2Bad arguments (unknown method, missing required flag)
3Platform 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:

  1. --config <PATH> flag (explicit)
  2. ./sign.toml (project root)
  3. ./.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]

FieldTypeDefaultDescription
identitystring"Developer ID Application"Signing identity substring
entitlementspathnonePath to entitlements plist
auth"api-key" or "apple-id"requiredNotarization auth mode

[macos.env]

Maps credential fields to environment variable names. Which fields are required depends on the auth mode:

apple-id mode:

FieldRequiredDescription
apple-idyesEnv var for Apple ID email
team-idyesEnv var for team ID
app-passwordyesEnv var for app-specific password

api-key mode:

FieldRequiredDescription
certificateyesEnv var for base64 .p12 certificate
certificate-passwordyesEnv var for .p12 password
notarization-keyyesEnv var for base64 .p8 API key
notarization-key-idyesEnv var for API key ID
notarization-issueryesEnv var for issuer ID

[windows]

FieldTypeDefaultDescription
timestamp-serverstringnoneTimestamp server URL

[windows.env]

FieldDescription
tenant-idAzure tenant ID
client-idAzure client ID
client-secretAzure client secret
endpointAzure signing endpoint
account-nameAzure signing account name
cert-profileAzure certificate profile

[linux]

FieldTypeDescription
method"cosign", "minisign", or "gpg"Signing method

[linux.env]

FieldDescription
keySigning key env var

[update]

FieldTypeDescription
public-keypathPath to public key file

[update.env]

FieldDescription
signing-keyEnv var for base64 Ed25519 private key

[status]

FieldTypeDescription
cert-warn-daysintegerWarn when cert expires within N days
cert-error-daysintegerError 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_IDApple ID email address
APPLE_TEAM_ID10-character Apple Developer Team ID
APPLE_APP_PASSWORDApp-specific password for notarization

macOS — api-key mode

Env var (default name)Description
MACOS_CERTIFICATEBase64-encoded .p12 certificate
MACOS_CERTIFICATE_PASSWORDPassword for the .p12 file
APPLE_NOTARIZATION_KEYBase64-encoded .p8 App Store Connect API key
APPLE_NOTARIZATION_KEY_IDAPI key ID (from App Store Connect)
APPLE_NOTARIZATION_ISSUER_IDIssuer ID (from App Store Connect)

Windows

Env var (default name)Description
AZURE_TENANT_IDAzure AD tenant ID
AZURE_CLIENT_IDAzure AD client/application ID
AZURE_CLIENT_SECRETAzure AD client secret
AZURE_SIGNING_ENDPOINTAzure Trusted Signing endpoint URL
AZURE_SIGNING_ACCOUNT_NAMETrusted Signing account name
AZURE_SIGNING_CERT_PROFILECertificate profile name

Linux

Env var (default name)Description
COSIGN_PRIVATE_KEYCosign private key (for cosign method)

Update signing

Env var (default name)Description
UPDATE_SIGNING_KEYBase64-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

CodeMeaningExample
0SuccessSigning completed, all checks passed
1Signing failedCredential rejected, tool error, Apple/Azure rejection
2Configuration errorMissing sign.toml, bad TOML syntax, missing required section
3Prerequisite missingcodesign not found, wrong platform (e.g., cargo codesign macos on Linux)
4Validation failedcargo 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.