App cannot open embedded command-line tool

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 SIGKILLed 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:

  1. "'bootstrap-mercury' is an app downloaded from the Internet. Are you sure you want to open it?"
  2. "'bootstrap-mercury' would like to access files in your Downloads folder."
  3. "'bootstrap-mercury-cli' can't be opened because the identity of the developer cannot be confirmed."
  4. "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?

Replies

In case they're useful, here's some additional details.

The full output of codesign:

$ codesign -d -vvv --entitlements :- bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury-cli
Executable=/Users/wiggles/bootstrap-mercury/target/release/bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury-cli
Identifier=com.mercury.bootstrap-mercury
Format=Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=42249 flags=0x10000(runtime) hashes=1309+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=6f611e6bf5d9542cbf5055cc667bb68fabf65ff6
CandidateCDHashFull sha256=6f611e6bf5d9542cbf5055cc667bb68fabf65ff6dae50c89113e73bc6bb20200
Hash choices=sha256
CMSDigest=6f611e6bf5d9542cbf5055cc667bb68fabf65ff6dae50c89113e73bc6bb20200
CMSDigestType=2
CDHash=6f611e6bf5d9542cbf5055cc667bb68fabf65ff6
Signature size=9067
Authority=Developer ID Application: Mercury Technologies, Inc. (4J49M7587W)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Sep 7, 2022 at 1:48:20 PM
Info.plist=not bound
TeamIdentifier=4J49M7587W
Runtime Version=12.3.0
Sealed Resources=none
Internal requirements count=1 size=192
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>

And for the wrapper script:

$ codesign -d -vvv --entitlements :- bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury
Executable=/Users/wiggles/bootstrap-mercury/target/release/bootstrap-mercury.app/Contents/MacOS/bootstrap-mercury
Identifier=bootstrap-mercury-app-arm64
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=1159 flags=0x10000(runtime) hashes=29+3 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=d276166d4570f957e1bccab20dfa6ec124904461
CandidateCDHashFull sha256=d276166d4570f957e1bccab20dfa6ec124904461fc0bd358e221b78fbb465e7d
Hash choices=sha256
CMSDigest=d276166d4570f957e1bccab20dfa6ec124904461fc0bd358e221b78fbb465e7d
CMSDigestType=2
CDHash=d276166d4570f957e1bccab20dfa6ec124904461
Signature size=9067
Authority=Developer ID Application: Mercury Technologies, Inc. (4J49M7587W)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Sep 7, 2022 at 1:48:20 PM
Info.plist entries=9
TeamIdentifier=4J49M7587W
Runtime Version=12.3.0
Sealed Resources version=2 rules=13 files=2
Internal requirements count=1 size=188
Warning: Specifying ':' in the path is deprecated and will not work in a future release

Actually, I'm not really sure what's going on here anymore.

spctl shows the same error with the /bin/ls command-line tool:

$ spctl -a -v --raw /bin/ls
/bin/ls: rejected (the code is valid but does not seem to be an app)
<?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>0</integer>
                <key>assessment:authority:source</key>
                <string>obsolete resource envelope</string>
                <key>assessment:authority:weak</key>
                <true/>
        </dict>
        <key>assessment:cserror</key>
        <integer>-67002</integer>
        <key>assessment:remote</key>
        <true/>
        <key>assessment:verdict</key>
        <false/>
</dict>
</plist>

But when I double-click /bin/ls in Finder, it opens and runs correctly without any prompting. Shouldn't that be subject to the same document-validation logic described in the “Tool Blocked by Gatekeeper” section of this Resolving Gatekeeper Problems post?

Especially weird because spctl says the document-open logic should pass?

$ spctl -a -t open --context context:primary-signature -v bootstrap-mercury-cli
bootstrap-mercury-cli: accepted
source=Notarized Developer ID

Why can't macOS identify that?

You shouldn’t read too much into that error message. Gatekeeper is showing something generic because there’s no hope of the user being able to understand what’s actually going on.

"The application 'Terminal' can't be opened. -128"

FYI, -128 is userCanceledErr, which is Gatekeeper’s way of telling you that it blocked this.

On the sandboxing front, you definitely don’t want to go down that path. Some of this might be possible from a sandboxed app but, given that you don’t intend to deploy via the Mac App Store, sandboxing is just an additional complication that it’s best to avoid.

As to what’s causing your main issue, I’m not 100% sure. I suspect that your overall strategy won’t work because telling NSWorkspace to open your helper tool will trigger the same execution path that double clicking it in the Finder does, and thus will suffer from the same problem. However, I’ve never actually tried this, so I’m not 100% sure.

To start, I recommend that you tweak your NSWorkspace code to run some other tool, like /bin/echo, and see if that works. If so, that confirms that your NSWorkspace code is working and the problem is specific to your tool.

After that, do revert this tweak and then try the following:

  1. Launch your app, agreeing to the Gatekeeper alert.

  2. Switch out of your app into Terminal.

  3. Run the copy of bootstrap-mercury-cli embedded in your app directly from Terminal.

Does that work? And does it trigger any further Gatekeeper alerts?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"