macOS 코드 서명과 공증 삽질기

macOS 코드 서명과 공증 삽질기

Go로 만든 CLI 도구를 배포하려고 했다. 내 Mac에서는 잘 되는데, 다른 사람 Mac에서는 “개발자를 확인할 수 없습니다” 경고가 뜬다. macOS Gatekeeper 때문이다.

이걸 해결하려면 코드 서명(Code Signing)과 공증(Notarization)이 필요하다.


문제 상황

동적 라이브러리를 런타임에 로드하는 Go 프로그램이 있었다.

// CGO로 dlopen 호출
lib := C.dlopen(C.CString(libPath), C.RTLD_NOW)

로컬에서 잘 되길래 codesign으로 서명하고 배포했더니:

Error: failed to load library: library not found or symbols missing

서명하기 전에는 되던 게 서명하니까 안 된다?


원인: Hardened Runtime

macOS 10.14+부터 공증을 받으려면 Hardened Runtime이 필수다.

# 공증 요구사항
codesign --options runtime ...

Hardened Runtime은 보안을 위해 여러 기능을 제한한다:

서명 없이는 dlopen()이 작동하는데, Hardened Runtime으로 서명하면 차단된다.


해결: Entitlements

Entitlements 파일로 특정 권한을 명시적으로 요청할 수 있다.

<!-- entitlements.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 서명되지 않은 동적 라이브러리 로딩 허용 -->
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>

    <!-- DYLD 환경 변수 허용 (디버깅용, 선택) -->
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
</dict>
</plist>

서명할 때 이 파일을 포함:

codesign --force \
    --options runtime \
    --entitlements entitlements.plist \
    --sign "Developer ID Application: Your Name" \
    ./build/myapp

이제 동적 라이브러리 로딩이 다시 작동한다.


코드 서명 전체 과정

1. 인증서 확인

security find-identity -v -p codesigning

“Developer ID Application” 인증서가 필요하다. Apple Developer Program ($99/년) 가입 필수.

2. 서명

codesign --force \
    --options runtime \
    --entitlements entitlements.plist \
    --sign "Developer ID Application: Your Name (TEAM_ID)" \
    ./build/myapp

3. 서명 확인

# 서명 검증
codesign --verify --verbose ./build/myapp

# 서명 정보
codesign -dvv ./build/myapp

# entitlements 확인
codesign -d --entitlements - ./build/myapp

공증(Notarization)

코드 서명만으로는 부족하다. Apple 서버에 바이너리를 제출해서 “공증”을 받아야 한다.

1. App-Specific Password 생성

https://appleid.apple.com → “App-Specific Passwords” → 생성

2. 자격 증명 저장

xcrun notarytool store-credentials "notary-profile" \
    --apple-id "[email protected]" \
    --team-id "YOUR_TEAM_ID" \
    --password "xxxx-xxxx-xxxx-xxxx"

3. 공증 제출

# ZIP으로 패키징
ditto -c -k --keepParent ./build/myapp myapp.zip

# 제출 (--wait으로 완료까지 대기)
xcrun notarytool submit myapp.zip \
    --keychain-profile "notary-profile" \
    --wait

보통 2-5분 정도 걸린다.

4. 확인

# Gatekeeper 검증
spctl -a -vv -t install ./build/myapp

# 성공 시 출력
# accepted
# source=Notarized Developer ID

Stapling의 함정

공증이 완료되면 “stapling”을 하라고 한다. 공증 티켓을 바이너리에 첨부하는 것.

xcrun stapler staple myapp.zip

문제: 단일 바이너리는 stapling이 안 된다.

The staple and validate action failed! Error 73.

Stapling은 .app 번들, .pkg, .dmg에만 가능하다. 단일 바이너리는 ZIP으로 배포하면 된다. macOS가 자동으로 Apple 서버에서 공증 상태를 확인한다.


GitHub Actions 자동화

매번 수동으로 하기 귀찮으니 자동화.

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - name: Import Certificates
        uses: Apple-Actions/import-codesign-certs@v2
        with:
          p12-file-base64: $
          p12-password: $

      - name: Build and Sign
        env:
          APPLE_SIGNING_IDENTITY: $
        run: |
          go build -o build/myapp .
          codesign --force --options runtime \
              --entitlements entitlements.plist \
              --sign "$APPLE_SIGNING_IDENTITY" \
              ./build/myapp

      - name: Notarize
        env:
          APPLE_ID: $
          APPLE_TEAM_ID: $
          APPLE_PASSWORD: $
        run: |
          ditto -c -k --keepParent build/myapp myapp.zip
          xcrun notarytool submit myapp.zip \
              --apple-id "$APPLE_ID" \
              --team-id "$APPLE_TEAM_ID" \
              --password "$APPLE_PASSWORD" \
              --wait

GitHub Secrets 설정

Secret 내용
CERTIFICATES_P12 인증서 .p12 파일 (base64)
CERTIFICATES_P12_PASSWORD .p12 암호
APPLE_SIGNING_IDENTITY Developer ID Application: ...
APPLE_ID Apple ID 이메일
APPLE_TEAM_ID 팀 ID (10자리)
APPLE_PASSWORD App-Specific Password

.p12 파일 생성:

# 키체인에서 인증서 내보내기
security export -k ~/Library/Keychains/login.keychain-db \
    -t identities -f pkcs12 -o certs.p12

# base64 인코딩
base64 -i certs.p12 | pbcopy

삽질 정리

문제 원인 해결
서명 후 dlopen 실패 Hardened Runtime disable-library-validation entitlement
stapling 실패 단일 바이너리 미지원 ZIP으로 배포
공증 거부 서명 안 됨 or 잘못된 서명 --options runtime 확인
CI에서 서명 실패 인증서 없음 import-codesign-certs 액션

결론

macOS 배포는 생각보다 복잡하다:

  1. Apple Developer Program 가입 ($99/년)
  2. “Developer ID Application” 인증서 생성
  3. Entitlements 파일 작성 (동적 로딩 필요 시)
  4. codesign --options runtime --entitlements ...
  5. notarytool submit --wait
  6. ZIP으로 배포

동적 라이브러리를 로드하는 프로그램이라면 entitlements를 잊지 말 것.

Hardened Runtime + 동적 로딩 = entitlements 필수

Back