Info.plistを埋め込んでmacOS用シングルバイナリをApp Sandbox化・Notarizationする
macOSやiOSにはApp Sandboxというアプリケーションをファイルシステムやネットワークなどのリソースへのアクセスから隔離できる機構が用意されています。Xcodeなどを使ってSwift/Objective-C製のアプリを作るときにはApp Sandboxを有効にする方法はよく書かれていますが、Goで作成した単体バイナリで有効にする方法はあまりないようなので結構苦戦しました。
App Sandboxを有効にすることで、依存ライブラリが突然悪意をもったライブラリになっていたとしてもネットワークアクセスやファイルアクセスを遮断しておければ、アプリ利用者の情報が盗み出されるリスクを減らすことができるかもしれません。
この記事ではGoで作ったmacOS向けの単体バイナリをXcodeを使わずにApp Sandboxを有効にする方法と、App Sandboxによりファイルアクセスやネットワークアクセスなど制限がどうかかるのかを紹介します。また、配布に向けてシングルバイナリでApple Notarizationを受ける方法と、stapleまわりの制約についても紹介します。記事の末尾におまけとしてRustで作成した単体バイナリのApp Sandbox有効化も紹介しているのでよろしければご覧ください。
動作確認環境
- macOS 26.0, 27.0
- Xcode 26.0
Go製macOS用シングルバイナリをApp Sandbox有効にする
ここではGoで書いたmacOS用のシングルバイナリ形式のアプリケーションをApp Sandbox有効にすることを考え、通常のmacOSアプリケーションのような複数ファイルをもてるバンドル形式は考えないものとします。
シングルバイナリのApp Sandboxの有効化を次の2ステップで行います。
-
CFBundleIdentifierをもつInfo.plistをリンカで埋め込む -
codesignコマンドでEntitlementsファイルを指定して署名する
Step 1. CFBundleIdentifier をもつ Info.plist をリンカで埋め込む
App Sandboxを有効にするためには最低限Bundle IdentifierをInfo.plistで定義しておく必要があります。例えばBundle Identifierが "net.mtgto.example-app-sandbox" とすると、Info.plistはこのようになります。
<?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>CFBundleIdentifier</key>
<string>net.mtgto.example-app-sandbox</string>
</dict>
</plist>
バンドル形式と違ってシングルバイナリの場合はInfo.plistファイルをバンドル内に同梱することはできません。Info.plistをもつシングルバイナリを作成するにはリンカで __TEXT セグメントに __info_plist セクションという名前で埋め込めばよいらしいです。
Enabling this setting creates a section called
__info_plistin the__TEXTsegment of the product’s linked binary containing the processedInfo.plistfile for the target.
https://developer.apple.com/documentation/Xcode/build-settings-reference#Create-Infoplist-Section-in-Binary
なお、埋め込む Info.plist の形式ですが、バンドル形式の場合xml形式のことが多いですが別にxml形式でなくとも構わないようです。おすすめはファイルサイズがより小さいバイナリ形式です。plutilコマンドで形式を変換できるのでバイナリ形式に変換しておきましょう (plutil -convert は Info.plist ファイル自体を書き換えるので注意)。
plutil -convert binary1 Info.plist
Goの場合はgo build時に次のようにリンカフラグを指定することで、Info.plistが埋め込まれたシングルバイナリを作成できます。
go build -ldflags='-linkmode=external -extldflags "-sectcreate __TEXT __info_plist /path/to/Info.plist"' ./foo.go
-extldflags は外部リンカ (cc) に渡すフラグなので、Go標準の内部リンカが使われると無視されてしまいます。-linkmode=external を指定して外部リンカを使うようにしましょう。
作成したバイナリにうまく Info.plist が埋め込めているかどうかは plutil コマンドで確認できます。
❯ plutil -p foo
{
"CFBundleIdentifier" => "net.mtgto.example-app-sandbox"
}
Step 2. codesign コマンドでEntitlementsファイルを指定して署名する
作成したバイナリにEntitlementsを設定し、App Sandboxを有効にします。
Entitlementsにはいくつかのキー値がありますが、キー com.apple.security.app-sandbox に値 true を設定することでApp Sandboxが有効になります。
Entitlementsは拡張子 .entitlements で作成することが多いようです。
<?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.app-sandbox</key>
<true/>
</dict>
</plist>
これを codesign コマンドでバイナリに書き込みます。ここでは簡単に ad hoc 署名を使っています。
codesign --sign - \
--identifier net.mtgto.example-app-sandbox \
--options runtime \
--entitlements foo.entitlements \
foo
-
--sign -ad hoc署名を使う -
--identifier <bundle identifier>Info.plistで指定したのと同じBundle Identifierを指定してください。 -
--options runtime実行時に危険な処理がないかを検査する Hardened Runtime を有効化。Hardened RuntimeはApp Sandboxに必須ではないんですが、Apple Notarizationを受けるときには必須です。 -
--entitlements <entitlements path>指定したEntitlementsを適用する
上記ではad hoc署名を使っていますが、Mac App Storeを使わずに配布するためにApple Notarizationを受ける場合などは Mac Developer や Developer ID などの専用の署名が必要になります。シングルバイナリをNotarizationする手順は後述の「シングルバイナリのNotarization」で紹介しています。
シングルバイナリのEntitlementsを表示
codesign --display --entitlements - <file> を実行することで、指定したバイナリファイルに適用されているEntitlementsを表示することができます。
先ほど署名したバイナリ foo をみてみると com.apple.security.app-sandbox にtrueがセットされていることが確認できます。
❯ codesign --display --entitlements - foo
Executable=/tmp/foo
[Dict]
[Key] com.apple.security.app-sandbox
[Value]
[Bool] true
これでシングルバイナリでもApp Sandboxを有効にすることができました。
Info.plistを埋め込まなかった場合
codesign --identifier でBundle Identifierを指定しているのだから、Info.plistの埋め込みは不要なのでは?と思って試してみました。Info.plistを埋め込まずにビルドしたバイナリ foo を用意します。
plutil で確認するとInfo.plistは埋め込まれていません。
❯ plutil -p foo
foo: file was executable or library type but did not contain an embedded Info.plist
これを同じように --identifier とEntitlementsを指定して署名します。
codesign --sign - \
--identifier net.mtgto.example-app-sandbox \
--entitlements foo.entitlements \
foo
起動すると即座にクラッシュしてしまいました。
❯ ./foo
zsh: trace trap ./foo
クラッシュレポートを見ると、署名に紐づくInfo.plistに CFBundleIdentifier がないためにApp Sandboxの初期化に失敗しているようです。--identifier の指定だけでは足りず、Info.plistの埋め込みも必要ということがわかります。
クラッシュレポート
Application Specific Signatures:
Unable to get bundle identifier for container id net.mtgto.example-app-sandbox: Unable to get bundle identifier because Info.plist from code signature information has no value for kCFBundleIdentifierKey.
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_secinit.dylib 0x19a8df160 _libsecinit_appsandbox.cold.6 + 60
1 libsystem_secinit.dylib 0x19a8de424 _libsecinit_appsandbox + 1968
2 libsystem_trace.dylib 0x1899fb814 _os_activity_initiate_impl + 64
3 libsystem_secinit.dylib 0x19a8ddb24 _libsecinit_initializer + 196
4 libSystem.B.dylib 0x19a8f337c libSystem_initializer + 280
5 dyld 0x18993d758 invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 360
6 dyld 0x18997106c invocation function for block in mach_o::Image::forEachInitializer(void (unsigned int) block_pointer) const + 84
7 dyld 0x189968508 mach_o::Header::SegmentInfo mach_o::Header::parse_segment_command<true>(mach_o::Header::LoadCommandInfo const&, unsigned short, void (mach_o::Header::SegmentInfo const&, mach_o::Header::SectionInfo const&) block_pointer, mach_o::Error*) const + 580
8 dyld 0x18996d974 invocation function for block in mach_o::Header::forEachSection(void (mach_o::Header::SectionInfo const&, bool&) block_pointer) const + 156
9 dyld 0x189967004 mach_o::Header::forEachLoadCommand(void (mach_o::Header::LoadCommandInfo const&, bool&) block_pointer, mach_o::Error*) const + 228
10 dyld 0x18996b07c mach_o::Header::forEachSection(void (mach_o::Header::SectionInfo const&, bool&) block_pointer) const + 128
11 dyld 0x189970f9c mach_o::Image::forEachInitializer(void (unsigned int) block_pointer) const + 244
12 dyld 0x189938614 dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const + 680
13 dyld 0x1899405a8 dyld4::PrebuiltLoader::runInitializers(dyld4::RuntimeState&) const + 44
14 dyld 0x189900e30 dyld4::APIs::runAllInitializersForMain() + 100
15 dyld 0x18990dd14 dyld4::prepare(dyld4::APIs&, mach_o::UnsafeHeader const*) + 4724
16 dyld 0x18990ca84 dyld4::start(dyld4::KernelArgs*, void*, void*, unsigned long long)::$_1::operator()() const + 320
17 dyld 0x18990be24 start + 6596
App Sandboxの有無による挙動の違い
ここまでの説明でシングルバイナリ形式でApp Sandboxを有効にすることができました。次にGoアプリがApp Sandboxの有無でどのように挙動が変わるかをみてみます。
ホームディレクトリの場所
ホームディレクトリのパスは os.UserHomeDir() で取得できます。これはApp Sandboxが有効になっていると次のように変わります。
- App Sandboxなし:
/Users/<username> - App Sandboxあり:
/Users/<username>/Library/Containers/<bundle-identifier>/Data
package main
import (
"fmt"
"os"
)
func main() {
homeDir, err := os.UserHomeDir()
if err != nil {
panic(err)
}
fmt.Printf("Home directory: %v\n", homeDir)
}
/Users/<username>/Library/Containers/<bundle-identifier> というディレクトリはApp Sandboxが有効なアプリケーションが初回に起動するときに作成されます。このディレクトリの中はApp Sandboxが有効なアプリケーションであっても自由にファイルを作成・更新・読み込みができます。
カレントディレクトリの場所
os.Getwd() で取れるカレントディレクトリはどうでしょうか。これもApp Sandboxだと ~/Library/Containers/<bundle-identifier>/Data というSandbox コンテナ内に変更されます。
- App Sandboxなし: 現在のディレクトリ
- App Sandboxあり:
/Users/<username>/Library/Containers/<bundle-identifier>/Data
package main
import (
"fmt"
"os"
)
func main() {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
fmt.Printf("os.Getwd(): %v\n", cwd)
}
自身のデータコンテナ以外のファイルへのアクセス
前述のように、App Sandboxが有効なアプリケーションは /Users/<username>/Library/Containers/<bundle-identifier> 内のファイルは読み書きできますが、それ以外の場所にはEntitlementsファイルで許可をしておくかユーザーに許可をしてもらう必要があります。
試しに許可がないファイル、ここではホームディレクトリにある ~/.zshrc を読み込んでみましょう。App Sandbox内では os.UserHomeDir() がコンテナ内のディレクトリを返すため、絶対パスで指定しています。
package main
import (
"fmt"
"os"
)
func main() {
bytes, err := os.ReadFile("/Users/mtgto/.zshrc")
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}
実行結果
- App Sandboxなし:
~/.zshrcの中身が表示される - App Sandboxあり:
operation not permittedエラーが返る
❯ ./open_file
panic: open /Users/mtgto/.zshrc: operation not permitted
goroutine 1 [running]:
main.main()
/path/to/open_file.go:11 +0x88
プロセスがクラッシュするわけではなく、Goからは通常のエラーとして扱えます。
なお、/etc/hosts のようなシステムファイルはApp Sandboxが有効でも読み込めました。
外部プロセス起動
App Sandboxが有効でも外部プロセスは起動できます。ただし、起動した子プロセスは親プロセスのApp Sandboxを引き継ぐため、子プロセスも同じ制限を受けます。
cat コマンドで先ほどの ~/.zshrc を読み込んでみます。
package main
import (
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("cat", "/Users/mtgto/.zshrc")
out, err := cmd.Output()
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
実行結果
- App Sandboxなし:
~/.zshrcの中身が表示される - App Sandboxあり:
catが~/.zshrcを読み込めずに終了コード1で終了する
❯ ./spawn
panic: exit status 1
goroutine 1 [running]:
main.main()
/path/to/spawn.go:12 +0xa8
cat の代わりに date を起動した場合は、App Sandboxが有効でも問題なく実行結果が表示されました。
github.com/zalando/go-keyring によるキーチェーン (Keychain Service) の使用
macOSのキーチェーンへの接続をサポートするライブラリはいくつかあります。その中の著名なライブラリに https://github.com/zalando/go-keyring があります。
このライブラリはマルチプラットフォーム対応していますが、macOSの場合、 /usr/bin/security コマンドを使用しているとREADMEにも書かれています。
The OS X implementation depends on the
/usr/bin/securitybinary for interfacing with the OS X keychain. It should be available by default.
外部プロセスである security コマンドもApp Sandboxを引き継いで動作しますが、次のコードはApp Sandboxが有効でもキーチェーンへの保存と取得に成功しました。
package main
import (
"log"
"github.com/zalando/go-keyring"
)
func main() {
service := "my-app"
user := "anon"
password := "secret"
err := keyring.Set(service, user, password)
if err != nil {
log.Fatal(err)
}
// get password
secret, err := keyring.Get(service, user)
if err != nil {
log.Fatal(err)
}
log.Println(secret)
}
❯ ./keyring
2026/09/25 17:57:13 secret
ネットワークアクセス (クライアント)
https://example.com へのHTTPアクセスをしてみます。
package main
import (
"fmt"
"net/http"
)
func main() {
resp, err := http.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Response status:", resp.Status)
}
App Sandboxなしだと "Response status: 200 OK" が表示されますが、App Sandboxが有効だと接続できず、エラーが発生します。
ただコンテナ外のファイルをオープンしたときや外部プロセス起動しようとしたときとは違ってプロセスのクラッシュはしないようです。
❯ ./foo
panic: Get "https://example.com": dial tcp: lookup example.com: no such host
goroutine 1 [running]:
main.main()
/tmp/example-app-sandbox/client.go:12 +0xec
Entitlementsでネットワークアクセスを許可してみます。HTTPリクエストはクライアント機能なので com.apple.security.network.client をtrueにします。
<?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.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
codesign --sign - \
--identifier net.mtgto.example-app-sandbox \
--options runtime \
--entitlements client.entitlements \
foo
Entitlementsでネットワークアクセスが許可されているため、App Sandboxが有効でもHTTPリクエストができるようになりました。
❯ ./foo
Response status: 200 OK
ネットワークアクセス (サーバー)
HTTPサーバーを作ってみます。
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
}
func main() {
server := http.Server{
Addr: ":8080",
Handler: nil,
}
http.HandleFunc("/hello", hello)
err := server.ListenAndServe()
if err != nil {
panic(err)
}
}
App Sandboxが無効な場合は正常に起動します。
❯ ./foo&
❯ curl localhost:8080/hello
Hello, world!
App Sandboxが有効な場合は ListenAndServe() でエラーが返ります。
❯ ./foo
panic: listen tcp :8080: bind: operation not permitted
goroutine 1 [running]:
main.main()
/tmp/server.go:22 +0x8c
Entitlementsで com.apple.security.network.server をtrueにしてみるとエラーが発生しなくなります。
<?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.app-sandbox</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
codesign --sign - \
--identifier net.mtgto.example-app-sandbox \
--options runtime \
--entitlements server.entitlements \
foo
シングルバイナリのNotarization
App Sandboxを有効にしたシングルバイナリを配布するなら、Gatekeeperに止められないようにApple Notarization (公証) も受けておきたいところです。Apple Developer Programに加入していれば、シングルバイナリでもNotarizationを受けられます。
Developer ID証明書で署名する
Notarizationを受けるには、ad hoc署名ではなく Developer ID Application 証明書で署名する必要があります。また、Hardened Runtime (--options runtime) とセキュアタイムスタンプ (--timestamp) も必須です。
codesign --sign "Developer ID Application: <Your Name> (<TEAM_ID>)" \
--identifier net.mtgto.example-app-sandbox \
--options runtime \
--timestamp \
--entitlements foo.entitlements \
foo
zipにして notarytool で提出する
xcrun notarytool submit で提出できる形式は .zip、.pkg、.dmg のいずれかで、Mach-Oバイナリをそのまま提出することはできません。そこでバイナリをzipで包んでから提出します。ここではzipの作成に ditto を使います。
ditto -c -k --keepParent foo foo.zip
notarytool で使う認証情報は、あらかじめ store-credentials でキーチェーンに保存しておくと便利です。App用パスワードは https://account.apple.com で発行できます。
xcrun notarytool store-credentials "notarytool-profile" \
--apple-id <Apple ID> \
--team-id <TEAM_ID>
保存したプロファイルを指定して提出します。--wait を付けると審査が終わるまで待ってくれます。
xcrun notarytool submit foo.zip --keychain-profile "notarytool-profile" --wait
Conducting pre-submission checks for foo.zip and initiating connection to the Apple notary service...
Submission ID received
id: 3325627d-4f2d-476b-b2c0-1eaf5b45f516
Upload progress: 100.00% (186 KB of 186 KB)
Successfully uploaded file
id: 3325627d-4f2d-476b-b2c0-1eaf5b45f516
path: /path/to/foo.zip
Waiting for processing to complete.
Current status: Accepted.......
Processing complete
id: 3325627d-4f2d-476b-b2c0-1eaf5b45f516
status: Accepted
status: Accepted になればNotarization成功です。
status: Invalid になった場合は xcrun notarytool log <submission id> --keychain-profile "notarytool-profile" で原因を確認できます。
Notarizationのチケットはzipではなくバイナリ自体 (のcdhash) に対して発行されるので、提出したzipを配布しても、zipを展開したバイナリを別の方法で配布しても構いません。
シングルバイナリにはチケットをstapleできない
通常の .app の場合、Notarization後に xcrun stapler staple でチケットを配布物に埋め込む (staple) ことで、オフライン環境でもGatekeeperがNotarization済みかどうかを確認できるようになります。
ただし stapler が対象にできるのは .app、.pkg、.dmg だけで、Mach-O単体のバイナリやzipにはstapleできません。
xcrun stapler staple foo
Processing: /path/to/foo
The staple and validate action failed! Error 73.
xcrun stapler validate で確認しても、当然チケットはstapleされていません。
xcrun stapler validate foo
Processing: /path/to/foo
foo does not have a ticket stapled to it.
Notarizationに提出したzipにstapleしようとしても、zipには対応していないと言われてしまいます。
xcrun stapler staple foo.zip
Processing: /path/to/foo.zip
Stapler is incapable of working with ZIP archive files.
stapleしていないバイナリでも、Gatekeeperは初回起動時にAppleのサーバーへ問い合わせてNotarization済みかどうかを確認するので、オンラインであれば問題なく実行できます。オフライン環境でも確認できるようにしたいなら、バイナリを .pkg か .dmg に入れてNotarizationを受け、そちらにstapleすることになります。
-
.pkg:pkgbuildでインストーラーを作成し、Developer ID Installer証明書で署名する (Developer ID Applicationとは別の証明書が必要です) -
.dmg:hdiutil createでディスクイメージを作成し、Developer ID Application証明書で署名する
dmgに入れてstapleする
ここではdmgで試してみます。まず foo だけを入れたディレクトリを作り、hdiutil create でdmgを作成します。
mkdir dmgroot
cp foo dmgroot/
hdiutil create -volname foo -srcfolder dmgroot -ov -format UDZO foo.dmg
dmg自体にも Developer ID Application 証明書で署名しておきます。
codesign --sign "Developer ID Application: <Your Name> (<TEAM_ID>)" --timestamp foo.dmg
zipのときと同様に notarytool で提出します。
xcrun notarytool submit foo.dmg --keychain-profile "notarytool-profile" --wait
Conducting pre-submission checks for foo.dmg and initiating connection to the Apple notary service...
Submission ID received
id: c3cdbb6e-72be-4035-a4cb-47b222307c4d
Upload progress: 100.00% (223 KB of 223 KB)
Successfully uploaded file
id: c3cdbb6e-72be-4035-a4cb-47b222307c4d
path: /path/to/foo.dmg
Waiting for processing to complete.
Current status: Accepted.......
Processing complete
id: c3cdbb6e-72be-4035-a4cb-47b222307c4d
status: Accepted
dmgであればstapleできます。
xcrun stapler staple foo.dmg
Processing: /path/to/foo.dmg
Processing: /path/to/foo.dmg
The staple and validate action worked!
xcrun stapler validate でもチケットがstapleされていることが確認できます。
xcrun stapler validate foo.dmg
Processing: /path/to/foo.dmg
The validate action worked!
私が調べた範囲では、シングルバイナリ単体やzipにチケットをstapleする方法は見つかりませんでした。もし方法をご存知の方がいたら、コメント等で教えていただけると嬉しいです。
おまけ: Rust製のシングルバイナリをApp Sandbox有効化
なぜかcargoは使わずrustcによる例を作ってみました。 rustc -C link-args=<space-separated-args> でリンカにフラグを渡せるため、Goと同様にInfo.plistを埋め込めます。
rustc -C link-args='-sectcreate __TEXT __info_plist Info.plist' foo.rs
codesign コマンドでEntitlementsを埋め込んだ署名をする方法はGoと同様なので省略します。
まとめ
GoアプリでのApp Sandboxを有効にできる方法とApp Sandboxの有無によるホームディレクトリの場所やファイルアクセス・ネットワークアクセスを行ったときの違いを紹介しました。
近年は広く使われているライブラリへのサプライチェーン攻撃など、自分が作成したアプリが急にマルウェアに変わってしまうリスクが現実的に存在しています。App Sandboxはそのような危険を完全に防ぐものではありませんが、リスクを減らす手段として知っておいて損はないのではないでしょうか。
App Sandboxが有効になっている場合は自由に書き込みが可能なファイルシステムが /Users/<username>/Library/Containers/<bundle-identifier> の中だけになり、アプリの中から取得できるホームディレクトリも通常のホームディレクトリではなくContainer専用のディレクトリになります。最終的な配布物をApp Sandboxを有効にするアプリケーションの開発時にはApp Sandboxが無効のときとの違いに気を付ける必要があるかもしれません。
また、シングルバイナリでもzipにすればApple Notarizationを受けられますが、バイナリ単体やzipにはチケットをstapleできないため、オフライン環境でも確認できるようにしたい場合はdmgなどに入れてstapleする必要があります。
参考文献
- https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html#//apple_ref/doc/uid/TP40005929-CH4-SW6
- https://developer.apple.com/documentation/security/accessing-files-from-the-macos-app-sandbox
- https://developer.apple.com/documentation/security/hardened-runtime
- https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution
- https://developer.apple.com/documentation/security/customizing-the-notarization-workflow
- https://pkg.go.dev/cmd/link
Discussion