macOS에서 Rust 앱을 배포하기 위한 코드 서명·노타리제이션·스테이플링 과정을 셸 스크립트 대신 `cargo-codesign` 한 명령으로 처리하는 방법을 소개한다.
Rust로 GUI 앱을 만들었다. 컴파일된다. 내 컴퓨터에서는 실행된다. 바이너리를 친구에게 보내면, 친구가 더블클릭하는 순간 macOS가 말한다: "Apple이 이 앱을 확인할 수 없습니다." 우클릭 → 열기 → 무서운 대화상자 확인 — 그러면 실행은 된다. 하지만 이건 출시 경험이 아니다.
App Store 밖에서 macOS 애플리케이션을 배포할 때, 사용자가 경고 없이 실행하려면 세 가지가 반드시 필요하다:
.app 번들 안의 모든 바이너리는 Developer ID 인증서로 서명되어야 한다.이 중 하나라도 건너뛰면 Gatekeeper가 앱을 차단한다. App Store는 이 모든 걸 투명하게 처리한다. 스토어 밖에서는 당신 문제다.
세부사항이 놀랄 만큼 까다롭다. .app만 서명하면 끝이 아니다 — 그 안에 중첩된 모든 바이너리와 프레임워크까지 전부 서명해야 한다. 앱을 감싸는 DMG도 자체 서명이 필요하다. 노타리제이션에는 두 가지 인증 모드(API 키 vs. Apple ID)가 있고, 각각 자격 증명 형식이 다르다. 그리고 스테이플링은 가장 바깥 컨테이너에만 동작한다.
JPEG Locker를 만들면서, 나는 이런 모서리 케이스를 전부 밟았다. 기존 생태계는 대략 이렇다:
rcodesign — Apple 코드 서명을 순수 Rust로 재구현한 도구. Apple이 아닌 호스트(예: Linux CI)에서 macOS 바이너리를 서명해야 한다면 여길 보라. 다만 노타리제이션에는 App Store Connect API Key가 필요하다 — 로컬 키체인이나 apple-id 인증은 지원하지 않는다. 나는 로그인 키체인에 있는 Developer ID 인증서로 로컬에서 전체 파이프라인이 동작하고, CI에서도 같은 명령을 그대로 쓰길 원했다.cargo-bundle — .app 번들은 만들지만 서명은 하지 않는다.cargo-packager — 설치 프로그램과 .app 번들을 생성하지만 서명이나 노타리제이션은 하지 않는다. Tauri의 번들러는 서명을 하긴 하지만, 자체 빌드 파이프라인 내부에서만 가능하다.나는 Zed, Lapce, 그리고 다른 Rust GUI 프로젝트들이 이걸 어떻게 처리하는지 살펴봤다. 그들 대부분은 codesign, xcrun notarytool, hdiutil, stapler를 올바른 순서로 올바른 플래그와 함께 호출하는 커스텀 셸 스크립트를 유지한다. 스크립트는 작지만 중요한 부분에서 서로 다르다 — 자격 증명 처리, 오류 복구, 어떤 산출물을 어떤 단계에서 서명하는지 등.
이건 cargo 서브커맨드로 한 번만 해결되어야 하는 문제처럼 느껴졌다.
cargo-codesigncargo-codesign은 그 스크립트들을 단일 명령으로 대체한다. 빌드 결과로 .app 번들이 생성된 뒤:
cargo codesign macos --app "target/release/bundle/My App.app"
출력:
[1/5] Codesigning .app bundle...
✓ App signed
[2/5] Creating DMG...
✓ DMG created: target/release/bundle/My App.dmg
[3/5] Codesigning DMG...
✓ DMG signed
[4/5] Notarizing DMG...
✓ Notarized
[5/5] Stapling...
✓ Stapled
✓ Done: target/release/bundle/My App.dmg
다섯 단계, 한 명령. 전체 체인: 내부 바이너리 서명 → .app 서명 → DMG 생성 → DMG 서명 → 노타리제이션 → 스테이플.
.app 번들이 없는 CLI 도구라면 --app 플래그를 빼면 된다. cargo-codesign은 cargo metadata로 워크스페이스 바이너리를 찾아 각각 서명하고, target/signed/로 복사한다.
cargo-codesign은 재구현이 아니라 오케스트레이터다. 플랫폼의 네이티브 도구(codesign, xcrun notarytool, signtool.exe, cosign)를 올바른 순서와 올바른 플래그로 호출한다. 빌드하거나 번들링하거나 릴리스 파이프라인을 대체하지 않는다 — 그 다음 단계인 “서명”을 처리한다.
설정은 프로젝트 루트의 sign.toml에 둔다. 대화형으로 하나 생성할 수 있다:
cargo codesign init
이 마법사는 어떤 플랫폼을 타깃으로 하는지, 어떤 인증 모드를 사용할지 묻는다. 그리고 설정을 생성하고, 환경에 어떤 자격 증명이 이미 있는지도 확인한다:
✓ 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
cargo-codesign book에는 전체 설정이 정리되어 있다: Developer ID 인증서를 내보내는 방법, 노타리제이션용 앱 전용 비밀번호를 만드는 방법, 어떤 Apple Developer Program에 가입해야 하는지, API 키와 Apple ID 인증 중 무엇을 선택해야 하는지 등.
내 컴퓨터에서 동작하는 같은 명령이 GitHub Actions에서도 동작한다. cargo-codesign은 환경 변수에서 자격 증명을 읽는다 — 이름은 sign.toml에서 설정되므로, CI에서는 시크릿을 매핑하기만 하면 된다:
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
sign.toml을 바탕으로 готов된 GitHub Actions 워크플로를 생성하는 cargo codesign ci도 있다.
어떤 서명 단계에 들어가기 전이든 cargo codesign status를 실행해 모든 도구와 자격 증명이 준비되어 있는지 검증할 수 있다. 그러면 노타리제이션에 8분을 기다린 뒤 “시크릿이 없다”는 말을 듣는 대신, 실행 가능한 메시지와 함께 빠르게 실패한다.
이 도구는 Windows(signtool.exe를 통한 Azure Trusted Signing)와 Linux(cosign, minisign, gpg)도 다룬다. 다만 이들은 macOS 파이프라인보다 개발 초기 단계다 — 거친 부분이 있을 것이고, 개발자 경험도 아직 내가 원하는 수준이 아니다. 검증을 거친 경로는 macOS 쪽이다: JPEG Locker는 오늘도 이걸로 배포되고 있다.
또한 OS 수준의 신뢰와 무관하게, 모든 플랫폼에서 동작하는 순수 Rust 기반의 앱 내 업데이트 검증이 필요한 프로젝트를 위해 Ed25519 업데이트 서명 기능(cargo codesign keygen + cargo codesign update)도 제공한다.
cargo install cargo-codesign