App Sandbox를 켜면 손쉬운 사용 권한을 받을 수 없다

이 글은 영어판도 있습니다.

메뉴 막대 앱이 시스템 설정의 손쉬운 사용 목록에 아예 나타나지 않았습니다. 오류도 없고, 권한 요청 창도 안 뜨고, 켜줄 항목조차 없었습니다. TCC 데몬이 로그에 실제로 뭐라고 남기는지, 그리고 원인을 하나로 좁힌 대조 실험을 정리했습니다.

지금 글을 입력하고 있는 앱에서 텍스트 캐럿 위치를 읽어오는 작은 메뉴 막대 유틸리티를 만들고 있었습니다. 시스템 전역 요소를 대상으로 AXUIElementCopyAttributeValue를 호출해야 하는 일이라, 사용자가 손쉬운 사용 권한을 켜줘야 동작합니다.

그래서 앱을 띄우면서 흔히 하는 대로 권한을 요청했습니다.

let trusted = AXIsProcessTrustedWithOptions(
    [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)

아무 반응이 없었습니다. 권한 요청 창이 뜨지 않았습니다. 더 이상한 건 시스템 설정 → 개인정보 보호 및 보안 → 손쉬운 사용 목록에 앱이 아예 없다는 점이었습니다. 켜줄 스위치 자체가 없으니 손쓸 방법이 없었습니다. + 버튼으로 앱을 직접 끌어다 넣어봐도 마찬가지였습니다.

검색하면 나오는 답은 전부 "시스템 설정에서 직접 추가하세요"의 변형이었습니다. 추가가 되는 앱에나 통하는 얘기입니다. 제 앱은 추가 자체가 안 됐습니다.

추측 대신 TCC 로그를 읽었다

macOS에서 권한을 판단하는 건 tccd입니다. 그리고 이 데몬은 통합 로그에 꽤 자세히 남깁니다. 가장 쓸모 있었던 명령은 이겁니다.

log show --last 5m --style compact --predicate 'process == "tccd"' \
  | grep -i "<번들 ID>"

로그 두 줄에 답이 다 있었습니다. 첫 줄은 샌드박스든 아니든 실행할 때마다 나옵니다.

tccd: AUTHREQ_CTX: msgID=..., service=kTCCServiceAccessibility, preflight=yes
tccd: Service kTCCServiceAccessibility does not allow prompting; returning Unknown

손쉬운 사용은 권한 요청 창을 띄우지 않습니다. macOS 26에서는 AXIsProcessTrustedWithOptions에 prompt 옵션을 줘도 손쉬운 사용에 대해서는 대화상자가 나오지 않습니다. 앱을 어떻게 빌드했든 똑같습니다. 요청이 tccd까지 가긴 하고, Unknown을 받아서 돌아옵니다.

창이 안 뜬 이유는 이걸로 풀렸습니다. 목록에 아예 없는 이유는 아직 남아 있습니다. 그건 두 번째 줄이 설명해주는데, 이 줄은 제가 만든 빌드 중 딱 하나에서만 나왔습니다.

tccd: Publishing <TCCDEvent: type=Modify,
      service=kTCCServiceAccessibility,
      identifier_type=Bundle ID,
      identifier=com.example.myapp> to 0 subscribers

TCC가 이 앱을 손쉬운 사용 목록에 올릴 레코드를 만드는 순간입니다. 이 이벤트가 없으면 목록에 항목이 안 생기고, 켤 것도 없습니다.

변수를 하나만 바꿔 원인을 좁혔다

처음 만든 비샌드박스 빌드는 한 번에 세 가지를 바꿨습니다. 샌드박스를 껐고, get-task-allow를 뺐고, Debug에서 Release로 옮겼습니다. 목록에 항목이 생기긴 했는데 셋 중 뭐가 결정적이었는지 알 수 없었습니다. get-task-allow도 의심할 만했습니다. 디버그 빌드라는 표시이고, TCC가 서명에 까다롭다는 건 알려진 얘기니까요.

그래서 변수를 하나만 남기고 다시 했습니다. 두 빌드 모두 Release, 모두 애드혹 서명, 모두 CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO를 줘서 어느 쪽에도 get-task-allow가 들어가지 않게 했고, 같은 경로에 설치했습니다. 실행 전에는 기존 레코드를 지워 출발선을 맞췄습니다.

tccutil reset Accessibility com.example.myapp

차이는 com.apple.security.app-sandbox 하나뿐이었습니다.

빌드샌드박스get-task-allowTCC 레코드 생성
A없음생성됨 (실행 0.03초 후)
B없음생성 안 됨 (18초 관찰)

빌드 B도 AUTHREQ_CTX를 똑같이 남겼고, does not allow prompting도 똑같이 남겼습니다. 요청은 했다는 뜻입니다. TCC가 레코드를 안 만들어줬을 뿐입니다.

앱스토어냐 손쉬운 사용이냐

샌드박스가 켜진 앱은 손쉬운 사용 권한을 받을 수 없습니다. 애초에 후보로 등록이 안 되기 때문입니다. 서명 문제도 아니고, 추가하면 되는 엔타이틀먼트가 있는 것도 아니고, 사용자가 목록에 끌어다 넣어서 우회할 수 있는 것도 아닙니다.

맥 앱스토어는 샌드박스를 요구합니다. 그래서 실무에서는 여기서 길이 갈립니다.

맥 앱스토어직접 배포
App Sandbox필수선택
손쉬운 사용 API사용 불가사용 가능
서명Mac App DistributionDeveloper ID + 공증

다른 앱의 화면을 읽어야 하는 앱이라면, 그러니까 캐럿 위치나 선택한 텍스트나 창 내용이 필요하다면, 앱스토어 밖에서 배포하게 됩니다. 둘 다 가져가는 방법은 없습니다.

시작하기 전에 알아둘 두 가지

애드혹 서명은 빌드할 때마다 권한을 잃는다

TCC는 코드 서명으로 앱을 알아봅니다. 애드혹 서명에는 바이너리 자신의 해시가 들어가기 때문에, 다시 빌드하면 다른 앱이 되고 사용자가 준 허가가 더는 붙지 않습니다. 개발 중에는 됐다 안 됐다 하는 버그처럼 보입니다.

자체 서명 인증서를 만들면 해결됩니다. 지정 요구사항이 바이너리 대신 인증서를 가리키게 됩니다.

designated => identifier "com.example.myapp"
              and certificate leaf = H"80645c1d..."

다시 빌드해도 요구사항이 그대로라 허가가 살아 있습니다. 인증서를 신뢰 목록에 넣을 필요도 없습니다. 로컬 빌드라면 codesign -s에 SHA-1 해시를 넘기는 것으로 충분합니다.

안 되는 기능은 실행 중에 감지해서 숨긴다

같은 코드베이스에서 두 가지 빌드를 낸다면, 앱이 자기가 어느 쪽인지 알아채고 못 하는 기능은 감추는 편이 낫습니다. 눌러도 아무 일이 없는 메뉴 항목은 없느니만 못합니다.

func isSandboxed() -> Bool {
    if ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil {
        return true
    }
    return NSHomeDirectory().contains("/Library/Containers/")
}

정리

  • 손쉬운 사용은 권한 요청 창을 띄우지 않습니다. macOS 26 기준으로 사용자가 직접 켜야 합니다.
  • 샌드박스 앱은 손쉬운 사용 목록에 등록조차 안 되니 켤 수도 없습니다.
  • 같은 Release 빌드에 서명도 같게 두고 샌드박스만 켜고 꺼서 확인했습니다.
  • 설정 화면을 보고 추측하지 말고 통합 로그에서 tccd를 보세요.
  • 개발 중에는 서명 신원을 고정해두세요. 아니면 허가가 계속 사라집니다.

Comments

Popular posts from this blog

What ArduPilot SITL catches, and where it stops telling the truth

App Sandbox silently blocks Accessibility permission