diff --git a/actions/sign/action.yml b/actions/sign/action.yml index a38cc3a..7be2204 100644 --- a/actions/sign/action.yml +++ b/actions/sign/action.yml @@ -13,6 +13,11 @@ inputs: expect-platforms: description: The platforms the reference must hold, e.g. "amd64 arm64"; empty takes what is there default: "" + attest-platforms: + description: >- + The platforms to attest, e.g. "arm64"; empty attests every one. A release + index joins a new platform to one already attested. + default: "" runs: using: composite steps: @@ -25,4 +30,5 @@ runs: REF: ${{ inputs.ref }} DIR: ${{ inputs.dir }} EXPECT_PLATFORMS: ${{ inputs.expect-platforms }} + ATTEST_PLATFORMS: ${{ inputs.attest-platforms }} run: bash "${{ github.action_path }}/../../bin/sign.sh" "$REF" "$DIR" diff --git a/bin/sign.sh b/bin/sign.sh index b72ec63..f0eae7c 100755 --- a/bin/sign.sh +++ b/bin/sign.sh @@ -10,7 +10,8 @@ # # The platforms are whatever the reference holds (image_tools/platforms.py): an # index of amd64 and arm64, or one manifest. EXPECT_PLATFORMS ("amd64 arm64") -# makes any other set an error. +# makes any other set an error. ATTEST_PLATFORMS ("arm64") attests only those: +# a release index joins arm64 to an amd64 image its dev build already attested. # # Nothing goes to a public transparency log: cosign-signing.json names none, so # the names in this registry stay inside. Everything is verified against @@ -49,6 +50,7 @@ if [ -n "${EXPECT_PLATFORMS:-}" ]; then fi while read -r arch digest; do + case " ${ATTEST_PLATFORMS:-$arch} " in *" $arch "*) ;; *) echo "signed: $image@$digest ($arch)"; continue ;; esac for attestation in "$CIS_TYPE cis-$arch.json" "cyclonedx sbom-$arch.cdx.json"; do read -r type file <<<"$attestation" quietly cosign attest --yes --key env://COSIGN_PRIVATE_KEY --signing-config cosign-signing.json \ diff --git a/tests/test_signing.py b/tests/test_signing.py index c08680e..fa20589 100644 --- a/tests/test_signing.py +++ b/tests/test_signing.py @@ -120,6 +120,30 @@ class TestSign: assert sum(f"@{AMD}" in ln for ln in verified) == 2 assert sum(f"@{ARM}" in ln for ln in verified) == 2 + def test_a_release_index_attests_only_the_platform_it_added(self, tmp_path): + """netork's release joins arm64 to the amd64 image its dev build already + signed and attested: the new index is signed throughout, and only arm64 + needs its CIS result and SBOM (ATTEST_PLATFORMS).""" + raw = { + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + {"digest": AMD, "platform": {"os": "linux", "architecture": "amd64"}}, + {"digest": ARM, "platform": {"os": "linux", "architecture": "arm64"}}, + ], + } + env = self._env(tmp_path, raw, {}, ATTEST_PLATFORMS="arm64") + attest = self._attest_dir(tmp_path, "arm64") + + result = _run(SIGN, f"{IMAGE}:0.41.0@{INDEX}", str(attest), env=env) + + assert result.returncode == 0, result.stderr + log = _log(tmp_path) + assert any("sign --yes --recursive" in ln and f"@{INDEX}" in ln for ln in log) + attests = [ln for ln in log if " attest " in ln] + assert len(attests) == 2 and all(f"@{ARM}" in ln for ln in attests) + assert f"signed and attested: {IMAGE}@{AMD}" not in result.stdout + assert f"signed: {IMAGE}@{AMD} (amd64)" in result.stdout + def test_a_missing_expected_platform_stops_it(self, tmp_path): env = self._env( tmp_path, {"mediaType": "x"}, {"architecture": "amd64"}, EXPECT_PLATFORMS="amd64 arm64"