Files
pulse/scripts/build-secure-runtime-qualification.sh
pulse-triage[bot] 35d4cb0e97 Advance release builds to Go 1.26.8
Go 1.26.8 supersedes the prior patch release, so every release builder and local toolchain guard must move together to prevent candidate artifacts from retaining an older compiler and runtime.

Contract-Neutral: toolchain-only patch update; no product or runtime contract changed
Change-source: pulse-maintainer
2026-09-04 07:20:28 +01:00

217 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build the exact Linux/amd64 secure-runtime qualification subjects on a
# GitHub-hosted runner. Candidate assembly publishes the three predecessor
# collectors and requires the current collector/helper/runner subjects to be
# byte-identical to the ordinary release payload before signing the packet.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${REPO_ROOT}"
VERSION="${1:-}"
OUTPUT_DIR="${2:-}"
TARGET_ARCH="${3:-amd64}"
if [[ -z "${VERSION}" || -z "${OUTPUT_DIR}" || -n "${4:-}" ]]; then
echo "Usage: $0 <release-version> <output-directory> [amd64|arm64]" >&2
exit 2
fi
case "${TARGET_ARCH}" in
amd64|arm64) ;;
*)
echo "Error: unsupported secure-runtime qualification architecture: ${TARGET_ARCH}" >&2
exit 2
;;
esac
if [[ "$(tr -d '\n\r[:space:]' < VERSION)" != "${VERSION}" ]]; then
echo "Error: requested version ${VERSION} does not match VERSION." >&2
exit 3
fi
if [[ -z "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ]]; then
echo "Error: PULSE_UPDATE_SIGNING_PUBLIC_KEY is required." >&2
exit 3
fi
required_go="go1.26.8"
go_version="$(go env GOVERSION 2>/dev/null || true)"
if [[ "${go_version}" != "${required_go}" ]]; then
echo "Error: Go toolchain must be ${required_go} (got ${go_version:-unknown})." >&2
exit 3
fi
OUTPUT_DIR="$(python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "${OUTPUT_DIR}")"
case "${OUTPUT_DIR}" in
/|"${REPO_ROOT}"|"${REPO_ROOT}/scripts")
echo "Error: refusing unsafe secure-runtime qualification output: ${OUTPUT_DIR}" >&2
exit 2
;;
esac
if [[ -e "${OUTPUT_DIR}" ]] && find "${OUTPUT_DIR}" -mindepth 1 -print -quit | grep -q .; then
echo "Error: secure-runtime qualification output must be absent or empty: ${OUTPUT_DIR}" >&2
exit 2
fi
mkdir -p "${OUTPUT_DIR}"
source_sha="$(git rev-parse HEAD)"
if [[ ! "${source_sha}" =~ ^[0-9a-f]{40}$ ]]; then
echo "Error: secure-runtime qualification requires an exact Git commit." >&2
exit 3
fi
update_public_key="$(printf '%s' "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" | tr -d '\r\n[:space:]')"
update_key_fingerprint="$(go run ./scripts/release_update_key.go fingerprint --public-key "${update_public_key}")"
release_tag="v${VERSION}"
predecessor_base="${VERSION%%-*}"
collector_v1_version="${predecessor_base}-0.secure.v6.1"
collector_v2_version="${predecessor_base}-0.secure.v6.2"
collector_v3_version="${predecessor_base}-0.secure.v6.3"
collector_v1_ldflags="$(./scripts/release_ldflags.sh agent --version "${collector_v1_version}" --update-public-keys "${update_public_key}")"
collector_v2_ldflags="$(./scripts/release_ldflags.sh agent --version "${collector_v2_version}" --update-public-keys "${update_public_key}")"
collector_v3_ldflags="$(./scripts/release_ldflags.sh agent --version "${collector_v3_version}" --update-public-keys "${update_public_key}")"
release_agent_ldflags="$(./scripts/release_ldflags.sh agent --version "${VERSION}" --update-public-keys "${update_public_key}")"
artifact_asset() {
case "$1" in
collector_v1) printf 'pulse-secure-runtime-collector-v1-linux-%s\n' "${TARGET_ARCH}" ;;
collector_v2) printf 'pulse-secure-runtime-collector-v2-linux-%s\n' "${TARGET_ARCH}" ;;
collector_v3) printf 'pulse-secure-runtime-collector-v3-linux-%s\n' "${TARGET_ARCH}" ;;
collector_v4) printf 'pulse-agent-linux-%s\n' "${TARGET_ARCH}" ;;
helper) printf 'pulse-agent-helper-linux-%s\n' "${TARGET_ARCH}" ;;
runner) printf 'pulse-agent-runner-linux-%s\n' "${TARGET_ARCH}" ;;
esac
}
artifact_package() {
case "$1" in
collector_v1|collector_v2|collector_v3|collector_v4)
printf '%s\n' 'github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent'
;;
helper) printf '%s\n' 'github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent-helper' ;;
runner) printf '%s\n' 'github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent-runner' ;;
esac
}
artifact_version() {
case "$1" in
collector_v1) printf '%s\n' "${collector_v1_version}" ;;
collector_v2) printf '%s\n' "${collector_v2_version}" ;;
collector_v3) printf '%s\n' "${collector_v3_version}" ;;
collector_v4|helper|runner) printf '%s\n' "${VERSION}" ;;
esac
}
artifact_ldflags() {
case "$1" in
collector_v1) printf '%s\n' "${collector_v1_ldflags}" ;;
collector_v2) printf '%s\n' "${collector_v2_ldflags}" ;;
collector_v3) printf '%s\n' "${collector_v3_ldflags}" ;;
collector_v4|helper) printf '%s\n' "${release_agent_ldflags}" ;;
runner) printf '\n' ;;
esac
}
build_subject() {
local name="$1"
local package="./cmd/pulse-agent"
local output="${OUTPUT_DIR}/$(artifact_asset "${name}")"
local -a command=(go build -buildvcs=false -trimpath)
case "${name}" in
helper) package="./cmd/pulse-agent-helper" ;;
runner) package="./cmd/pulse-agent-runner" ;;
esac
local ldflags="$(artifact_ldflags "${name}")"
if [[ -n "${ldflags}" ]]; then
command+=("-ldflags=${ldflags}")
fi
command+=(-o "${output}" "${package}")
env CGO_ENABLED=0 GOOS=linux GOARCH="${TARGET_ARCH}" "${command[@]}"
}
for name in collector_v1 collector_v2 collector_v3 collector_v4 helper runner; do
build_subject "${name}"
done
export SECURE_RUNTIME_OUTPUT_DIR="${OUTPUT_DIR}"
export SECURE_RUNTIME_VERSION="${VERSION}"
export SECURE_RUNTIME_TAG="${release_tag}"
export SECURE_RUNTIME_SOURCE_SHA="${source_sha}"
export SECURE_RUNTIME_TARGET_ARCH="${TARGET_ARCH}"
export SECURE_RUNTIME_GO_VERSION="${go_version}"
export SECURE_RUNTIME_UPDATE_PUBLIC_KEY="${update_public_key}"
export SECURE_RUNTIME_UPDATE_KEY_FINGERPRINT="${update_key_fingerprint}"
for name in collector_v1 collector_v2 collector_v3 collector_v4 helper runner; do
upper_name="$(printf '%s' "${name}" | tr '[:lower:]' '[:upper:]')"
export "SECURE_RUNTIME_${upper_name}_ASSET=$(artifact_asset "${name}")"
export "SECURE_RUNTIME_${upper_name}_PACKAGE=$(artifact_package "${name}")"
export "SECURE_RUNTIME_${upper_name}_VERSION=$(artifact_version "${name}")"
export "SECURE_RUNTIME_${upper_name}_LDFLAGS=$(artifact_ldflags "${name}")"
done
python3 - <<'PY'
import hashlib
import json
import os
from pathlib import Path
root = Path(os.environ["SECURE_RUNTIME_OUTPUT_DIR"])
names = ("collector_v1", "collector_v2", "collector_v3", "collector_v4", "helper", "runner")
artifacts = {}
subject_lines = []
for name in names:
prefix = f"SECURE_RUNTIME_{name.upper()}_"
asset = os.environ[prefix + "ASSET"]
path = root / asset
digest = hashlib.sha256(path.read_bytes()).hexdigest()
ldflags = os.environ[prefix + "LDFLAGS"]
artifacts[name] = {
"release_asset": asset,
"sha256": digest,
"build": {
"tool": "go build",
"package": os.environ[prefix + "PACKAGE"],
"target_os": "linux",
"target_arch": os.environ["SECURE_RUNTIME_TARGET_ARCH"],
"cgo_enabled": 0,
"go_version": os.environ["SECURE_RUNTIME_GO_VERSION"],
"trimpath": True,
"buildvcs": False,
"build_args": ["-buildvcs=false", "-trimpath"],
"ldflags": ldflags,
"ldflags_sha256": hashlib.sha256(ldflags.encode()).hexdigest(),
"version": os.environ[prefix + "VERSION"],
"update_key_fingerprint": os.environ["SECURE_RUNTIME_UPDATE_KEY_FINGERPRINT"],
},
}
subject_lines.append(f"{digest} {asset}")
contract = {
"schema_version": 1,
"repository": "rcourtman/Pulse",
"assembly_signer_workflow": "github.com/rcourtman/Pulse/.github/workflows/build-release-candidate.yml",
"compiler_signer_workflow": "github.com/rcourtman/Pulse/.github/workflows/compile-release-payload.yml",
"compiler_runner_trust": "github-hosted-deny-self-hosted",
"tag": os.environ["SECURE_RUNTIME_TAG"],
"version": os.environ["SECURE_RUNTIME_VERSION"],
"source_sha": os.environ["SECURE_RUNTIME_SOURCE_SHA"],
"update_public_keys": os.environ["SECURE_RUNTIME_UPDATE_PUBLIC_KEY"],
"update_key_fingerprint": os.environ["SECURE_RUNTIME_UPDATE_KEY_FINGERPRINT"],
"artifacts": artifacts,
}
(root / "secure-runtime-build-contract-v1.json").write_text(
json.dumps(contract, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
contract_digest = hashlib.sha256(
(root / "secure-runtime-build-contract-v1.json").read_bytes()
).hexdigest()
subject_lines.append(f"{contract_digest} secure-runtime-build-contract-v1.json")
(root / "secure-runtime-compiler-subjects.sha256").write_text(
"\n".join(subject_lines) + "\n", encoding="utf-8"
)
PY
echo "Built hosted secure-runtime qualification subjects in ${OUTPUT_DIR}."