Electronで作ったアプリをGitHub ActionsでMac向けにリリースするときの署名と公証について

TechAcademyでメンターをしています

この情報は随時変わっている可能性があります

投稿時での最新情報を検証して記載していますが、パッケージ側のバージョンアップなどに伴い、随時状況が変わっている可能性がありますので、必ず公式情報もご確認ください。

また、問題のない環境でテストした上で実運用することをおすすめします。

ご無沙汰していますね。すっかりブログを書いている時間が取れないというか、アレコレ書きたいと思いつつもさっぱり…という感じでした。むぅ。

前々からではありますが、Electronを使ったアプリ開発をしていて、自分の業務が楽になるようなツールを作ったりしていました。今回、その中の1つを他人様にも公開しようと思い、いろいろと試行錯誤したのですが、各所の情報通りでは上手くいかなかったりしたので、書き残しておこうと思います。

作った物の説明

作った物はVue-CLIとElectronを使った物です。

元々はjQueryを使って書いていたのですが冗長な部分も多く、別にツールを作る中でもVueに触れたことから、手習いとしてVueに書きなおすという作業をやってみようということで、リファクタリングという名の完全リライトを行った格好です。

ツールの役割としては、特定のお仕事をしている方に向けて、必要な情報を入れるとスケジュールを生成し、Googleカレンダーに登録してくれるというものです。

今回のツールの構成

以下の構成で作っていきました。

  • Vue-CLI: v4 系
  • Vue: 3.0.0
  • Electron: vue-cli-plugin-electron-builder を利用して v12 系
  • vuex: 4.0.0-0
  • vue-router: 4.0.0-0
  • electron-notarize: 1.0.0
  • electron-updater: 4.3.8
  • electron-log: 4.3.2

今回のポイント

毎度ビルドしてアップロードするのは避けたいので、CIでビルドをかけたい。そうすればWindows版も同時にビルドできるはずなので、スムーズなリリースができるはず。

旧バージョンではTravis CIを利用していたのですが、折角GitHub Actionsが登場しているので、そちらに移行してスムーズさを増すことができないかという考えで移行することに。

認証を通さないと、Macでの起動ができなくなってきてしまうので、署名をするのは大前提。しかし、他人様に利用頂くのであれば、起動時に「安全なソフトか確認出来ませんでした」という確認が入るのは避けたい。ということで、公証が必要になりました。

公証を通すことにより、初回起動時の確認は「このアプリはgithub.comからダウンロードされました。起動してよろしいですか?」という感じの確認程度になります。

公証とは

調べるといくらでも情報は出てくると思うのですが、いきなりここを見たという方も居るかもしれないのでその必要性などについてお話を簡単に。

署名というのは、アプリがどの開発者によって作られた物かを示す物を付けるイメージです。コレを通していないと、起動するのが結構面倒になります。もう署名はするのが前提と考えた方が良いでしょう。Apple Developerに登録しないと署名できませんので、年間12,000円程度支払ってデベロッパー登録しましょう。

そして、公証。コレは今回私が調べている中で初めて知ったのですが、今はAppleがアプリをチェックして悪意のあるコードが含まれていないかという事を確認します。コレが通ると、問題なく起動できるアプリとして処理され、アプリにもそのデータが含まれるようになります。

これはリリースビルドごとに必要で、チェックをかけている時間に5分くらいかかっている印象があります。その分Windowsビルドより時間がかかりますね。

Windows版を署名しないことにした理由

Windows版については署名の取得のために25,000円くらいかかるので諦めました。そこまでかけるほどリリースするのかというのと、使って頂く範囲を考えて、起動するまでの手間を考えてもMac程では無いと言うことがあり、今回はスルーすることにしました。

とりあえずやってみた

いろいろな情報をかき集めてやってみました。

しかし、軒並み古い情報で、上手くいかないという状態…困ったもんです。公式から「Readmeが古いんや、すまんやで」って書いてあったりする情報が出ていたりして、「だったらReadme直してよぉ!」ってなったりしました。

