diff --git a/.github/workflows/runtime-neutral-verification.yml b/.github/workflows/runtime-neutral-verification.yml index 875ed83..a36f9fe 100644 --- a/.github/workflows/runtime-neutral-verification.yml +++ b/.github/workflows/runtime-neutral-verification.yml @@ -31,7 +31,9 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 - name: Validate script syntax - run: bash -n scripts/verify-release.sh + run: | + bash -n scripts/verify-release.sh + bash -n scripts/qualify_negative_verification.sh - name: Verify clean immutable output shell: bash run: | @@ -50,11 +52,6 @@ jobs: set -euo pipefail ref="$(VERSION="$VERSION" PULL=1 RUNTIME=podman bash scripts/verify-release.sh)" podman image inspect "$ref" >/dev/null - - name: Reject wrong OIDC issuer + - name: Qualify fail-closed negative paths shell: bash - run: | - set -euo pipefail - if VERSION="$VERSION" OIDC_ISSUER=https://example.invalid PULL=0 bash scripts/verify-release.sh >/dev/null 2>&1; then - echo "verification unexpectedly accepted the wrong issuer" >&2 - exit 1 - fi + run: VERSION="$VERSION" bash scripts/qualify_negative_verification.sh diff --git a/docs/M0_14_NEGATIVE_PATH_HARDENING.md b/docs/M0_14_NEGATIVE_PATH_HARDENING.md new file mode 100644 index 0000000..c3d3917 --- /dev/null +++ b/docs/M0_14_NEGATIVE_PATH_HARDENING.md @@ -0,0 +1,41 @@ +# M0.14 negative-path hardening + +M0.14 proves that the verified deployment path fails closed when trust, image, release, or runtime inputs are wrong. + +## Scope + +The positive trust path remains unchanged: release tags are resolved with Skopeo, the resulting immutable digest is verified with Cosign against the expected GitHub Actions certificate identity and OIDC issuer, and only then may Docker or Podman pull the image. + +M0.14 adds an explicit negative qualification matrix. Each case must fail with a non-zero exit status and must not emit an accepted Glowing Bear immutable image reference on stdout. + +## Negative cases + +CI rejects all of the following: + +- wrong GitHub Actions OIDC issuer; +- wrong certificate identity / signer workflow; +- an unsigned unrelated container image; +- a nonexistent Glowing Bear release tag; +- an unsupported runtime selector; +- an invalid PULL selector; +- a missing VERSION value. + +The signed `v0.2.3` release remains the positive qualification target. Existing tags are not moved or rewritten. + +## Testability of signer identity + +`scripts/verify-release.sh` still defaults to the exact production signer identity: + +`https://github.com/Ploos-AS/glowing-bear/.github/workflows/sign-release.yml@refs/tags/v` + +`EXPECTED_IDENTITY` exists only as an explicit override so CI and operators can test a stricter or intentionally mismatching identity. It does not weaken the default policy: with no override, the repository's release-signing workflow and exact tag remain mandatory. + +## Acceptance criteria + +M0.14 is complete when: + +1. the normal signed release verification succeeds; +2. Docker and Podman verified pull paths still succeed; +3. every negative case fails; +4. no negative case emits an accepted immutable Glowing Bear ref on stdout; +5. all existing repository workflows remain green. diff --git a/scripts/qualify_negative_verification.sh b/scripts/qualify_negative_verification.sh new file mode 100644 index 0000000..140e88e --- /dev/null +++ b/scripts/qualify_negative_verification.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION="${VERSION:-0.2.3}" + +expect_fail() { + local name="$1" + shift + local stdout_file stderr_file + stdout_file="$(mktemp)" + stderr_file="$(mktemp)" + + if "$@" >"$stdout_file" 2>"$stderr_file"; then + echo "negative test unexpectedly succeeded: $name" >&2 + cat "$stdout_file" >&2 || true + cat "$stderr_file" >&2 || true + rm -f "$stdout_file" "$stderr_file" + exit 1 + fi + + if grep -Eq '^ghcr\.io/ploos-as/glowing-bear@sha256:[0-9a-f]{64}$' "$stdout_file"; then + echo "negative test leaked an accepted immutable ref on stdout: $name" >&2 + cat "$stdout_file" >&2 || true + rm -f "$stdout_file" "$stderr_file" + exit 1 + fi + + echo "PASS (rejected): $name" + rm -f "$stdout_file" "$stderr_file" +} + +expect_fail "wrong OIDC issuer" \ + env VERSION="$VERSION" OIDC_ISSUER=https://example.invalid PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "wrong signer identity" \ + env VERSION="$VERSION" EXPECTED_IDENTITY=https://github.com/Ploos-AS/glowing-bear/.github/workflows/not-the-signer.yml@refs/tags/v$VERSION PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "unsigned foreign image" \ + env IMAGE=docker.io/library/alpine VERSION=3.20 PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "nonexistent release tag" \ + env VERSION=99.99.99 PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "invalid runtime selector" \ + env VERSION="$VERSION" RUNTIME=containerd PULL=0 \ + bash scripts/verify-release.sh + +expect_fail "invalid pull selector" \ + env VERSION="$VERSION" PULL=yes \ + bash scripts/verify-release.sh + +expect_fail "missing VERSION" \ + env -u VERSION bash scripts/verify-release.sh + +echo "All negative verification cases rejected as expected." diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 89a2c07..a274cf7 100644 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -4,6 +4,7 @@ set -euo pipefail IMAGE="${IMAGE:-ghcr.io/ploos-as/glowing-bear}" VERSION="${VERSION:?VERSION is required}" OIDC_ISSUER="${OIDC_ISSUER:-https://token.actions.githubusercontent.com}" +EXPECTED_IDENTITY="${EXPECTED_IDENTITY:-}" PULL="${PULL:-0}" RUNTIME="${RUNTIME:-auto}" @@ -40,7 +41,8 @@ for command in skopeo cosign; do done primary="$IMAGE:$VERSION" -identity="https://github.com/Ploos-AS/glowing-bear/.github/workflows/sign-release.yml@refs/tags/$TAG" +default_identity="https://github.com/Ploos-AS/glowing-bear/.github/workflows/sign-release.yml@refs/tags/$TAG" +identity="${EXPECTED_IDENTITY:-$default_identity}" image_digest="$(skopeo inspect --no-tags --format '{{.Digest}}' "docker://$primary")" if [[ ! "$image_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then