We generate a lot of tooling at Anchore. We chose to write most of these tools in Go for a few reasons: the development process is delightful, cross-platform builds are easy, and the distribution of artifacts is very simple (curl the binary). 

Since we target releasing tools for macOS we are beholden to the requirements put forth by Apple, something we’ve written about at length in the past. Tools like gon have made the process of signing and notarizing our releases much easier by wrapping xcrun and codesign utilities and hiding some of the inherent complexities. However, since gon shells out to these tools you still must be on a mac to sign and notarize your binaries. This nullifies one of the reasons why we chose Go in the first place: having simple cross-platform builds from any platform. 

We’ve reworked our release process a few times over to account for this, all with unpleasant tradeoffs. It seems to come down to a couple of points:

  1. Running macOS in CI is more expensive than running on linux.
  2. Using Docker on macOS in CI is annoying. Due to licensing restrictions Docker is not included on the default mac runners. This is problematic since we use goreleaser to perform the build and release steps in one shot, which means we need to be able to sign/notarize our binaries at the same time as we package and release them for all platforms. This has only very recently been alleviated with the addition of colima on the default mac runner, but before this it has caused us to slice-and-dice up our release pipeline in awkward ways.

After a while we started to wonder to ourselves: Is it intrinsically necessary to require the signing and notarization steps to run on a mac? The more we looked the more we were certain the answer was “no”.

What’s in a signed binary anyway?

When you run codesign to sign your binary a new payload is added at the end of the binary with (usually) the following sections:

  • A Code Directory: essentially a table of hashes. Each hash is a digest of each page in the binary before the new payload. The code directory is “what” gets signed.
  • A PKCS7 (CMS) envelope: contains the cryptographic signature made against the Code Directory.
  • A Set of Requirements (optional): expressions that are evaluated against the signature that should hold true. “Designated Requirements” are a special set of requirements that describe how to determine the identity of the code being signed.
  • A Set of Entitlements (optional): a list of key-value pairs in XML that represent privileges an executable can request, e.g. com.apple.developer.avfoundation.multitasking-camera-access=true request for camera access while running alongside another foreground app.

There is nothing inherent about any of these payload elements that require the signing process to run on a mac.

What about notarization? What’s involved to get your binary notarized by Apple? This is an easier answer:

  1. Put your binary in a zip
  2. Upload the zip to Apple via their Notarization API
  3. Poll their Notarization API until there is a result

It seems that the only reason why we are signing and notarizing our releases on macOS is because Apple does not yet provide cross-platform tooling to do so…

Introducing Quill

We created a new tool called Quill to sign and notarize your macOS binary from any platform. 

demo of quill

This works quite well with goreleaser as a post-build step:

using quill with goreleaser

In this way you can use a single goreleaser file for local builds and production builds:

  • Signing and notarization are performed for production builds.
  • Ad-hoc signing is done for snapshot builds (notarization is skipped). This means that no cryptographic material is needed as input, so the Code Directory is added to the binary but there is no signature attached.

You can additionally use Quill to:

  • View your previous notarization submissions with “quill submission list”
  • Get the logs from Apple about the details of a submission result with “quill submission logs <submission-id>”
  • Parse and describe a macOS binary (including all signing details) with “quill describe ./path/to/binary”

We are now using quill for our production releases of Syft and Grype and have room to implement more features in the future to expand the capabilities of quill to match that of codesign. Quill is an open source project — we would love feedback, bug reports, and pull requests!