試行錯誤でやってみた結果が以下の通りです。

実際の作業手順

electron-notarizeを入れる

Electronの公証にはelectron-notarizeを使います。

インストールは、プロジェクトのディレクトリ内で

# npm 
npm install electron-notarize --save-dev
 
# yarn 
yarn add electron-notarize --dev

とすれば終わりです。シンプル。

entitlements.mac.plistを用意する

Electronアプリで使っている機能について「コレ使ってます」と主張するファイル(という認識)です。

最低でも1つ入れないといけない物があると言う事で、以下のような記述をします。

<?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.allow-unsigned-executable-memory</key>
    <true/>
  </dict>
</plist>

このファイルは、entitlements.mac.plistという名前でプロジェクト内の buildディレクトリに入れます。無い場合にはこのディレクトリを作りましょう。

ここでハマリポイント1が

上記で作った entitlements.mac.plist ですが、ポイントとして改行コードなどが間違っているとエラーになります。

コレをチェックしてくれるのが

plutil -lint <plistファイル>

というコマンドになります。エラーがあると教えてくれるので直しましょう。改行コードはLFじゃないとダメみたいです。

このままelectron-notarizeを使おうとしてもダメ

普通に調べて行くと、この後electron-notarizeを使うスクリプトを書いて…となるのですが、それで上手くいくのはローカルな環境の時。GitHub Actionsではそう一筋縄ではいきません。

ローカル環境ではデベロッパーアカウントに関するログインを済ませていたりするので、あまり気にならないのですが、公証をかけるに当たってはキーチェーンのログイン情報が必要になるようです。

キーチェーンに登録…といわれてもローカル環境ではGUIを使う方法ばかり。どうしたら良いのー!?ってなったところでコマンドがあります。

xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u <APPLEID> -p <APPLEIDPASS>

これで AC_PASSWORDというキーチェーンができて、ログインに対して使う事ができる様になるのです。つまり、このコマンドをGitHub Actionsに加えてあげる事になります。GitHub Actionsの環境は、都度構成されて使用後破棄されるので、都度やって上げる必要があると言う事です。

パスワードの扱いにも注意

と、焦ってここでIDやパスワードを直接書いては行けません。情報ダダ漏れです。

この様な、直接プログラム内に書くべきでは無い情報を扱う方法としてsecretsという方法が用意されています

コレはGitHub Actionsの中で呼び出してあげる事で環境変数とすることができる仕組みです。環境変数にしてあげれば、後はスクリプト内で引っ張り出すだけですね。

引っ張りだす方法は後述するとして、secretに指定した文字列はlogの中でもマスク(非表示に)され、自分でも見ることができなくなります。再設定する際も見ることができないので、一回きりで見た目からは消えてしまうと言う事に注意しましょう。

二段階認証をかけている方はそれ以外にも注意点があります。それは自分で手打ちするパスワードが使えないと言う点です。必ずApple IDのページでApp用パスワードを生成し、それを利用します。こちらも生成するとその場からすぐに(見た目上)消えてしまうので、注意が必要です。

先にWorkflowを作ってしまおう

スクリプト書いてからにしろよという意見もありそうですが、コレは双方向で関連してくるので、先に環境変数周りの事を済ませてしまうことで、イメージしやすくしてみようと思います。

まずWorkflowは何かと言う事をお話ししておくと、GitHub Actionsに対しての命令を一式書いたYAML形式ファイルです。ファイルの位置だけ決まっていて、プロジェクトルートから見て、.github/workflowsに配置する必要があります。その中にファイルを置きますが、ファイル自体の名前は自由です。YAMLファイルなので拡張子を .ymlなどにしておきましょう。

