Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/scripts/verify_builder_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,95 @@ def check_image_attested(ctx, params):
return errs


def _digest_ok(ctx, a, errs):
d = ctx.digests.get(a)
if isinstance(d, str) and DIGEST_RE.match(d):
return d
errs.append(f"{a}: image digest missing or malformed: {d!r}")
return None


def _attested_predicates(ctx, a, d, params, ptype, source_ref, errs):
"""gh attestation verify the image digest for one predicate type; return the list of
verified predicates (empty list if none), or None after recording an error."""
rc, out = ctx.run([
"gh", "attestation", "verify", f"oci://{params['image']}@{d}",
"--repo", params["repo"],
"--signer-workflow", params["signer_workflow"],
"--source-ref", source_ref,
"--source-digest", ctx.tag_sha,
"--predicate-type", ptype,
"--format", "json",
])
if rc != 0:
errs.append(f"{a}: gh attestation verify failed for {ptype} on {d}")
return None
try:
results = json.loads(out)
except ValueError:
errs.append(f"{a}: gh attestation verify output is not JSON ({ptype})")
return None
preds = []
for r in results if isinstance(results, list) else []:
stmt = ((r or {}).get("verificationResult") or {}).get("statement") or {}
preds.append(stmt.get("predicate"))
if not preds:
errs.append(f"{a}: no {ptype} attestation returned for {d}")
return None
return preds


def _predicate_equals(ctx, a, d, params, ptype, source_ref, doc, label, errs):
if not isinstance(doc, dict):
errs.append(f"{a}: released {label} missing")
return
preds = _attested_predicates(ctx, a, d, params, ptype, source_ref, errs)
if preds is not None and doc not in preds:
errs.append(f"{a}: released {label} differs from the attested predicate on {d}")


def check_inputs_attested(ctx, params):
"""builder-inputs.<arch>.json is the predicate of a verified attestation on that arch's
image digest (named workflow, main, tagged commit, named predicate type)."""
errs = []
for a in ARCHES:
d = _digest_ok(ctx, a, errs)
if d:
_predicate_equals(ctx, a, d, params, params["predicate_type"], params["source_ref"],
ctx.inputs.get(a), "builder-inputs.json", errs)
return errs


def check_sbom_attested(ctx, params):
"""The released CycloneDX and SPDX SBOMs are the predicates of verified attestations on the
image digest, made by the release workflow from the release tag."""
errs = []
sref = params["source_ref_template"].format(tag=ctx.tag)
for a in ARCHES:
d = _digest_ok(ctx, a, errs)
if not d:
continue
cdx, spdx = ctx.sboms.get(a), ctx.spdx.get(a)
_predicate_equals(ctx, a, d, params, params["cyclonedx_predicate_type"], sref, cdx, "CycloneDX SBOM", errs)
ver = str((spdx or {}).get("spdxVersion", "")).partition("-")[2]
if not ver:
errs.append(f"{a}: SPDX SBOM missing or has no spdxVersion")
continue
_predicate_equals(ctx, a, d, params, params["spdx_predicate_type_prefix"] + ver, sref, spdx, "SPDX SBOM", errs)
return errs


def check_vex_attested(ctx, params):
"""The released OpenVEX file is the predicate of a verified attestation on each image digest."""
errs = []
sref = params["source_ref_template"].format(tag=ctx.tag)
for a in ARCHES:
d = _digest_ok(ctx, a, errs)
if d:
_predicate_equals(ctx, a, d, params, params["predicate_type"], sref, ctx.vex, "builder.openvex.json", errs)
return errs


def check_gate_script_hash(ctx, params):
import hashlib
want = hashlib.sha256(ctx.git_show(params["script_path"])).hexdigest()
Expand Down
26 changes: 25 additions & 1 deletion .github/workflows/build-environment-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/python-wheels-builder-alpine
CONTEXT: environments/alpine-3.24-musl
INPUTS_PREDICATE_TYPE: https://github.com/patrickryankenneth/python-wheels-builder/predicate/builder-inputs/v1

# No default permissions; each job asks for exactly what it needs.
permissions: {}
Expand Down Expand Up @@ -106,7 +107,7 @@ jobs:
echo "\`$IMAGE@$DIGEST\`" >> "$GITHUB_STEP_SUMMARY"

- name: Attest build provenance
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: ghcr.io/${{ github.repository_owner }}/python-wheels-builder-alpine
subject-digest: ${{ steps.push.outputs.digest }}
Expand Down Expand Up @@ -176,6 +177,29 @@ jobs:
builder_image:$builder_image, source_commit:$commit}' > "builder-inputs.$ARCH.json"
cat "builder-inputs.$ARCH.json"

- name: Attest builder inputs
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: ghcr.io/${{ github.repository_owner }}/python-wheels-builder-alpine
subject-digest: ${{ steps.push.outputs.digest }}
predicate-type: ${{ env.INPUTS_PREDICATE_TYPE }}
predicate-path: builder-inputs.${{ matrix.arch }}.json
push-to-registry: true

- name: Verify the builder-inputs attestation we just made
env:
GH_TOKEN: ${{ github.token }}
DIGEST: ${{ steps.push.outputs.digest }}
ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
gh attestation verify "oci://$IMAGE@$DIGEST" \
--repo "${{ github.repository }}" \
--signer-workflow "${{ github.repository }}/.github/workflows/build-environment-image.yml" \
--predicate-type "$INPUTS_PREDICATE_TYPE" --format json \
| jq -e --slurpfile want "builder-inputs.$ARCH.json" \
'any(.[]; .verificationResult.statement.predicate == $want[0])' > /dev/null

- name: Upload digest and inputs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
Expand Down
Loading
Loading