본문 바로가기
C.W.K.
Stream
Lesson 03 of 06 · published

일부러 쓰는 자체 서명 신원

~16 min · bundle-signing, self-signed, code-signing-identity, tcc, keychain

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"자체 서명한 로컬 신원은 커밋해도 돼. 애플이 믿어주는 인증서는 절대 안 돼."

이 방식이 푸는 문제

애드혹 서명한 앱은 requirement가 자기 해시로 돼 있어서, 다시 빌드할 때마다 TCC와 키체인 입장에선 새 앱이야. 손쉬운 사용 권한을 또 묻고, 키체인 항목의 "항상 허용"은 절대 안 남아. 만든 Mac 밖으로 나갈 일 없는 Mac 앱에 대해 가족이 내린 답은 자체 서명한 코드 서명 신원 하나야. 네이티브 앱, 런처, 권한 쓰는 헬퍼가 전부 같이 쓰고, 빌드하는 모든 Mac의 로그인 키체인에 들어가 있어. 그걸로 서명하면 앱의 designated requirement가 identifier "…" and certificate leaf = H"…"가 돼. 다시 빌드해도 같고, 같은 인증서로 서명하는 모든 Mac에서 같아.

왜 애플 인증서가 아니냐면

애플 Developer ID 인증서도 안정적인 requirement를 만들어 주고, 다른 사람에게 배포하려면 그 인증서가 필요해(이 트랙 마지막 레슨에서 다뤄). 근데 애플이 믿어주고, Gatekeeper가 받아주고, 공증까지 되는 인증서는 자격 증명이야. 유출되면 누군가 macOS가 네 서명으로 믿는 소프트웨어를 내보낼 수 있어. 자체 서명 신원은 그걸 믿는 Mac 바깥에선 아무것도 못 열어. Gatekeeper는 거부하고, 계정도 서버도 열리지 않아. 대신 그 Mac들 안에선 TCC 권한과 키체인 접근 목록이 가리키는 게 바로 이 신원이야. 그래서 개인 키를 가진 사람은 같은 식별자로 서명한 바이너리에 그 권한을 그대로 물려받게 할 수 있어. 결국 피해 범위는 비공개 레포에 더해 이 Mac들에서 코드를 돌릴 수단까지 가진 경우인데, 둘 다 가진 사람은 그 권한이 지키는 걸 이미 손에 쥔 거야. 가족이 자체 서명 신원은 비공개 레포에 두면서, 신뢰받는 인증서는 어디에도 절대 커밋하지 않는 이유가 이거야.

치르는 값, 솔직하게

  • Gatekeeper가 안 믿어줘. 앱은 인증서가 깔려 있고 네가 직접 실행하는 Mac에서만 돌아. 남의 Mac에선 안 돼.
  • codesign이 키를 처음 쓸 때 Mac마다 키체인이 "codesign wants to use key …"라고 한 번 물어봐. Mac마다 한 번씩 항상 허용을 눌러.
  • 앱을 애드혹에서 안정적인 신원으로 옮긴 뒤엔 옛 권한을 한 번 초기화해(tccutil reset Accessibility com.example.spark). 옛 cdhash에 묶인 권한은 새 requirement와 안 맞거든. 한 번만 다시 허용하면 그 뒤로는 계속 유지돼.
  • 인증서를 다시 만들면 leaf 해시가 바뀌어서, 모든 Mac이 한 번씩 다시 허용하고 모든 앱을 다시 빌드해야 해.

신원 찾기의 함정

security find-identity -v -p codesigning으로 신원을 확인하는 빌드 스크립트는 신원이 없다고 잘못 결론 내려. -v는 신뢰할 수 있는 루트로 이어지는 유효한 신원만 남기거든. 자체 서명 신원은 CSSMERR_TP_NOT_TRUSTED로 보고돼서 목록에서 빠져. 그러니 -v를 빼. 가족 받아쓰기 앱은 이 함정을 찾아내기 전까지, 이것 하나 때문에 애드혹 서명에 머물면서 빌드할 때마다 권한을 다시 물었어.

Code

자체 서명 코드 서명 신원 만들고, 가져오고, 찾기(가져오기는 Mac마다 한 번)·bash
# Create (once, then keep the .p12 in your PRIVATE repository):
openssl req -x509 -newkey rsa:2048 -sha256 -nodes \
  -keyout spark-signing.key -out spark-signing.crt -days 3650 \
  -subj "/CN=Spark Local Signing" \
  -addext "basicConstraints=critical,CA:false" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=critical,codeSigning"
# OpenSSL 3 (Homebrew openssl@3): macOS's /usr/bin/openssl is LibreSSL and rejects -legacy.
# macOS 'security import' needs the legacy MAC algorithm and a non-empty password:
openssl pkcs12 -export -legacy -macalg sha1 \
  -inkey spark-signing.key -in spark-signing.crt \
  -name "Spark Local Signing" -out spark-signing.p12 -passout pass:local-import
rm spark-signing.key

# Import on each build Mac:
security import spark-signing.p12 -k ~/Library/Keychains/login.keychain-db \
  -P local-import -T /usr/bin/codesign

# Detect it — WITHOUT -v, which hides untrusted identities:
security find-identity -p codesigning | grep '"Spark Local Signing"'
#   1) 3B81...  "Spark Local Signing" (CSSMERR_TP_NOT_TRUSTED)

# After the first stable-signed build of an app that used to be ad-hoc:
tccutil reset Accessibility com.example.spark

External links

Exercise

위 명령으로 Spark용 자체 서명 코드 서명 신원을 만들어서 로그인 키체인에 가져와. security find-identity -v -p codesigning엔 안 나오고 -v 없는 명령엔 나오는지 확인해. SPARK_SIGN_IDENTITY가 그 신원을 가리키게 하고, verify의 인증서 검사를 되살려서 빌드 스크립트를 돌려. 새 designated requirement를 적어. 이어서 소스를 바꾸고 다시 빌드해서 requirement 문자열이 똑같은지 확인해.
Hint
첫 서명 때 키체인 창이 뜨면 항상 허용을 골라. codesign이 errSecInternalComponent로 실패하면 아마 ssh로 돌리고 있는 거야. 서명엔 GUI 로그인 세션이 필요하고, 그건 다음 트랙 레슨에서 다뤄. 명령은 OpenSSL 3로 돌려. /usr/bin/openssl version이 LibreSSL을 찍으면 pkcs12 -legacy가 "unknown option"으로 멈춰.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.