mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Wire secure runtime RC qualification
This commit is contained in:
@@ -89,6 +89,9 @@ jobs:
|
||||
artifact_digest: ${{ steps.wait.outputs.artifact_digest }}
|
||||
artifact_name: ${{ steps.wait.outputs.artifact_name }}
|
||||
compiler_run_id: ${{ steps.dispatch.outputs.compiler_run_id }}
|
||||
secure_runtime_artifact_id: ${{ steps.wait.outputs.secure_runtime_artifact_id }}
|
||||
secure_runtime_artifact_digest: ${{ steps.wait.outputs.secure_runtime_artifact_digest }}
|
||||
secure_runtime_artifact_name: ${{ steps.wait.outputs.secure_runtime_artifact_name }}
|
||||
steps:
|
||||
- name: Dispatch exact-SHA compiler workflow
|
||||
id: dispatch
|
||||
@@ -172,6 +175,17 @@ jobs:
|
||||
echo "artifact_digest=${artifact_digest}" >> "$GITHUB_OUTPUT"
|
||||
echo "artifact_name=${artifact_name}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
secure_runtime_artifact_name="secure-runtime-qualification-${SOURCE_SHA}-${VERSION}-${REQUEST_ID}"
|
||||
secure_runtime_artifact_json="$(jq -ce \
|
||||
--arg artifact_name "${secure_runtime_artifact_name}" \
|
||||
'[.artifacts[] | select(.name == $artifact_name and .expired == false and .size_in_bytes > 0)] | if length == 1 then .[0] else error("expected exactly one secure-runtime compiler artifact") end' \
|
||||
<<<"${artifacts_json}")"
|
||||
secure_runtime_artifact_id="$(jq -er '.id | select(type == "number")' <<<"${secure_runtime_artifact_json}")"
|
||||
secure_runtime_artifact_digest="$(jq -er '.digest | select(test("^sha256:[0-9a-f]{64}$")) | sub("^sha256:"; "")' <<<"${secure_runtime_artifact_json}")"
|
||||
echo "secure_runtime_artifact_id=${secure_runtime_artifact_id}" >> "$GITHUB_OUTPUT"
|
||||
echo "secure_runtime_artifact_digest=${secure_runtime_artifact_digest}" >> "$GITHUB_OUTPUT"
|
||||
echo "secure_runtime_artifact_name=${secure_runtime_artifact_name}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
signing-configuration:
|
||||
name: Verify Native Signing Configuration
|
||||
if: ${{ inputs.require_macos_signing || inputs.require_windows_signing }}
|
||||
@@ -731,6 +745,65 @@ jobs:
|
||||
mkdir -p release-compiled
|
||||
unzip -q "${artifact_zip}" -d release-compiled
|
||||
|
||||
- name: Verify hosted secure-runtime compiler packet
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EXPECTED_ARTIFACT_ID: ${{ needs.obtain-release-payload.outputs.secure_runtime_artifact_id }}
|
||||
EXPECTED_ARTIFACT_DIGEST: ${{ needs.obtain-release-payload.outputs.secure_runtime_artifact_digest }}
|
||||
EXPECTED_ARTIFACT_NAME: ${{ needs.obtain-release-payload.outputs.secure_runtime_artifact_name }}
|
||||
EXPECTED_COMPILER_RUN_ID: ${{ needs.obtain-release-payload.outputs.compiler_run_id }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "${EXPECTED_ARTIFACT_ID}" =~ ^[0-9]+$ ]]
|
||||
[[ "${EXPECTED_ARTIFACT_DIGEST}" =~ ^[0-9a-f]{64}$ ]]
|
||||
artifact_json="$RUNNER_TEMP/secure-runtime-compiled-artifact.json"
|
||||
artifact_zip="$RUNNER_TEMP/secure-runtime-compiled-artifact.zip"
|
||||
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${EXPECTED_ARTIFACT_ID}" > "${artifact_json}"
|
||||
jq -e \
|
||||
--argjson artifact_id "${EXPECTED_ARTIFACT_ID}" \
|
||||
--arg artifact_name "${EXPECTED_ARTIFACT_NAME}" \
|
||||
--arg artifact_digest "sha256:${EXPECTED_ARTIFACT_DIGEST}" \
|
||||
--argjson run_id "${EXPECTED_COMPILER_RUN_ID}" \
|
||||
--arg source_sha "${GITHUB_SHA}" \
|
||||
'.id == $artifact_id and .name == $artifact_name and .expired == false and .size_in_bytes > 0 and .digest == $artifact_digest and .workflow_run.id == $run_id and .workflow_run.head_sha == $source_sha' \
|
||||
"${artifact_json}" >/dev/null
|
||||
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${EXPECTED_ARTIFACT_ID}/zip" > "${artifact_zip}"
|
||||
printf '%s %s\n' "${EXPECTED_ARTIFACT_DIGEST}" "${artifact_zip}" | sha256sum --check --
|
||||
mkdir -p secure-runtime-qualification
|
||||
unzip -q "${artifact_zip}" -d secure-runtime-qualification
|
||||
|
||||
jq -e \
|
||||
--arg version "${VERSION}" \
|
||||
--arg tag "v${VERSION}" \
|
||||
--arg source_sha "${GITHUB_SHA}" \
|
||||
'.schema_version == 1 and .version == $version and .tag == $tag and .source_sha == $source_sha and .compiler_runner_trust == "github-hosted-deny-self-hosted" and (.artifacts | keys | sort) == ["collector_v1", "collector_v2", "collector_v3", "collector_v4", "helper", "runner"]' \
|
||||
secure-runtime-qualification/secure-runtime-build-contract-v1.json >/dev/null
|
||||
(
|
||||
cd secure-runtime-qualification
|
||||
sha256sum --check secure-runtime-compiler-subjects.sha256
|
||||
)
|
||||
for subject in \
|
||||
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; do
|
||||
gh attestation verify \
|
||||
"secure-runtime-qualification/${subject}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--signer-workflow "github.com/rcourtman/Pulse/.github/workflows/compile-release-payload.yml" \
|
||||
--source-digest "${GITHUB_SHA}" \
|
||||
--deny-self-hosted-runners \
|
||||
--predicate-type "https://slsa.dev/provenance/v1" \
|
||||
--bundle secure-runtime-qualification/secure-runtime-compiler-provenance.sigstore.json
|
||||
done
|
||||
cmp secure-runtime-qualification/pulse-agent-linux-amd64 release-compiled/payload/binaries/pulse-agent-linux-amd64
|
||||
cmp secure-runtime-qualification/pulse-agent-helper-linux-amd64 release-compiled/payload/binaries/pulse-agent-helper-linux-amd64
|
||||
cmp secure-runtime-qualification/pulse-agent-runner-linux-amd64 release-compiled/payload/binaries/pulse-agent-runner-linux-amd64
|
||||
|
||||
- name: Verify exact-SHA compiled payload
|
||||
env:
|
||||
EXPECTED_ARTIFACT_ID: ${{ needs.obtain-release-payload.outputs.artifact_id }}
|
||||
@@ -789,6 +862,8 @@ jobs:
|
||||
PULSE_REQUIRE_WINDOWS_SIGNING: ${{ inputs.require_windows_signing }}
|
||||
PULSE_AGENT_NATIVE_BINARIES_DIR: ${{ (inputs.require_macos_signing || inputs.require_windows_signing) && format('{0}/native-agent-binaries', github.workspace) || '' }}
|
||||
PULSE_RELEASE_COMPILED_PAYLOAD_DIR: ${{ github.workspace }}/release-compiled/payload
|
||||
PULSE_SECURE_RUNTIME_QUALIFICATION_DIR: ${{ github.workspace }}/secure-runtime-qualification
|
||||
PULSE_REQUIRE_SECURE_RUNTIME_QUALIFICATION: "true"
|
||||
VERSION: ${{ inputs.version }}
|
||||
|
||||
- name: Validate installer signing key pins
|
||||
@@ -816,6 +891,7 @@ jobs:
|
||||
|
||||
- name: Validate complete candidate locally
|
||||
env:
|
||||
PULSE_REQUIRE_SECURE_RUNTIME_QUALIFICATION: "true"
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: ./scripts/validate-release.sh "${VERSION}" --skip-docker
|
||||
|
||||
|
||||
@@ -99,3 +99,76 @@ jobs:
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
overwrite: true
|
||||
|
||||
compile-secure-runtime-qualification:
|
||||
name: Compile Hosted Secure Runtime Qualification Packet
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
attestations: write
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Validate isolated compiler request
|
||||
env:
|
||||
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
REQUEST_ID: ${{ inputs.request_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "${EXPECTED_SOURCE_SHA}" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "${REQUEST_ID}" =~ ^[0-9]+-[0-9]+$ ]]
|
||||
test "${GITHUB_SHA}" = "${EXPECTED_SOURCE_SHA}"
|
||||
test "${GITHUB_WORKFLOW_SHA}" = "${EXPECTED_SOURCE_SHA}"
|
||||
|
||||
- name: Checkout exact release source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.source_sha }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Build hosted qualification subjects
|
||||
env:
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$(tr -d '\n' < VERSION)" = "${VERSION}"
|
||||
test "$(git rev-parse HEAD)" = "${{ inputs.source_sha }}"
|
||||
./scripts/build-secure-runtime-qualification.sh \
|
||||
"${VERSION}" \
|
||||
"$RUNNER_TEMP/secure-runtime-qualification" \
|
||||
amd64
|
||||
|
||||
- name: Attest hosted qualification subjects
|
||||
id: attest_secure_runtime
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-checksums: ${{ runner.temp }}/secure-runtime-qualification/secure-runtime-compiler-subjects.sha256
|
||||
|
||||
- name: Preserve portable compiler provenance
|
||||
env:
|
||||
PROVENANCE_BUNDLE: ${{ steps.attest_secure_runtime.outputs.bundle-path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -s "${PROVENANCE_BUNDLE}"
|
||||
jq -e 'type == "object"' "${PROVENANCE_BUNDLE}" >/dev/null
|
||||
install -m 0644 \
|
||||
"${PROVENANCE_BUNDLE}" \
|
||||
"$RUNNER_TEMP/secure-runtime-qualification/secure-runtime-compiler-provenance.sigstore.json"
|
||||
|
||||
- name: Upload hosted qualification packet
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: secure-runtime-qualification-${{ inputs.source_sha }}-${{ inputs.version }}-${{ inputs.request_id }}
|
||||
path: ${{ runner.temp }}/secure-runtime-qualification/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
overwrite: true
|
||||
|
||||
@@ -1163,6 +1163,14 @@ jobs:
|
||||
"${TAG}" \
|
||||
release/release-build-provenance.sigstore.json \
|
||||
--clobber
|
||||
release_upload_with_retry \
|
||||
"${TAG}" \
|
||||
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-v2-linux-amd64 \
|
||||
release/pulse-secure-runtime-collector-v3-linux-amd64 \
|
||||
--clobber
|
||||
for bare_agent in \
|
||||
release/pulse-agent-linux-amd64 \
|
||||
release/pulse-agent-linux-arm64 \
|
||||
@@ -1628,8 +1636,11 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
actions: read
|
||||
actions: write
|
||||
contents: write
|
||||
outputs:
|
||||
secure_runtime_qualification_run_id: ${{ steps.secure_runtime_qualification.outputs.run_id }}
|
||||
secure_runtime_qualification_run_url: ${{ steps.secure_runtime_qualification.outputs.run_url }}
|
||||
steps:
|
||||
- name: Checkout release integrity control
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
@@ -1946,6 +1957,39 @@ jobs:
|
||||
rm -rf "$activation_marker_dir"
|
||||
echo "[OK] Immutably committed, attested, and publicly verified ${TAG}; convergence run ${CONVERGENCE_RUN_ID} owns customer rollout."
|
||||
|
||||
# A release published with GITHUB_TOKEN does not emit a workflow-triggering
|
||||
# release event. Dispatch the qualification explicitly after the immutable
|
||||
# packet and activation marker have both been verified.
|
||||
- name: Dispatch exact RC secure-runtime qualification
|
||||
id: secure_runtime_qualification
|
||||
if: ${{ needs.prepare.outputs.is_prerelease == 'true' && contains(needs.prepare.outputs.version, '-rc.') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[1-9][0-9]*$ ]]
|
||||
dispatch="$(
|
||||
jq -n \
|
||||
--arg tag "${TAG}" \
|
||||
'{ref: "main", return_run_details: true, inputs: {tag: $tag}}' | \
|
||||
gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2026-03-10" \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/workflows/qualify-secure-runtime-release.yml/dispatches" \
|
||||
--input -
|
||||
)"
|
||||
run_id="$(jq -r '.workflow_run_id // empty' <<<"${dispatch}")"
|
||||
run_url="$(jq -r '.html_url // empty' <<<"${dispatch}")"
|
||||
if [[ ! "${run_id}" =~ ^[0-9]+$ ]] || [[ -z "${run_url}" ]]; then
|
||||
echo "::error::Secure-runtime qualification dispatch did not return an exact workflow run."
|
||||
exit 1
|
||||
fi
|
||||
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
||||
echo "run_url=${run_url}" >> "$GITHUB_OUTPUT"
|
||||
echo "[OK] Secure-runtime qualification is durably queued as ${run_url}."
|
||||
|
||||
release_commit_verdict:
|
||||
name: Release Activation Commit Verdict
|
||||
needs:
|
||||
@@ -1991,6 +2035,8 @@ jobs:
|
||||
CONVERGENCE_DISPATCH_RESULT: ${{ needs.dispatch_release_convergence.result }}
|
||||
CONVERGENCE_RUN_ID: ${{ needs.dispatch_release_convergence.outputs.run_id }}
|
||||
CONVERGENCE_RUN_URL: ${{ needs.dispatch_release_convergence.outputs.run_url }}
|
||||
SECURE_RUNTIME_QUALIFICATION_RUN_ID: ${{ needs.activate_release.outputs.secure_runtime_qualification_run_id }}
|
||||
SECURE_RUNTIME_QUALIFICATION_RUN_URL: ${{ needs.activate_release.outputs.secure_runtime_qualification_run_url }}
|
||||
R2_PREFIX: ${{ needs.stage_private_pro_runtime.outputs.r2_prefix }}
|
||||
SERVER_IMAGE_DIGEST: ${{ needs.publish_docker.outputs.server_digest }}
|
||||
CONTROL_PLANE_IMAGE_DIGEST: ${{ needs.publish_docker.outputs.control_plane_digest }}
|
||||
@@ -2019,6 +2065,14 @@ jobs:
|
||||
require_result "Helm staging" "$HELM_RESULT" success
|
||||
require_result "immutable release readiness" "$READINESS_RESULT" success
|
||||
require_result "durable customer convergence dispatch" "$CONVERGENCE_DISPATCH_RESULT" success
|
||||
if [[ "$VERSION" =~ -rc\.[1-9][0-9]*$ ]]; then
|
||||
if [[ ! "$SECURE_RUNTIME_QUALIFICATION_RUN_ID" =~ ^[0-9]+$ ]] || \
|
||||
[[ ! "$SECURE_RUNTIME_QUALIFICATION_RUN_URL" =~ ^https://github\.com/${GITHUB_REPOSITORY}/actions/runs/[0-9]+$ ]]; then
|
||||
echo "::error::Immutable RC publication did not retain an exact secure-runtime qualification run identity."
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] Secure-runtime qualification run: ${SECURE_RUNTIME_QUALIFICATION_RUN_URL}"
|
||||
fi
|
||||
if [[ "$VERSION" == 6.* ]]; then
|
||||
require_result "private Pro staging" "$PRIVATE_PRO_STAGE_RESULT" success
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
name: Qualify Secure Runtime Release Candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Immutable vX.Y.Z-rc.N release tag to qualify'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
attestations: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
qualify-secure-runtime-release:
|
||||
name: Qualify Immutable RC on Disposable systemd
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- name: Resolve immutable RC identity
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag="${INPUT_TAG:-}"
|
||||
[[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[1-9][0-9]*$ ]] || {
|
||||
echo "::error::Secure-runtime release qualification requires an exact vX.Y.Z-rc.N tag."
|
||||
exit 1
|
||||
}
|
||||
release_json="$RUNNER_TEMP/secure-runtime-release.json"
|
||||
for attempt in $(seq 1 30); do
|
||||
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "${release_json}"
|
||||
if jq -e --arg tag "${tag}" '.tag_name == $tag and .draft == false and .prerelease == true and .immutable == true' "${release_json}" >/dev/null; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" == 30 ]]; then
|
||||
jq -c '{tag_name, draft, prerelease, immutable}' "${release_json}" >&2
|
||||
echo "::error::Release did not reach immutable prerelease state."
|
||||
exit 1
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
release_id="$(jq -er '.id | select(type == "number" and . > 0)' "${release_json}")"
|
||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_id=${release_id}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout exact release tag
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
ref: ${{ steps.release.outputs.tag }}
|
||||
|
||||
- name: Verify detached release source
|
||||
id: source
|
||||
env:
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin refs/heads/main:refs/remotes/origin/main
|
||||
commit="$(git rev-parse "${TAG}^{commit}")"
|
||||
test "$(git branch --show-current)" = ""
|
||||
test -z "$(git status --short --untracked-files=all)"
|
||||
git merge-base --is-ancestor "${commit}" origin/main
|
||||
echo "commit=${commit}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download exact immutable qualification packet
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/secure-runtime-release" "$RUNNER_TEMP/secure-runtime-evidence"
|
||||
for asset in \
|
||||
checksums.txt \
|
||||
release-build-provenance.sigstore.json \
|
||||
secure-runtime-compiler-provenance.sigstore.json \
|
||||
secure-runtime-build-contract-v1.json \
|
||||
pulse-secure-runtime-collector-v1-linux-amd64 \
|
||||
pulse-secure-runtime-collector-v1-linux-amd64.sig \
|
||||
pulse-secure-runtime-collector-v2-linux-amd64 \
|
||||
pulse-secure-runtime-collector-v2-linux-amd64.sig \
|
||||
pulse-secure-runtime-collector-v3-linux-amd64 \
|
||||
pulse-secure-runtime-collector-v3-linux-amd64.sig \
|
||||
pulse-agent-linux-amd64 \
|
||||
pulse-agent-linux-amd64.sig \
|
||||
pulse-agent-helper-linux-amd64 \
|
||||
pulse-agent-runner-linux-amd64; do
|
||||
gh release download "${TAG}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--dir "$RUNNER_TEMP/secure-runtime-release" \
|
||||
--pattern "${asset}"
|
||||
done
|
||||
chmod 0755 \
|
||||
"$RUNNER_TEMP/secure-runtime-release"/pulse-secure-runtime-collector-v*-linux-amd64 \
|
||||
"$RUNNER_TEMP/secure-runtime-release"/pulse-agent-linux-amd64 \
|
||||
"$RUNNER_TEMP/secure-runtime-release"/pulse-agent-helper-linux-amd64 \
|
||||
"$RUNNER_TEMP/secure-runtime-release"/pulse-agent-runner-linux-amd64
|
||||
|
||||
- name: Build exact-source qualification harness
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go test -c \
|
||||
-o "$RUNNER_TEMP/secure-runtime-release/installtests-linux-amd64.test" \
|
||||
./scripts/installtests
|
||||
|
||||
- name: Build disposable systemd host image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
context="$RUNNER_TEMP/secure-runtime-systemd-image"
|
||||
mkdir -p "${context}/empty"
|
||||
tee "${context}/Dockerfile" >/dev/null <<'DOCKERFILE'
|
||||
FROM ubuntu:24.04@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517
|
||||
ENV container=docker
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl dbus systemd systemd-sysv util-linux && \
|
||||
apt-get clean && \
|
||||
systemctl mask dev-hugepages.mount sys-fs-fuse-connections.mount systemd-remount-fs.service
|
||||
STOPSIGNAL SIGRTMIN+3
|
||||
CMD ["/sbin/init"]
|
||||
DOCKERFILE
|
||||
docker build \
|
||||
--tag pulse-secure-runtime-systemd:ubuntu-24.04 \
|
||||
--file "${context}/Dockerfile" \
|
||||
"${context}/empty"
|
||||
|
||||
- name: Run schema-v6 systemd qualification
|
||||
id: lab
|
||||
env:
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
container="pulse-secure-runtime-qualification-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
echo "container=${container}" >> "$GITHUB_OUTPUT"
|
||||
docker run \
|
||||
--detach \
|
||||
--name "${container}" \
|
||||
--privileged \
|
||||
--cgroupns=host \
|
||||
--tmpfs /run \
|
||||
--tmpfs /run/lock \
|
||||
--volume /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
||||
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
||||
--volume "$RUNNER_TEMP/secure-runtime-release:/release:ro" \
|
||||
--volume "$RUNNER_TEMP/secure-runtime-evidence:/evidence:rw" \
|
||||
pulse-secure-runtime-systemd:ubuntu-24.04
|
||||
for attempt in $(seq 1 30); do
|
||||
if docker exec "${container}" systemctl show-environment >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" == 30 ]]; then
|
||||
docker logs "${container}"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
docker exec "${container}" sh -c \
|
||||
'for command in curl id nsenter runuser systemctl; do command -v "$command" >/dev/null || exit 1; done'
|
||||
docker exec "${container}" sh -c \
|
||||
'printf "%s\n" "PULSE_SECURE_RUNTIME_SYSTEMD_LAB=disposable-v1" > /etc/pulse-secure-runtime-lab'
|
||||
start_epoch="$(date +%s)"
|
||||
docker exec \
|
||||
--env PULSE_SECURE_RUNTIME_SYSTEMD_LAB=1 \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V1=/release/pulse-secure-runtime-collector-v1-linux-amd64 \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V1_SIGNATURE=/release/pulse-secure-runtime-collector-v1-linux-amd64.sig \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V2=/release/pulse-secure-runtime-collector-v2-linux-amd64 \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V2_SIGNATURE=/release/pulse-secure-runtime-collector-v2-linux-amd64.sig \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V3=/release/pulse-secure-runtime-collector-v3-linux-amd64 \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V3_SIGNATURE=/release/pulse-secure-runtime-collector-v3-linux-amd64.sig \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V4=/release/pulse-agent-linux-amd64 \
|
||||
--env PULSE_SECURE_RUNTIME_COLLECTOR_V4_SIGNATURE=/release/pulse-agent-linux-amd64.sig \
|
||||
--env PULSE_SECURE_RUNTIME_HELPER=/release/pulse-agent-helper-linux-amd64 \
|
||||
--env PULSE_SECURE_RUNTIME_RUNNER=/release/pulse-agent-runner-linux-amd64 \
|
||||
--env PULSE_SECURE_RUNTIME_RECEIPT=/evidence/receipt.json \
|
||||
--env "PULSE_SECURE_RUNTIME_RECEIPT_RECORD_PATH=docs/release-control/v6/internal/records/secure-agent-runtime-systemd-receipt-v6-${TAG}.json" \
|
||||
--env PULSE_SECURE_RUNTIME_TRANSCRIPT=/evidence/transcript.jsonl \
|
||||
--env "PULSE_SECURE_RUNTIME_TRANSCRIPT_RECORD_PATH=docs/release-control/v6/internal/records/secure-agent-runtime-systemd-transcript-v6-${TAG}.jsonl" \
|
||||
"${container}" \
|
||||
sh -c 'cd /workspace/scripts/installtests && exec /release/installtests-linux-amd64.test -test.run "^TestSecureRuntimeSystemdLab$" -test.count=1 -test.v'
|
||||
elapsed="$(( $(date +%s) - start_epoch ))"
|
||||
if [[ "${elapsed}" -lt 1 ]]; then elapsed=1; fi
|
||||
echo "elapsed_seconds=${elapsed}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create release-candidate secure-runtime attestation
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
QUALIFIED_COMMIT: ${{ steps.source.outputs.commit }}
|
||||
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_dir="$RUNNER_TEMP/secure-runtime-release"
|
||||
evidence_dir="$RUNNER_TEMP/secure-runtime-evidence"
|
||||
update_fingerprint="$(jq -er '.update_key_fingerprint' "${release_dir}/secure-runtime-build-contract-v1.json")"
|
||||
python3 scripts/release_control/secure_runtime_attestation_v6.py \
|
||||
--checkout "${GITHUB_WORKSPACE}" \
|
||||
--commit "${QUALIFIED_COMMIT}" \
|
||||
--main-ref origin/main \
|
||||
--receipt "${evidence_dir}/receipt.json" \
|
||||
--receipt-record-path "docs/release-control/v6/internal/records/secure-agent-runtime-systemd-receipt-v6-${TAG}.json" \
|
||||
--transcript "${evidence_dir}/transcript.jsonl" \
|
||||
--collector-v1 "${release_dir}/pulse-secure-runtime-collector-v1-linux-amd64" \
|
||||
--collector-v2 "${release_dir}/pulse-secure-runtime-collector-v2-linux-amd64" \
|
||||
--collector-v3 "${release_dir}/pulse-secure-runtime-collector-v3-linux-amd64" \
|
||||
--collector-v4 "${release_dir}/pulse-agent-linux-amd64" \
|
||||
--helper "${release_dir}/pulse-agent-helper-linux-amd64" \
|
||||
--runner "${release_dir}/pulse-agent-runner-linux-amd64" \
|
||||
--collector-v1-signature "${release_dir}/pulse-secure-runtime-collector-v1-linux-amd64.sig" \
|
||||
--collector-v2-signature "${release_dir}/pulse-secure-runtime-collector-v2-linux-amd64.sig" \
|
||||
--collector-v3-signature "${release_dir}/pulse-secure-runtime-collector-v3-linux-amd64.sig" \
|
||||
--collector-v4-signature "${release_dir}/pulse-agent-linux-amd64.sig" \
|
||||
--elapsed-seconds "${{ steps.lab.outputs.elapsed_seconds }}" \
|
||||
--release-candidate-tag "${TAG}" \
|
||||
--release-repository "${GITHUB_REPOSITORY}" \
|
||||
--release-id "${RELEASE_ID}" \
|
||||
--release-checksums "${release_dir}/checksums.txt" \
|
||||
--release-assembly-provenance "${release_dir}/release-build-provenance.sigstore.json" \
|
||||
--release-compiler-provenance "${release_dir}/secure-runtime-compiler-provenance.sigstore.json" \
|
||||
--release-build-contract "${release_dir}/secure-runtime-build-contract-v1.json" \
|
||||
--expected-release-update-key-fingerprint "${update_fingerprint}" \
|
||||
--output "${evidence_dir}/attestation.json"
|
||||
|
||||
- name: Stop disposable systemd host
|
||||
if: ${{ always() && steps.lab.outputs.container != '' }}
|
||||
run: docker rm --force "${{ steps.lab.outputs.container }}"
|
||||
|
||||
- name: Upload immutable RC qualification evidence
|
||||
if: ${{ success() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: secure-runtime-qualification-${{ steps.release.outputs.tag }}-${{ steps.source.outputs.commit }}
|
||||
path: ${{ runner.temp }}/secure-runtime-evidence/
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
compression-level: 0
|
||||
@@ -204,9 +204,14 @@ an immutable signed GitHub Release packet, release-attested checksums, trusted
|
||||
hosted-builder SLSA provenance, and a signed build contract that binds every
|
||||
qualification artifact to its package, toolchain, exact build settings and
|
||||
ldflags, version, production update-key fingerprint, and release checksum.
|
||||
The current release workflow does not yet emit that complete secure-runtime
|
||||
build contract or the four-version qualification artifact set, so a local tag
|
||||
or VCS-stamped lab binary cannot upgrade the proof to RC status.
|
||||
The release workflow now builds that four-version Linux amd64 qualification
|
||||
set on a separate GitHub-hosted compiler job, verifies the current collector,
|
||||
helper, and runner against the ordinary release bytes, publishes compiler
|
||||
provenance and the signed build contract, and automatically runs the guarded
|
||||
systemd lab after an immutable prerelease is published. No release candidate
|
||||
containing this wiring has been published yet, so current evidence remains
|
||||
committed-main proof; a local tag or VCS-stamped lab binary still cannot
|
||||
upgrade it to RC status.
|
||||
|
||||
The typed-helper profile cannot be combined with `--grant-smart` or
|
||||
`--grant-pct`. The collector never joins the rootful Docker group. When no
|
||||
|
||||
@@ -206,12 +206,17 @@ artifact to the checksums and record both workflow identities, a
|
||||
GitHub-hosted-only compiler policy, the source commit, Go toolchain, package,
|
||||
target, `CGO_ENABLED`, `-trimpath`, `-buildvcs=false`, exact ldflags, version,
|
||||
and production update-key fingerprint. Hosted assembly of a payload emitted by
|
||||
a self-hosted compiler is not trusted compilation provenance. The current
|
||||
compiler workflow is self-hosted and the release workflow does not yet publish
|
||||
the compiler provenance, secure-runtime build contract, or full multi-version
|
||||
qualification artifact set. Therefore release-candidate classification fails
|
||||
closed, and current local lab evidence remains committed-main, artifact-bound,
|
||||
and self-attested even if a local tag exists.
|
||||
a self-hosted compiler is not trusted compilation provenance. The canonical
|
||||
compiler workflow now has a separate GitHub-hosted qualification job that
|
||||
builds and attests the six Linux amd64 subjects without signing secrets. Hosted
|
||||
candidate assembly requires its current collector, helper, and runner to match
|
||||
the ordinary release payload byte for byte, then publishes the three predecessor
|
||||
collectors, compiler provenance, and build contract through the normal signed
|
||||
release packet. A published prerelease automatically runs the twenty-scenario
|
||||
lab on a disposable systemd host and invokes this verifier with all four
|
||||
release signatures. No qualifying RC containing this wiring has been published
|
||||
yet, so current evidence remains committed-main, artifact-bound, and
|
||||
self-attested; a local tag still cannot upgrade it to RC status.
|
||||
|
||||
Schema-v6 committed-main classification is likewise not caller-relative. The
|
||||
attester accepts only `origin/main`, requires the canonical Pulse origin URL,
|
||||
|
||||
@@ -225,6 +225,25 @@ Release builds and archives carry both helper and runner binaries for the five
|
||||
Linux targets (`amd64`, `arm64`, `armv7`, `armv6`, and `386`) with checksum,
|
||||
Ed25519, and SSH signature sidecars. Exact archive/container-context validation
|
||||
must prove those assets rather than inferring them from collector packaging.
|
||||
Every canonical candidate also carries one Linux amd64 secure-runtime
|
||||
qualification packet compiled on a GitHub-hosted runner. That compiler emits
|
||||
three version-distinct predecessor collectors plus byte-for-byte
|
||||
reproductions of the ordinary release collector, helper, and runner, a closed
|
||||
build contract, and portable SLSA compiler provenance. Hosted candidate
|
||||
assembly rejects the packet unless those three current binaries reproduce the
|
||||
ordinary payload exactly, then publishes only the predecessor collectors next
|
||||
to the ordinary current binaries and binds the compiler provenance and build
|
||||
contract through the normal SBOM, checksum, Ed25519, SSH-signature, immutable
|
||||
manifest, and assembly-provenance path. The update signing private key remains
|
||||
absent from compilation and is used only by hosted candidate assembly.
|
||||
Published prerelease packets automatically enter the separate disposable
|
||||
Ubuntu/systemd qualification workflow. That workflow consumes the immutable
|
||||
release assets and four release signatures, runs the canonical twenty-scenario
|
||||
schema-v6 lab, and applies the release-candidate attester against the exact
|
||||
GitHub release ID, tag, source commit, checksums, compiler provenance, assembly
|
||||
provenance, and update-key fingerprint. Its retained receipt remains explicit
|
||||
self-attestation rather than external security review, and successful packet
|
||||
production or execution does not change the safe profile from opt-in.
|
||||
This packaging proof does not establish live platform qualification: helper
|
||||
update staging/activation/restart/rollback on a real systemd host, real
|
||||
Docker/Podman and Proxmox action execution, systemd migration rehearsal, and
|
||||
@@ -291,6 +310,8 @@ release-latency optimization.
|
||||
16. `internal/cloudcp/docker/labels.go`
|
||||
17. `internal/cloudcp/tenant_runtime_rollout.go`
|
||||
13. `.github/workflows/build-release-candidate.yml`
|
||||
13a. `.github/workflows/compile-release-payload.yml`
|
||||
13b. `.github/workflows/qualify-secure-runtime-release.yml`
|
||||
14. `.github/workflows/build-and-test.yml`
|
||||
14. `.github/workflows/create-release.yml`
|
||||
14. `.github/workflows/deploy-demo-server.yml`
|
||||
@@ -338,6 +359,7 @@ release-latency optimization.
|
||||
35. `go.sum`
|
||||
36. `scripts/build-release.sh`
|
||||
37. `scripts/build-release-binaries.sh`
|
||||
37a. `scripts/build-secure-runtime-qualification.sh`
|
||||
38. `scripts/release_build_targets.sh`
|
||||
39. `scripts/run-release-backend-tests.sh`
|
||||
40. `scripts/shard_go_tests.py`
|
||||
@@ -374,7 +396,10 @@ release-latency optimization.
|
||||
63. `scripts/release_control/live_runtime_proof_test.py`
|
||||
64. `scripts/release_candidate_manifest.py`
|
||||
65. `scripts/release_control/validate_artifact_release_line.py`
|
||||
65a. `scripts/release_control/secure_runtime_attestation_v6.py`
|
||||
65b. `scripts/release_control/secure_runtime_source_manifest_v6.json`
|
||||
66. `scripts/release_ldflags.sh`
|
||||
66a. `scripts/release_update_key.go`
|
||||
67. `scripts/run_cloud_public_signup_smoke.sh`
|
||||
68. `scripts/run_demo_public_browser_smoke.sh`
|
||||
69. `scripts/demo_public_browser_smoke.cjs`
|
||||
@@ -744,7 +769,7 @@ artifact-selection behaviour.
|
||||
## Extension Points
|
||||
|
||||
1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/`
|
||||
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, artifact release-line validation, post-install live-runtime claim proof, the canonical version file, operator-facing release packet content, model-selected visual release-note capture, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, end-to-end install.sh smoke against staged or published release assets, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/build-release-binaries.sh`, `scripts/release_build_targets.sh`, `scripts/run-release-backend-tests.sh`, `scripts/shard_go_tests.py`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/capture-release-note-visuals.sh`, `scripts/release-preflight-worker.sh`, `scripts/run-release-preflight.sh`, `scripts/verify-github-release-integrity.sh`, `scripts/verify-release-container-images.sh`, `scripts/release_control/verify_release_container_images_test.py`, `scripts/release_control/capture_release_note_visuals.mjs`, `scripts/release_control/release_note_visuals.py`, `scripts/release_control/live_runtime_proof.py`, `scripts/release_control/live_runtime_proof_test.py`, `scripts/release_control/mobile_release_gate.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/validate_artifact_release_line.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `pulse-enterprise:scripts/build-pro-binaries.sh`, `pulse-enterprise:scripts/build-pro-release.sh`, `pulse-enterprise:scripts/validate-pro-release-line.sh`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/build-release-candidate.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/install-sh-smoke.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/promote-private-pro-runtime.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/recover-release-activation.yml`, `.github/workflows/release-convergence.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/retry-release-convergence.yml`, `.github/workflows/update-demo-server.yml`, `.github/workflows/validate-release-assets.yml`, and `pulse-enterprise:.github/workflows/build-pro-release.yml`
|
||||
2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, hosted secure-runtime qualification compilation and attestation, governed promotion metadata resolution, artifact release-line validation, post-install live-runtime claim proof, the canonical version file, operator-facing release packet content, model-selected visual release-note capture, prerelease feedback intake wording, historical published-release integrity backfill, release asset validation status publication, download endpoint checksum/signature header proof, end-to-end install.sh smoke against staged or published release assets, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/build-release-binaries.sh`, `scripts/build-secure-runtime-qualification.sh`, `scripts/release_build_targets.sh`, `scripts/run-release-backend-tests.sh`, `scripts/shard_go_tests.py`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/release_update_key.go`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/capture-release-note-visuals.sh`, `scripts/release-preflight-worker.sh`, `scripts/run-release-preflight.sh`, `scripts/verify-github-release-integrity.sh`, `scripts/verify-release-container-images.sh`, `scripts/release_control/secure_runtime_attestation_v6.py`, `scripts/release_control/secure_runtime_source_manifest_v6.json`, `scripts/release_control/verify_release_container_images_test.py`, `scripts/release_control/capture_release_note_visuals.mjs`, `scripts/release_control/release_note_visuals.py`, `scripts/release_control/live_runtime_proof.py`, `scripts/release_control/live_runtime_proof_test.py`, `scripts/release_control/mobile_release_gate.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.py`, `scripts/release_control/validate_artifact_release_line.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `pulse-enterprise:scripts/build-pro-binaries.sh`, `pulse-enterprise:scripts/build-pro-release.sh`, `pulse-enterprise:scripts/validate-pro-release-line.sh`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/build-release-candidate.yml`, `.github/workflows/compile-release-payload.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/install-sh-smoke.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/promote-private-pro-runtime.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/qualify-secure-runtime-release.yml`, `.github/workflows/recover-release-activation.yml`, `.github/workflows/release-convergence.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/retry-release-convergence.yml`, `.github/workflows/update-demo-server.yml`, `.github/workflows/validate-release-assets.yml`, and `pulse-enterprise:.github/workflows/build-pro-release.yml`
|
||||
The governed release-build surface also includes
|
||||
`scripts/prepare-release-container-context.sh` for exact-candidate container
|
||||
assembly.
|
||||
|
||||
@@ -4375,6 +4375,7 @@
|
||||
".github/workflows/backfill-release-assets.yml",
|
||||
".github/workflows/build-and-test.yml",
|
||||
".github/workflows/build-release-candidate.yml",
|
||||
".github/workflows/compile-release-payload.yml",
|
||||
".github/workflows/create-release.yml",
|
||||
".github/workflows/deploy-demo-server.yml",
|
||||
".github/workflows/helm-pages.yml",
|
||||
@@ -4383,6 +4384,7 @@
|
||||
".github/workflows/publish-docker.yml",
|
||||
".github/workflows/publish-helm-chart.yml",
|
||||
".github/workflows/qualify-release-containers.yml",
|
||||
".github/workflows/qualify-secure-runtime-release.yml",
|
||||
".github/workflows/recover-demo-server.yml",
|
||||
".github/workflows/recover-release-activation.yml",
|
||||
".github/workflows/release-convergence.yml",
|
||||
@@ -4438,6 +4440,7 @@
|
||||
"scripts/backfill-release-assets.sh",
|
||||
"scripts/build-release-binaries.sh",
|
||||
"scripts/build-release.sh",
|
||||
"scripts/build-secure-runtime-qualification.sh",
|
||||
"scripts/check-workflow-dispatch-inputs.py",
|
||||
"scripts/clean-mock-alerts.sh",
|
||||
"scripts/com.pulse.hot-dev.plist.template",
|
||||
@@ -4469,8 +4472,11 @@
|
||||
"scripts/release_control/release_promotion_policy_support.py",
|
||||
"scripts/release_control/render_release_body.py",
|
||||
"scripts/release_control/resolve_release_promotion.py",
|
||||
"scripts/release_control/secure_runtime_attestation_v6.py",
|
||||
"scripts/release_control/secure_runtime_source_manifest_v6.json",
|
||||
"scripts/release_control/validate_artifact_release_line.py",
|
||||
"scripts/release_ldflags.sh",
|
||||
"scripts/release_update_key.go",
|
||||
"scripts/run-release-backend-tests.sh",
|
||||
"scripts/run-release-preflight.sh",
|
||||
"scripts/run_cloud_public_signup_smoke.sh",
|
||||
@@ -4584,6 +4590,7 @@
|
||||
".github/scripts/setup-demo-ssh.sh",
|
||||
".github/workflows/backfill-release-assets.yml",
|
||||
".github/workflows/build-release-candidate.yml",
|
||||
".github/workflows/compile-release-payload.yml",
|
||||
".github/workflows/create-release.yml",
|
||||
".github/workflows/helm-pages.yml",
|
||||
".github/workflows/install-sh-smoke.yml",
|
||||
@@ -4592,6 +4599,7 @@
|
||||
".github/workflows/publish-docker.yml",
|
||||
".github/workflows/publish-helm-chart.yml",
|
||||
".github/workflows/qualify-release-containers.yml",
|
||||
".github/workflows/qualify-secure-runtime-release.yml",
|
||||
".github/workflows/recover-demo-server.yml",
|
||||
".github/workflows/recover-release-activation.yml",
|
||||
".github/workflows/release-convergence.yml",
|
||||
@@ -4680,10 +4688,14 @@
|
||||
"scripts/backfill-release-assets.sh",
|
||||
"scripts/build-release-binaries.sh",
|
||||
"scripts/build-release.sh",
|
||||
"scripts/build-secure-runtime-qualification.sh",
|
||||
"scripts/prepare-release-container-context.sh",
|
||||
"scripts/release_asset_common.sh",
|
||||
"scripts/release_build_targets.sh",
|
||||
"scripts/release_control/secure_runtime_attestation_v6.py",
|
||||
"scripts/release_control/secure_runtime_source_manifest_v6.json",
|
||||
"scripts/release_ldflags.sh",
|
||||
"scripts/release_update_key.go",
|
||||
"scripts/validate-release.sh"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
@@ -4693,7 +4705,8 @@
|
||||
"scripts/installtests/backfill_release_assets_test.go",
|
||||
"scripts/installtests/build_release_assets_test.go",
|
||||
"scripts/installtests/release_ldflags_test.go",
|
||||
"scripts/installtests/safe_profile_migration_test.go"
|
||||
"scripts/installtests/safe_profile_migration_test.go",
|
||||
"scripts/release_control/secure_runtime_attestation_v6_test.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -204,9 +204,14 @@ an immutable signed GitHub Release packet, release-attested checksums, trusted
|
||||
hosted-builder SLSA provenance, and a signed build contract that binds every
|
||||
qualification artifact to its package, toolchain, exact build settings and
|
||||
ldflags, version, production update-key fingerprint, and release checksum.
|
||||
The current release workflow does not yet emit that complete secure-runtime
|
||||
build contract or the four-version qualification artifact set, so a local tag
|
||||
or VCS-stamped lab binary cannot upgrade the proof to RC status.
|
||||
The release workflow now builds that four-version Linux amd64 qualification
|
||||
set on a separate GitHub-hosted compiler job, verifies the current collector,
|
||||
helper, and runner against the ordinary release bytes, publishes compiler
|
||||
provenance and the signed build contract, and automatically runs the guarded
|
||||
systemd lab after an immutable prerelease is published. No release candidate
|
||||
containing this wiring has been published yet, so current evidence remains
|
||||
committed-main proof; a local tag or VCS-stamped lab binary still cannot
|
||||
upgrade it to RC status.
|
||||
|
||||
The typed-helper profile cannot be combined with `--grant-smart` or
|
||||
`--grant-pct`. The collector never joins the rootful Docker group. When no
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user