mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Wire secure runtime RC qualification
This commit is contained in:
parent
92b5f3a9e1
commit
7650fdaba3
18 changed files with 1184 additions and 18 deletions
|
|
@ -559,6 +559,65 @@ sed -i "s|^CP_PULSE_IMAGE=.*|CP_PULSE_IMAGE=ghcr.io/rcourtman/pulse:v${VERSION}|
|
|||
printf '%s\n' "${VERSION}" > "${provider_msp_bundle_dir}/VERSION"
|
||||
tar -czf "${provider_msp_bundle_asset}" -C "${BUILD_DIR}" "${provider_msp_bundle_root}"
|
||||
|
||||
# Import the small GitHub-hosted secure-runtime qualification packet before
|
||||
# SBOM, checksum, and signature generation. The ordinary release payload stays
|
||||
# canonical for current binaries: hosted compiler output is accepted only when
|
||||
# collector-v4, helper, and runner reproduce those exact bytes.
|
||||
if [[ -n "${PULSE_SECURE_RUNTIME_QUALIFICATION_DIR:-}" ]]; then
|
||||
qualification_dir="$(cd "${PULSE_SECURE_RUNTIME_QUALIFICATION_DIR}" && pwd)"
|
||||
qualification_expected=(
|
||||
pulse-secure-runtime-collector-v1-linux-amd64
|
||||
pulse-secure-runtime-collector-v2-linux-amd64
|
||||
pulse-secure-runtime-collector-v3-linux-amd64
|
||||
pulse-agent-linux-amd64
|
||||
pulse-agent-helper-linux-amd64
|
||||
pulse-agent-runner-linux-amd64
|
||||
secure-runtime-build-contract-v1.json
|
||||
secure-runtime-compiler-provenance.sigstore.json
|
||||
secure-runtime-compiler-subjects.sha256
|
||||
)
|
||||
mapfile -t qualification_actual < <(find "${qualification_dir}" -mindepth 1 -maxdepth 1 -printf '%f\n' | sort)
|
||||
mapfile -t qualification_expected_sorted < <(printf '%s\n' "${qualification_expected[@]}" | sort)
|
||||
if [[ "$(printf '%s\n' "${qualification_actual[@]}")" != "$(printf '%s\n' "${qualification_expected_sorted[@]}")" ]]; then
|
||||
echo "Error: hosted secure-runtime qualification packet has an unexpected file set." >&2
|
||||
printf 'Expected:\n%s\nActual:\n%s\n' \
|
||||
"$(printf '%s\n' "${qualification_expected_sorted[@]}")" \
|
||||
"$(printf '%s\n' "${qualification_actual[@]}")" >&2
|
||||
exit 1
|
||||
fi
|
||||
for qualification_name in "${qualification_expected[@]}"; do
|
||||
if [[ ! -f "${qualification_dir}/${qualification_name}" || -L "${qualification_dir}/${qualification_name}" ]]; then
|
||||
echo "Error: hosted secure-runtime qualification subject is not a regular non-symlink file: ${qualification_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
cmp "${qualification_dir}/pulse-agent-linux-amd64" "${BUILD_DIR}/pulse-agent-linux-amd64" || {
|
||||
echo "Error: hosted secure-runtime collector-v4 does not reproduce the release collector." >&2
|
||||
exit 1
|
||||
}
|
||||
cmp "${qualification_dir}/pulse-agent-helper-linux-amd64" "${BUILD_DIR}/pulse-agent-helper-linux-amd64" || {
|
||||
echo "Error: hosted secure-runtime helper does not reproduce the release helper." >&2
|
||||
exit 1
|
||||
}
|
||||
cmp "${qualification_dir}/pulse-agent-runner-linux-amd64" "${BUILD_DIR}/pulse-agent-runner-linux-amd64" || {
|
||||
echo "Error: hosted secure-runtime runner does not reproduce the release runner." >&2
|
||||
exit 1
|
||||
}
|
||||
for qualification_name in \
|
||||
pulse-secure-runtime-collector-v1-linux-amd64 \
|
||||
pulse-secure-runtime-collector-v2-linux-amd64 \
|
||||
pulse-secure-runtime-collector-v3-linux-amd64 \
|
||||
secure-runtime-build-contract-v1.json \
|
||||
secure-runtime-compiler-provenance.sigstore.json; do
|
||||
install -m 0644 "${qualification_dir}/${qualification_name}" "${RELEASE_DIR}/${qualification_name}"
|
||||
done
|
||||
chmod 0755 "${RELEASE_DIR}"/pulse-secure-runtime-collector-v*-linux-amd64
|
||||
echo "Imported hosted secure-runtime qualification packet."
|
||||
elif [[ "${PULSE_REQUIRE_SECURE_RUNTIME_QUALIFICATION:-false}" == "true" ]]; then
|
||||
echo "Error: release requires the hosted secure-runtime qualification packet." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pulse_release_generate_packet_sbom "${RELEASE_DIR}" "${RELEASE_PACKET_SBOM}"
|
||||
mapfile -t checksum_files < <(pulse_release_collect_checksum_files "${RELEASE_DIR}")
|
||||
pulse_release_write_checksums_and_signatures "${RELEASE_DIR}" "${checksum_files[@]}"
|
||||
|
|
|
|||
216
scripts/build-secure-runtime-qualification.sh
Executable file
216
scripts/build-secure-runtime-qualification.sh
Executable file
|
|
@ -0,0 +1,216 @@
|
|||
#!/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.7"
|
||||
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}."
|
||||
|
|
@ -2491,6 +2491,160 @@ func TestReleaseUpdateKeyPublicKeySSHAcceptsPublicKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestReleaseUpdateKeyVerifiesDetachedUpdateSignature(t *testing.T) {
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate signing key: %v", err)
|
||||
}
|
||||
root := t.TempDir()
|
||||
artifact := filepath.Join(root, "artifact")
|
||||
signature := filepath.Join(root, "artifact.sig")
|
||||
if err := os.WriteFile(artifact, []byte("release bytes"), 0o600); err != nil {
|
||||
t.Fatalf("write artifact: %v", err)
|
||||
}
|
||||
|
||||
sign := exec.Command("go", "run", "./scripts/release_update_key.go", "sign", "--private-key", base64.StdEncoding.EncodeToString(privateKey), "--file", artifact)
|
||||
sign.Dir = repoFile()
|
||||
signatureBytes, err := sign.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("sign release artifact: %v\n%s", err, signatureBytes)
|
||||
}
|
||||
if err := os.WriteFile(signature, signatureBytes, 0o600); err != nil {
|
||||
t.Fatalf("write detached signature: %v", err)
|
||||
}
|
||||
|
||||
verify := exec.Command("go", "run", "./scripts/release_update_key.go", "verify", "--public-key", base64.StdEncoding.EncodeToString(publicKey), "--file", artifact, "--signature-file", signature)
|
||||
verify.Dir = repoFile()
|
||||
if output, err := verify.CombinedOutput(); err != nil {
|
||||
t.Fatalf("verify release artifact: %v\n%s", err, output)
|
||||
}
|
||||
if err := os.WriteFile(artifact, []byte("tampered bytes"), 0o600); err != nil {
|
||||
t.Fatalf("tamper artifact: %v", err)
|
||||
}
|
||||
verify = exec.Command("go", "run", "./scripts/release_update_key.go", "verify", "--public-key", base64.StdEncoding.EncodeToString(publicKey), "--file", artifact, "--signature-file", signature)
|
||||
verify.Dir = repoFile()
|
||||
if output, err := verify.CombinedOutput(); err == nil || !strings.Contains(string(output), "signature verification failed") {
|
||||
t.Fatalf("tampered release artifact passed verification: err=%v output=%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecureRuntimeQualificationPacketIsHostedAndReleaseBound(t *testing.T) {
|
||||
read := func(parts ...string) string {
|
||||
t.Helper()
|
||||
content, err := os.ReadFile(repoFile(parts...))
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", strings.Join(parts, "/"), err)
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
|
||||
compilerWorkflow := read(".github", "workflows", "compile-release-payload.yml")
|
||||
hostedJob := workflowJobBlock(t, compilerWorkflow, "compile-secure-runtime-qualification")
|
||||
for _, required := range []string{
|
||||
"runs-on: ubuntu-24.04",
|
||||
"attestations: write",
|
||||
"id-token: write",
|
||||
"./scripts/build-secure-runtime-qualification.sh",
|
||||
"secure-runtime-compiler-subjects.sha256",
|
||||
"secure-runtime-compiler-provenance.sigstore.json",
|
||||
} {
|
||||
if !strings.Contains(hostedJob, required) {
|
||||
t.Fatalf("hosted secure-runtime compiler job missing %q", required)
|
||||
}
|
||||
}
|
||||
if strings.Contains(hostedJob, "PULSE_UPDATE_SIGNING_KEY") || strings.Contains(hostedJob, "PULSE_LICENSE_PUBLIC_KEY") {
|
||||
t.Fatal("hosted secure-runtime compiler must not receive private signing or license material")
|
||||
}
|
||||
|
||||
builder := read("scripts", "build-secure-runtime-qualification.sh")
|
||||
for _, required := range []string{
|
||||
"go build -buildvcs=false -trimpath",
|
||||
"collector_v1_version=\"${predecessor_base}-0.secure.v6.1\"",
|
||||
"collector_v3_version=\"${predecessor_base}-0.secure.v6.3\"",
|
||||
"compiler_runner_trust\": \"github-hosted-deny-self-hosted\"",
|
||||
"secure-runtime-build-contract-v1.json",
|
||||
} {
|
||||
if !strings.Contains(builder, required) {
|
||||
t.Fatalf("secure-runtime qualification builder missing %q", required)
|
||||
}
|
||||
}
|
||||
|
||||
candidateWorkflow := read(".github", "workflows", "build-release-candidate.yml")
|
||||
for _, required := range []string{
|
||||
"secure_runtime_artifact_digest",
|
||||
"Verify hosted secure-runtime compiler packet",
|
||||
"--deny-self-hosted-runners",
|
||||
"secure-runtime-compiler-provenance.sigstore.json",
|
||||
"cmp secure-runtime-qualification/pulse-agent-linux-amd64 release-compiled/payload/binaries/pulse-agent-linux-amd64",
|
||||
"PULSE_REQUIRE_SECURE_RUNTIME_QUALIFICATION: \"true\"",
|
||||
} {
|
||||
if !strings.Contains(candidateWorkflow, required) {
|
||||
t.Fatalf("candidate workflow missing secure-runtime packet binding %q", required)
|
||||
}
|
||||
}
|
||||
|
||||
buildRelease := read("scripts", "build-release.sh")
|
||||
if strings.Index(buildRelease, "Imported hosted secure-runtime qualification packet") > strings.Index(buildRelease, "pulse_release_generate_packet_sbom") {
|
||||
t.Fatal("secure-runtime packet must be imported before SBOM and checksum generation")
|
||||
}
|
||||
for _, required := range []string{
|
||||
"hosted secure-runtime collector-v4 does not reproduce the release collector",
|
||||
"secure-runtime-build-contract-v1.json",
|
||||
"secure-runtime-compiler-provenance.sigstore.json",
|
||||
} {
|
||||
if !strings.Contains(buildRelease, required) {
|
||||
t.Fatalf("build-release.sh missing secure-runtime import guard %q", required)
|
||||
}
|
||||
}
|
||||
|
||||
assetHelper := read("scripts", "release_asset_common.sh")
|
||||
for _, required := range []string{
|
||||
`pulse-agent-runner-linux-*`,
|
||||
`pulse-secure-runtime-collector-v*-linux-*`,
|
||||
`secure-runtime-build-contract-v1.json`,
|
||||
`secure-runtime-compiler-provenance.sigstore.json`,
|
||||
} {
|
||||
if !strings.Contains(assetHelper, required) {
|
||||
t.Fatalf("release checksum inventory missing %q", required)
|
||||
}
|
||||
}
|
||||
|
||||
publicationWorkflow := read(".github", "workflows", "create-release.yml")
|
||||
for _, required := range []string{
|
||||
"release/secure-runtime-build-contract-v1.json",
|
||||
"release/secure-runtime-compiler-provenance.sigstore.json",
|
||||
"release/pulse-secure-runtime-collector-v1-linux-amd64",
|
||||
"release/pulse-secure-runtime-collector-v3-linux-amd64",
|
||||
"qualify-secure-runtime-release.yml/dispatches",
|
||||
`{ref: "main", return_run_details: true, inputs: {tag: $tag}}`,
|
||||
"Secure-runtime qualification dispatch did not return an exact workflow run.",
|
||||
"Immutable RC publication did not retain an exact secure-runtime qualification run identity.",
|
||||
} {
|
||||
if !strings.Contains(publicationWorkflow, required) {
|
||||
t.Fatalf("release publication missing secure-runtime asset %q", required)
|
||||
}
|
||||
}
|
||||
|
||||
qualificationWorkflow := read(".github", "workflows", "qualify-secure-runtime-release.yml")
|
||||
if strings.Contains(qualificationWorkflow, "release:\n types: [published]") {
|
||||
t.Fatal("secure-runtime qualification must be explicitly dispatched after immutable publication, not rely on suppressed release events")
|
||||
}
|
||||
for _, required := range []string{
|
||||
".immutable == true",
|
||||
"ca-certificates curl dbus systemd systemd-sysv util-linux",
|
||||
`for command in curl id nsenter runuser systemctl`,
|
||||
"PULSE_SECURE_RUNTIME_SYSTEMD_LAB=disposable-v1",
|
||||
"^TestSecureRuntimeSystemdLab$",
|
||||
"--release-candidate-tag",
|
||||
"--collector-v4-signature",
|
||||
"secure_runtime_attestation_v6.py",
|
||||
} {
|
||||
if !strings.Contains(qualificationWorkflow, required) {
|
||||
t.Fatalf("post-publication secure-runtime qualification missing %q", required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseAssetCommonRunsUpdateKeyThroughModulePath(t *testing.T) {
|
||||
if _, err := exec.LookPath("bash"); err != nil {
|
||||
t.Skip("bash not installed")
|
||||
|
|
|
|||
|
|
@ -254,6 +254,18 @@ pulse_release_collect_checksum_files() {
|
|||
if compgen -G "pulse-agent-helper-linux-*" > /dev/null; then
|
||||
checksum_files+=( pulse-agent-helper-linux-* )
|
||||
fi
|
||||
if compgen -G "pulse-agent-runner-linux-*" > /dev/null; then
|
||||
checksum_files+=( pulse-agent-runner-linux-* )
|
||||
fi
|
||||
if compgen -G "pulse-secure-runtime-collector-v*-linux-*" > /dev/null; then
|
||||
checksum_files+=( pulse-secure-runtime-collector-v*-linux-* )
|
||||
fi
|
||||
if [[ -f "secure-runtime-build-contract-v1.json" ]]; then
|
||||
checksum_files+=( secure-runtime-build-contract-v1.json )
|
||||
fi
|
||||
if [[ -f "secure-runtime-compiler-provenance.sigstore.json" ]]; then
|
||||
checksum_files+=( secure-runtime-compiler-provenance.sigstore.json )
|
||||
fi
|
||||
if compgen -G "pulse-agent-freebsd-*" > /dev/null; then
|
||||
checksum_files+=( pulse-agent-freebsd-* )
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ BUILD_CONTRACT_NAME = "secure-runtime-build-contract-v1.json"
|
|||
CHECKSUMS_NAME = "checksums.txt"
|
||||
ASSEMBLY_PROVENANCE_NAME = "release-build-provenance.sigstore.json"
|
||||
COMPILER_PROVENANCE_NAME = "secure-runtime-compiler-provenance.sigstore.json"
|
||||
COLLECTOR_SIGNATURE_NAMES = ("collector_v1", "collector_v2", "collector_v3", "collector_v4")
|
||||
|
||||
REQUIRED_SCENARIOS = (
|
||||
"legacy_root_command_capable_install",
|
||||
|
|
@ -376,6 +377,37 @@ def immutable_artifact_snapshot(
|
|||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def immutable_signature_snapshot(
|
||||
signatures: dict[str, Path], expected_assets: dict[str, str]
|
||||
) -> Iterator[tuple[dict[str, Path], dict[str, str]]]:
|
||||
if set(signatures) != set(COLLECTOR_SIGNATURE_NAMES):
|
||||
raise v5.AttestationError("collector release signature set is incomplete")
|
||||
with tempfile.TemporaryDirectory(prefix="pulse-secure-runtime-signatures-") as temporary:
|
||||
snapshot_root = Path(temporary)
|
||||
snapshot_root.chmod(0o700)
|
||||
snapshots: dict[str, Path] = {}
|
||||
digests: dict[str, str] = {}
|
||||
for name in COLLECTOR_SIGNATURE_NAMES:
|
||||
expected_filename = expected_assets[name] + ".sig"
|
||||
source = signatures[name]
|
||||
if source.name != expected_filename:
|
||||
raise v5.AttestationError(
|
||||
f"collector release signature {name} must be named {expected_filename}"
|
||||
)
|
||||
snapshot = snapshot_root / expected_filename
|
||||
digests[name] = copy_immutable_input(
|
||||
source, snapshot, f"collector release signature {name}"
|
||||
)
|
||||
snapshots[name] = snapshot
|
||||
yield snapshots, digests
|
||||
for name, path in snapshots.items():
|
||||
if sha256_file(path) != digests[name]:
|
||||
raise v5.AttestationError(
|
||||
f"private collector release signature snapshot {name} changed during verification"
|
||||
)
|
||||
|
||||
|
||||
def verify_release_build_contract(
|
||||
*,
|
||||
path: Path,
|
||||
|
|
@ -516,6 +548,7 @@ def verify_release_candidate_packet(
|
|||
receipt: dict[str, Any],
|
||||
artifacts: dict[str, Path],
|
||||
artifact_hashes: dict[str, str],
|
||||
collector_signatures: dict[str, Path],
|
||||
) -> dict[str, Any]:
|
||||
tag_identity = verify_release_candidate_tag_identity(checkout, qualified_commit, tag, repository)
|
||||
if not re.fullmatch(r"[1-9][0-9]*", release_id):
|
||||
|
|
@ -595,6 +628,27 @@ def verify_release_candidate_packet(
|
|||
cwd=checkout,
|
||||
label=f"hosted compiler provenance verification for {artifact_name}",
|
||||
)
|
||||
run_checked(
|
||||
[
|
||||
"gh",
|
||||
"attestation",
|
||||
"verify",
|
||||
str(build_contract_snapshot),
|
||||
"--repo",
|
||||
repository,
|
||||
"--signer-workflow",
|
||||
COMPILER_SIGNER_WORKFLOW,
|
||||
"--source-digest",
|
||||
qualified_commit,
|
||||
"--deny-self-hosted-runners",
|
||||
"--predicate-type",
|
||||
"https://slsa.dev/provenance/v1",
|
||||
"--bundle",
|
||||
str(compiler_provenance_snapshot),
|
||||
],
|
||||
cwd=checkout,
|
||||
label="hosted compiler provenance verification for the build contract",
|
||||
)
|
||||
verify_release_sidecar_snapshot_unchanged(snapshots, snapshot_digests)
|
||||
checksums = parse_checksums(checksums_snapshot)
|
||||
build_identity = verify_release_build_contract(
|
||||
|
|
@ -607,6 +661,50 @@ def verify_release_candidate_packet(
|
|||
artifact_hashes=artifact_hashes,
|
||||
checksums=checksums,
|
||||
)
|
||||
release_assets = {
|
||||
name: str(build_identity[name]["release_asset"])
|
||||
for name in COLLECTOR_SIGNATURE_NAMES
|
||||
}
|
||||
with immutable_signature_snapshot(collector_signatures, release_assets) as (
|
||||
signature_snapshots,
|
||||
signature_digests,
|
||||
):
|
||||
update_public_key = load_json_object(
|
||||
build_contract_snapshot, "secure-runtime build contract"
|
||||
)["update_public_keys"]
|
||||
for name in COLLECTOR_SIGNATURE_NAMES:
|
||||
signature_path = signature_snapshots[name]
|
||||
run_checked(
|
||||
[
|
||||
"gh",
|
||||
"release",
|
||||
"verify-asset",
|
||||
tag,
|
||||
str(signature_path),
|
||||
"--repo",
|
||||
repository,
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
cwd=checkout,
|
||||
label=f"release attestation verification for {signature_path.name}",
|
||||
)
|
||||
run_checked(
|
||||
[
|
||||
"go",
|
||||
"run",
|
||||
"./scripts/release_update_key.go",
|
||||
"verify",
|
||||
"--public-key",
|
||||
str(update_public_key),
|
||||
"--file",
|
||||
str(artifacts[name]),
|
||||
"--signature-file",
|
||||
str(signature_path),
|
||||
],
|
||||
cwd=checkout,
|
||||
label=f"production update signature verification for {name}",
|
||||
)
|
||||
verify_release_sidecar_snapshot_unchanged(snapshots, snapshot_digests)
|
||||
return {
|
||||
**tag_identity,
|
||||
|
|
@ -619,6 +717,14 @@ def verify_release_candidate_packet(
|
|||
"compiler_signer_workflow": COMPILER_SIGNER_WORKFLOW,
|
||||
"compiler_runner_trust": "github-hosted-deny-self-hosted",
|
||||
"build_identity": build_identity,
|
||||
"collector_signatures": {
|
||||
name: {
|
||||
"release_asset": release_assets[name] + ".sig",
|
||||
"sha256": signature_digests[name],
|
||||
"verified_with_release_update_key": True,
|
||||
}
|
||||
for name in COLLECTOR_SIGNATURE_NAMES
|
||||
},
|
||||
"update_key_fingerprint": expected_update_key_fingerprint,
|
||||
}
|
||||
|
||||
|
|
@ -641,6 +747,7 @@ def _create_attestation_with_snapshotted_artifacts(
|
|||
release_compiler_provenance_path: Path | None = None,
|
||||
release_build_contract_path: Path | None = None,
|
||||
expected_release_update_key_fingerprint: str | None = None,
|
||||
collector_signatures: dict[str, Path] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
checkout = checkout.resolve()
|
||||
qualified_commit = v5.resolve_commit(checkout, commit, "qualified commit")
|
||||
|
|
@ -673,6 +780,7 @@ def _create_attestation_with_snapshotted_artifacts(
|
|||
release_compiler_provenance_path,
|
||||
release_build_contract_path,
|
||||
expected_release_update_key_fingerprint,
|
||||
collector_signatures,
|
||||
)
|
||||
release_packet: dict[str, Any] | None = None
|
||||
if release_candidate_tag is None:
|
||||
|
|
@ -683,7 +791,7 @@ def _create_attestation_with_snapshotted_artifacts(
|
|||
else:
|
||||
if any(value is None for value in release_arguments):
|
||||
raise v5.AttestationError(
|
||||
"release-candidate classification requires repository, release id, signed checksums, hosted assembly and compiler provenance, build contract, and update-key fingerprint"
|
||||
"release-candidate classification requires repository, release id, signed checksums, hosted assembly and compiler provenance, build contract, four collector signatures, and update-key fingerprint"
|
||||
)
|
||||
release_packet = verify_release_candidate_packet(
|
||||
checkout=checkout,
|
||||
|
|
@ -699,6 +807,7 @@ def _create_attestation_with_snapshotted_artifacts(
|
|||
receipt=receipt,
|
||||
artifacts=artifacts,
|
||||
artifact_hashes=artifact_hashes,
|
||||
collector_signatures=dict(collector_signatures),
|
||||
)
|
||||
artifact_build_identity = release_packet["build_identity"]
|
||||
classification = "release-candidate-hosted-compiler-chain-artifact-bound-self-attested-systemd"
|
||||
|
|
@ -777,6 +886,7 @@ def create_attestation(
|
|||
release_compiler_provenance_path: Path | None = None,
|
||||
release_build_contract_path: Path | None = None,
|
||||
expected_release_update_key_fingerprint: str | None = None,
|
||||
collector_signatures: dict[str, Path] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
# Hash, inspect, and externally verify the same private artifact bytes.
|
||||
# Caller-owned paths can otherwise be swapped between receipt hashing,
|
||||
|
|
@ -799,6 +909,7 @@ def create_attestation(
|
|||
release_compiler_provenance_path=release_compiler_provenance_path,
|
||||
release_build_contract_path=release_build_contract_path,
|
||||
expected_release_update_key_fingerprint=expected_release_update_key_fingerprint,
|
||||
collector_signatures=collector_signatures,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -825,6 +936,10 @@ def parse_args(argv: Sequence[str]) -> argparse.Namespace:
|
|||
parser.add_argument("--release-compiler-provenance", type=Path)
|
||||
parser.add_argument("--release-build-contract", type=Path)
|
||||
parser.add_argument("--expected-release-update-key-fingerprint")
|
||||
parser.add_argument("--collector-v1-signature", type=Path)
|
||||
parser.add_argument("--collector-v2-signature", type=Path)
|
||||
parser.add_argument("--collector-v3-signature", type=Path)
|
||||
parser.add_argument("--collector-v4-signature", type=Path)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
|
@ -839,6 +954,21 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||
"helper": args.helper,
|
||||
"runner": args.runner,
|
||||
}
|
||||
collector_signatures = None
|
||||
signature_arguments = (
|
||||
args.collector_v1_signature,
|
||||
args.collector_v2_signature,
|
||||
args.collector_v3_signature,
|
||||
args.collector_v4_signature,
|
||||
)
|
||||
if any(value is not None for value in signature_arguments):
|
||||
if any(value is None for value in signature_arguments):
|
||||
print(
|
||||
"secure runtime schema-v6 attestation failed: all four collector signatures are required together",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
collector_signatures = dict(zip(COLLECTOR_SIGNATURE_NAMES, signature_arguments, strict=True))
|
||||
try:
|
||||
attestation = create_attestation(
|
||||
checkout=args.checkout,
|
||||
|
|
@ -857,6 +987,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||
release_compiler_provenance_path=args.release_compiler_provenance,
|
||||
release_build_contract_path=args.release_build_contract,
|
||||
expected_release_update_key_fingerprint=args.expected_release_update_key_fingerprint,
|
||||
collector_signatures=collector_signatures,
|
||||
)
|
||||
args.output.write_text(json.dumps(attestation, indent=2) + "\n", encoding="utf-8")
|
||||
except v5.AttestationError as exc:
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ class SecureRuntimeAttestationV6Test(unittest.TestCase):
|
|||
path = self.root / f"artifact-{name}"
|
||||
path.write_bytes(name.encode())
|
||||
self.artifacts[name] = path
|
||||
contract = self.build_contract()
|
||||
self.collector_signatures = {}
|
||||
for name in ("collector_v1", "collector_v2", "collector_v3", "collector_v4"):
|
||||
path = self.root / f"{contract['artifacts'][name]['release_asset']}.sig"
|
||||
path.write_text(base64.b64encode(name.encode()).decode() + "\n", encoding="utf-8")
|
||||
self.collector_signatures[name] = path
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temporary.cleanup()
|
||||
|
|
@ -453,17 +459,60 @@ class SecureRuntimeAttestationV6Test(unittest.TestCase):
|
|||
receipt=self.receipt,
|
||||
artifacts=self.artifacts,
|
||||
artifact_hashes=self.artifact_hashes,
|
||||
collector_signatures=self.collector_signatures,
|
||||
)
|
||||
self.assertEqual(packet["assembly_signer_workflow"], ASSEMBLY_SIGNER_WORKFLOW)
|
||||
self.assertEqual(packet["compiler_signer_workflow"], COMPILER_SIGNER_WORKFLOW)
|
||||
self.assertEqual(packet["compiler_runner_trust"], "github-hosted-deny-self-hosted")
|
||||
self.assertTrue(any(Path(call[0]).name == "verify-github-release-integrity.sh" for call in calls))
|
||||
self.assertEqual(sum(call[:3] == ["gh", "release", "verify-asset"] for call in calls), 4)
|
||||
self.assertEqual(sum(call[:3] == ["gh", "release", "verify-asset"] for call in calls), 8)
|
||||
provenance_calls = [call for call in calls if call[:3] == ["gh", "attestation", "verify"]]
|
||||
self.assertEqual(len(provenance_calls), 1 + len(v5.ARTIFACT_ARGUMENTS))
|
||||
self.assertEqual(len(provenance_calls), 2 + len(v5.ARTIFACT_ARGUMENTS))
|
||||
self.assertTrue(all("--deny-self-hosted-runners" in call for call in provenance_calls))
|
||||
self.assertIn(ASSEMBLY_SIGNER_WORKFLOW, provenance_calls[0])
|
||||
self.assertTrue(all(COMPILER_SIGNER_WORKFLOW in call for call in provenance_calls[1:]))
|
||||
signature_calls = [call for call in calls if call[:4] == ["go", "run", "./scripts/release_update_key.go", "verify"]]
|
||||
self.assertEqual(len(signature_calls), 4)
|
||||
self.assertTrue(all(self.update_public_keys in call for call in signature_calls))
|
||||
self.assertEqual(set(packet["collector_signatures"]), {"collector_v1", "collector_v2", "collector_v3", "collector_v4"})
|
||||
|
||||
def test_release_packet_rejects_misnamed_collector_signature(self) -> None:
|
||||
contract_path, checksums_path, _ = self.write_contract_and_checksums()
|
||||
assembly_provenance_path = self.root / ASSEMBLY_PROVENANCE_NAME
|
||||
assembly_provenance_path.write_text("{}\n", encoding="utf-8")
|
||||
compiler_provenance_path = self.root / COMPILER_PROVENANCE_NAME
|
||||
compiler_provenance_path.write_text("{}\n", encoding="utf-8")
|
||||
signatures = dict(self.collector_signatures)
|
||||
signatures["collector_v1"] = self.root / "wrong.sig"
|
||||
signatures["collector_v1"].write_text("bad\n", encoding="utf-8")
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"secure_runtime_attestation_v6.verify_release_candidate_tag_identity",
|
||||
return_value={"tag": self.tag},
|
||||
),
|
||||
mock.patch(
|
||||
"secure_runtime_attestation_v6.subprocess.run",
|
||||
return_value=subprocess.CompletedProcess([], 0, b"{}\n", b""),
|
||||
),
|
||||
self.assertRaisesRegex(v5.AttestationError, "must be named"),
|
||||
):
|
||||
verify_release_candidate_packet(
|
||||
checkout=self.root,
|
||||
qualified_commit=self.commit,
|
||||
tag=self.tag,
|
||||
repository=CANONICAL_REPOSITORY,
|
||||
release_id="12345",
|
||||
checksums_path=checksums_path,
|
||||
assembly_provenance_path=assembly_provenance_path,
|
||||
compiler_provenance_path=compiler_provenance_path,
|
||||
build_contract_path=contract_path,
|
||||
expected_update_key_fingerprint=self.fingerprint,
|
||||
receipt=self.receipt,
|
||||
artifacts=self.artifacts,
|
||||
artifact_hashes=self.artifact_hashes,
|
||||
collector_signatures=signatures,
|
||||
)
|
||||
|
||||
def test_release_packet_rejects_private_snapshot_swap_during_verification(self) -> None:
|
||||
contract_path, checksums_path, _ = self.write_contract_and_checksums()
|
||||
|
|
@ -501,6 +550,7 @@ class SecureRuntimeAttestationV6Test(unittest.TestCase):
|
|||
receipt=self.receipt,
|
||||
artifacts=self.artifacts,
|
||||
artifact_hashes=self.artifact_hashes,
|
||||
collector_signatures=self.collector_signatures,
|
||||
)
|
||||
|
||||
def test_cli_does_not_accept_v5_release_candidate_ref_shortcut(self) -> None:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
".github/workflows/build-release-candidate.yml",
|
||||
".github/workflows/compile-release-payload.yml",
|
||||
".github/workflows/create-release.yml",
|
||||
".github/workflows/qualify-secure-runtime-release.yml",
|
||||
"VERSION",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
"scripts/backfill-release-assets.sh",
|
||||
"scripts/build-release-binaries.sh",
|
||||
"scripts/build-release.sh",
|
||||
"scripts/build-secure-runtime-qualification.sh",
|
||||
"scripts/check-github-release-immutability.sh",
|
||||
"scripts/package-helm-chart.sh",
|
||||
"scripts/prepare-release-container-context.sh",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
|
|
@ -21,6 +22,7 @@ func usage() {
|
|||
fmt.Fprintln(os.Stderr, " release_update_key.go openssh-private-key --private-key <base64-ed25519-key-or-seed> [--comment <comment>]")
|
||||
fmt.Fprintln(os.Stderr, " release_update_key.go fingerprint (--private-key <base64-ed25519-key-or-seed> | --public-key <base64-ed25519-public-key>)")
|
||||
fmt.Fprintln(os.Stderr, " release_update_key.go sign --private-key <base64-ed25519-key-or-seed> --file <path>")
|
||||
fmt.Fprintln(os.Stderr, " release_update_key.go verify --public-key <base64-ed25519-public-key> --file <path> --signature-file <path>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -147,6 +149,36 @@ func main() {
|
|||
fail(err)
|
||||
}
|
||||
fmt.Println(signature)
|
||||
case "verify":
|
||||
verifyCmd := flag.NewFlagSet("verify", flag.ExitOnError)
|
||||
publicKey := verifyCmd.String("public-key", "", "base64-encoded Ed25519 public key or PKIX public key")
|
||||
filePath := verifyCmd.String("file", "", "path to the signed file")
|
||||
signaturePath := verifyCmd.String("signature-file", "", "path to the base64-encoded Ed25519 signature")
|
||||
_ = verifyCmd.Parse(os.Args[2:])
|
||||
|
||||
if *publicKey == "" || *filePath == "" || *signaturePath == "" {
|
||||
usage()
|
||||
}
|
||||
key, err := decodePublicKey(*publicKey)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
data, err := os.ReadFile(*filePath)
|
||||
if err != nil {
|
||||
fail(fmt.Errorf("read file for verification: %w", err))
|
||||
}
|
||||
signatureText, err := os.ReadFile(*signaturePath)
|
||||
if err != nil {
|
||||
fail(fmt.Errorf("read signature for verification: %w", err))
|
||||
}
|
||||
signature, err := base64.StdEncoding.DecodeString(string(bytes.TrimSpace(signatureText)))
|
||||
if err != nil {
|
||||
fail(fmt.Errorf("invalid base64 signature: %w", err))
|
||||
}
|
||||
if !ed25519.Verify(key, data, signature) {
|
||||
fail(fmt.Errorf("signature verification failed"))
|
||||
}
|
||||
fmt.Println("signature verified")
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,6 +466,15 @@ required_assets=(
|
|||
"pulse-agent-v${PULSE_VERSION}-freebsd-amd64.tar.gz"
|
||||
"pulse-agent-v${PULSE_VERSION}-freebsd-arm64.tar.gz"
|
||||
)
|
||||
if [[ "${PULSE_REQUIRE_SECURE_RUNTIME_QUALIFICATION:-false}" == "true" ]]; then
|
||||
required_assets+=(
|
||||
"pulse-secure-runtime-collector-v1-linux-amd64"
|
||||
"pulse-secure-runtime-collector-v2-linux-amd64"
|
||||
"pulse-secure-runtime-collector-v3-linux-amd64"
|
||||
"secure-runtime-build-contract-v1.json"
|
||||
"secure-runtime-compiler-provenance.sigstore.json"
|
||||
)
|
||||
fi
|
||||
|
||||
missing_count=0
|
||||
for asset in "${required_assets[@]}"; do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue