From dfc822d5b87a7afc54f87318cc9ded741d138d25 Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 17:41:12 +0200 Subject: [PATCH 1/5] M11.3: qualify published image by exact digest --- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5dc69f3..9b68773 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,6 +69,42 @@ jobs: provenance: mode=max sbom: true + - name: Qualify published image by exact digest + env: + IMAGE_DIGEST: ${{ steps.push.outputs.digest }} + run: | + set -eu + case "$IMAGE_DIGEST" in + sha256:[0-9a-f][0-9a-f]*) ;; + *) echo "invalid published image digest: $IMAGE_DIGEST" >&2; exit 1 ;; + esac + test "${#IMAGE_DIGEST}" -eq 71 + image_ref="${IMAGE_NAME}@${IMAGE_DIGEST}" + docker pull "$image_ref" + resolved="$(docker image inspect "$image_ref" --format '{{index .RepoDigests 0}}')" + test "$resolved" = "$image_ref" + revision="$(docker image inspect "$image_ref" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" + version="$(docker image inspect "$image_ref" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" + test "$revision" = "$GITHUB_SHA" + test "$version" = "$GITHUB_REF_NAME" + test "$(docker run --rm --entrypoint id "$image_ref" -u)" = "1000" + test "$(docker run --rm --entrypoint id "$image_ref" -g)" = "1000" + cid=$(docker run -d --rm \ + -e SOJU_WEB_ADMIN_PASSWORD=release-qualification-password \ + -e SOJU_WEB_SESSION_SECRET=release-qualification-session-secret-0123456789 \ + -p 127.0.0.1:18080:8080 "$image_ref") + trap 'docker logs "$cid" 2>&1 || true; docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT + for i in $(seq 1 30); do + if curl -fsS http://127.0.0.1:18080/healthz | grep -qx ok; then + healthy=true + break + fi + sleep 1 + done + test "${healthy:-false}" = true + docker rm -f "$cid" + trap - EXIT + - name: Attest image uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3 with: From 0fdc763ffef82b6daa0f5caebe11c4b3bba35572 Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 17:41:24 +0200 Subject: [PATCH 2/5] M11.3: add exact-digest policy qualification --- scripts/verify-exact-digest-runtime.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 scripts/verify-exact-digest-runtime.sh diff --git a/scripts/verify-exact-digest-runtime.sh b/scripts/verify-exact-digest-runtime.sh new file mode 100644 index 0000000..3a68384 --- /dev/null +++ b/scripts/verify-exact-digest-runtime.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu + +image_name=${1:-ghcr.io/ploos-as/soju-web} +digest=${2:-} + +case "$digest" in + sha256:[0-9a-f][0-9a-f]*) ;; + *) echo "invalid digest: $digest" >&2; exit 1 ;; +esac + +if [ "${#digest}" -ne 71 ]; then + echo "digest must be sha256 plus exactly 64 lowercase hex characters" >&2 + exit 1 +fi + +hex=${digest#sha256:} +case "$hex" in + *[!0-9a-f]*) echo "digest contains non-hex characters" >&2; exit 1 ;; +esac + +printf '%s@%s\n' "$image_name" "$digest" From fd15694b9c6b1d00384fe08d54d6a7e9e5afaecc Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 17:41:50 +0200 Subject: [PATCH 3/5] M11.3: qualify exact-digest reference policy in CI --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0baae85..a03b43f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,17 @@ jobs: exit 1 fi + - name: Verify exact-digest policy + run: | + digest="sha256:$(printf '%064d' 0)" + test "$(sh scripts/verify-exact-digest-runtime.sh ghcr.io/ploos-as/soju-web "$digest")" = "ghcr.io/ploos-as/soju-web@$digest" + for invalid in latest v0.1.0 sha256:deadbeef "sha256:$(printf '%064d' 0)X"; do + if sh scripts/verify-exact-digest-runtime.sh ghcr.io/ploos-as/soju-web "$invalid"; then + echo "invalid digest unexpectedly accepted: $invalid" >&2 + exit 1 + fi + done + - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 From 25764a0d522c6559787c8b4769423da667c7c615 Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 17:42:01 +0200 Subject: [PATCH 4/5] M11.3: document exact-digest runtime gate --- docs/M11_3_EXACT_DIGEST_RUNTIME.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/M11_3_EXACT_DIGEST_RUNTIME.md diff --git a/docs/M11_3_EXACT_DIGEST_RUNTIME.md b/docs/M11_3_EXACT_DIGEST_RUNTIME.md new file mode 100644 index 0000000..bb344d6 --- /dev/null +++ b/docs/M11_3_EXACT_DIGEST_RUNTIME.md @@ -0,0 +1,9 @@ +# M11.3 exact-digest runtime qualification + +Release qualification is bound to the immutable OCI digest emitted by the successful multi-platform build/push step. + +The release workflow constructs only `ghcr.io/ploos-as/soju-web@sha256:<64 lowercase hex>` for qualification. It pulls that exact reference, verifies Docker resolves the same RepoDigest, checks the OCI revision and version labels against the release commit/tag, verifies UID/GID 1000, and starts the exact-digest image to require a successful `/healthz` response. + +Mutable release tags are not used as the runtime qualification target. Attestation and GitHub Release creation remain downstream of this runtime gate, so a failed exact-digest qualification prevents release completion. + +CI separately exercises the digest-reference validator with accepted and rejected inputs before any release tag is created. From 8ce512b67169cfc7543fe9c1b0cb3bcfb26a01c8 Mon Sep 17 00:00:00 2001 From: pos Date: Sat, 5 Sep 2026 17:42:11 +0200 Subject: [PATCH 5/5] M11.3: add reusable digest validator test --- scripts/test-exact-digest-runtime.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 scripts/test-exact-digest-runtime.sh diff --git a/scripts/test-exact-digest-runtime.sh b/scripts/test-exact-digest-runtime.sh new file mode 100644 index 0000000..73b7217 --- /dev/null +++ b/scripts/test-exact-digest-runtime.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +digest="sha256:$(printf '%064d' 0)" +expected="ghcr.io/ploos-as/soju-web@$digest" +actual=$(sh scripts/verify-exact-digest-runtime.sh ghcr.io/ploos-as/soju-web "$digest") +test "$actual" = "$expected" + +for invalid in \ + latest \ + v0.1.0 \ + sha256:deadbeef \ + "sha256:$(printf '%064d' 0)X" \ + "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +do + if sh scripts/verify-exact-digest-runtime.sh ghcr.io/ploos-as/soju-web "$invalid" >/dev/null 2>&1; then + echo "invalid digest unexpectedly accepted: $invalid" >&2 + exit 1 + fi +done