From 040af6419194044bc0167c11410368a1bae39198 Mon Sep 17 00:00:00 2001 From: thanhnv Date: Fri, 10 Jul 2026 16:47:45 +0900 Subject: [PATCH] feat: enforce live supply chain provenance gates --- .gitea/workflows/casan-supply-chain.yml | 97 +++++++++++++++++++ .../production/CI_SECURITY_AND_PROVENANCE.md | 40 ++++++++ .../scripts/bash/ci-harness-gate.sh | 2 + .../scripts/bash/live-supply-chain-scan.sh | 66 +++++++++++++ .../scripts/bash/release-provenance.sh | 65 +++++++++++++ .../scripts/bash/verify-signed-commits.sh | 19 ++++ .../tests/phase-live-supply-chain-tests.sh | 38 ++++++++ .../tests/phase-release-provenance-tests.sh | 24 +++++ 8 files changed, 351 insertions(+) create mode 100644 .gitea/workflows/casan-supply-chain.yml create mode 100644 infra/production/CI_SECURITY_AND_PROVENANCE.md create mode 100755 packages/casan-harness/scripts/bash/live-supply-chain-scan.sh create mode 100755 packages/casan-harness/scripts/bash/release-provenance.sh create mode 100755 packages/casan-harness/scripts/bash/verify-signed-commits.sh create mode 100755 packages/casan-harness/tests/phase-live-supply-chain-tests.sh create mode 100755 packages/casan-harness/tests/phase-release-provenance-tests.sh diff --git a/.gitea/workflows/casan-supply-chain.yml b/.gitea/workflows/casan-supply-chain.yml new file mode 100644 index 0000000..6db7589 --- /dev/null +++ b/.gitea/workflows/casan-supply-chain.yml @@ -0,0 +1,97 @@ +name: CASAN Supply Chain and Provenance + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + verify-scan-attest: + runs-on: ci-runner + timeout-minutes: 60 + env: + CASAN_CI_RUN_FRONTEND: "1" + CASAN_CI_RUN_BACKEND: "1" + CASAN_CI_RUN_INFRA_LAB: "0" + CASAN_REQUIRE_LIVE_SCANNERS: "1" + CASAN_TOOLCHAIN_TRUSTED_DIRS: /usr/bin:/bin:/usr/local/bin + steps: + - name: Checkout full signed history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify trusted toolchain + run: | + set -euo pipefail + bash packages/casan-harness/scripts/bash/toolchain-verify.sh npm osv-scanner trivy docker openssl python3 git + + - name: Enforce signed commits on main + if: github.ref == 'refs/heads/main' + run: | + set -euo pipefail + RANGE="${{ github.event.before }}..${{ github.sha }}" + if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then RANGE="${{ github.sha }}"; fi + bash packages/casan-harness/scripts/bash/verify-signed-commits.sh "$RANGE" + + - name: Install locked dependencies + run: npm ci + + - name: Run governed test gate + run: bash packages/casan-harness/scripts/bash/ci-harness-gate.sh + + - name: Build immutable candidate images + run: | + set -euo pipefail + docker build --pull -t casan-control-panel-api:${GITHUB_SHA} -f Dockerfile.control-panel-api . + docker build --pull -t casan-control-panel-ui:${GITHUB_SHA} -f Dockerfile.control-panel-ui . + + - name: Live CVE, secret and misconfiguration scan + run: | + set -euo pipefail + mkdir -p dist/security + bash packages/casan-harness/scripts/bash/live-supply-chain-scan.sh dist/security \ + casan-control-panel-api:${GITHUB_SHA} casan-control-panel-ui:${GITHUB_SHA} + + - name: Push scanned images by immutable digest (main only) + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + CASAN_REGISTRY: ${{ vars.CASAN_REGISTRY }} + run: | + set -euo pipefail + [ -n "${GITEA_TOKEN:-}" ] || { echo 'missing GITEA_TOKEN'; exit 1; } + REGISTRY="${CASAN_REGISTRY:-${GITHUB_SERVER_URL#https://}}" + API_IMAGE="$REGISTRY/${GITHUB_REPOSITORY_OWNER}/casan-control-panel-api:${GITHUB_SHA}" + UI_IMAGE="$REGISTRY/${GITHUB_REPOSITORY_OWNER}/casan-control-panel-ui:${GITHUB_SHA}" + printf '%s' "$GITEA_TOKEN" | docker login "$REGISTRY" -u "$GITHUB_ACTOR" --password-stdin + docker tag casan-control-panel-api:${GITHUB_SHA} "$API_IMAGE" + docker tag casan-control-panel-ui:${GITHUB_SHA} "$UI_IMAGE" + docker push "$API_IMAGE"; docker push "$UI_IMAGE" + docker image inspect --format '{{index .RepoDigests 0}}' "$API_IMAGE" > dist/security/api-image.digest + docker image inspect --format '{{index .RepoDigests 0}}' "$UI_IMAGE" > dist/security/ui-image.digest + + - name: Sign SLSA provenance (main only) + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + env: + CASAN_ATTESTATION_PRIVATE_KEY: ${{ secrets.CASAN_ATTESTATION_PRIVATE_KEY }} + run: | + set -euo pipefail + [ -n "${CASAN_ATTESTATION_PRIVATE_KEY:-}" ] || { echo 'missing CASAN_ATTESTATION_PRIVATE_KEY'; exit 1; } + key="$(mktemp)"; trap 'rm -f "$key"' EXIT + printf '%s\n' "$CASAN_ATTESTATION_PRIVATE_KEY" > "$key"; chmod 600 "$key" + mapfile -t images < <(cat dist/security/api-image.digest dist/security/ui-image.digest) + bash packages/casan-harness/scripts/bash/release-provenance.sh create \ + dist/security/provenance.intoto.json "$key" dist/security/npm.cyclonedx.json \ + docs/output/casan/evidence/harness-test-report.md "${images[@]}" + openssl pkey -in "$key" -pubout -out dist/security/provenance-public.pem + + - name: Upload security evidence and provenance + if: always() + uses: actions/upload-artifact@v4 + with: + name: casan-security-${{ github.sha }} + path: dist/security/ + retention-days: 90 diff --git a/infra/production/CI_SECURITY_AND_PROVENANCE.md b/infra/production/CI_SECURITY_AND_PROVENANCE.md new file mode 100644 index 0000000..9cf6d6b --- /dev/null +++ b/infra/production/CI_SECURITY_AND_PROVENANCE.md @@ -0,0 +1,40 @@ +# CI Security, Signing and Provenance + +`.gitea/workflows/casan-supply-chain.yml` is the release gate for Control +Panel images. It fails closed if the runner is missing `osv-scanner` or Trivy, +if any high/critical source or image issue is found, if a main-branch commit is +not cryptographically signed, or if the attestation signing key is unavailable. + +## Runner bootstrap + +Provision the dedicated `ci-runner` with Node 22/npm (for `npm sbom`), Docker, +OpenSSL, Python 3, Git, OSV-Scanner v2 and Trivy. Verify each downloaded scanner +against vendor provenance/checksums before making it available under a trusted +system directory (`/usr/local/bin` or `/usr/bin`). Do not make a project-local +binary trusted. + +The job generates a CycloneDX SBOM (`npm sbom`), queries OSV live for lockfile +vulnerabilities, and uses Trivy for filesystem, secret, misconfiguration and +image scanning. This follows the tools' supported source/image scan interfaces. + +## Gitea secrets and policies + +Configure these repository/organization values before enabling production +deployments: + +| Value | Purpose | +|---|---| +| `GITEA_TOKEN` | Push only the scanned images to the Gitea registry. Scope: package write. | +| `CASAN_ATTESTATION_PRIVATE_KEY` | PEM private key held in the secret manager; signs SLSA/in-toto provenance. | +| `CASAN_REGISTRY` (variable) | Optional container registry host; defaults to the Gitea server host. | + +Register the matching public key in the release-verification trust store, then +protect `main`: require the `CASAN Supply Chain and Provenance` check, reject +force-pushes, require signed commits, and restrict direct pushes to enrolled +signers. The workflow independently checks every new commit's Git signature; +branch UI policy alone is not treated as evidence. + +The output artifact contains `npm.cyclonedx.json`, OSV/Trivy reports, image +digest files, the signed `provenance.intoto.json` and public key. Copy only the +two digest references into `infra/production/casan-prod.env`; tags are refused +by production preflight. diff --git a/packages/casan-harness/scripts/bash/ci-harness-gate.sh b/packages/casan-harness/scripts/bash/ci-harness-gate.sh index c6b1bf9..6119715 100755 --- a/packages/casan-harness/scripts/bash/ci-harness-gate.sh +++ b/packages/casan-harness/scripts/bash/ci-harness-gate.sh @@ -135,6 +135,8 @@ run "phase-sec23-registry-crypt" bash "$TESTS/phase-sec23-registry-crypt-tests.s # Plan-16 SEC-24/25 supply-chain (offline slice) run "phase-sec24-supplychain" bash "$TESTS/phase-sec24-tests.sh" run "phase-sec25-attestation" bash "$TESTS/phase-sec25-tests.sh" +run "phase-production-handoff" bash "$TESTS/phase-production-preflight-tests.sh" +run "phase-release-provenance" bash "$TESTS/phase-release-provenance-tests.sh" # Plan-17 loop engineering (Agentic Loop Governance) — each primitive fail-closed. run "phase-loop-governor" bash "$TESTS/phase-loop-governor-tests.sh" diff --git a/packages/casan-harness/scripts/bash/live-supply-chain-scan.sh b/packages/casan-harness/scripts/bash/live-supply-chain-scan.sh new file mode 100755 index 0000000..ce4ba3b --- /dev/null +++ b/packages/casan-harness/scripts/bash/live-supply-chain-scan.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Live CI supply-chain gate. Unlike the deterministic local denylist gate, this +# obtains current advisory data and scans the built images. Missing scanners are +# an error in strict CI; no "scanner unavailable => green" path exists. +# +# Usage: live-supply-chain-scan.sh [image-ref ...] +# Required tools: npm, osv-scanner, trivy (when CASAN_REQUIRE_LIVE_SCANNERS=1) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/casan-paths.sh" +ROOT="$CASAN_APP_ROOT" +OUT="${1:-}"; shift || true +[[ -n "$OUT" ]] || { echo "LIVE_SCAN_USAGE report-dir [image-ref ...]" >&2; exit 64; } +mkdir -p "$OUT" + +fail() { echo "LIVE_SUPPLY_CHAIN_FAIL $*" >&2; exit 1; } +require() { command -v "$1" >/dev/null 2>&1 || fail "tool_missing name=$1"; } +require npm +if [[ "${CASAN_REQUIRE_LIVE_SCANNERS:-1}" == "1" ]]; then + require osv-scanner + require trivy +fi + +[[ -f "$ROOT/package-lock.json" ]] || fail "package_lock_missing" +cd "$ROOT" + +# npm audit exits non-zero for findings at/above the requested level. Preserve +# JSON evidence even on failure without converting a failed scan into success. +set +e +npm audit --omit=dev --audit-level=high --json > "$OUT/npm-audit.json" 2> "$OUT/npm-audit.stderr" +audit_rc=$? +set -e +[[ "$audit_rc" -eq 0 ]] || fail "npm_audit_high_or_critical rc=$audit_rc report=$OUT/npm-audit.json" + +npm sbom --sbom-format cyclonedx --package-lock-only --omit=dev > "$OUT/npm.cyclonedx.json" \ + || fail "npm_sbom_failed" +python3 - "$OUT/npm.cyclonedx.json" <<'PY' || exit 1 +import json, sys +d = json.load(open(sys.argv[1])) +assert d.get("bomFormat") == "CycloneDX", d +PY + +if command -v osv-scanner >/dev/null 2>&1; then + osv-scanner scan source --recursive --format json --output-file "$OUT/osv-source.json" "$ROOT" \ + || fail "osv_source_findings_or_scan_error report=$OUT/osv-source.json" +fi +if command -v trivy >/dev/null 2>&1; then + trivy fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 --format json --output "$OUT/trivy-source.json" "$ROOT" \ + || fail "trivy_source_findings_or_scan_error report=$OUT/trivy-source.json" +fi + +for image in "$@"; do + safe="$(printf '%s' "$image" | tr '/:@' '___')" + if command -v osv-scanner >/dev/null 2>&1; then + osv-scanner scan image --format json --output-file "$OUT/osv-image-${safe}.json" "$image" \ + || fail "osv_image_findings_or_scan_error image=$image" + fi + if command -v trivy >/dev/null 2>&1; then + trivy image --severity HIGH,CRITICAL --exit-code 1 --format json --output "$OUT/trivy-image-${safe}.json" "$image" \ + || fail "trivy_image_findings_or_scan_error image=$image" + fi +done + +echo "LIVE_SUPPLY_CHAIN_PASS sbom=$OUT/npm.cyclonedx.json images=$#" diff --git a/packages/casan-harness/scripts/bash/release-provenance.sh b/packages/casan-harness/scripts/bash/release-provenance.sh new file mode 100755 index 0000000..415282e --- /dev/null +++ b/packages/casan-harness/scripts/bash/release-provenance.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Create/verify a signed release provenance statement. The statement binds the +# exact commit, SBOM, test evidence and immutable image digests. The signing key +# is injected by CI from the secret manager and never lives in the repository. +# +# release-provenance.sh create ... +# release-provenance.sh verify [expected-commit] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/casan-paths.sh" +ROOT="$CASAN_APP_ROOT" + +hash_of() { openssl dgst -sha256 "$1" | awk '{print $NF}'; } +fail() { echo "RELEASE_PROVENANCE_FAIL $*" >&2; exit 1; } +cmd="${1:-}"; shift || true + +case "$cmd" in + create) + statement="${1:-}"; key="${2:-}"; sbom="${3:-}"; evidence="${4:-}"; shift 4 || true + [[ -n "$statement" && -f "$key" && -s "$sbom" && -s "$evidence" && "$#" -ge 1 ]] || fail "create_usage_or_missing_input" + for image in "$@"; do [[ "$image" =~ @sha256:[a-f0-9]{64}$ ]] || fail "image_not_digest_pinned image=$image"; done + commit="$(git -C "$ROOT" rev-parse HEAD)" + python3 - "$statement" "$commit" "$sbom" "$evidence" "$@" <<'PY' +import hashlib, json, sys +out, commit, sbom, evidence, *images = sys.argv[1:] +def sha(path): + h=hashlib.sha256() + with open(path,'rb') as f: + for c in iter(lambda:f.read(65536), b''): h.update(c) + return h.hexdigest() +payload = { + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/provenance/v1", + "subject": [{"name": image, "digest": {"sha256": image.rsplit("@sha256:",1)[1]}} for image in images], + "predicate": {"buildDefinition": {"resolvedDependencies": [{"uri": "git+casan", "digest": {"sha1": commit}}]}, + "runDetails": {"builder": {"id": "casan-gitea-ci"}, + "byproducts": [{"name": "npm.cyclonedx.json", "sha256": sha(sbom)}, {"name": "test-evidence", "sha256": sha(evidence)}]}} +} +with open(out, 'w', encoding='utf-8') as f: json.dump(payload, f, sort_keys=True, separators=(',', ':')) +PY + openssl dgst -sha256 -sign "$key" -out "$statement.sig" "$statement" || fail "sign_failed" + echo "RELEASE_PROVENANCE_CREATED statement=$statement commit=$commit images=$# sbom_sha256=$(hash_of "$sbom")" + ;; + verify) + statement="${1:-}"; key="${2:-}"; expected="${3:-$(git -C "$ROOT" rev-parse HEAD)}" + [[ -s "$statement" && -s "$statement.sig" && -f "$key" ]] || fail "verify_missing_statement_signature_or_key" + openssl dgst -sha256 -verify "$key" -signature "$statement.sig" "$statement" >/dev/null || fail "signature_invalid" + python3 - "$statement" "$expected" <<'PY' || exit 1 +import json, re, sys +d=json.load(open(sys.argv[1])); expected=sys.argv[2] +assert d.get('_type') == 'https://in-toto.io/Statement/v1' +assert d.get('predicateType') == 'https://slsa.dev/provenance/v1' +deps=d.get('predicate',{}).get('buildDefinition',{}).get('resolvedDependencies',[]) +assert deps and deps[0].get('digest',{}).get('sha1') == expected +subjects=d.get('subject',[]); assert subjects +for s in subjects: + digest=s.get('digest',{}).get('sha256','') + assert re.fullmatch(r'[a-f0-9]{64}', digest), digest +PY + echo "RELEASE_PROVENANCE_VERIFIED statement=$statement commit=$expected" + ;; + *) fail "usage create|verify" ;; +esac diff --git a/packages/casan-harness/scripts/bash/verify-signed-commits.sh b/packages/casan-harness/scripts/bash/verify-signed-commits.sh new file mode 100755 index 0000000..cc0f1aa --- /dev/null +++ b/packages/casan-harness/scripts/bash/verify-signed-commits.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Verify every commit in a release range has a cryptographically good Git +# signature. This is deliberately strict for release/main enforcement: unsigned +# or merely unknown signatures cannot become production provenance. +# Usage: verify-signed-commits.sh [range] (default: HEAD) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/casan-paths.sh" +ROOT="$CASAN_APP_ROOT"; RANGE="${1:-HEAD}" +commits=() +while IFS= read -r commit; do [[ -n "$commit" ]] && commits+=("$commit"); done < <(git -C "$ROOT" rev-list --reverse "$RANGE") +[[ "${#commits[@]}" -gt 0 ]] || { echo "SIGNED_COMMITS_FAIL empty_range=$RANGE" >&2; exit 1; } +for commit in "${commits[@]}"; do + status="$(git -C "$ROOT" log -1 --format='%G?' "$commit")" + [[ "$status" == G ]] || { echo "SIGNED_COMMITS_FAIL commit=$commit signature_status=$status" >&2; exit 1; } +done +echo "SIGNED_COMMITS_VERIFIED range=$RANGE count=${#commits[@]}" diff --git a/packages/casan-harness/tests/phase-live-supply-chain-tests.sh b/packages/casan-harness/tests/phase-live-supply-chain-tests.sh new file mode 100755 index 0000000..2d873bd --- /dev/null +++ b/packages/casan-harness/tests/phase-live-supply-chain-tests.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh" +SCAN="$CASAN_HARNESS_ROOT/scripts/bash/live-supply-chain-scan.sh" +W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT +PASS=0; FAIL=0 +pass() { echo "PASS: $1"; PASS=$((PASS+1)); } +fail() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } +rc() { set +e; "$@" >/dev/null 2>&1; local c=$?; set -e; echo "$c"; } +mkdir -p "$W/bin" +cat > "$W/bin/npm" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$FAKE_LOG" +if [[ "$1" == sbom ]]; then printf '%s\n' '{"bomFormat":"CycloneDX"}'; fi +exit 0 +SH +cat > "$W/bin/osv-scanner" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "osv $*" >> "$FAKE_LOG" +for ((i=1; i<=$#; i++)); do [[ "${!i}" == --output-file ]] && { j=$((i+1)); printf '{}' > "${!j}"; }; done +exit 0 +SH +cat > "$W/bin/trivy" <<'SH' +#!/usr/bin/env bash +printf '%s\n' "trivy $*" >> "$FAKE_LOG" +for ((i=1; i<=$#; i++)); do [[ "${!i}" == --output ]] && { j=$((i+1)); printf '{}' > "${!j}"; }; done +exit 0 +SH +chmod +x "$W/bin/"* +FAKE_LOG="$W/log" PATH="$W/bin:$PATH" CASAN_REQUIRE_LIVE_SCANNERS=1 bash "$SCAN" "$W/out" casan-test:image >/dev/null \ + && [[ -s "$W/out/npm.cyclonedx.json" ]] && grep -q 'osv scan image' "$W/log" && grep -q 'trivy image' "$W/log" \ + && pass "live gate invokes SBOM, OSV and Trivy source/image scans" || fail "live scanner contract failed" +mkdir -p "$W/npm-only"; cp "$W/bin/npm" "$W/npm-only/npm" +[[ "$(rc env PATH="$W/npm-only:/usr/bin:/bin" CASAN_REQUIRE_LIVE_SCANNERS=1 bash "$SCAN" "$W/missing")" -ne 0 ]] \ + && pass "missing required scanner fails closed" || fail "missing scanner accepted" +echo "===== LIVE SUPPLY CHAIN SUMMARY: PASS=$PASS FAIL=$FAIL =====" +[[ "$FAIL" -eq 0 ]] diff --git a/packages/casan-harness/tests/phase-release-provenance-tests.sh b/packages/casan-harness/tests/phase-release-provenance-tests.sh new file mode 100755 index 0000000..d47024c --- /dev/null +++ b/packages/casan-harness/tests/phase-release-provenance-tests.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh" +P="$CASAN_HARNESS_ROOT/scripts/bash/release-provenance.sh" +W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT +PASS=0; FAIL=0 +pass() { echo "PASS: $1"; PASS=$((PASS+1)); } +fail() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } +rc() { set +e; "$@" >/dev/null 2>&1; local c=$?; set -e; echo "$c"; } +openssl genrsa -out "$W/key.pem" 2048 2>/dev/null +openssl rsa -in "$W/key.pem" -pubout -out "$W/pub.pem" 2>/dev/null +openssl genrsa -out "$W/other.pem" 2048 2>/dev/null +printf '{"bomFormat":"CycloneDX"}\n' > "$W/sbom.json"; printf 'tests passed\n' > "$W/evidence.txt" +DIGEST="sha256:$(printf 'a%.0s' {1..64})" +bash "$P" create "$W/provenance.json" "$W/key.pem" "$W/sbom.json" "$W/evidence.txt" "registry.local/casan/api@$DIGEST" >/dev/null \ + && bash "$P" verify "$W/provenance.json" "$W/pub.pem" >/dev/null && pass "signed provenance binds commit and image digest" || fail "valid provenance rejected" +printf 'tamper' >> "$W/provenance.json" +[[ "$(rc bash "$P" verify "$W/provenance.json" "$W/pub.pem")" -ne 0 ]] && pass "tampered provenance is refused" || fail "tampered provenance accepted" +[[ "$(rc bash "$P" create "$W/bad.json" "$W/key.pem" "$W/sbom.json" "$W/evidence.txt" 'registry.local/casan/api:latest')" -ne 0 ]] && pass "floating image tag is refused" || fail "floating image accepted" +bash "$P" create "$W/other.json" "$W/other.pem" "$W/sbom.json" "$W/evidence.txt" "registry.local/casan/api@$DIGEST" >/dev/null +[[ "$(rc bash "$P" verify "$W/other.json" "$W/pub.pem")" -ne 0 ]] && pass "provenance signed by another key is refused" || fail "wrong signing key accepted" +echo "===== RELEASE PROVENANCE SUMMARY: PASS=$PASS FAIL=$FAIL =====" +[[ "$FAIL" -eq 0 ]]