mitchellh / gon

Sign, notarize, and package macOS CLI tools and applications written in any language. Available as both a CLI and a Go library.
MIT License
1.47k stars 96 forks source link
build-tool golang macos notary

Archived: I unfortunately no longer make active use of this project and haven't properly maintained it since early 2022. I welcome anyone to fork and take over this project.


gon - CLI and Go Library for macOS Notarization

gon is a simple, no-frills tool for signing and notarizing your CLI binaries for macOS. gon is available as a CLI that can be run manually or in automation pipelines. It is also available as a Go library for embedding in projects written in Go. gon can sign and notarize binaries written in any language.

Beginning with macOS Catalina (10.15), Apple is requiring all software distributed outside of the Mac App Store to be signed and notarized. Software that isn't properly signed or notarized will be shown an error message with the only actionable option being to "Move to Bin". The software cannot be run even from the command-line. The workarounds are painful for users. gon helps you automate the process of notarization.

Features

See roadmap for features that we want to support but don't yet.

Example

The example below runs gon against itself to generate a zip and dmg.

gon Example

Installation

The easiest way to install gon is via Homebrew:

$ brew install mitchellh/gon/gon

You may also download the appropriate release for your platform from the releases page. These are all signed and notarized to run out of the box on macOS 10.15+.

You can also compile from source using Go 1.13 or later using standard go build. Please ensure that Go modules are enabled.

Usage

gon requires a configuration file that can be specified as a file path or passed in via stdin. The configuration specifies all the settings gon will use to sign and package your files.

gon must be run on a macOS machine with XCode 11.0 or later. Code signing, notarization, and packaging all require tools that are only available on macOS machines.

$ gon [flags] [CONFIG]

When executed, gon will sign, package, and notarize configured files into requested formats. gon will exit with a 0 exit code on success and any other value on failure.

Prerequisite: Acquiring a Developer ID Certificate

Before using gon, you must acquire a Developer ID Certificate. To do this, you can either do it via the web or via Xcode locally on a Mac. Using Xcode is easier if you already have it installed.

Via the web:

  1. Sign into developer.apple.com with valid Apple ID credentials. You may need to sign up for an Apple developer account.

  2. Navigate to the certificates page.

  3. Click the "+" icon, select "Developer ID Application" and follow the steps.

  4. After downloading the certificate, double-click to import it into your keychain. If you're building on a CI machine, every CI machine must have this certificate in their keychain.

Via Xcode:

  1. Open Xcode and go to Xcode => Preferences => Accounts

  2. Click the "+" in the bottom left and add your Apple ID if you haven't already.

  3. Select your Apple account and click "Manage Certificates" in the bottom right corner.

  4. Click "+" in the bottom left corner and click "Developer ID Application".

  5. Right-click the newly created cert in the list, click "export" and export the file as a p12-formatted certificate. Save this somewhere. You'll never be able to download it again.

To verify you did this correctly, you can inspect your keychain:

$ security find-identity -v
  1) 97E4A93EAA8BAC7A8FD2383BFA459D2898100E56 "Developer ID Application: Mitchell Hashimoto (GK79KXBF4F)"
     1 valid identities found

You should see one or more certificates and at least one should be your Developer ID Application certificate. The hexadecimal string prefix is the value you can use in your configuration file to specify the identity.

Configuration File

The configuration file can specify allow/deny lists of licenses for reports, license overrides for specific dependencies, and more. The configuration file format is HCL or JSON.

Example:

source = ["./terraform"]
bundle_id = "com.mitchellh.example.terraform"

apple_id {
  username = "mitchell@example.com"
  password = "@env:AC_PASSWORD"
  provider = "UL304B4VGY"
}

sign {
  application_identity = "Developer ID Application: Mitchell Hashimoto"
}

dmg {
  output_path = "terraform.dmg"
  volume_name = "Terraform"
}

zip {
  output_path = "terraform.zip"
}
{
    "source" : ["./terraform"],
    "bundle_id" : "com.mitchellh.example.terraform",
    "apple_id": {
        "username" : "mitchell@example.com",
        "password":  "@env:AC_PASSWORD",
        "provider":  "UL304B4VGY"
    },
    "sign" :{
        "application_identity" : "Developer ID Application: Mitchell Hashimoto"
    },
    "dmg" :{
        "output_path":  "terraform.dmg",
        "volume_name":  "Terraform"
    },
    "zip" :{
        "output_path" : "terraform.zip"
    }
}

