teleport/build.assets/keychain-setup.sh
Cam Hutchison af8fe6f57a
release: Update build for product signing (#23820)
Update the build scripts to properly set up the key for signing packages
using `productsign`, and parameterise the bundle ID for packages in the
packaging scripts.
2023-04-03 00:30:48 +00:00

224 lines
7 KiB
Bash
Executable file

#!/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 <<EOF
Usage: ${0##*/} {<options>...}
Available options:
-a <envvar> take base64-encoded application key from <envvar>
-a @<file> take application key from <file>
-A <envvar> take password for application key from <envvar>
-i <envvar> take base64-encoded installation key from <envvar>
-i @<file> take installation key from <file>
-I <envvar> take password for installation key from <envvar>
-k <keychain> create <keychain>.keychain (default "build")
-p <password> use <password> 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` and
# `/usr/bin/productsign` 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 \
-T /usr/bin/productsign
# 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