Verifying releases

Every Shepherd release is signed by the workflow that built it, so you can check that what you install came from this repository’s release workflow, at the tag it claims, and was not altered afterwards:

  • Release archives (and the SBOM file next to each) carry a SLSA build provenance attestation, and checksums.txt lists their digests.
  • Container images (shepherd and shepherd-simulator) carry build provenance and an SPDX SBOM, both stored in GitHub and pushed to the registry next to the image.
  • The Helm chart carries build provenance, stored in GitHub and in the registry next to the chart.
  • Release tags are SSH-signed with a key listed in .github/allowed_signers; the release workflow refuses a tag that is not.

The release workflow runs these same commands against everything it publishes before the run turns green. You need the GitHub CLI for gh attestation; the cosign alternatives need cosign 3 or later. Every gh attestation verify below pins three things: the repository (-R), the workflow that signed (--signer-workflow) and the tag it ran for (--source-ref). Drop none of them — without them, an attestation from some other workflow would also pass.

Set the release you are checking

The tag is the app version with a v; image tags drop the v; the chart has its own version (Kubernetes (Helm)).

shell
TAG=vX.Y.Z               # the release tag
VERSION="${TAG#v}"       # the image tag
CHART_VERSION=A.B.C      # the chart version that release published
REPO=procoduck/shepherd
SIGNER="$REPO/.github/workflows/release.yml"

A release archive

Download the archive and checksums.txt, check the digest, then check the archive’s provenance:

shell
gh release download "$TAG" -R "$REPO" -p checksums.txt -p "shepherd_${VERSION}_linux_amd64.tar.gz"
sha256sum --ignore-missing --check checksums.txt
gh attestation verify "shepherd_${VERSION}_linux_amd64.tar.gz" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"

Every file checksums.txt lists is attested, so the same command works for the shepherd-mcp archives and the .sbom.json files. On macOS use shasum -a 256 --ignore-missing --check checksums.txt.

An image

By tag, or — better, since a tag can move — by the digest you actually deploy. Repeat for shepherd-simulator.

shell
IMAGE=ghcr.io/procoduck/shepherd
gh attestation verify "oci://$IMAGE:$VERSION" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"

# The digest the tag resolves to (or the one in your manifests)
DIGEST=$(docker buildx imagetools inspect "$IMAGE:$VERSION" --format '{{json .Manifest.Digest}}' | tr -d '"')
gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"

The Helm chart

helm pull prints the chart’s digest as it downloads it:

shell
CHART=ghcr.io/procoduck/charts/shepherd
gh attestation verify "oci://$CHART:$CHART_VERSION" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"

CHART_DIGEST=$(helm pull "oci://$CHART" --version "$CHART_VERSION" 2>&1 | awk '$1 == "Digest:" { print $2 }')
gh attestation verify "oci://$CHART@$CHART_DIGEST" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG"

From the registry, without GitHub’s API

Image and chart attestations are also stored in the registry beside the artifact, so a mirror or an air-gapped cluster can verify them. Add --bundle-from-oci to any image or chart command above:

shell
gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --bundle-from-oci
gh attestation verify "oci://$CHART@$CHART_DIGEST" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --bundle-from-oci

Or with cosign, which reads only the registry copy:

shell
cosign verify-attestation --type slsaprovenance1 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity "https://github.com/$SIGNER@refs/tags/$TAG" \
  "$IMAGE@$DIGEST" > /dev/null

cosign verify-attestation --type slsaprovenance1 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity "https://github.com/$SIGNER@refs/tags/$TAG" \
  "$CHART@$CHART_DIGEST" > /dev/null

Both commands exit non-zero unless an attestation verifies; the redirect only hides the attestation payload cosign prints on success.

An image’s SBOM

The SBOM attestation has the predicate type https://spdx.dev/Document/v2.3. Verify it, then write the verified SPDX document to a file:

shell
SPDX=https://spdx.dev/Document/v2.3
gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --predicate-type "$SPDX"

gh attestation verify "oci://$IMAGE@$DIGEST" -R "$REPO" \
  --signer-workflow "$SIGNER" --source-ref "refs/tags/$TAG" --predicate-type "$SPDX" \
  --format json --jq '.[0].verificationResult.statement.predicate' > shepherd.spdx.json

The same SBOM, from the registry with cosign:

shell
cosign verify-attestation --type "$SPDX" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity "https://github.com/$SIGNER@refs/tags/$TAG" \
  "$IMAGE@$DIGEST" > /dev/null

The SBOM describes the image’s linux/amd64 member; the linux/arm64 member is built from the same Dockerfile, Go modules and base image.

A release tag’s signature

Check the tag against the signers list on main, which only a reviewed pull request can change:

shell
git clone https://github.com/procoduck/shepherd.git && cd shepherd
git -c gpg.ssh.allowedSignersFile=.github/allowed_signers verify-tag "$TAG"

A good tag prints Good "git" signature for …. Tags from before signing was enforced are unsigned and fail this check. Their archives and images still verify through GitHub’s API as above; chart provenance, image SBOMs and the registry copies start with the first release that has a signed tag.