Alacritty fork に署名と公証を追加

/ , ,
簡体中文日本語
要約:
友人の Mac に警告が出ないよう、Alacritty fork の GitHub Actions に macOS の署名と公証を組み込みました。

以前、Alacritty を fork し、便利なのに上流のメンテナが取り込もうとしないパッチをいくつかマージしました。上下左右を個別に設定できる padding、選択範囲のずれの修正、Big Sur 風のアイコンなどです。さらに自分用の変更も加えました。

以前の CI はビルド成果物を出力するだけで、署名も公証もしていませんでした。友人に送ると、相手の Mac に、Apple がマルウェアの有無を確認できないため開けない、という警告が表示されました。毎回手動で許可するのは不便です。

ちょうど有料の開発者アカウントを持っていたので、ビルドしたアプリの署名と公証を CI 内で済ませることにしました。途中でいろいろつまずいたので、ここに記録しておきます。具体的なコードは PR にあります。

署名と公証

署名は、開発者証明書を使ってアプリの発行元を示し、署名後に改変されていないか確認できるようにするものです。公証は、署名済みのアプリを Apple に提出し、自動スキャンで問題がなければ結果が記録される仕組みです。macOS の Gatekeeper は、インターネットからダウンロードしたアプリを開く際に、署名と公証を確認します。両方をそろえることで、先ほどの警告を避けられます。

CI 内でビルド、署名、公証を順に行い、最後に DMG にまとめてアップロードします。

Mermaid diagram

CI で証明書をインポート

署名には Developer ID Application 証明書が必要です。まずローカルのキーチェーンから証明書と秘密鍵を .p12 形式で書き出し、base64 に変換してリポジトリの Secrets に保存します。用意したのは、証明書本体、.p12 のパスワード、一時キーチェーンのパスワードの3つです。

CI の実行ごとに一時キーチェーンを作り、証明書をインポートします。使い終わったら、そのキーチェーンを削除します。

YAML
- name: Install Apple certificate
  env:
    BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
    P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
    KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
  run: |
    CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
    KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
    echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
    security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
    security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
    security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
    security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
    security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
    security list-keychain -d user -s $KEYCHAIN_PATH
    security default-keychain -s $KEYCHAIN_PATH

この中の set-key-partition-list の行は欠かせません。これがないと、codesign が秘密鍵を使う際に macOS が許可を求めるダイアログを表示します。CI では誰もボタンを押せないので、ビルドが止まったままになります。(痛い教訓😭)

ジョブの最後にはクリーンアップも入れ、if: always() を指定して、前の処理が成功しても失敗しても一時キーチェーンを削除するようにしました。

また、ビルド前に security find-identity -v -p codesigning も追加しました。利用できる署名 ID がログに出力されるので、デバッグに役立ちます。

Makefile を編集

Alacritty の Makefile は、.app をパッケージする際に codesign --sign - で ad hoc 署名を行います。これは証明書を使わない署名で、Gatekeeper が信頼する Developer ID の署名にはなりません。そこで、自分の証明書を使うように変更しました。

MAKE
@codesign --force --deep --entitlements $(APP_ENTITLEMENTS) --options runtime --timestamp --sign "Developer ID Application: Bojin Li (TEAMID)" -v "$(APP_DIR)/$(APP_NAME)"

ここには、公証に関係する3つのオプションがあります。--options runtime は Hardened Runtime を有効にし、--timestamp は Apple のタイムスタンプを付けます。この2つは公証に必須で、どちらかが欠けると拒否されます。--entitlements は権限を宣言するファイルを指定します。

また、Info.plist の CFBundleIdentifier を、上流の org.alacritty から自分用の xxxbrian.alacritty に変更しました。

Entitlements

Hardened Runtime を有効にしたアプリで、カメラ、マイク、連絡先、カレンダー、位置情報、写真などの保護されたリソースを使うには、対応する entitlement を署名時に宣言する必要があります。宣言がなければ、アクセスは拒否されます。

ターミナルは少し特殊です。ターミナル自体がこれらのリソースを使わなくても、内部で動くプログラムが使うことはあります。たとえば、カメラを使うコマンドラインツールです。このとき権限はターミナル側のものとして扱われます。宣言しないと、そうしたツールが私の Alacritty 内でリソースにアクセスできません。そのため、macos-entitlement.plist を追加し、よく使う権限をまとめて宣言しました。

XML
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<key>com.apple.security.personal-information.calendars</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>

公証と staple

署名済みのユニバーサル .app を公証に出します。notarytool の呼び出しを自分で書く代わりに、lando/notarize-action を使いました。Apple ID、アプリ用パスワード、Team ID を渡せば利用できます。アプリ用パスワードは appleid.apple.com で別途生成するもので、Apple ID 自体のパスワードは使えません。

公証が通ったら、公証チケットをアプリに添付します。この手順を staple と呼び、私は BoundfoxStudios/action-xcode-staple を使いました。チケットを添付しておくと、オフラインでもアプリが公証済みであることを検証できます。

YAML
- name: Make Universal
  run: |
    security find-identity -v -p codesigning
    make app-universal
- name: Notarize Release Build
  uses: lando/notarize-action@v2
  with:
    product-path: "./target/release/osx/Alacritty.app"
    appstore-connect-username: ${{ secrets.NOTARIZATION_USERNAME }}
    appstore-connect-password: ${{ secrets.NOTARIZATION_PASSWORD }}
    appstore-connect-team-id: TEAMID
    verbose: true
- name: Staple Release Build
  uses: BoundfoxStudios/action-xcode-staple@v1
  with:
    product-path: "./target/release/osx/Alacritty.app"
- name: Make DMG
  run: make dmg-universal

DMG の作成

一番手間がかかったのは、最後の DMG でした。

Alacritty の Makefile では、DMG のターゲットが .app のターゲットに依存しています。そのため、make dmg-universal を実行すると、.app が再ビルドされ、ファイルのコピーや署名もやり直されます。しかし、署名、公証、staple は前の手順ですでに終わっています。

最初は、.app に依存しない dmg-universal-exist というターゲットを追加し、既存の .app をそのままパッケージしようと考えました。ところが、書いたルールは元のものとほぼ同じで、依存関係も残ったまま。これでは何の効果もありません。結局そのターゲットを削除し、make dmg-universal に戻しました。PR の最後の fix: corrected DMG creation process というコミットは、この変更です。

Important

ここで記録しているのは当時の PR の最終状態で、推奨するパッケージ手順ではありません。make dmg-universal はアプリのビルドと署名を再実行するため、前の手順で添付した公証チケットが残っているとは限りません。署名、公証、staple 済みのアプリを再ビルドせずにパッケージする方が確実です。公開前には、最終 DMG から実際に配布するアプリを取り出し、文末のコマンドで検証してください。

最後に

リリースの流れは、タグの push から始まります。CI が macOS 上で x86 と ARM の2種類をビルドし、ユニバーサルバイナリにまとめ、自分の証明書で署名します。その後、公証、staple、DMG の作成を経て、GitHub Release にアップロードします。

出来上がったアプリは、ローカルで検証できます。

BASH
codesign -dv --verbose=4 Alacritty.app
spctl -a -vv Alacritty.app
xcrun stapler validate Alacritty.app

コマンド spctl が accepted を出力し、source=Notarized Developer ID と表示されれば、Gatekeeper の検査に通ったことを確認できます。

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...