macOS Keychain attaches an Access Control List (ACL) to each stored credential. For unsigned binaries, the ACL records the binary's path and hash. Every recompilation changes the hash, causing macOS to prompt "foundry-dev-tools-oauth wants to access your keychain" on first access after every update.
Windows Credential Manager does not gate access by code signing identity -- it uses the logged-in user session. Code signing on Windows is only about SmartScreen reputation and user trust.
Keychain does not match by file path or binary hash. It stores a Designated Requirement (DR) extracted from the binary's code signature. For a Developer ID-signed binary, the DR looks like:
identifier "com.yourcompany.foundry-dev-tools-oauth-cli"
and anchor apple generic
and certificate leaf[subject.OU] = "K1234ABCDE"
This checks:
- The code signing identifier (the
--identifier/-ivalue passed tocodesign) - The certificate chain is rooted in Apple's CA
- The leaf certificate is a Developer ID Application certificate
- The Team ID (
subject.OU) matches
The DR does not depend on the binary hash, file path, file size, modification date, or specific certificate serial number. This means any future build signed with the same identifier and Team ID is granted Keychain access silently.
- Changing the code signing identifier
- Switching to a different Apple Developer account (different Team ID)
- Distributing an unsigned binary
- Switching certificate types (e.g., Developer ID to ad-hoc)
| Item | Notes |
|---|---|
| Apple Developer Program | $99/year, mandatory, no free path |
| Developer ID Application certificate | Created in Apple Developer portal, valid 5 years |
| Stable code signing identifier | Must never change once users store Keychain items |
| Notarization | Required for Gatekeeper since macOS 10.15 |
| Item | Notes |
|---|---|
| OV code signing certificate | $200-500/year |
| Hardware-backed private key | Required since June 2023 (FIPS 140-2 Level 2) |
| Cloud HSM (Azure Key Vault or SSL.com eSigner) | Physical USB tokens impractical for CI |
This value is permanent. Changing it breaks Keychain access for all existing users.
com.palantir.foundry-dev-tools-oauth-cli
- Enroll in the Apple Developer Program at https://developer.apple.com ($99/year)
- Go to Certificates, Identifiers & Profiles
- Create a Developer ID Application certificate
- Generate a CSR via Keychain Access on a Mac
- Upload CSR, download the
.cer, import into Keychain Access - Export as
.p12(select cert + private key, right-click, "Export 2 items") - Base64-encode for GitHub Secrets:
base64 -i DeveloperIDApplication.p12 -o cert_base64.txt
- Generate an app-specific password at https://appleid.apple.com (for notarization)
- Create an Azure Key Vault instance (Premium tier, ~$1/month)
- Generate a certificate request with RSA-HSM key type
- Submit CSR to a CA (DigiCert, Sectigo, SSL.com) for an OV code signing certificate
- Merge the issued certificate back into Key Vault
- Create an Azure AD service principal with Sign + Get permissions on the vault
| Secret | Value |
|---|---|
MACOS_CERTIFICATE |
Base64-encoded .p12 file |
MACOS_CERTIFICATE_PWD |
Password used when exporting .p12 |
MACOS_CERTIFICATE_NAME |
"Developer ID Application: Name (TEAMID)" |
APPLE_ID |
Apple ID email address |
APPLE_TEAM_ID |
10-character Team ID from developer.apple.com |
APPLE_APP_PASSWORD |
App-specific password from appleid.apple.com |
KEYCHAIN_PWD |
Any random password (for temporary CI keychain) |
| Secret | Value |
|---|---|
AZURE_KEY_VAULT_URI |
https://your-vault.vault.azure.net |
AZURE_CLIENT_ID |
Service principal client ID |
AZURE_TENANT_ID |
Azure AD tenant ID |
AZURE_CLIENT_SECRET |
Service principal secret |
AZURE_CERT_NAME |
Certificate name in Key Vault |
jobs:
build-macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64-apple-darwin, aarch64-apple-darwin]
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: rustup target add ${{ matrix.target }}
- name: Import code signing certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
working-directory: libs/oauth-cli
- name: Sign binary
run: |
codesign --sign "${{ secrets.MACOS_CERTIFICATE_NAME }}" \
--force \
--timestamp \
--options runtime \
--identifier "com.palantir.foundry-dev-tools-oauth-cli" \
target/${{ matrix.target }}/release/foundry-dev-tools-oauth
working-directory: libs/oauth-cli
- name: Verify signature
run: |
codesign --verify --verbose=2 \
target/${{ matrix.target }}/release/foundry-dev-tools-oauth
codesign -d -r- \
target/${{ matrix.target }}/release/foundry-dev-tools-oauth
working-directory: libs/oauth-cli
- name: Notarize binary
run: |
xcrun notarytool store-credentials "notary-profile" \
--apple-id "${{ secrets.APPLE_ID }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--password "${{ secrets.APPLE_APP_PASSWORD }}"
cd libs/oauth-cli
zip -j foundry-dev-tools-oauth-${{ matrix.target }}.zip \
target/${{ matrix.target }}/release/foundry-dev-tools-oauth
xcrun notarytool submit \
foundry-dev-tools-oauth-${{ matrix.target }}.zip \
--keychain-profile "notary-profile" \
--wait
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: foundry-dev-tools-oauth-${{ matrix.target }}
path: libs/oauth-cli/foundry-dev-tools-oauth-${{ matrix.target }}.zipjobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --release
working-directory: libs/oauth-cli
- name: Sign binary with AzureSignTool
run: |
dotnet tool install --global AzureSignTool
AzureSignTool sign `
-kvu "${{ secrets.AZURE_KEY_VAULT_URI }}" `
-kvi "${{ secrets.AZURE_CLIENT_ID }}" `
-kvt "${{ secrets.AZURE_TENANT_ID }}" `
-kvs "${{ secrets.AZURE_CLIENT_SECRET }}" `
-kvc "${{ secrets.AZURE_CERT_NAME }}" `
-tr http://timestamp.digicert.com `
-td sha256 `
libs/oauth-cli/target/release/foundry-dev-tools-oauth.exejobs:
build-linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu]
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --release --target ${{ matrix.target }}
working-directory: libs/oauth-cli
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: foundry-dev-tools-oauth-${{ matrix.target }}
path: libs/oauth-cli/target/${{ matrix.target }}/release/foundry-dev-tools-oauth| Flag | Purpose |
|---|---|
--sign / -s |
Signing identity (full cert CN or Team ID hash) |
--force / -f |
Replace any existing signature |
--timestamp |
Embed secure timestamp (required for notarization, ensures signature validity after cert expiry) |
--options runtime |
Enable Hardened Runtime (required for notarization) |
--identifier / -i |
Code signing identifier in reverse-DNS format |
xcrun notarytool submitrequires a.zip,.pkg, or.dmg-- not a bare Mach-O binary- Processing typically takes 1-5 minutes, can take 30+ minutes
--waitflag polls until complete- Stapling is not possible for bare Mach-O binaries --
xcrun stapleronly works on.app,.pkg,.dmg - First-run Gatekeeper verification requires an internet connection (since stapling is not possible)
- To enable offline verification, distribute inside a
.pkgand staple the ticket to the.pkg - Check failure details:
xcrun notarytool log <submission-id> --keychain-profile "notary-profile"
- Developer ID Application certificates are valid for 5 years
- Renewal does not change the Team ID (it's tied to the Apple Developer account)
- Keychain ACLs continue to work after renewal without user prompts
- Update the
MACOS_CERTIFICATEGitHub Secret with the new.p12 - Azure Key Vault credentials should be rotated every 90 days
The apple-codesign Rust crate provides rcodesign, an open-source reimplementation of Apple code signing and notarization that runs on Linux. This could allow signing on cheaper Linux runners instead of macOS runners. Still requires the Apple Developer Program membership for the certificate.
brew install does not apply the quarantine attribute to binaries, which means Gatekeeper does not check the binary and Keychain prompts are handled differently. This is how gh, ripgrep, and rustup handle macOS distribution without code signing. If Homebrew is the primary distribution channel, code signing becomes less urgent (but still recommended for direct downloads).
| Item | Cost | Frequency | Required? |
|---|---|---|---|
| Apple Developer Program (Individual) | $99 | Annual | Yes (macOS Keychain) |
| Windows OV code signing certificate | $200-500 | Annual | No |
| Azure Key Vault (Premium) | ~$12 | Annual | Only for Windows |
| Minimum (macOS only) | $99 | Annual | |
| Both platforms | $310-610 | Annual |
- Federico Terzi: macOS code signing in GitHub Actions
- PaperAge: Notarizing Rust CLI binaries
- Apple: Notarizing macOS software
- Tauri: macOS signing docs
- apple-actions/import-codesign-certs
- taiki-e/upload-rust-binary-action
- Windows code signing with EV cert on GitHub Actions
- Gregory Szorc: Open-source Apple code signing
- Apple TN2206: Code Signing In Depth