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
66 changes: 66 additions & 0 deletions .github/workflows/attestation-verification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Attestation verification

on:
pull_request:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: Existing release version to verify (for example 0.2.1 or v0.2.1)
required: true
default: '0.2.1'
type: string

permissions:
contents: read
packages: read

concurrency:
group: attestation-verification-${{ github.ref }}
cancel-in-progress: true

env:
IMAGE: ghcr.io/ploos-as/glowing-bear

jobs:
validate-script:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate M0.9 script syntax
run: bash -n scripts/qualify_attestations.sh

verify-attestations:
needs: validate-script
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Select release version
id: release
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
version="${{ inputs.version }}"
elif [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
version="${GITHUB_REF_NAME}"
else
version="0.2.1"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Verify published provenance and SBOM attestations
env:
VERSION: ${{ steps.release.outputs.version }}
run: bash scripts/qualify_attestations.sh
18 changes: 18 additions & 0 deletions docs/M0_9_ATTESTATION_VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# M0.9 attestation verification

M0.8 verifies that a published release exposes Buildx attestation manifests. M0.9 strengthens that contract by validating how those attestations are attached and what predicate types they advertise.

For every required Linux platform manifest (`linux/amd64` and `linux/arm64`), M0.9 requires:

- a BuildKit attestation manifest whose `vnd.docker.reference.digest` points to that exact platform digest;
- `vnd.docker.reference.type=attestation-manifest`;
- in-toto layers using media type `application/vnd.in-toto+json`;
- a non-empty SLSA provenance descriptor with predicate type `https://slsa.dev/provenance/v0.2`;
- a non-empty SPDX SBOM descriptor with predicate type `https://spdx.dev/Document`;
- sha256 layer digests for both descriptors.

This closes the M0.8 gap where merely counting `unknown/unknown` manifests could not distinguish the required provenance/SBOM contract from unrelated attestations.

The workflow runs on release tags and can also be run manually against an existing immutable release. Pull requests verify the currently published `0.2.1` release as a real registry-backed integration test while also syntax-checking the script.

M0.9 does not claim cryptographic signer identity verification. Signature policy and keyless identity verification, if desired, belong in a later milestone rather than being conflated with attestation-content qualification.
110 changes: 110 additions & 0 deletions scripts/qualify_attestations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail

IMAGE="${IMAGE:-ghcr.io/ploos-as/glowing-bear}"
VERSION="${VERSION:?VERSION is required}"

if [[ "$VERSION" == v* ]]; then
VERSION="${VERSION#v}"
fi

if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "release version is not semver-like: $VERSION" >&2
exit 1
fi

primary="$IMAGE:$VERSION"
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT

index_json="$workdir/index.json"
docker buildx imagetools inspect --raw "$primary" > "$index_json"

platform_rows="$workdir/platforms.tsv"
jq -r '.manifests[]
| select(.platform.os == "linux")
| select(.platform.architecture == "amd64" or .platform.architecture == "arm64")
| [.platform.architecture, .digest] | @tsv' "$index_json" > "$platform_rows"

for arch in amd64 arm64; do
if ! awk -F '\t' -v a="$arch" '$1 == a {found=1} END {exit !found}' "$platform_rows"; then
echo "$primary is missing linux/$arch" >&2
exit 1
fi
done

# BuildKit stores the subject linkage on the attestation descriptor in the OCI
# index. The descriptor binds each attestation manifest to its exact platform
# digest; Buildx exposes decoded provenance/SBOM payloads from the parent
# multi-platform release reference.
attestation_rows="$workdir/attestations.tsv"
jq -r '.manifests[]
| select(.platform.os == "unknown" and .platform.architecture == "unknown")
| select(.annotations["vnd.docker.reference.type"] == "attestation-manifest")
| select((.annotations["vnd.docker.reference.digest"] // "") | test("^sha256:[0-9a-f]{64}$"))
| [.digest, .annotations["vnd.docker.reference.digest"]] | @tsv' \
"$index_json" > "$attestation_rows"

if [[ ! -s "$attestation_rows" ]]; then
echo "$primary has no valid BuildKit attestation descriptors" >&2
exit 1
fi

while IFS=$'\t' read -r arch platform_digest; do
attestation_digest="$(awk -F '\t' -v d="$platform_digest" '$2 == d {print $1; exit}' "$attestation_rows")"
if [[ -z "$attestation_digest" ]]; then
echo "missing attestation manifest for linux/$arch digest $platform_digest" >&2
exit 1
fi

manifest_json="$workdir/${attestation_digest#sha256:}.manifest.json"
docker buildx imagetools inspect --raw "$IMAGE@$attestation_digest" > "$manifest_json"

jq -e '[.layers[] | select(.mediaType == "application/vnd.in-toto+json") | select(.size > 0) | .digest | select(test("^sha256:[0-9a-f]{64}$"))] | length >= 2' \
"$manifest_json" >/dev/null || {
echo "attestation $attestation_digest does not contain at least two valid in-toto layers" >&2
exit 1
}
done < "$platform_rows"

provenance_json="$workdir/provenance.json"
sbom_json="$workdir/sbom.json"

docker buildx imagetools inspect \
--format '{{ json .Provenance }}' \
"$primary" > "$provenance_json"
docker buildx imagetools inspect \
--format '{{ json .SBOM }}' \
"$primary" > "$sbom_json"

for arch in amd64 arm64; do
jq -e --arg platform "linux/$arch" '
.[$platform].SLSA
| type == "object"
and (.buildDefinition | type == "object")
and (.buildDefinition.buildType | type == "string" and length > 0)
and (.runDetails | type == "object")
and (.runDetails.builder.id | type == "string" and length > 0)
' "$provenance_json" >/dev/null || {
echo "$primary is missing readable SLSA provenance content for linux/$arch" >&2
cat "$provenance_json" >&2 || true
exit 1
}
done

jq -e '
. != null and
([.. | objects | .spdxVersion? // empty] | any(startswith("SPDX-")))
' "$sbom_json" >/dev/null || {
echo "$primary is missing readable SPDX SBOM content" >&2
cat "$sbom_json" >&2 || true
exit 1
}

while IFS=$'\t' read -r arch platform_digest; do
attestation_digest="$(awk -F '\t' -v d="$platform_digest" '$2 == d {print $1; exit}' "$attestation_rows")"
echo "Qualified attestation linkage for linux/$arch $platform_digest via $attestation_digest"
done < "$platform_rows"

echo "Qualified decoded SLSA provenance and SPDX SBOM payloads for $primary"
echo "M0.9 attestation verification passed for $primary"
Loading