「他人のMacで動かない」の正体 — Flutter製Macアプリを配布するまでに踏んだ地雷

自作の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 DistributionMac 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_IDENTITYDeveloper ID Application
CODE_SIGN_STYLEManual
ENABLE_HARDENED_RUNTIMEYES
CODE_SIGN_INJECT_BASE_ENTITLEMENTSNO
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ステップが分かった時点で、怖さの半分は消えました。残り半分は、ログが教えてくれました。


そして実は、この日いちばん実害の大きかった問題は、公証とはまったく別のところに潜んでいました。その話は次回に。

目次