This is very similar to the (unresolved) issue here: Signed Command Line Tool Rejected by spctl
I'm attempting to distribute an internal command-line tool called bootstrap-mercury
to engineers at my organization. The command-line tool sets up a development environment; it installs Nix, the Xcode command-line tools, sets up a Postgres instance, edits configuration files, and so on. I would like users to be able to download the file and double-click it to run it.
I know that Gatekeeper blocks command-line tools, so I've embedded the command-line tool in an app.
The app is a simple Swift executable with this source code:
import Foundation
import AppKit
import OSLog
import SwiftUI
@main
struct BootstrapMercuryApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
Text("bootstrap-mercury")
.padding()
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
func applicationDidFinishLaunching(_ notification: Notification) {
if let window = NSApplication.shared.windows.first {
window.close()
}
let logger = Logger()
guard let executableUrl = Bundle.main.url(forAuxiliaryExecutable: "bootstrap-mercury-cli")
else {
fatalError("Couldn't find embedded bootstrap-mercury executable")
}
logger.log("Embedded bootstrap-mercury executable URL: \(executableUrl.absoluteString, privacy: .public)")
guard let terminalUrl = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal")
else {
fatalError("Couldn't find Terminal.app")
}
logger.log("Terminal.app URL: \(terminalUrl, privacy: .public)")
let configuration = NSWorkspace.OpenConfiguration()
configuration.activates = true
configuration.allowsRunningApplicationSubstitution = false
configuration.createsNewApplicationInstance = true
configuration.hides = false
NSWorkspace.shared.open(
[executableUrl],
withApplicationAt: terminalUrl,
configuration: configuration,
completionHandler: { (maybeApp: NSRunningApplication?, maybeError: Error?) -> Void in
logger.log("Terminal.app completionHandler fired")
if let app = maybeApp {
logger.log("Terminal.app started succesfully: \(app, privacy: .public)")
}
if let error = maybeError {
logger.log("Terminal.app failed to start: \(error as NSError, privacy: .public)")
}
exit(0)
}
)
logger.log("Launched Terminal.app")
}
}
The key here is it attempts to open the embedded command-line tool with Terminal.app
, so that the user can see log messages printed to STDOUT and reply to prompts on STDIN.
I assemble the app like this:
bootstrap-mercury.app
└── Contents
├── Info.plist
├── MacOS
│ ├── bootstrap-mercury
│ └── bootstrap-mercury-cli
└── Resources
└── bootstrap-mercury.icns
Then, I sign and notarize/staple the app. (It later gets stuffed into a .dmg
, which is in turn notarized/stapled, but I don't think that's related.)
spctl
verifies it:
$ spctl -a -v --raw bootstrap-mercury.app
bootstrap-mercury.app: accepted
<?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>assessment:authority</key>
<dict>
<key>assessment:authority:flags</key>
<integer>2</integer>
<key>assessment:authority:row</key>
<integer>11</integer>
<key>assessment:authority:source</key>
<string>Notarized Developer ID</string>
</dict>
<key>assessment:remote</key>
<true/>
<key>assessment:verdict</key>
<true/>
</dict>
</plist>
codesign
verifies it:
$ codesign -vvvv -verify bootstrap-mercury.app
bootstrap-mercury.app: edited signature app bundle with Mach-O universal (x86_64 arm64) [bootstrap-mercury-app-arm64]
$ codesign -vvvv -R="notarized" --check-notarization bootstrap-mercury.app
--prepared:/Users/wiggles/bootstrap-mercury/target/release/bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury-cli
--validated:/Users/wiggles/bootstrap-mercury/target/release/bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury-cli
bootstrap-mercury.app: valid on disk
bootstrap-mercury.app: satisfies its Designated Requirement
bootstrap-mercury.app: explicit requirement satisfied
The list of entitlements is empty:
$ codesign -d -vvv --entitlements :- bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury-cli
...
Warning: Specifying ':' in the path is deprecated and will not work in a future release
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict></dict></plist>
(Note: I've tried setting com.apple.security.app-sandbox
for the Swift wrapper executable bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury
and com.apple.security.app-sandbox
as well as com.apple.security.inherit
for the bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury-cli
, but the bootstrap-mercury-cli
gets immediately SIGKILL
ed on launch and I'm not sure why; it's a relatively simple Rust command-line tool that doesn't do anything too complex.)
When launching the app on my machine, it works correctly. On a coworker's machine, it shows several errors:
- "'bootstrap-mercury' is an app downloaded from the Internet. Are you sure you want to open it?"
- "'bootstrap-mercury' would like to access files in your Downloads folder."
- "'bootstrap-mercury-cli' can't be opened because the identity of the developer cannot be confirmed."
- "The application 'Terminal' can't be opened. -128"
The first two are expected. The last two are quite strange. bootstrap-mercury-cli
is signed with my Developer ID Application. Why can't macOS identify that?