Verify release manifests with Cosign bundles before deployment

John Burns

A release manifest is often the small file that connects an approved build to a deployment. It might list image digests, package hashes, chart versions, or a generated SBOM. If an automation job fetches that file and treats it as trusted without checking its signature, changing one line can redirect a deployment to a different artifact.

This guide builds a small verification boundary around a detached release manifest using Cosign 3.1.3 and a self-managed signing key. The result is a script that accepts only a manifest that matches its Cosign bundle and configured public key, and exits nonzero before a deployment step can read it. A successful run prints Verified OK; a manifest changed after signing is rejected with an invalid-signature error.

The procedure was tested in an isolated Linux AMD64 directory with Cosign 3.1.3. The test generated a new local key pair, signed a synthetic text manifest into a bundle, verified the original file, then changed the version line and verified that Cosign rejected the modified copy. It does not test a production key-management service, an identity-based keyless policy, or a registry workflow. Those require a separate staging test with the actual delivery system.

Cosign 3.1.3 was released on 6 August 2026. Its release notes address GHSA-fx35-mq7g-6g98, a verification bypass involving an unexpected public key in a legacy bundle. GitHub’s advisory lists Cosign v3 versions through 3.1.2 as affected and identifies 3.1.3 as the patched v3 release. That makes a release-verification check a useful place to record the exact Cosign binary and bundle format in use, rather than assuming every machine’s cosign command behaves identically.

Define what the signature protects

A signature proves that the bytes presented to the verifier are the bytes that were signed by the configured key. It does not prove that the manifest is appropriate for every environment. Keep deployment policy separate: the verification gate establishes integrity and signer authorization, while the deployment workflow still decides whether the listed image repository, digest, chart, and target environment are permitted.

Start with a deliberately narrow manifest. It should contain stable artifact identifiers rather than mutable image tags. For example, a release process might create this file after the build and approval steps have selected immutable digests:

api=registry.example.invalid/team/api@sha256:<api-digest>
worker=registry.example.invalid/team/worker@sha256:<worker-digest>
chart=oci://registry.example.invalid/charts/service@sha256:<chart-digest>
release=2026.08.07

Use the manifest as an input to a later deployment command, not as a place to store credentials, kubeconfig content, internal hostnames, or unredacted change-ticket data. A signed secret is still a secret. If a deployment needs credentials, retrieve them from the platform’s approved secret mechanism after the signature check succeeds.

The verifier also needs an explicit trust anchor: the public key that is allowed to sign this class of manifest. Store the public key in source control only after review, or distribute it through the same controlled configuration channel used for other deployment policy. Do not download a public key from an arbitrary URL during the deployment. If an attacker can replace both the manifest and the key, a valid signature does not help.

Obtain one known Cosign binary

Do not use an unpinned package or a browser-downloaded latest binary for the first verification test. The Cosign 3.1.3 release includes a cosign-linux-amd64 binary and cosign_checksums.txt. Download the checksum list first, compare its SHA-256 value with the official release asset digest, then check the binary against the entry in that list before making it executable.

The following example is for Linux x86-64. Run it in a temporary tools directory or an image-build stage, not in a repository that will be deployed. The expected checksum-list digest below is the value published for the 3.1.3 release asset. Change the version, URLs, and expected digest together when intentionally upgrading.

set -eu
version=3.1.3
base="https://github.com/sigstore/cosign/releases/download/v${version}"
workdir=$(mktemp -d)
cd "$workdir"

curl --fail --location --output cosign_checksums.txt \
  "$base/cosign_checksums.txt"
expected_manifest_sha="aec2a6f68d307b09ae196e388dc691a146fa8bdba7fcce9ca4ca41b918adfa63"
actual_manifest_sha=$(sha256sum cosign_checksums.txt | cut -d ' ' -f1)
test "$actual_manifest_sha" = "$expected_manifest_sha"

curl --fail --location --output cosign-linux-amd64 \
  "$base/cosign-linux-amd64"
grep -E '  cosign-linux-amd64$' cosign_checksums.txt | sha256sum -c -
chmod 0755 cosign-linux-amd64
./cosign-linux-amd64 version

The checksum command must print OK. Stop on any download or comparison failure; do not run the candidate binary. In the isolated test, the verified binary reported GitVersion: v3.1.3 on linux/amd64. A system package can be appropriate in a managed fleet, but record its version and package provenance in the change record instead of silently substituting it for the reviewed tool.