Supported configurations:

Notarization-only mode:

Notarization-Only Configuration

You can configure gon to notarize already-signed files. This is useful if you're integrating gon into an existing build pipeline that may already support creation of pkg, app, etc. files.

Because notarization requires the payload of packages to also be signed, this mode assumes that you have codesigned the payload as well as the package itself. gon will not sign your package in the notarize blocks. Please do not confuse this with when source is set and gon itself creates your packages, in which case it will also sign them.

You can use this in addition to specifying source as well. In this case, we will codesign & package the files specified in source and then notarize those results as well as those in notarize blocks.

Example in HCL and then the identical configuration in JSON:

notarize {
  path = "/path/to/terraform.pkg"
  bundle_id = "com.mitchellh.example.terraform"
  staple = true
}

apple_id {
  username = "mitchell@example.com"
  password = "@env:AC_PASSWORD"
}
{
  "notarize": [{
    "path": "/path/to/terraform.pkg",
    "bundle_id": "com.mitchellh.example.terraform",
    "staple": true
  }],

  "apple_id": {
     "username": "mitchell@example.com",
     "password": "@env:AC_PASSWORD"
  }
}

Note you may specify multiple notarize blocks to notarize multipel files concurrently.

Processing Time

The notarization process requires submitting your package(s) to Apple and waiting for them to scan them. Apple provides no public SLA as far as I can tell.

In developing gon and working with the notarization process, I've found the process to be fast on average (< 10 minutes) but in some cases notarization requests have been queued for an hour or more.

gon will output status updates as it goes, and will wait indefinitely for notarization to complete. If gon is interrupted, you can check the status of a request yourself using the request UUID that gon outputs after submission.

Using within Automation

gon is built to support running within automated environments such as CI pipelines. In this environment, you should use JSON configuration files with gon and the -log-json flag to get structured logging output.

Machine-Readable Output

gon always outputs human-readable output on stdout (including errors) and all log output on stderr. By specifying -log-json the log entries will be structured with JSON. You can process the stream of JSON using a tool such as jq or any scripting language to extract critical information such as the request UUID, status, and more.

When gon is run in an environment with no TTY, the human output will not be colored. This makes it friendlier for output logs.

Example:

$ gon -log-level=info -log-json ./config.hcl
...

Note you must specify both -log-level and -log-json. The -log-level flag enables logging in general. An info level is enough in automation environments to get all the information you'd want.

Prompts

On first-run may be prompted multiple times for passwords. If you click "Always Allow" then you will not be prompted again. These prompts are originating from Apple software that gon is subprocessing, and not from gon itself.

I do not currently know how to script the approvals, so the recommendation on build machines is to run gon manually once. If anyone finds a way to automate this please open an issue, let me know, and I'll update this README.

Usage with GoReleaser

GoReleaser is a popular full featured release automation tool for Go-based projects. Gon can be used with GoReleaser to augment the signing step to notarize your binaries as part of a GoReleaser pipeline.

Here is an example GoReleaser configuration to sign your binaries:

builds:
- binary: foo
  id: foo
  goos:
  - linux
  - windows
  goarch:
  - amd64
# notice that we need a separated build for the macos binary only:
- binary: foo
  id: foo-macos
  goos:
  - darwin
  goarch:
  - amd64
signs:
  - signature: "${artifact}.dmg"
    ids:
    - foo-macos # here we filter the macos only build id
    # you'll need to have gon on PATH
    cmd: gon
    # you can follow the gon docs to properly create the gon.hcl config file:
    # https://github.com/mitchellh/gon
    args:
    - gon.hcl
    artifacts: all

To learn more, see the GoReleaser documentation.

Go Library

Godoc

We also expose a supported API for signing, packaging, and notarizing files using the Go programming language. Please see the linked Go documentation for more details.

The libraries exposed are purposely lower level and separate out the sign, package, notarization, and stapling steps. This lets you integrate this functionality into any tooling easily vs. having an opinionated gon-CLI experience.

Troubleshooting

"We are unable to create an authentication session. (-22016)"

You likely have Apple 2FA enabled. You'll need to generate an application password and use that instead of your Apple ID password.

Roadmap

These are some things I'd love to see but aren't currently implemented.