Skip to content

Commit 7a96bf6

Browse files
authored
Merge pull request #12 from gruntwork-io/sign-macos
Add support for signing macOS binaries
2 parents 232bcf9 + 5db0364 commit 7a96bf6

File tree

12 files changed

+805
-5
lines changed

12 files changed

+805
-5
lines changed

.github/assets/.gon_amd64.hcl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# See https://github.com/gruntwork-io/terraform-aws-ci/blob/main/modules/sign-binary-helpers/
2+
# for further instructions on how to sign the binary + submitting for notarization.
3+
4+
source = ["./bin/runbooks_darwin_amd64"]
5+
6+
bundle_id = "io.gruntwork.app.runbooks"
7+
8+
apple_id {
9+
username = "@env:AC_USERNAME"
10+
}
11+
12+
sign {
13+
application_identity = "Developer ID Application: Gruntwork, Inc."
14+
}
15+
16+
zip {
17+
output_path = "runbooks_darwin_amd64.zip"
18+
}
19+

.github/assets/.gon_arm64.hcl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# See https://github.com/gruntwork-io/terraform-aws-ci/blob/main/modules/sign-binary-helpers/
2+
# for further instructions on how to sign the binary + submitting for notarization.
3+
4+
source = ["./bin/runbooks_darwin_arm64"]
5+
bundle_id = "io.gruntwork.app.runbooks"
6+
7+
apple_id {
8+
username = "@env:AC_USERNAME"
9+
}
10+
11+
sign {
12+
application_identity = "Developer ID Application: Gruntwork, Inc."
13+
}
14+
15+
zip {
16+
output_path = "runbooks_darwin_arm64.zip"
17+
}
18+