YAMLファイルの構造としては、

  1. ワークフローの名前を指定する
  2. このワークフローを実行する条件を指定する
  3. 処理の塊(jobs)を書く
    1. jobsはいくつもかける(今回の例だとWindows向けとMac向けに分ける)
    2. jobsごとに環境を指定する必要がある
    3. jobsの中で環境変数を指定することができる
    4. jobsの中の処理の順番をstepsに分けて書くことができる
    5. stepsに対して名前を付けることになる
    6. stepsに対してuseを指定すると、GitHub Actions Marketplaceで公開されているスクリプトを使うこともできる
    7. 勿論、スクリプトを書くこともできる

こんな感じの構造です。実際に書いた物がこちらです。

name: GitHub Release

on:
  push:
    tags:
      # "v" で始まる Tag 名の push だけをトリガーの対象にします (e.g., "v0.0.2")
      - "v*"

jobs:
  # macOS 用の job 定義
  build_on_mac:
    runs-on: macos-latest
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js 15.x
        uses: actions/setup-node@v1
        with:
          node-version: 15.x
      - name: npm install
        run: npm install
      # code signing
      - name: apple codesigning
        uses: apple-actions/import-codesign-certs@v1
        with:
          p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
          p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
      # ここでアプリをビルド
      - name: build application
        env:
          APPLEID: ${{ secrets.APPLEID }}
          APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
          ASC_PROVIDER: ${{ secrets.ASC_PROVIDER }}
        run: |
          xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u $APPLEID -p $APPLEIDPASS
          echo ${{ secrets.TCE_GOOGLE_CLIENT_SECRET }} | base64 -d > src/json/client_secret.json
          npm run electron:release --mac

  # Windows 用の job 定義
  build_on_win:
    runs-on: windows-latest
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js 15.x
        uses: actions/setup-node@v1
        with:
          node-version: 15.x
      - name: npm install
        run: npm install
      - name: build application
        run: |
          echo ${{ secrets.TCE_GOOGLE_CLIENT_SECRET }} | base64 -d > src/json/client_secret.json
          npm run electron:release --win

ちょっとプログラムに対して必要なGoogle APIのClient Secretを書き込む流れとかも書かれちゃっているのですが、重要なのはこのあたりです。

      # code signing
      - name: apple codesigning
        uses: apple-actions/import-codesign-certs@v1
        with:
          p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
          p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}

この部分ではコードサイン(=署名)の前準備をしています。

そして、

    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        env:
          APPLEID: ${{ secrets.APPLEID }}
          APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
          ASC_PROVIDER: ${{ secrets.ASC_PROVIDER }}

この2カ所が環境変数を定義しているところですね。

${{ secrets.SECRETS_NAME }}とすることで、Workflowファイル内でもsecretsを呼び出すことができます。コレを環境変数に設定しているのが env: のセクションで、普通に<ENV_NAME>: <ENV_VALUE>の形式で列挙すれば全て環境変数になります。

ちなみに私はここで最後にカンマが付いていたせいでエラーになりました。YAMLはそういうの要らないんですね。クセで付けちゃったのでした。

ということで、この様に書いたと言う事は、secretsの設定が必要ですね。

ここでハマリポイント2

上記の環境変数に入れたい物で登場したASC_PROVIDER ですが、Team Shortnameなどと書かれている物ばかりで、中にはApple Developersのマイページ的な所からコピーすると良いなんて事が書いてあったりするのですが、それでもうまくは行きません。

これ、取得するのにコマンドが必要です。ローカル環境で以下のコマンドを実行します。(IDやパスワードは自分の物に置き換えましょう。パスワードは先に生成したAPP用パスワードが良いです。)

xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "<APPLE DEV ID>" -p <APP PASSWORD>
xcrun altool --list-providers -u <APPLE DEV ID> -p @keychain:AC_PASSWORD

この2コマンドを実行すると、いくつかエラーを吐きつつも表が表示されます。そこにある ProviderShortnameの欄が必要な内容になりますので、メモしておきましょう。

secretsを設定しましょう

secretsの設定は簡単です。GitHubのプロジェクトをブラウザで開き、SettingsからSecretsに進みます。ポイントとしてはEnvironment Secrets ではなくRepository Secrets に追加するという点です。

