コマンドラインでmacOSアプリのNotarization(公証) [Xcode11以降]

コマンドラインでmacOSアプリのnotarization(公証)を行う方法を説明します。JenkinsなどのCIで実行することを前提にしています。ネットの少し古い情報ではコマンドラインのオプション名が異なっていたり、パスワードの管理の扱いが異なっているので、2020年1月に確認した情報を記載しています。

# macOSのGateKeeperとNotarization(公証)の必須化

2020年2月3日以降、macOS 10.15 Catalinaでは「公証(Notarization)」を受けていないアプリの起動ができなくなります。以前のmacOSバージョンでもサードパーティアプリの起動を制限する仕組み（GateKeeper）が段階的に強化されてきましたが、エンドユーザに注意喚起をしたり、確認のために余計な手順を踏ませるものでした。今回初めて、デベロッパ側で事前作業を行わない限り強制的に起動できなくするという強硬手段が取られたと言えます。

[Update to Notarization Prerequisites](https://developer.apple.com/news/?id=12232019a)

### コマンドラインでNotarizationが必要になる場合

notarizationに必要な手順はXcodeに統合されているので簡単に行えますが、何らかの都合でXcodeの仕組みが使えない場合はコマンドラインで手順を踏む必要があります。

- QtやElectronなどのサードパーティアプリケーションフレームワークで作成されたアプリで最終段階でXcodeを使っていない
- 過去にビルドしたアプリのバイナリはあるが、再ビルドが難しい

# macOSのNotarization（公証）とは

GateKeeperの仕組みを使ってエンドユーザがアプリを起動する際にアプリの改ざんがされていないことを確認します。また、署名の際にdeveloper.apple.comのアカウントと紐付けされているので、アプリの作成者をトレース（追跡）することができます。

# コマンドラインでNotarizationを行う手順

なんらかの手段でローカルで起動可能なアプリケーション(.app)まで用意されているとします。

> 0. 証明書の用意
> 0. macOSアプリの署名
> 0. App-specificパスワードの用意
> 1. （オプション）App-specificパスワードのKeyChainへの登録
> 2. 署名済みアプリのApple Notary Serviceへのアップロード
> 3. （オプション）Notarization Ticketをアプリに付与する

## Notarizationのための証明書の用意

developer.apple.comでnotarization用の証明書を作成します。"Certificates, Identifiers & Profiles"の項目でCertificatesを選択して、＋マークを押して証明書を追加します。"Create a New Certificate"のSoftwareの中から適切なTYPEを選びます。
いわゆる野良アプリの場合は"Developer ID Application"(This certificate is used to code sign your app for distribution outside of the Mac App Store.)を選択します。

証明書ができたら、ダウンロードして、ローカルのKeyChainに登録します。

## macOSアプリの署名

macOSアプリのコード(.appバンドル)を署名します。

```
$ codesign --deep --force --verify --verbose --sign "CERTIFICATE" --option runtime SampleApp.app
SampleApp.app: signed app bundle with Mach-O thin (x86_64) [com.sample.XXXXXXXXXXX]
```
CERTIFICATEには先ほどKeyChainに登録した証明書の名前を入れます。上と同じTYPEにした場合、"Developer ID Application: XXXXXXXXXX"という名前になります。

ここの"--option runtime"がキモで、アプリの実行時にランタイム強化(hardened runtime)が指定されます。これによりコードハイジャックやDLL乗っ取りなどコードの同一性を保証できなくするクラックから保護されます。一方で自己書き換えなどのhackyな挙動するアプリが動作しなくなります。JavaアプリのJITなどもできなくなりますので、注意が必要です。
公式情報 → [Hardened Runtime Entitlements](https://developer.apple.com/documentation/security/hardened_runtime_entitlements)

無事、コード署名されたか確認するためには以下のようにします。

```
$ codesign --verify --verbose=4 SampleApp.app
--prepared:/Users/foo/Desktop/SampleApp.app/Contents/MacOS/adb
--validated:/Users/foo/Desktop/SampleApp.app/Contents/MacOS/adb
（略）
SampleApp.app: valid on disk
SampleApp.app: satisfies its Designated Requirement
```

## App-specificパスワードを作成して登録する

developer.apple.comは２段階認証が必須になったので、通常のIDとパスワードのみではコマンドラインからアップロードできません。そこでアップロード用にサブのパスワードを作ります。

以下の手順にしたがってアップロードするコマンドaltool用に「App用パスワード」（英名：App-specific password）を作成します。
公式情報 → [App 用パスワードを使う](https://support.apple.com/en-al/HT204397)

作成時にラベルを指定できるので、Notarization用に作ったことをわかりやすいラベル名をつけます。

## （オプション）App-specificパスワードのKeyChainへの登録（Xcode11以降）

上で作ったApp-specificパスワードをコマンドラインのオプションに直接書くこともできますが、Jenkinsなどのビルドプロセスに組み込む場合、パスワードを平文でスクリプトに書くことになるので、セキュリティ上よろしくありません。
そのため、パスワードをKeyChainに登録できるようになりました。

```
$ xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "AC_USERNAME" -p SECRET_PASSWORD
```
AC_PASSWORDはKeyChainのキーの名前なのでそのままAC_PASSWORDでよいです。AC_USERNAMEにはdeveloper.apple.comのIDを入力します。SECRET_PASSWORDは上で作成したApp-specificパスワードを入れます。

## Apple Notary Serviceへのアップロード

### ProviderShortNameの確認

秘密キーが（apple.developer.comの）単一のTEAM IDに関連付けされている場合は必要ないのですが、企業などで開発している場合は複数である場合が多いので、識別するためにアップロード先のProviderShortNameを確認する必要があります。apple.developer.comのMembershipの項目からTeam IDで確認できるほか、コマンドでも一覧を表示できます。

先ほどKeyChainに登録した場合は以下のコマンドになります。

```
$ xcrun altool --list-providers -u "AC_USERNAME" -p "@keychain:AC_PASSWORD"
```
AC_USERNAMEにはdeveloper.apple.comのIDを入力します。
KeyChainを利用しなかった場合は、-p以下を省略するとパスワードプロンプトが表示されますので、App-specificパスワードを入力します。

### アップロード

AppleのNotary Serverへアップロードします。アップロードするためにアプリケーション(.app)をzipやdmgでパッケージングします。

```
$ xcrun altool --notarize-app --file SampleApp.zip --primary-bundle-id "BUNDLE_ID" -u "AC_USERNAME" -p "@keychain:AC_PASSWORD" --asc-provider PROVIDER_SHORT_NAME
No errors uploading 'SampleApp.zip'.
RequestUUID = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
BUNDLE_IDにはアプリケーションのBundle Idetifierを入れます。AC_USERNAMEにはdeveloper.apple.comのIDを入れます。PROVIDER_SHORT_NAMEは上で調べたProviderShortNameを使います。
成功した場合、RequestUUIDが表示されます。

以上で、notarization自体の手順は終了です。

### 確認


正しくアップロードされたかを確認します。上のRequestUUIDが表示されます。

```
$ xcrun altool --notarization-history 0 -u "AC_USRENAME" -p "@keychain:AC_PASSWORD" --asc-provider PROVIDER_SHORT_NAME

Notarization History - page 0

Date RequestUUID Status Status Code Status Message
------------------------- ------------------------------------ ------- ----------- ----------------
2020-01-28 08:15:19 +0000 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx success 0 Package Approved
 
Next page value: XXXXXXXXXXXXXX
```

## （オプション）Notarization Ticketをアプリに付与する

前項でnotarizationは終わっていますが、配布先のマシンがオフラインの場合でもGataKeeperが認証できるようにするために、アプリ自体にTicketを追加することができます。

```
$ xcrun stapler staple SampleApp.app
Processing: /Users/foo/Desktop/SampleApp.app
Processing: /Users/foo/Desktop/SampleApp.app
The staple and validate action worked!
```
zipファイルには対応していないので、アプリケーション(.appバンドル)自体を指定する必要があります。上記でTicket付与した後、配布するために再度zipなどでパッケージングする必要があります。

以上。

# 参考情報

ちょっと古めの情報はそのまま使えなかったので、結局のところ公式情報を見るのがおすすめです。

- 基本情報： https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution
- コマンドラインで実行する場合の補足情報：  https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow
- トラブルシューティング：  https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/resolving_common_notarization_issues