.github/assets/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# GitHub Assets
2+
3+
This directory contains configuration files used in the release and CI/CD workflows.
4+
5+
## Files
6+
7+
### `release-assets-config.json`
8+
9+
Configuration file that defines the release asset matrix for the runbooks binary. It specifies:
10+
11+
- **Platforms**: The target OS/architecture combinations, including which ones require code signing.
12+
13+
- **Archive Formats**: The compression formats used for distribution (e.g. `zip`, `tar.gz`)
14+
15+
- **Additional Files**: Extra files included in releases (e.g., `SHA256SUMS` for checksum verification)
16+
17+
> **Note:** As of Dec 1, 20225, this config is only consumed by the macOS signing workflow (`sign-macos.yml`) to determine which binaries need signing. The build matrix in `release.yml` is maintained separately.
18+
19+
### `.gon_xxx.hcl`
20+
21+
[Gon](https://github.com/mitchellh/gon) configuration file for signing and notarizing the **macOS ARM64** (Apple Silicon) binary. Gon is a tool that automates the Apple code signing and notarization process required for distributing macOS binaries outside the App Store.
22+
23+
## Related
24+
25+
The [lib-release-config.sh](/.github/scripts/release/lib-release-config.sh) script consumes the `release-assets-config.json` file.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"platforms": [
3+
{
4+
"os": "darwin",
5+
"arch": "amd64",
6+
"binary": "runbooks_darwin_amd64",
7+
"signed": true
8+
},
9+
{
10+
"os": "darwin",
11+
"arch": "arm64",
12+
"binary": "runbooks_darwin_arm64",
13+
"signed": true
14+
},
15+
{
16+
"os": "linux",
17+
"arch": "386",
18+
"signed": false,
19+
"binary": "runbooks_linux_386"
20+
},
21+
{
22+
"os": "linux",
23+
"arch": "amd64",
24+
"signed": false,
25+
"binary": "runbooks_linux_amd64"
26+
},
27+
{
28+
"os": "linux",
29+
"arch": "arm64",
30+
"signed": false,
31+
"binary": "runbooks_linux_arm64"
32+
},
33+
{
34+
"os": "windows",
35+
"arch": "386",
36+
"signed": false,
37+
"binary": "runbooks_windows_386.exe"
38+
},
39+
{
40+
"os": "windows",
41+
"arch": "amd64",
42+
"signed": false,
43+
"binary": "runbooks_windows_amd64.exe"
44+
}
45+
],
46+
"archive_formats": [
47+
{
48+
"extension": "zip",
49+
"description": "ZIP archive"
50+
},
51+
{
52+
"extension": "tar.gz",
53+
"description": "TAR.GZ archive"
54+
}
55+
],
56+
"additional_files": [
57+
{
58+
"name": "SHA256SUMS",
59+
"description": "Checksums file"
60+
}
61+
]
62+
}
63+
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/bin/bash
2+
3+
# Library script to read release assets configuration
4+
# Usage: source .github/scripts/release/lib-release-config.sh
5+
6+
readonly RELEASE_CONFIG_FILE=".github/assets/release-assets-config.json"
7+
8+
# Get list of all binary filenames
9+
get_all_binaries() {
10+
jq -r '.platforms[].binary' "$RELEASE_CONFIG_FILE"
11+
}
12+
13+
# Get list of binaries for a specific OS
14+
get_binaries_for_os() {
15+
local os="$1"
16+
jq -r --arg os "$os" '.platforms[] | select(.os == $os) | .binary' "$RELEASE_CONFIG_FILE"
17+
}
18+
19+
# Get total binary count (computed from platforms array)
20+
get_binary_count() {
21+
jq -r '.platforms | length' "$RELEASE_CONFIG_FILE"
22+
}
23+
24+
# Get total expected file count (computed: binaries + archives + additional files)
25+
get_total_file_count() {
26+
jq -r '
27+
(.platforms | length) as $binaries |
28+
(.archive_formats | length) as $formats |
29+
(.additional_files | length) as $additional |
30+
$binaries + ($binaries * $formats) + $additional
31+
' "$RELEASE_CONFIG_FILE"
32+
}
33+
34+
# Get list of archive extensions
35+
get_archive_extensions() {
36+
jq -r '.archive_formats[].extension' "$RELEASE_CONFIG_FILE"
37+
}
38+
39+
# Get list of additional files
40+
get_additional_files() {
41+
jq -r '.additional_files[].name' "$RELEASE_CONFIG_FILE"
42+
}
43+
44+
# Generate expected files list (for verification)
45+
get_all_expected_files() {
46+
local binaries archive_ext additional
47+
48+
# Get binaries
49+
binaries=$(get_all_binaries)
50+
51+
# Generate list: binaries + archives + additional files
52+
echo "$binaries"
53+
54+
# Add archives for each binary
55+
for binary in $binaries; do
56+
while IFS= read -r ext; do
57+
echo "${binary}.${ext}"
58+
done < <(get_archive_extensions)
59+
done
60+
61+
# Add additional files
62+
get_additional_files
63+
}
64+
65+
# Get platform info as JSON for a specific binary
66+
get_platform_info() {
67+
local binary="$1"
68+
jq --arg binary "$binary" '.platforms[] | select(.binary == $binary)' "$RELEASE_CONFIG_FILE"
69+
}
70+
71+
# Generate markdown table rows for summary
72+
generate_platform_table_rows() {
73+
jq -r '.platforms[] | "| \(.os | ascii_downcase) | \(.arch) | \(if .signed then "Yes" else "No" end) | Uploaded |"' "$RELEASE_CONFIG_FILE" |
74+
awk '{
75+
# Capitalize first letter of OS
76+
if ($2 == "darwin") $2 = "macOS"
77+
else if ($2 == "linux") $2 = "Linux"
78+
else if ($2 == "windows") $2 = "Windows"
79+
print
80+
}'
81+
}
82+
83+
# Check if config file exists
84+
verify_config_file() {
85+
if [[ ! -f "$RELEASE_CONFIG_FILE" ]]; then
86+
echo "ERROR: Release config file not found: $RELEASE_CONFIG_FILE" >&2
87+
return 1
88+
fi
89+
}
90+
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
################################################################################
6+
# Script: import-cert.sh
7+
# Description: Imports the macOS developer certificate into the system keychain.
8+
# Creates a temporary keychain, imports the P12 certificate, and
9+
# downloads the Apple root certificate for validation. This is a
10+
# one-time setup step before signing binaries.
11+
#
12+
# Usage: import-cert.sh [--macos-skip-root-certificate]
13+
#
14+
# Required Environment Variables:
15+
# MACOS_CERTIFICATE: macOS developer certificate in P12 format (base64 encoded)
16+
# MACOS_CERTIFICATE_PASSWORD: macOS certificate password
17+
#
18+
# Optional Arguments:
19+
# --macos-skip-root-certificate: Skip importing Apple Root certificate
20+
################################################################################
21+
22+
# Apple certificate used to validate developer certificates https://www.apple.com/certificateauthority/
23+
readonly APPLE_ROOT_CERTIFICATE="http://certs.apple.com/devidg2.der"
24+
25+
function print_usage {
26+
echo
27+
echo "Usage: $0 [OPTIONS]"
28+
echo
29+
echo "Required Environment Variables:"
30+
echo -e " MACOS_CERTIFICATE\t\tmacOS developer certificate in P12 format, encoded in base64."
31+
echo -e " MACOS_CERTIFICATE_PASSWORD\tmacOS certificate password"
32+
echo
33+
echo "Optional Arguments:"
34+
echo -e " --macos-skip-root-certificate\t\tSkip importing Apple Root certificate. Useful when running in already configured environment."
35+
echo -e " --help\t\t\t\tShow this help text and exit."
36+
}
37+
38+
function main {
39+
local mac_skip_root_certificate=""
40+
41+
while [[ $# -gt 0 ]]; do
42+
local key="$1"
43+
case "$key" in
44+
--macos-skip-root-certificate)
45+
mac_skip_root_certificate=true
46+
shift
47+
;;
48+
--help)
49+
print_usage
50+
exit
51+
;;
52+
-* )
53+
echo "ERROR: Unrecognized argument: $key"
54+
print_usage
55+
exit 1
56+
;;
57+
* )
58+
echo "ERROR: Unexpected positional argument: $1"
59+
print_usage
60+
exit 1
61+
;;
62+
esac
63+
done
64+
65+
ensure_macos
66+
import_certificate_mac "${mac_skip_root_certificate}"
67+
}
68+
69+
function ensure_macos {
70+
if [[ $OSTYPE != 'darwin'* ]]; then
71+
echo -e "Certificate import is supported only on macOS"
72+
exit 1
73+
fi
74+
}
75+
76+
function import_certificate_mac {
77+
local -r mac_skip_root_certificate="$1"
78+
assert_env_var_not_empty "MACOS_CERTIFICATE"
79+
assert_env_var_not_empty "MACOS_CERTIFICATE_PASSWORD"
80+
81+
local mac_certificate_pwd="${MACOS_CERTIFICATE_PASSWORD}"
82+
local keystore_pw="${RANDOM}"
83+
84+
# Create separated keychain file to store certificate and do quick cleanup of sensitive data
85+
local db_file
86+
db_file=$(mktemp "/tmp/XXXXXX-keychain")
87+
rm -rf "${db_file}"
88+
echo "Creating separated keychain for certificate"
89+
security create-keychain -p "${keystore_pw}" "${db_file}"
90+
security default-keychain -s "${db_file}"
91+
security unlock-keychain -p "${keystore_pw}" "${db_file}"
92+
echo "${MACOS_CERTIFICATE}" | base64 -d | security import /dev/stdin -f pkcs12 -k "${db_file}" -P "${mac_certificate_pwd}" -T /usr/bin/codesign
93+
if [[ "${mac_skip_root_certificate}" == "" ]]; then
94+
# Download Apple root certificate used as root for developer certificate
95+
curl -v "${APPLE_ROOT_CERTIFICATE}" --output certificate.der
96+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.der
97+
fi
98+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${keystore_pw}" "${db_file}"
99+
100+
echo "Certificate imported successfully"
101+
102+
# NOTE: Do NOT add a trap to clean up the keychain here!
103+
# The keychain must persist for sign.sh to use it.
104+
# Cleanup is handled by sign-and-verify-binaries.sh after signing is complete.
105+
}
106+
107+
function assert_env_var_not_empty {
108+
local -r var_name="$1"
109+
local -r var_value="${!var_name}"
110+
111+
if [[ -z "$var_value" ]]; then
112+
echo "ERROR: Required environment variable $var_name not set."
113+
exit 1
114+
fi
115+
}
116+
117+
main "$@"
118+

0 commit comments

Comments
 (0)