Distributing CLI apps on MacOS

Hi All,

I know this question is more about Mac app development than Rust but I figured that some people in this forum will have run into this. Apologies if off-topic.

One thing I loved about Rust at my old company was that I could build a single executable and give it to someone to run, but there we used windows.

At my new company we use Mac's. When I now give the app to someone the app is blocked due to the fact that the app comes from an "unidentified developer".

From my research it seems my options are to get my users to disable gatekeeper globally (a terrible idea) or to pay for an apple developer account and sign the app with my "Developer ID". Is my thinking here correct? This seem crazy to me (that I have to pay apple just to distribute my application)...

If this is the case, can anyone point to a blog post, etc. that describes the most no-fuss way of carrying out this process from someone that just wants to distribute a simple Rust cli app.

Thanks

The only way to avoid that for apps launched through LaunchServices is code signing, which means paying $100/year to Apple for a signing certificate. Developers with self-signed certificates can also authorize their own computer to run that code, but distributing these certs to end users is a non-starter for a couple of reasons. LaunchServices is responsible for everything downloaded from the App Store, as well as anything launched from a Finder window or a Dock icon. This is baked into the OS, and is pitched as a security measure but is also a market management measure.

Starting in MacOS 11, this also applies to CLI applications. This Homebrew bug talks about the impact this had on the Homebrew user community, and links to a pull request that implements supporting features. You'd have to reach out to Homebrew's maintainers to ask the key question, though: I'm not sure if individual developers are signing their binaries, or whether Homebrew signs them on peoples' behalf.

4 Likes

After cargo build, you have to run something like:

codesign -s "Developer ID" target/release/yourexecutablegoeshere

and you can do that only if you pay $100/year to Apple, not get blocked by them for any reason, and use Xcode to set up all the required certificates.

Rust can't do anything about it. It's Apple's policy.

2 Likes

Thanks for all the information.

What's described here is what I thought from searching online. I was hoping that there was some loophole I wasn't aware of. Seems crazy to me but is what it is I guess...

Gatekeeper only checks files which have been downloaded from the net using a browser, email or any other application, which sets the quarantine xattr flag on downloaded files. curl, scp, etc. do not know anything about this flag and thus files downloaded/copied with it are not quarantined. Also, any user can remove the quarantine flag using xattr -d com.apple.quarantine path/to/binary. Additionally for ARM64 it is important that the binary is signed, but an ad-hoc signature is sufficient. The Macos linker adds an ad-hoc signature automatically.

Homebrew binaries are only ad-hoc signed and downloaded via curl and work fine. They are not signed with an ADC certificate.

4 Likes

When you say "ad-hoc signed" do you mean self-signed and why is it important for ARM64?

By the way I tried this and it works perfectly. Of course, I signed up to be an Apple Developer yesterday...

Ad-hoc signed means that it does not contain any cryptographic identity proof. As far as I can see it does not contain a signature at all, just a hash. ld creates ad-hoc signatures automatically during linking. man codesign has some information on it.

You can use codesign to show the signature/hash information.

cmake - signed with an ADC cert:

$ codesign -dvvv which cmake
Executable=/Applications/CMake.app/Contents/bin/cmake
Identifier=cmake
Format=Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=63761 flags=0x10000(runtime) hashes=1982+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=19b3efe95437d860109d5e75a2e3788c94aa1ef9
CandidateCDHashFull sha256=19b3efe95437d860109d5e75a2e3788c94aa1ef9e8cf3058daddba47c0d5e1f4
Hash choices=sha256
CMSDigest=19b3efe95437d860109d5e75a2e3788c94aa1ef9e8cf3058daddba47c0d5e1f4
CMSDigestType=2
CDHash=19b3efe95437d860109d5e75a2e3788c94aa1ef9
Signature size=8924
Authority=Developer ID Application: Kitware Inc. (W38PE5Y733)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=29. Apr 2021 at 16:17:28
Info.plist=not bound
TeamIdentifier=W38PE5Y733
Runtime Version=11.1.0
Sealed Resources=none
Internal requirements count=1 size=168```

Homebrew bash - ad-hoc signed:

$ codesign -dvvv which bash
Executable=/opt/homebrew/Cellar/bash/5.1.12/bin/bash
Identifier=bash
Format=Mach-O thin (arm64)
CodeDirectory v=20400 size=8221 flags=0x20002(adhoc,linker-signed) hashes=254+0 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=a6db345a59d27f30ab496f7f14a7b5b9c4b2c56e
CandidateCDHashFull sha256=a6db345a59d27f30ab496f7f14a7b5b9c4b2c56e3746c4a777103bf0f9445044
Hash choices=sha256
CMSDigest=a6db345a59d27f30ab496f7f14a7b5b9c4b2c56e3746c4a777103bf0f9445044
CMSDigestType=2
CDHash=a6db345a59d27f30ab496f7f14a7b5b9c4b2c56e
Signature=adhoc
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements=none

Rust helloworld after cargo build --release, automatically ad-hoc signed:

$ codesign -dvvv target/release/helloworld
Executable=/Users/hans/dev/helloworld/target/release/helloworld
Identifier=helloworld-fec3635a5f1afad0
Format=Mach-O thin (arm64)
CodeDirectory v=20400 size=3444 flags=0x20002(adhoc,linker-signed) hashes=104+0 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=0f1cff0facf8e1a1ce5a78b60422e4c78936c0c3
CandidateCDHashFull sha256=0f1cff0facf8e1a1ce5a78b60422e4c78936c0c3060332fff90961db3a1b3f50
Hash choices=sha256
CMSDigest=0f1cff0facf8e1a1ce5a78b60422e4c78936c0c3060332fff90961db3a1b3f50
CMSDigestType=2
CDHash=0f1cff0facf8e1a1ce5a78b60422e4c78936c0c3
Signature=adhoc
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements=none

3 Likes

Naïve question: Could you setup a Cargo registry and distribute your program as source code (through that registry), install rustc and cargo on the user's machine and install through cargo install?

1 Like

I could. I even thought that it could be easier to just have them install rustup, etc. and compile from source. My users are not developers though, I don't really want them exposed to all this.

I think having them run xattr as @kryps has indicated is likely the easiest way.

Thanks so much @kryps, very much appreciated.

It’s also possible to right-click the file and select Open, which will present a dialog which will remove the quarantine flag.

Additionally, the Gatekeeper panel in System Preferences will also have a button which removes the flag for a recently executed file which was blocked. It has a associated message saying something like “an executable from an unknown source tried to run…”.

1 Like

That is only possible for users with admin rights.

P.S. I am not saying that that makes any sense...