証明書の方もありましたので、一緒にやっていきましょう。証明書をp12形式で書き出して、

base64 -i <CERT_FILE_NAME>.p12 | pbcopy

としてあげると必要なコードがBase64形式にエンコードされ、クリップボードに保存されます。つまりこのまま貼り付け作業をすれば良いと言うことですね。コレを CERTIFICATES_P12にします。CERTIFICATES_P12_PASSWORD の方は証明書を書き出したときのパスワードになります。コレはそのまま打ち込む形で大丈夫です。

この2つをwithで指定することで他の方が作ってくれたActionsを利用して、署名に関する準備をすることができます。

ということで、話を戻すと、今回の例で行くとCERTIFICATES_P12 CERTIFICATES_P12_PASSWORD APPLEID APPLEIDPASS ASC_PROVIDER の5つをSecretsに指定します。このときsecrets.は不要です。コレが必要なのはYAMLファイルの中だけです。

client_secret等を扱うときにやることは?

clienent_secret.jsonを置きたかった(実は元々置いていた)のですが、本来置くべきでは無いものなので裏に隠すべくSecretsに追加することにしました。しかしオブジェクト形式(JSON形式)は上手くファイルに出力されなかったので、Base64形式にエンコードしてSecretsに保存、それをWorkflow内でechoしてデコードしてファイルに出力するという手段を執りました。
ココまでの内容を踏まえてWorkflowファイルを見ていただくとわかるかと思います。

あとはスクリプトを用意するだけ

GitHub Actionsが作る仮想環境にもキーチェーンを用意したので、それを使ってnotarizeすればOKです。

私の場合は以下のコードを用意しました。

require("dotenv").config();
process.env.DEBUG = "electron-notarize*";
const { notarize } = require("electron-notarize");

const password = `@keychain:AC_PASSWORD`;

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context;
  if (electronPlatformName !== "darwin") {
    return;
  }

  const appName = context.packager.appInfo.productFilename;

  return await notarize({
    appBundleId: "com.example.myapp", //★自分のアプリのBundleID(appId)に変更★
    appPath: `${appOutDir}/${appName}.app`,
    appleId: process.env.APPLEID,
    appleIdPassword: password,
    ascProvider: process.env.ASC_PROVIDER,
  });
};

このファイルをプロジェクトルート配下の scripts/notarize.js として保存しました。

後はビルドスクリプトに反映して欲しいので、electron-builderのオプションを調整します。私はVue-CLIとElectronなので、 vue.config.js に以下の部分を追記しました。

module.exports = {
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        appId: "com.example.myapp", //★自分のアプリのBundleID(appId)に変更★
        afterSign: "./scripts/notarize.js",
      mac: {
          hardenedRuntime: true,
          gatekeeperAssess: false,
          entitlements: "build/entitlements.mac.plist",
          entitlementsInherit: "build/entitlements.mac.plist",
        },
      },
    },
  },
};

この様な感じです。ポイントは afterSignnotarize.jsを呼び出すことですね。言ってもそれだけ。

無事に公証できた!

こんな長い作業を経て、公証をかけることができました。

どうしても古い情報が多かったり、動きの多い業界ということもあって、ちょっと前の情報が古くなったりもするので、ここに書き残すことで少しでも誰かの助けになればと思います。

この記事を書いた人

ささぴよ

TechAcademy公認エバンジェリスト

中学時代に買ってもらったPerforma 550以降ずっとアップル製品漬けの人生。iPhoneは日本上陸時から愛用し続けている。
「誰かが作っているなら、自分でも作れるはず!」という安易な発想からHP制作などを始め、ネットの世界から情報を集めてひたすらいろんなことをやってみるようになった。

TechAcademyでは、Webデザイン / WordPress / UI/UX / Node.js / OSS活動 / デザイン実践ポートフォリオ / フリーランスサポートのコース を担当するメンター。

よく歌って踊っている。