自作のMacアプリ(領収書PDFを自動リネームするやつ)の新バージョンをリリースするにあたって、ずっと先送りにしていた作業に手をつけました。Mac公証(Notarization) です。
READMEの残タスクに ⬜ Mac公証(Notarization) と書いたまま、何ヶ月も ✅ にならなかった項目。正直、いちばん苦手な領域でした。
結論から言うと、魔法は一つもありませんでした。ハマった理由も、通った理由も、全部ログと設定で説明がつきました。同じところで止まっている人のために、踏んだ地雷を順番に書きます。
前提:Macアプリ配布の4ステップ
まず全体像。ここが分かっていないと、どこで詰まっているのかも分かりません。
1. 署名 … 「誰が作ったか」を証明書で刻印する
2. 公証 … Appleにアップして「マルウェアじゃない」と検査してもらう
3. ステープル … 検査の合格証をアプリ本体に貼り付ける
4. 配布 … DMGにまとめて配る
Gatekeeper(macOSの門番)は、他人のMacでアプリが初回起動するとき、2と3を見ます。無いと「開発元を検証できません」で止まる。
地雷1:証明書の種類が違った
いちばん最初に気づくべきだったのがこれです。設定を見たら、こうなっていました。
CODE_SIGN_IDENTITY = "Apple Development"
Appleの証明書には種類があって、用途が完全に分かれています。
| 証明書 | 用途 | 他人のMacで動くか |
|---|---|---|
| Apple Development | 自分の開発機でのテスト用 | ❌ 動かない |
| Developer ID Application | 直接配布(サイト販売など) | ✅ 動く |
| Apple Distribution | Mac App Store 用 | ストア経由のみ |
私が使っていたのは1番目。自分のMacでしか動かない署名でした。
つまり「他人のMacで動かない」原因は、公証をしていないことだけではなく、その手前の署名の種類からだったのです。ここに気づかず公証だけ試すと、延々ハマります。
証明書の在庫確認はこれで一発です。
security find-identity -v -p codesigning
Developer ID Application: ... があれば持っています。私は既に持っていました。必要なものは最初からあって、使っていなかっただけでした。
地雷2:Hardened Runtime が無効
ENABLE_HARDENED_RUNTIME が設定されていませんでした。これは公証の必須条件なので、このままAppleに送っても弾かれます。
地雷3:ここが本命 — flutter build macos は「通常ビルド」扱い
証明書を直して、Hardened Runtimeも有効にして、意気揚々と提出したら Invalid。ログを読むとこう出ました。
The signature does not include a secure timestamp.
The executable requests the com.apple.security.get-task-allow entitlement.
get-task-allow は「デバッガの接続を許可する」権限です。当然、配布アプリに付いていてはいけない。でも Release.entitlements には書いていない。Xcodeが勝手に注入していました。
理由が分かって腑に落ちました。Xcodeは通常のビルドでは「開発中だろう」と判断して、デバッグ用の権限を足し、タイムスタンプ(ネットワーク通信が必要で遅い)を省略します。この2つは Archive → Distribute の配布フローでしか自動的に付かない。
flutter build macos --release はコマンドラインからの通常ビルドなので、配布フローとは見なされません。だから明示する必要があります。
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO
OTHER_CODE_SIGN_FLAGS = --timestamp
つまり証明書は正しいのに、署名のやり方が開発モードのままだった。これが最後のピースでした。
最終的に、Xcode の Release構成(Debugは触らない)に入れた設定はこの5つです。
| 設定 | 値 |
|---|---|
CODE_SIGN_IDENTITY | Developer ID Application |
CODE_SIGN_STYLE | Manual |
ENABLE_HARDENED_RUNTIME | YES |
CODE_SIGN_INJECT_BASE_ENTITLEMENTS | NO |
OTHER_CODE_SIGN_FLAGS | --timestamp |
公証の Invalid は、理由が全部読める
これは救いでした。Invalid が返ってきても、推測する必要はありません。
xcrun notarytool log <提出ID> --keychain-profile "プロファイル名"
どのファイルが、なぜ弾かれたか、アーキテクチャ別にJSONで出てきます。私の2つのエラーも、これで一発でした。エラーメッセージが親切な世界は、それだけで優しい。
その他の小さな地雷
提出用のzipは配布してはいけない。 公証に出すzipは、合格証が入っていない状態です。配布物はステープルした後に改めて作る。ここは引っかかる人が多いそうです。
zipは ditto で作る。
ditto -c -k --keepParent "アプリ.app" "提出用.zip"
zip コマンドだとmacOS固有の情報(署名を含む)が壊れることがあります。
DMGにも署名と公証が要る。 DMGはアプリとは別のファイルです。アプリとDMGの両方にステープルすると、どちらの経路でも警告が出ません。
検証で踏んだ地雷:隔離属性
別のノートPCで動作確認したのですが、ここにも罠がありました。
Mac miniから直接コピーしても、検証になりません。
ブラウザやAirDrop経由でダウンロードしたファイルには com.apple.quarantine という印が付き、Gatekeeperはこの印があるときだけ厳密に検証します。直接コピーだと素通りしてしまう。
確認方法:
xattr -p com.apple.quarantine ~/Downloads/ファイル.dmg
値が返れば、購入者と同じ状態です。ちなみにAirDropでは付きました(sharingd というプロセス名が記録される)。
このコマンドが良いのは、人の言うことを信じなくて済むところです。 「AirDropだと付かないのでは?」と迷う必要がない。叩けば分かる。
最後の地雷:アプリの名前を変えてはいけなかった
DMGを作るとき、見た目を良くしようとしてアプリ名を日本語に変えました。電子帳簿リネーマ.app に。
これが失敗でした。元のビルドは electronic_bookkeeping_renamer.app です。ファイル名が違うので、古いバージョンを上書きせず、両方が並んで存在する。
テスト機で確認したら、こうなっていました。
/Applications/電子帳簿リネーマ.app → 1.2.2 (新)
/Applications/electronic_bookkeeping_renamer.app → 1.2.1 (旧)
同じBundle IDのアプリが2つ。しかも古い方を開いて「アップデートされてない」と勘違いする。既存の購入者全員に同じことが起きるところでした。
日本語表示名がほしいなら、InfoPlist.strings を使えばファイル名を変えずに実現できます。そちらが正規の方法。
そして全部スクリプトにした
一度通ってしまえば、あとは同じことの繰り返しです。忘れるに決まっているので、スクリプトにしました。
./scripts/release_macos.sh 1.2.3
クリーンビルド → 署名検証 → 公証 → ステープル → DMG作成 → DMG公証 → 最終検証。今日踏んだ地雷は全部スクリプト側で潰してあります。Appleに送る前に get-task-allow とタイムスタンプを自分でチェックして、問題があればその場で止まる。 往復5分を無駄にしないために。
学んだこと
「動かない」の原因は、疑っていた場所より手前にあることがある。 私は公証の問題だと思っていましたが、実際は証明書の種類という、その前段階の話でした。
エラーは読めば書いてある。 Invalid の2行を読むまで、私は原因を推測しようとしていました。読んだら5秒で分かりました。
苦手意識は、たいてい全体像を知らないことから来る。 署名・公証・ステープル・配布の4ステップが分かった時点で、怖さの半分は消えました。残り半分は、ログが教えてくれました。
そして実は、この日いちばん実害の大きかった問題は、公証とはまったく別のところに潜んでいました。その話は次回に。
