From 4899787a703f4a00783ff918af90a1ea446d316f Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Wed, 7 Oct 2026 12:23:19 +0200 Subject: [PATCH] fix: verify a fragment's signature before validating beside it --- .github/workflows/publish-fragment.yml | 20 ++++++++++++++------ tests/test_publish_fragment.py | 9 +++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-fragment.yml b/.github/workflows/publish-fragment.yml index 66c3ae6..3a031ec 100644 --- a/.github/workflows/publish-fragment.yml +++ b/.github/workflows/publish-fragment.yml @@ -184,6 +184,9 @@ jobs: - uses: oras-project/setup-oras@005458ad77f1c8facd38a094e4af2e69e5607ff4 # v2.0.2 with: version: 1.3.4 + - uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 + with: + cosign-release: v2.6.1 - name: Pull the fragments the project file is validated beside id: beside if: ${{ inputs.validate-with-fragments != '' }} @@ -194,14 +197,23 @@ jobs: OUT: ${{ runner.temp }}/beside run: | echo "$GITHUB_TOKEN" | oras login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + echo "$GITHUB_TOKEN" | cosign login ghcr.io --username "$GITHUB_ACTOR" --password-stdin owner="$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]')" paths="" for project in $PROJECTS; do ref="ghcr.io/${owner}/intent-${project}:latest" - oras pull "$ref" --output "${OUT}/${project}" \ + digest="$(oras resolve "$ref")" \ || { echo "::error::${ref} is not published, and the project file is validated beside it"; exit 1; } + # Read only what this workflow published and signed, by digest, so + # a fragment pushed by anything else cannot change what is accepted. + cosign verify "${ref%:latest}@${digest}" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github\.com/JorisJonkers-dev/github-workflows/\.github/workflows/publish-fragment\.yml@' \ + >/dev/null \ + || { echo "::error::${ref%:latest}@${digest} is not signed by publish-fragment"; exit 1; } + oras pull "${ref%:latest}@${digest}" --output "${OUT}/${project}" paths="${paths} ${OUT}/${project}" - echo "validating beside ${ref}" + echo "validating beside ${ref%:latest}@${digest}" done echo "paths=${paths# }" >> "$GITHUB_OUTPUT" - name: Validate and pack @@ -220,10 +232,6 @@ jobs: run: bash .github-workflows/actions/publish-fragment/pack.sh - - uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 - with: - cosign-release: v2.6.1 - - name: Push, sign and read back id: push env: diff --git a/tests/test_publish_fragment.py b/tests/test_publish_fragment.py index af9f1c1..b727280 100644 --- a/tests/test_publish_fragment.py +++ b/tests/test_publish_fragment.py @@ -306,6 +306,15 @@ def test_fragments_to_validate_beside_are_pulled_outside_the_checkout_and_only_w self.assertIn("if: ${{ inputs.validate-with-fragments != '' }}", self.text) self.assertIn("OUT: ${{ runner.temp }}/beside", self.text) self.assertIn('ref="ghcr.io/${owner}/intent-${project}:latest"', self.text) + # Only a fragment publish-fragment signed is read, and by digest. + step = self.text.split("- name: Pull the fragments the project file is validated beside", 1)[1].split("- name:", 1)[0] + self.assertIn('cosign verify "${ref%:latest}@${digest}"', step) + self.assertIn('oras pull "${ref%:latest}@${digest}"', step) + self.assertLess(step.index("cosign verify"), step.index("oras pull")) + self.assertLess( + self.text.index("sigstore/cosign-installer@"), + self.text.index("- name: Pull the fragments the project file is validated beside"), + ) self.assertIn( "VALIDATE_WITH: ${{ inputs.validate-with }} ${{ steps.beside.outputs.paths }}", self.text,