Generate a test key outside the deployment path

A self-managed key is a useful lab boundary because it makes the signer and verifier roles visible. Generate a disposable key pair only in an isolated directory. Cosign encrypts the private key using COSIGN_PASSWORD; use a secret manager, protected CI secret, or interactive prompt for a real signing key. Never place a real password or private key in shell history, a repository, an artifact, or a build log.

export COSIGN_PASSWORD="<retrieve-from-approved-secret-store>"
./cosign-linux-amd64 generate-key-pair --output-key-prefix release-manifest

The command writes release-manifest.key and release-manifest.pub. The .key file is signing authority and belongs in the signing system only. Restrict its permissions and back it with an organization-approved key-management process before using this pattern for a production release. The .pub file is the trust anchor that deployment environments need to verify manifests.

Cosign 3 uses a bundle as the preferred carrier for the verification material associated with a blob signature. Create the bundle at signing time and keep it beside the manifest. The signing job should run only after the manifest has been generated from the approved release inputs; editing the file afterwards invalidates the signature.

./cosign-linux-amd64 sign-blob \
  --key release-manifest.key \
  --bundle release-manifest.sigstore.json \
  release-manifest.txt

For a production signing service, replace the local key path with the organization’s approved KMS or HSM key reference and protect the job’s authorization boundary. The important operational rule remains the same: the signing job receives the final bytes, emits a bundle, and does not permit an unreviewed step to rewrite the manifest before deployment.

Make verification a hard deployment prerequisite

Put verification in a separate shell step that runs before parsing the manifest or contacting the deployment API. This prevents a future script edit from accidentally using an unverified file. Require the public key, manifest, and bundle as explicit inputs.

#!/bin/sh
set -eu

cosign_bin="${COSIGN_BIN:-./cosign-linux-amd64}"
public_key="${RELEASE_MANIFEST_PUBLIC_KEY:-./release-manifest.pub}"
manifest="${1:?usage: verify-release-manifest <manifest> <bundle>}"
bundle="${2:?usage: verify-release-manifest <manifest> <bundle>}"

"$cosign_bin" verify-blob \
  --key "$public_key" \
  --bundle "$bundle" \
  "$manifest"

printf '%s\n' "Signature accepted; manifest may now be parsed by the deployment step."

Do not add || true, suppress the exit status, or print the success message before verify-blob returns. With set -e, a failed Cosign command terminates the script. The deployment runner should make this script a required job step, not a best-effort report uploaded after an apply action.

Run the positive check first:

./verify-release-manifest release-manifest.txt release-manifest.sigstore.json

The isolated test returned:

Verified OK
Signature accepted; manifest may now be parsed by the deployment step.

Then exercise the failure path without touching a real release. Copy the manifest, change one harmless value, and verify the copy with the original bundle:

cp release-manifest.txt release-manifest-tampered.txt
printf '%s\n' 'release=changed-after-signing' >> release-manifest-tampered.txt
./verify-release-manifest release-manifest-tampered.txt release-manifest.sigstore.json

In the test, Cosign exited nonzero and reported an invalid signature while validating the ASN.1-encoded signature. The exact wording can change between releases, so key automation on the nonzero exit status, not a substring of the error. This negative check is worth retaining in CI because it proves that the gate is connected to the file actually consumed by the deployment logic.

Move from the lab key to a release policy

The self-managed-key exercise proves a byte-integrity boundary, not a complete release policy. Before connecting it to a production deployment, identify the authorized signer, the public-key distribution method, key rotation procedure, and the rollback behavior when a bundle or key is unavailable. Test failure handling in a staging environment: a missing bundle, a wrong public key, and a modified manifest must all stop before the deployment API is called.

If the release system uses keyless signing, verify the certificate identity and OIDC issuer expected by the policy rather than accepting any valid certificate. If it uses a KMS or HSM, test the service identity and least-privilege permissions separately. Keep the manifest format minimal and validate its allowed fields after signature verification; a trusted signer can still make a configuration mistake.

Cosign 3.1.3 gives the verification command a current baseline after the legacy-bundle advisory. The decisive result is not that a signature command completed once. It is that a changed manifest was rejected before downstream automation acted on it, while the original signed manifest remained verifiable with the reviewed public key and bundle.

Sources