From 9a1fc992529eb585c96e92a2bec9fc40c7272059 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 13:59:53 +0200 Subject: [PATCH 1/2] Provide macOs binary signing and notarization. --- .github/workflows/release-binaries.yml | 99 +++++++++++++++++++++++++- PROJECT.md | 3 +- README.md | 11 +-- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 122550d..7a35426 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -17,8 +17,8 @@ concurrency: jobs: release: - runs-on: ubuntu-latest - timeout-minutes: 10 + runs-on: macos-latest + timeout-minutes: 30 steps: - name: Checkout Repository @@ -73,6 +73,101 @@ jobs: GOOS=darwin GOARCH=amd64 go build -trimpath -o dist/embed-code-macos main.go chmod +x dist/embed-code-linux dist/embed-code-macos + # Sign the macOS binary with a Developer ID certificate stored in GitHub + # secrets. The certificate must be exported as a base64-encoded .p12 file. + - name: Sign macOS Binary + if: steps.release.outputs.publish == 'true' + shell: bash + env: + MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + run: | + missing=() + [[ -n "$MACOS_CERTIFICATE_P12_BASE64" ]] || missing+=("MACOS_CERTIFICATE_P12_BASE64") + [[ -n "$MACOS_CERTIFICATE_PASSWORD" ]] || missing+=("MACOS_CERTIFICATE_PASSWORD") + [[ -n "$MACOS_CODESIGN_IDENTITY" ]] || missing+=("MACOS_CODESIGN_IDENTITY") + if (( ${#missing[@]} > 0 )); then + printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 + exit 1 + fi + + keychain_path="$RUNNER_TEMP/embed-code-signing.keychain-db" + certificate_path="$RUNNER_TEMP/embed-code-signing-certificate.p12" + keychain_password="$(uuidgen)" + + # GNU base64 supports --decode; macOS base64 uses -D. + if printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then + : + else + printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" + fi + + security create-keychain -p "$keychain_password" "$keychain_path" + security set-keychain-settings -lut 21600 "$keychain_path" + security unlock-keychain -p "$keychain_password" "$keychain_path" + + curl -fsSL https://www.apple.com/certificateauthority/AppleRootCA-G3.cer \ + -o "$RUNNER_TEMP/AppleRootCA-G3.cer" + curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer \ + -o "$RUNNER_TEMP/AppleWWDRCAG6.cer" + curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ + -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" + + security import "$RUNNER_TEMP/AppleRootCA-G3.cer" -k "$keychain_path" + security import "$RUNNER_TEMP/AppleWWDRCAG6.cer" -k "$keychain_path" + security import "$RUNNER_TEMP/DeveloperIDG2CA.cer" -k "$keychain_path" + security import "$certificate_path" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -A \ + -t cert \ + -f pkcs12 \ + -k "$keychain_path" + security list-keychains -d user -s "$keychain_path" + security set-key-partition-list \ + -S apple-tool:,apple: \ + -s \ + -k "$keychain_password" \ + "$keychain_path" + security find-identity -v -p codesigning "$keychain_path" + + codesign \ + --force \ + --options runtime \ + --timestamp \ + --sign "$MACOS_CODESIGN_IDENTITY" \ + dist/embed-code-macos + codesign --verify --verbose=4 dist/embed-code-macos + + # Notarize the macOS ZIP that will be published as the release asset. + - name: Notarize macOS Binary + if: steps.release.outputs.publish == 'true' + shell: bash + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + run: | + missing=() + [[ -n "$APPLE_ID" ]] || missing+=("APPLE_ID") + [[ -n "$APPLE_TEAM_ID" ]] || missing+=("APPLE_TEAM_ID") + [[ -n "$APPLE_APP_SPECIFIC_PASSWORD" ]] || missing+=("APPLE_APP_SPECIFIC_PASSWORD") + if (( ${#missing[@]} > 0 )); then + printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 + exit 1 + fi + + pushd dist + ditto -c -k --keepParent embed-code-macos embed-code-macos.zip + popd + + xcrun notarytool submit dist/embed-code-macos.zip \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --wait + rm dist/embed-code-macos + # Create the release for the current commit and attach all generated binaries. - name: Publish GitHub Release if: steps.release.outputs.publish == 'true' diff --git a/PROJECT.md b/PROJECT.md index f2076c5..6c38188 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -56,7 +56,8 @@ This repository is configured with these GitHub workflows: - `check`: runs linting, the normal Go test suite, and the showcase end-to-end tests across supported platforms. - `release-binaries`: reads `VERSION`, builds Linux, macOS, and Windows - binaries, and creates the matching GitHub Release on pushes to `master`. + binaries, signs and notarizes the macOS ZIP, and creates the matching GitHub + Release on pushes to `master`. The release tag is `v` from `VERSION`. When the release already exists, the workflow emits a warning and finishes successfully without rebuilding or diff --git a/README.md b/README.md index 7f070e0..d20fac6 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,12 @@ On Linux, for example: > chmod +x embed-code-linux > ``` -> Since binary file for macOS is not signed, it may be necessary -> to change its attributes to allow execution: -> ```bash -> xattr -d com.apple.quarantine embed-code-macos -> ``` +On macOS, download `embed-code-macos.zip`, unzip it, and run the binary: + +```bash +unzip embed-code-macos.zip +./embed-code-macos -mode=check -config-path=showcase/embedding/embed-code.yml +``` Or run it with Go: From 3e6b272ff99bb7197aef29b933f5717ee25242b6 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 14:20:54 +0200 Subject: [PATCH 2/2] Extract bash scripts. --- .github/workflows/release-binaries.yml | 75 +---------------- PROJECT.md | 2 + scripts/release/README.md | 47 +++++++++++ scripts/release/notarize-macos-zip.sh | 73 ++++++++++++++++ scripts/release/sign-macos-binary.sh | 111 +++++++++++++++++++++++++ 5 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 scripts/release/README.md create mode 100755 scripts/release/notarize-macos-zip.sh create mode 100755 scripts/release/sign-macos-binary.sh diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 7a35426..51fb8b6 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -83,61 +83,7 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} run: | - missing=() - [[ -n "$MACOS_CERTIFICATE_P12_BASE64" ]] || missing+=("MACOS_CERTIFICATE_P12_BASE64") - [[ -n "$MACOS_CERTIFICATE_PASSWORD" ]] || missing+=("MACOS_CERTIFICATE_PASSWORD") - [[ -n "$MACOS_CODESIGN_IDENTITY" ]] || missing+=("MACOS_CODESIGN_IDENTITY") - if (( ${#missing[@]} > 0 )); then - printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 - exit 1 - fi - - keychain_path="$RUNNER_TEMP/embed-code-signing.keychain-db" - certificate_path="$RUNNER_TEMP/embed-code-signing-certificate.p12" - keychain_password="$(uuidgen)" - - # GNU base64 supports --decode; macOS base64 uses -D. - if printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then - : - else - printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" - fi - - security create-keychain -p "$keychain_password" "$keychain_path" - security set-keychain-settings -lut 21600 "$keychain_path" - security unlock-keychain -p "$keychain_password" "$keychain_path" - - curl -fsSL https://www.apple.com/certificateauthority/AppleRootCA-G3.cer \ - -o "$RUNNER_TEMP/AppleRootCA-G3.cer" - curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer \ - -o "$RUNNER_TEMP/AppleWWDRCAG6.cer" - curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ - -o "$RUNNER_TEMP/DeveloperIDG2CA.cer" - - security import "$RUNNER_TEMP/AppleRootCA-G3.cer" -k "$keychain_path" - security import "$RUNNER_TEMP/AppleWWDRCAG6.cer" -k "$keychain_path" - security import "$RUNNER_TEMP/DeveloperIDG2CA.cer" -k "$keychain_path" - security import "$certificate_path" \ - -P "$MACOS_CERTIFICATE_PASSWORD" \ - -A \ - -t cert \ - -f pkcs12 \ - -k "$keychain_path" - security list-keychains -d user -s "$keychain_path" - security set-key-partition-list \ - -S apple-tool:,apple: \ - -s \ - -k "$keychain_password" \ - "$keychain_path" - security find-identity -v -p codesigning "$keychain_path" - - codesign \ - --force \ - --options runtime \ - --timestamp \ - --sign "$MACOS_CODESIGN_IDENTITY" \ - dist/embed-code-macos - codesign --verify --verbose=4 dist/embed-code-macos + scripts/release/sign-macos-binary.sh dist/embed-code-macos # Notarize the macOS ZIP that will be published as the release asset. - name: Notarize macOS Binary @@ -148,24 +94,7 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} run: | - missing=() - [[ -n "$APPLE_ID" ]] || missing+=("APPLE_ID") - [[ -n "$APPLE_TEAM_ID" ]] || missing+=("APPLE_TEAM_ID") - [[ -n "$APPLE_APP_SPECIFIC_PASSWORD" ]] || missing+=("APPLE_APP_SPECIFIC_PASSWORD") - if (( ${#missing[@]} > 0 )); then - printf 'Missing required GitHub secret: %s\n' "${missing[@]}" >&2 - exit 1 - fi - - pushd dist - ditto -c -k --keepParent embed-code-macos embed-code-macos.zip - popd - - xcrun notarytool submit dist/embed-code-macos.zip \ - --apple-id "$APPLE_ID" \ - --team-id "$APPLE_TEAM_ID" \ - --password "$APPLE_APP_SPECIFIC_PASSWORD" \ - --wait + scripts/release/notarize-macos-zip.sh dist/embed-code-macos dist/embed-code-macos.zip rm dist/embed-code-macos # Create the release for the current commit and attach all generated binaries. diff --git a/PROJECT.md b/PROJECT.md index 6c38188..fc120d9 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -30,6 +30,8 @@ them inside code fences, and checks whether existing snippets are up-to-date. - `type/`: YAML-compatible string and named-path list types. The import path segment is `type`, but the Go package identifier is `_type` because `type` is a Go keyword. +- `scripts/release/`: helper scripts used by release workflows for signing and + notarizing macOS binaries. - `test/resources/`: parser, embedding, configuration, and source-code fixtures. - `showcase/`: executable user guide and end-to-end example suite. diff --git a/scripts/release/README.md b/scripts/release/README.md new file mode 100644 index 0000000..3755ffb --- /dev/null +++ b/scripts/release/README.md @@ -0,0 +1,47 @@ +# Release Scripts + +These scripts support the `release-binaries` GitHub workflow. +They are intended for the macOS release job. + +## Signing + +Use `sign-macos-binary.sh` to import the Developer ID certificate into a +temporary keychain and sign the macOS binary. + +Required environment variables: + +- `MACOS_CERTIFICATE_P12_BASE64`: base64-encoded `.p12` export of the Developer + ID Application certificate and private key. +- `MACOS_CERTIFICATE_PASSWORD`: password used when exporting the `.p12` file. +- `MACOS_CODESIGN_IDENTITY`: full Developer ID Application identity, such as + `Developer ID Application: Company Name (TEAMID)`. + +Optional environment variable: + +- `MACOS_KEYCHAIN_PASSWORD`: password for the temporary keychain. When omitted, + the script generates one. + +Example: + +```bash +scripts/release/sign-macos-binary.sh dist/embed-code-macos +``` + +## Notarization + +Use `notarize-macos-zip.sh` to package the signed CLI binary as a ZIP archive +and submit that archive to Apple notarization. + +Required environment variables: + +- `APPLE_ID`: Apple Account email used for notarization. +- `APPLE_TEAM_ID`: 10-character Apple Developer Team ID. +- `APPLE_APP_SPECIFIC_PASSWORD`: app-specific password for the Apple Account. + +Example: + +```bash +scripts/release/notarize-macos-zip.sh \ + dist/embed-code-macos \ + dist/embed-code-macos.zip +``` diff --git a/scripts/release/notarize-macos-zip.sh b/scripts/release/notarize-macos-zip.sh new file mode 100755 index 0000000..957050e --- /dev/null +++ b/scripts/release/notarize-macos-zip.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# Packages and notarizes the signed macOS CLI binary. +# +# Usage: +# scripts/release/notarize-macos-zip.sh \ +# dist/embed-code-macos \ +# dist/embed-code-macos.zip +# +# The script expects these environment variables: +# APPLE_ID +# APPLE_TEAM_ID +# APPLE_APP_SPECIFIC_PASSWORD +# +# Apple notarization accepts an archive/package submission, so the CLI binary is +# wrapped in a ZIP instead of being uploaded as a loose executable. +# +# The script writes the ZIP path passed as its second argument and waits for +# Apple to accept or reject the notarization submission. +set -euo pipefail + +# Verifies, that secrets are set. +required_vars=( + APPLE_ID + APPLE_TEAM_ID + APPLE_APP_SPECIFIC_PASSWORD +) + +missing=() +for var_name in "${required_vars[@]}"; do + if [[ -z "${!var_name:-}" ]]; then + missing+=("$var_name") + fi +done +if (( ${#missing[@]} > 0 )); then + printf 'Missing required environment variable: %s\n' "${missing[@]}" >&2 + exit 1 +fi + +if (( $# != 2 )); then + echo "Usage: $0 " >&2 + exit 1 +fi + +binary_path="$1" +zip_path="$2" + +if [[ ! -f "$binary_path" ]]; then + echo "Signed macOS binary does not exist: $binary_path" >&2 + exit 1 +fi + +binary_dir="$(cd "$(dirname "$binary_path")" && pwd)" +binary_name="$(basename "$binary_path")" +zip_dir="$(dirname "$zip_path")" +zip_name="$(basename "$zip_path")" + +# Resolve the ZIP path before changing directories. +mkdir -p "$zip_dir" +zip_dir="$(cd "$zip_dir" && pwd)" +zip_path="$zip_dir/$zip_name" + +pushd "$binary_dir" >/dev/null +ditto -c -k --keepParent "$binary_name" "$zip_path" +popd >/dev/null + +# Wait for the notarization result before publishing. +# ZIP archives do not support stapling, so the release publishes the accepted archive as-is. +xcrun notarytool submit "$zip_path" \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --wait diff --git a/scripts/release/sign-macos-binary.sh b/scripts/release/sign-macos-binary.sh new file mode 100755 index 0000000..91cebe2 --- /dev/null +++ b/scripts/release/sign-macos-binary.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +# Signs the macOS CLI binary with a Developer ID Application certificate. +# +# Usage: +# scripts/release/sign-macos-binary.sh dist/embed-code-macos +# +# The script expects these environment variables: +# MACOS_CERTIFICATE_P12_BASE64 +# MACOS_CERTIFICATE_PASSWORD +# MACOS_CODESIGN_IDENTITY +# +# The certificate value must be a base64-encoded `.p12` export that contains the +# Developer ID Application certificate and private key. +# `MACOS_CODESIGN_IDENTITY` is the full identity printed by +# `security find-identity`, such as: +# +# Developer ID Application: Company Name (TEAMID) +# +# For local diagnostics, `MACOS_KEYCHAIN_PASSWORD` can be set to reuse a known +# temporary keychain password. +# +# The script writes temporary certificate and keychain files under RUNNER_TEMP, +# or TMPDIR when RUNNER_TEMP is not set. It signs the binary in place and verifies +# the resulting signature. +set -euo pipefail + +# Verifies, that secrets are set. +required_vars=( + MACOS_CERTIFICATE_P12_BASE64 + MACOS_CERTIFICATE_PASSWORD + MACOS_CODESIGN_IDENTITY +) + +missing=() +for var_name in "${required_vars[@]}"; do + if [[ -z "${!var_name:-}" ]]; then + missing+=("$var_name") + fi +done +if (( ${#missing[@]} > 0 )); then + printf 'Missing required environment variable: %s\n' "${missing[@]}" >&2 + exit 1 +fi + +if (( $# != 1 )); then + echo "Usage: $0 " >&2 + exit 1 +fi + +binary_path="$1" +if [[ ! -f "$binary_path" ]]; then + echo "macOS binary does not exist: $binary_path" >&2 + exit 1 +fi + +temp_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" +keychain_path="$temp_dir/embed-code-signing.keychain-db" +certificate_path="$temp_dir/embed-code-signing-certificate.p12" +keychain_password="${MACOS_KEYCHAIN_PASSWORD:-$(uuidgen)}" + +# Decode the certificate at runtime so only the temporary runner filesystem ever +# contains the `.p12`. +if printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path" 2>/dev/null; then + : +else + printf '%s' "$MACOS_CERTIFICATE_P12_BASE64" | base64 -D > "$certificate_path" +fi + +security create-keychain -p "$keychain_password" "$keychain_path" +security set-keychain-settings -lut 21600 "$keychain_path" +security unlock-keychain -p "$keychain_password" "$keychain_path" + +# Import Apple's public certificate chain into the temporary keychain. +# Some runners do not have the current Developer ID intermediate certificates. +curl -fsSL https://www.apple.com/certificateauthority/AppleRootCA-G3.cer \ + -o "$temp_dir/AppleRootCA-G3.cer" +curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer \ + -o "$temp_dir/AppleWWDRCAG6.cer" +curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer \ + -o "$temp_dir/DeveloperIDG2CA.cer" + +security import "$temp_dir/AppleRootCA-G3.cer" -k "$keychain_path" +security import "$temp_dir/AppleWWDRCAG6.cer" -k "$keychain_path" +security import "$temp_dir/DeveloperIDG2CA.cer" -k "$keychain_path" + +# Import the private signing identity and allow Apple signing tools to access +# the key non-interactively. +security import "$certificate_path" \ + -P "$MACOS_CERTIFICATE_PASSWORD" \ + -A \ + -t cert \ + -f pkcs12 \ + -k "$keychain_path" +security list-keychains -d user -s "$keychain_path" +security set-key-partition-list \ + -S apple-tool:,apple: \ + -s \ + -k "$keychain_password" \ + "$keychain_path" +security find-identity -v -p codesigning "$keychain_path" + +# Use the hardened runtime and a trusted timestamp so Gatekeeper can evaluate +# the signature after the Developer ID certificate eventually expires. +codesign \ + --force \ + --options runtime \ + --timestamp \ + --sign "$MACOS_CODESIGN_IDENTITY" \ + "$binary_path" +codesign --verify --verbose=4 "$binary_path"