diff --git a/.gitea/workflows/custom-release.yml b/.gitea/workflows/custom-release.yml index 1bd3fddd9..6f2a454d1 100644 --- a/.gitea/workflows/custom-release.yml +++ b/.gitea/workflows/custom-release.yml @@ -25,7 +25,7 @@ on: jobs: build-custom-release: - runs-on: ubuntu-latest + runs-on: ffmini_macos_arm64 steps: - name: Check out repository uses: actions/checkout@v4 @@ -51,6 +51,26 @@ jobs: env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + - name: Import Developer ID certificate + run: | + set -euo pipefail + keychain_path="$RUNNER_TEMP/syncthing-release-signing.keychain-db" + keychain_password="$(openssl rand -hex 24)" + certificate_path="$RUNNER_TEMP/developer-id-application.p12" + + printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path" + security create-keychain -p "$keychain_password" "$keychain_path" + security set-keychain-settings -lut 21600 "$keychain_path" + security unlock-keychain -p "$keychain_password" "$keychain_path" + security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security + security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | sed 's/[ "]//g') + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path" + + echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV" + env: + DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }} + DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }} + - name: Build patched Syncthing release run: ./scripts/update-custom-release.sh env: @@ -59,7 +79,15 @@ jobs: CUSTOM_RELEASE_PUSH: "1" CUSTOM_RELEASE_PUSH_BRANCH: "0" CUSTOM_RELEASE_REMOTE: origin - CUSTOM_RELEASE_BUILDS: "darwin/amd64/zip darwin/arm64/zip linux/amd64/tar linux/arm64/tar" - CUSTOM_RELEASE_CGO_ENABLED: "0" + CUSTOM_RELEASE_BUILDS: "darwin/arm64/zip/1 linux/amd64/tar/0 linux/arm64/tar/0" + CUSTOM_RELEASE_CODESIGN_IDENTITY: "Developer ID Application: Felix Foertsch (NG5W75WE8U)" + CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U" CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "1" CUSTOM_RELEASE_TEA_REPO: felixfoertsch/syncthing + + - name: Delete temporary keychain + if: always() + run: | + if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then + security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true + fi diff --git a/scripts/tests/test-custom-release-macos-runner.bats b/scripts/tests/test-custom-release-macos-runner.bats new file mode 100644 index 000000000..381ebfaa6 --- /dev/null +++ b/scripts/tests/test-custom-release-macos-runner.bats @@ -0,0 +1,71 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(git rev-parse --show-toplevel)" + WORKFLOW="$REPO_ROOT/.gitea/workflows/custom-release.yml" + RELEASE_SCRIPT="$REPO_ROOT/scripts/update-custom-release.sh" +} + +@test "custom release runs as one job on ffmini macos runner" { + run rg -n 'runs-on:[[:space:]]*ffmini_macos_arm64' "$WORKFLOW" + [ "$status" -eq 0 ] + + run rg -n 'ubuntu-latest' "$WORKFLOW" + [ "$status" -ne 0 ] + + run rg -n 'actions/upload-artifact' "$WORKFLOW" + [ "$status" -ne 0 ] +} + +@test "custom release workflow imports developer id signing material into temporary keychain" { + run rg -n 'DEVELOPER_ID_APPLICATION_P12_BASE64' "$WORKFLOW" + [ "$status" -eq 0 ] + + run rg -n 'DEVELOPER_ID_APPLICATION_P12_PASSWORD' "$WORKFLOW" + [ "$status" -eq 0 ] + + run rg -n 'security create-keychain' "$WORKFLOW" + [ "$status" -eq 0 ] + + run rg -n 'security import' "$WORKFLOW" + [ "$status" -eq 0 ] + + run rg -n 'security delete-keychain' "$WORKFLOW" + [ "$status" -eq 0 ] +} + +@test "custom release carries per-target cgo mode" { + run rg -n 'darwin/arm64/zip/1' "$WORKFLOW" "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] + + run rg -n 'linux/amd64/tar/0' "$WORKFLOW" "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] + + run rg -n 'CUSTOM_RELEASE_CGO_ENABLED' "$WORKFLOW" + [ "$status" -ne 0 ] +} + +@test "custom release signs darwin assets with hardened runtime and timestamp" { + run rg -n 'codesign .*--options runtime .*--timestamp' "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] + + run rg -n 'Developer ID Application' "$WORKFLOW" "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] + + run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY' "$WORKFLOW" "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] +} + +@test "custom release validates darwin binaries before publishing" { + run rg -n 'modernc-sqlite' "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] + + run rg -n 'codesign --verify --strict --verbose=2' "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] + + run rg -n 'TeamIdentifier=NG5W75WE8U|TeamIdentifier.*NG5W75WE8U' "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] + + run rg -n 'spctl -a -vv --type execute' "$RELEASE_SCRIPT" + [ "$status" -eq 0 ] +} diff --git a/scripts/update-custom-release.sh b/scripts/update-custom-release.sh index d302b397f..b7e01483d 100755 --- a/scripts/update-custom-release.sh +++ b/scripts/update-custom-release.sh @@ -12,7 +12,9 @@ dist_dir="${CUSTOM_RELEASE_DIST_DIR:-dist}" target="${CUSTOM_RELEASE_TARGET:-syncthing}" archive_kind="${CUSTOM_RELEASE_ARCHIVE:-tar}" build_specs="${CUSTOM_RELEASE_BUILDS:-}" -cgo_enabled="${CUSTOM_RELEASE_CGO_ENABLED:-0}" +default_cgo_enabled="${CUSTOM_RELEASE_CGO_ENABLED:-0}" +codesign_identity="${CUSTOM_RELEASE_CODESIGN_IDENTITY:-}" +codesign_team_id="${CUSTOM_RELEASE_CODESIGN_TEAM_ID:-NG5W75WE8U}" push_release="${CUSTOM_RELEASE_PUSH:-0}" push_branch="${CUSTOM_RELEASE_PUSH_BRANCH:-0}" push_remote="${CUSTOM_RELEASE_REMOTE:-origin}" @@ -148,6 +150,14 @@ rebuild_assets_once() { assets_rebuilt=1 } +format_build_specs() { + local spec + + for spec in $build_specs; do + printf -- '- %s\n' "$spec" + done +} + build_release() { local custom_tag="$1" @@ -170,7 +180,7 @@ build_release() { Syncthing $upstream_tag with the local patches applied. Builds: -$(printf -- '- %s\n' $build_specs) +$(format_build_specs) Patches: $(printf -- '- %s\n' "${patch_files[@]}") @@ -197,9 +207,11 @@ build_one() { local goos local goarch local kind + local cgo_enabled - IFS=/ read -r goos goarch kind <<< "$spec" + IFS=/ read -r goos goarch kind cgo_enabled <<< "$spec" [[ -n "$goos" && -n "$goarch" && -n "$kind" ]] || die "invalid build spec: $spec" + cgo_enabled="${cgo_enabled:-$default_cgo_enabled}" log "Building $target for $goos/$goarch as $kind with CGO_ENABLED=$cgo_enabled" @@ -207,10 +219,16 @@ build_one() { tar|zip) local archive archive="$(CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" "$kind" "$target" | tail -n 1)" + if [[ "$goos" == "darwin" ]]; then + sign_and_validate_darwin_archive "$archive" "$kind" + fi mv "$archive" "$dist_dir/" ;; binary) CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" -build-out "$dist_dir/$target-$goos-$goarch" build "$target" + if [[ "$goos" == "darwin" ]]; then + sign_and_validate_darwin_binary "$dist_dir/$target-$goos-$goarch" + fi ;; *) die "unknown build archive kind in $spec" @@ -218,6 +236,91 @@ build_one() { esac } +find_release_binary() { + local root="$1" + local candidate + + while IFS= read -r candidate; do + if [[ -x "$candidate" ]]; then + printf '%s\n' "$candidate" + return + fi + done < <(find "$root" -type f -name "$target") + + die "could not find executable $target in $root" +} + +sign_and_validate_darwin_archive() { + local archive="$1" + local kind="$2" + local tmp + local archive_abs + local binary + + [[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds" + + tmp="$(mktemp -d)" + archive_abs="$(cd "$(dirname "$archive")" && pwd -P)/$(basename "$archive")" + case "$kind" in + zip) + unzip -q "$archive_abs" -d "$tmp" + ;; + tar) + tar -xf "$archive_abs" -C "$tmp" + ;; + *) + die "cannot sign archive kind $kind" + ;; + esac + + binary="$(find_release_binary "$tmp")" + sign_and_validate_darwin_binary "$binary" + + rm -f "$archive_abs" + case "$kind" in + zip) + ( + cd "$tmp" + zip -qr "$archive_abs" . + ) + ;; + tar) + ( + cd "$tmp" + tar -czf "$archive_abs" . + ) + ;; + esac + rm -rf "$tmp" +} + +sign_and_validate_darwin_binary() { + local binary="$1" + local version_output + local codesign_details + + [[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds" + + codesign --force --sign "$codesign_identity" --options runtime --timestamp "$binary" + + version_output="$("$binary" --version)" + if [[ "$version_output" == *modernc-sqlite* ]]; then + die "darwin build unexpectedly reports [modernc-sqlite]: $version_output" + fi + + codesign --verify --strict --verbose=2 "$binary" + codesign_details="$(codesign -dv --verbose=4 "$binary" 2>&1)" + if [[ "$codesign_team_id" == "NG5W75WE8U" && "$codesign_details" != *"TeamIdentifier=NG5W75WE8U"* ]]; then + printf '%s\n' "$codesign_details" >&2 + die "darwin build is not signed by TeamIdentifier=NG5W75WE8U" + fi + if [[ "$codesign_details" != *"TeamIdentifier=$codesign_team_id"* ]]; then + printf '%s\n' "$codesign_details" >&2 + die "darwin build is not signed by TeamIdentifier=$codesign_team_id" + fi + spctl -a -vv --type execute "$binary" +} + push_refs() { local branch="$1" local custom_tag="$2"