本文へスキップ
Articles

2026-09-08 公開 / アプリ開発

Macアプリを署名して配るまでに踏んだ4つ。どれもエラーを出さずに壊れる

macOSの小さな常駐アプリを作って、署名と公証(Notarization)を通して配るところまでやりました。ビルド自体は最初から通っていたのに、配る形にする過程で4回つまずきました。共通しているのは、どれも「失敗した」と言ってくれないことです。手元では正常に見えて、受け取った人の環境で初めて壊れます。

1. 証明書の名前が紛らわしい。開発用では配れない

署名するには証明書が要ります。手元にはこれがありました。

$ security find-identity -v -p codesigning
  1) ... "Apple Development: 名前 (XXXXXXXXXX)"

「Development」とあるので開発用だと分かりますが、これで署名しても Gatekeeper は「署名されていない」と同じ扱いをします。配布に必要なのは Developer ID Application という別の証明書です。

紛らわしいのは、どちらも codesign コマンドは成功することです。署名は付きます。付いたうえで、配布したときだけ効かない。

そして Developer ID は、有料の開発者プログラムに登録しないと発行できません。「署名しておこう」と思い立った日には作れない、という時間差があります。配布の予定が少しでもあるなら、登録だけ先に済ませておくと後の作業が詰まりません。

2. zip で固めると、ゴミが同梱される

アプリをZIPにして配ろうとして、こうしていました。

zip -r MyApp.zip MyApp.app

できたZIPの中身を見ると、こうなっていました。

MyApp.app/
__MACOSX/                      ← これ
__MACOSX/MyApp.app/
__MACOSX/MyApp.app/Contents/
...

macOS の拡張属性を別ファイルとして保存したものです。受け取った人が展開すると、アプリの隣に謎のフォルダが並びます。実害は小さいのですが、配布物としては雑に見えます。

Apple が案内しているのは ditto です。

ditto -c -k --keepParent MyApp.app MyApp.zip

ひとつ注意があります。--sequesterRsrc というオプションを足すと、また __MACOSX が入ります(拡張属性をそこへ隔離する、というのがこのオプションの意味なので当然です)。私は「Apple製ツールなら安全だろう」と付けて、12エントリ混入させました。

展開方法で見え方が変わる

もうひとつ、確認するときの落とし穴があります。ditto で作ったZIPをコマンドラインの unzip で展開すると、._MyApp のようなファイルが現れます。

これは unzip 側の挙動です。Finder でダブルクリックした場合(内部的には ditto 相当)は、拡張属性として正しく復元されるので何も現れません。

検証は、利用者と同じ経路で行う必要があります。私は最初 unzip で確認して「まだゴミが残っている」と誤判定し、直さなくていいものを直そうとしました。

# 利用者と同じ経路で展開して確かめる
ditto -x -k MyApp.zip /tmp/check
find /tmp/check -name '._*' -o -name '__MACOSX'

3. 署名の「後」に拡張属性を消すと、封印が壊れる

これがいちばん厄介でした。

2番の対策として、ZIPを作る前に拡張属性を落とそうと考えました。

codesign --sign "..." MyApp.app     # 署名する
xattr -cr MyApp.app                 # 拡張属性を消す  ← ここが間違い
ditto -c -k --keepParent MyApp.app MyApp.zip

手元では codesign --verify が通ります。ところが、できたZIPを展開して検証すると落ちます。

MyApp.app: a sealed resource is missing or invalid
file added: .../Contents/._Info.plist

署名は「このファイル群がこの状態であること」を封印する操作です。そのあとに属性を触ると、封印した内容と実物がずれます。順序を入れ替えるだけで直りました。

xattr -cr MyApp.app                 # 先に消す
codesign --sign "..." MyApp.app     # そのあと署名

厄介な点は、壊れたことが手元では分かりにくいことです。署名した直後の .app を検証すると通ります。ZIPにして、展開して、初めて壊れているのが見えます。配布形態まで作って検証しないと気づけません。

4. CIに「未署名でも公開できる経路」を残さない

タグを打つと自動でリリースを作る仕組みを先に用意していました。証明書がまだ無かった時期なので、こういう分岐にしていました。

当時は正しい判断でした。未公証でも先に出して使ってもらう、と決めていたからです。

問題は証明書を手に入れた後でした。手元で署名・公証まで通したものを配りたいのに、CI側の設定はまだ空のままです。この状態でタグを打つと、CIが仮署名のZIPでリリースを作り、手元の公証済みファイルを上書きします。

気づいたのは、タグを打つ直前に「この分岐は今どちらへ行くのか」を読み直したときでした。実行していたら、公証したはずのものが未公証で公開されていました。

直し方は、分岐を消して署名できないならリリースを作らないようにするだけです。

# 署名用の設定が無いときはリリースを作らない。
# ビルドと成果物の生成だけ行い、理由をログに残す。
- uses: softprops/action-gh-release@v2
  if: steps.signing.outputs.signed == 'true'
  ...

「とりあえず出す」ための逃げ道は、必要がなくなった時点で塞ぐ。残しておくと、条件が変わったときに黙って古い方へ流れます。

ついでに: 自動化を止める要素がひとつある

Developer ID で初めて署名するとき、macOSが確認ダイアログを出します。「codesign がキーチェーンに保存されている機密情報を使おうとしています」というものです。

ここで「常に許可」を選ぶまで、codesign は無言で待ち続けます。タイムアウトもエラーも出ません。スクリプトから実行していると、ただ固まっているように見えます。

初回だけは、ダイアログが確実に見える状態で手動で実行するのが早いです。一度許可すれば、以降は止まりません。

持ち帰れること

最後にひとつ。これらは全部「配布物を作ったあとに、それを開いて確かめる」だけで見つかりました。ビルドが通ったことと、配れる状態になっていることは別です。間に検証を1回挟むかどうかで、気づく場所が自分の手元か、受け取った人の手元かに分かれます。