diff --git a/Makefile b/Makefile index f6d91e6ecbeea..5f694cf6e34aa 100644 --- a/Makefile +++ b/Makefile @@ -429,18 +429,25 @@ build-archive: release-unix: clean full build-archive @if [ -f e/Makefile ]; then $(MAKE) -C e release; fi +include darwin-signing.mk + .PHONY: release-darwin-unsigned release-darwin-unsigned: RELEASE:=$(RELEASE)-unsigned release-darwin-unsigned: clean full build-archive .PHONY: release-darwin +# Only run signing/notarization if Apple username/pass are provided. +# Export DEVELOPER_ID_APPLICATION so it can be used by e/Makefile. release-darwin: ABSOLUTE_BINARY_PATHS:=$(addprefix $(CURDIR)/,$(BINARIES)) release-darwin: release-darwin-unsigned - # Only run if Apple username/pass for notarization are provided if [ -n "$$APPLE_USERNAME" -a -n "$$APPLE_PASSWORD" ]; then \ + $(eval export DEVELOPER_ID_APPLICATION) \ cd ./build.assets/tooling/ && \ go run ./cmd/notarize-apple-binaries/*.go \ - --log-level=debug $(ABSOLUTE_BINARY_PATHS); \ + --developer-id=$(DEVELOPER_ID_APPLICATION) \ + --bundle-id=$(TELEPORT_BUNDLEID) \ + --log-level=debug \ + $(ABSOLUTE_BINARY_PATHS); \ fi $(MAKE) build-archive @if [ -f e/Makefile ]; then $(MAKE) -C e release; fi @@ -1091,6 +1098,7 @@ endif # build .pkg .PHONY: pkg pkg: + $(eval export DEVELOPER_ID_APPLICATION DEVELOPER_ID_INSTALLER) mkdir -p $(BUILDDIR)/ cp ./build.assets/build-package.sh ./build.assets/build-common.sh $(BUILDDIR)/ chmod +x $(BUILDDIR)/build-package.sh @@ -1102,6 +1110,7 @@ pkg: # build tsh client-only .pkg .PHONY: pkg-tsh pkg-tsh: + $(eval export DEVELOPER_ID_APPLICATION DEVELOPER_ID_INSTALLER) ./build.assets/build-pkg-tsh.sh -t oss -v $(VERSION) $(TARBALL_PATH_SECTION) mkdir -p $(BUILDDIR)/ mv tsh*.pkg* $(BUILDDIR)/ diff --git a/build.assets/build-common.sh b/build.assets/build-common.sh index 841e2bffb8659..7549bcc39d2a6 100644 --- a/build.assets/build-common.sh +++ b/build.assets/build-common.sh @@ -6,25 +6,6 @@ # Toggle this via flags in your main script. DRY_RUN_PREFIX='' -# Teleport / tsh certificates/info. -# Used by other scripts. -#shellcheck disable=SC2034 -readonly DEVELOPER_ID_APPLICATION='0FFD3E3413AB4C599C53FBB1D8CA690915E33D83' -#shellcheck disable=SC2034 -readonly DEVELOPER_ID_INSTALLER='82B625AD327C241B378A54B4B254BB08CE71B5DF' -readonly TEAMID='QH8AA5B8UP' -#shellcheck disable=SC2034 -readonly TSH_BUNDLEID="$TEAMID.com.gravitational.teleport.tsh" -#shellcheck disable=SC2034 -readonly TSH_SKELETON='tsh' # relative to build.assets/macos/ - -# tshdev certs/info. -#readonly DEVELOPER_ID_APPLICATION='A5604F285B0957134EA099AC515BD9E0787228AC' -#readonly DEVELOPER_ID_INSTALLER='C1A831A974DF69563432C87A4979F7982DD91FBE' -#readonly TEAMID='K497G57PDJ' -#readonly TSH_BUNDLEID="$TEAMID.com.goteleport.tshdev" -#readonly TSH_SKELETON='tshdev' # relative to build.assets/macos/ - # TARBALL_CACHE is used by find_or_fetch_tarball. readonly TARBALL_CACHE=/tmp/teleport-tarballs diff --git a/build.assets/keychain-setup.sh b/build.assets/keychain-setup.sh new file mode 100755 index 0000000000000..d3f0ae440f67a --- /dev/null +++ b/build.assets/keychain-setup.sh @@ -0,0 +1,219 @@ +#!/bin/bash +# +# keychain-setup.sh creates a MacOS keychain for application binary signing +# and for installer package signing. Each use separate keys that need to +# be loaded into a keychain. +# +# This is intended to be called from CI to set up the keychain for signing +# and notarizing MacOS binaries and packages/images. It can be called manually +# during development if you have the development keys and notarization +# username/password in your environment. +# +# This is MVP - the intention is to write all the keychain management, signing +# and notarizing as a Go program. +# +#----------------------------------------------------------------------------- +usage() { + cat <...} +Available options: + -a take base64-encoded application key from + -a @ take application key from + -A take password for application key from + -i take base64-encoded installation key from + -i @ take installation key from + -I take password for installation key from + -k create .keychain (default "build") + -p use on keychain (default "insecure") + -v verbose. Print commands before running them + -n dry run. Do not run commands, just print them +EOF +} + +#----------------------------------------------------------------------------- +APPLICATION_KEY_FILE='' +APPLICATION_KEY_PASSWORD='' +INSTALLER_KEY_FILE='' +INSTALLER_KEY_PASSWORD='' +KEYCHAIN='build' +KEYCHAIN_PASSWORD='insecure' # Does not need to be secret on CI, as the keychain is removed after. +VERBOSE=false +DRY_RUN=false + +# This should be an array of filenames, but bash on MacOS (3.2.57) seems to unset +# the array before the EXIT trap fires and we get an unset variable error when +# trying to remove the files listed in the array. +tmpfiles='' + +#----------------------------------------------------------------------------- +main() { + set -euo pipefail + + # Always remove temp files, even if dry run + trap 'DRY_RUN=false rm -f ${tmpfiles}' EXIT + parse_args "$@" + + create_keychain "${KEYCHAIN}" "${KEYCHAIN_PASSWORD}" + add_key "${APPLICATION_KEY_FILE}" "${APPLICATION_KEY_PASSWORD}" "${KEYCHAIN}" "${KEYCHAIN_PASSWORD}" + add_key "${INSTALLER_KEY_FILE}" "${INSTALLER_KEY_PASSWORD}" "${KEYCHAIN}" "${KEYCHAIN_PASSWORD}" +} + +# Create a keychain ($1) with a password ($2) and put it on the user keychain +# search path. +create_keychain() { + local keychain="$1" password="$2" + run security create-keychain -p "${password}" "${keychain}" + run security unlock-keychain -p "${password}" "${keychain}" + run security set-keychain-settings "${keychain}" # keep keychain unlocked + + # Add the new keychain to the search path, otherwise codesign does not find the keys + local kpath + kpath="$(security list-keychains -d user | sed 's/.*"\([^"]*\)"/\1/')" + # shellcheck disable=SC2086 # Double quote to prevent globbing and word splitting. + # We want word splitting on ${kpath} + run security list-keychains -d user -s "${keychain}" ${kpath} +} + +# Add a key from a file ($1) protected with a passphrase ($2) to a keychain ($3) +# protected with a password ($4). This is to allow `/usr/bin/codesign` to access +# the key. If the key file name is empty, add_key returns without doing anything. +add_key() { + local keyfile="$1" passphrase="$2" keychain="$3" keychain_password="$4" + if [[ -z "${keyfile}" ]]; then + return 0 + fi + run security import "${keyfile}" -k "${keychain}" -P "${passphrase}" -T /usr/bin/codesign + # Set ACLs so the key can be used for code signing. + # Note: This selects all the signing keys (-s) in the keychain to be usable + # for code signing. Not a problem because the keychain is just for that only + # and only contains the keys we've just added. + run security set-key-partition-list \ + -S 'apple-tool:,apple:,codesign:' \ + -s -k "${keychain_password}" "${keychain}" +} + +#----------------------------------------------------------------------------- +parse_args() { + OPTSTRING=':A:I:a:i:k:np:v' + while getopts "${OPTSTRING}" opt; do + case "${opt}" in + a) + if [[ -n "${APPLICATION_KEY_FILE}" ]]; then + error 'Application key specified multiple times' + fi + APPLICATION_KEY_FILE="$(get_key "${OPTARG}" appkey)" + ;; + A) + require_var "${OPTARG}" + APPLICATION_KEY_PASSWORD="${!OPTARG}" + ;; + i) + if [[ -n "${INSTALLER_KEY_FILE}" ]]; then + error 'Installation key specified multiple times' + fi + INSTALLER_KEY_FILE="$(get_key "${OPTARG}" instkey)" + ;; + I) + require_var "${OPTARG}" + INSTALLER_KEY_PASSWORD="${!OPTARG}" + ;; + k) + KEYCHAIN="${OPTARG}" + ;; + p) + if [[ -z "${OPTARG}" ]]; then + error 'Keychain password cannot be empty' + fi + KEYCHAIN_PASSWORD="${OPTARG}" + ;; + + n) + DRY_RUN=true + ;; + v) + VERBOSE=true + ;; + \?) + error_usage 'Invalid option: -%s\n' "${OPTARG}" >&2 + ;; + :) + error_usage 'Option -%s requires an argument\n' "${OPTARG}" >&2 + ;; + esac + done + shift $((OPTIND - 1)) + + # Keychains have to end with ".keychain". Add it if necessary. + KEYCHAIN="${KEYCHAIN%.keychain}.keychain" +} + +#----------------------------------------------------------------------------- +# Run a command. If $VERBOSE or $DRY_RUN is true, echo the command. If +# $DRY_RUN is true, don't actually run the command, only print it. +run() { + if "${VERBOSE}" || "${DRY_RUN}"; then + echo "$@" + fi + "${DRY_RUN}" || "$@" +} + +# Require an environment variable be set and not empty +require_var() { + local var="$1" + if [[ -z "${!var:-}" ]]; then + error 'env var "%s" unset or empty' "${var}" + fi + return 0 +} + +# Require a file exists +require_file() { + local file="$1" + if ! [[ -f "${file}" ]]; then + error 'File does not exist: %s' "${file}" + fi + return 0 +} + +# Get a key from an argument. If the argument starts with @, the rest is taken +# as a filename containing the key. Otherwise it is taken as an environment +# variable name which contains a base64 encoded key, which is decoded and placed +# into a temp file. The filename of the key is output to stdout. If a temp file +# was created, it will be removed when the script exits. +get_key() { + local key="$1" keytype="$2" fname + # If the key starts with an @, take the rest as a filename. Otherwise + # its an environment variable name. + if [[ "${key}" =~ ^@ ]]; then + fname="${key:1}" + require_file "${fname}" + else + require_var "${key}" + fname="$(mktemp -t "${keytype}").p12" + tmpfiles="${tmpfiles} ${fname}" + printenv "${key}" | base64 --decode > "${fname}" + fi + echo "${fname}" +} + +error() { + # shellcheck disable=SC2059 # (Don't use variables in the printf format string) + # we take a format string arg - the format string IS a variable + printf "$@" >&2 + printf '\n' >&2 + exit 1 +} + +error_usage() { + # shellcheck disable=SC2059 # (Don't use variables in the printf format string) + # we take a format string arg - the format string IS a variable + printf "$@" >&2 + printf '\n' >&2 + usage >&2 + exit 1 +} + +#----------------------------------------------------------------------------- +# Only run main if executed as a script and not sourced. +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@"; fi + diff --git a/darwin-signing.mk b/darwin-signing.mk new file mode 100644 index 0000000000000..bd88265c8d4b2 --- /dev/null +++ b/darwin-signing.mk @@ -0,0 +1,76 @@ +# MacOS/Darwin variables for packaging, signing and notarizing. +# +# These are parameterized per environment, with `promote` for official +# releases and `build` for development testing. These environment names +# come from our configuration in GitHub Actions. + +# Default environment name if not specified. This is currently for Drone +# which does not set `ENVIRONMENT_NAME`. Once migrated fully to GitHub +# actions, we should change this to `build` as the default. +ENVIRONMENT_NAME ?= promote + +# Variables defined here are defined with the environment name suffix +# to specify the appropriate value for that environment. The unsuffixed +# names select the appropriate value based on `ENVIRONMENT_NAME` + +# Developer "team" and keys. +# TEAMID is an Apple-assigned identifier for a developer. It has two keys, +# one for signing binaries (application) and one for signing packages/images +# (installer). The keys are identified by name per-environment which we use +# to extract the key IDs. Key names can be view by running `security find-identity`. +# +# NOTE: If you need to export the DEVELOPER_ID_{APPLICATION,INSTALLER} +# variables to the environment for a command, it should be done within the +# recipe containing the command using $(eval export DEVELOPER_ID_APPLICATION ...). +# This is so the `security` shell command is only run to extract the key ID +# if necessary. If exported at the top level, it will run every time `make` +# is run. +# +# e.g. +# pkg: +# $(eval export DEVELOPER_ID_APPLICATION DEVELOPER_ID_INSTALLER) +# ./build.assets/build-package.sh ... +# +TEAMID = $(TEAMID_$(ENVIRONMENT_NAME)) +DEVELOPER_ID_APPLICATION = $(call get_key_id,$(DEVELOPER_KEY_NAME_$(ENVIRONMENT_NAME))) +DEVELOPER_ID_INSTALLER = $(call get_key_id,$(INSTALLER_KEY_NAME_$(ENVIRONMENT_NAME))) + +# Don't export DEVELOPER_ID_APPLICATION or DEVELOPER_ID_INSTALLER as it +# evaluates them. They should only be evaluated them if used. +unexport DEVELOPER_ID_APPLICATION DEVELOPER_ID_INSTALLER + +# Bundle IDs identify packages/images. We use different bundle IDs for +# release and development. +TELEPORT_BUNDLEID = $(TELEPORT_BUNDLEID_$(ENVIRONMENT_NAME)) +TSH_BUNDLEID = $(TSH_BUNDLEID_$(ENVIRONMENT_NAME)) + +# TSH_SKELETON is a directory name relative to build.assets/macos/ +TSH_SKELETON = $(TSH_SKELETON_$(ENVIRONMENT_NAME)) + +# --- promote environment +# Key names can be found on https://goteleport.com/security +TEAMID_promote = QH8AA5B8UP +DEVELOPER_KEY_NAME_promote = Developer ID Application: Gravitational Inc. +INSTALLER_KEY_NAME_promote = Developer ID Installer: Gravitational Inc. +TELEPORT_BUNDLEID_promote = com.gravitational.teleport +TSH_BUNDLEID_promote = $(TEAMID).com.gravitational.teleport.tsh +TSH_SKELETON_promote = tsh + +# --- build environment +TEAMID_build = K497G57PDJ +DEVELOPER_KEY_NAME_build = Developer ID Application: Ada Lin +INSTALLER_KEY_NAME_build = Developer ID Installer: Ada Lin +TELEPORT_BUNDLEID_build = com.goteleport.dev +TSH_BUNDLEID_build = $(TEAMID).com.goteleport.tshdev +TSH_SKELETON_build = tshdev + +# --- utility +# Extract application/installer key ID from keychain. This looks at all +# keychains in the search path. It should be used with $(call ...). +# e.g. $(call get_key_id,Key Name goes here) +get_key_id = $(or $(word 2,$(shell $(get_key_id_cmd))), $(missing_key_error)) +get_key_id_cmd = security find-identity -v -s codesigning | grep --fixed-strings --max-count=1 "$(1)" +missing_key_error = $(error Could not find key named "$(1)" in keychain) + +# Dont export missing_key_error or get_key_id as it evaluates them +unexport missing_key_error get_key_id