A valid signature only proves who signed; your policy decides whether that signer may release this artifact. Verify the immutable digest with cosign verify or cosign verify-blob, passing the exact --certificate-identity (release workflow and ref) and --certificate-oidc-issuer, and pin the trigger and repository claims as well. Deploy the digest you verified, never a tag, and enforce the same identity at admission with a policy such as a Sigstore ClusterImagePolicy.
- Anyone can produce a valid keyless signature with their own identity, so verification without an identity policy proves nothing.
- For GitHub Actions the certificate identity is the workflow file and ref; if you must use a regular expression, anchor it with
^and$. - Fulcio certificates also record the trigger, source repository, ref and runner environment, and each can be pinned.
- A Sigstore bundle carries the certificate, the signature or DSSE envelope, and transparency-log or timestamp evidence, so verification can run offline.
- Verify and deploy the same digest; a tag can move between the check and the pull.
| Setting | Value | Note | Source |
|---|---|---|---|
| Signer identity | --certificate-identity with the exact workflow URI and ref | If a pattern is unavoidable, anchor --certificate-identity-regexp with ^ and $. | cosign verify reference |
| OIDC issuer for GitHub Actions | https://token.actions.githubusercontent.com | — | Sigstore: verifying signatures and attestations |
| Additional GitHub claims | --certificate-github-workflow-trigger and --certificate-github-workflow-repository | — | cosign verify reference |
| Artifact reference | A digest (@sha256:...), never a tag | — | Sigstore: verifying signatures and attestations |
| Fulcio certificate lifetime | 10 minutes | The transparency-log entry or signed timestamp shows the signature was made while the certificate was valid. | Sigstore: certificate authority overview |
| policy-controller scope | Namespaces labeled policy.sigstore.dev/include: "true"; unmatched images are rejected unless no-match-policy says otherwise | — | Sigstore: policy-controller overview |
- Run
cosign verify <image>@sha256:<digest>with the exact identity, issuer, trigger and repository flags and expect exit code 0. - Change one expected value, such as
--certificate-github-workflow-trigger, and confirm that verification fails. - Test the
ClusterImagePolicywith the policy-controller tester (-policyand-image) before applying it to the cluster. - Label a test namespace with
policy.sigstore.dev/include=trueand confirm that an unsigned image and an image signed by another workflow are both rejected. - Compare the digest in the running Pod status with the verified digest.
- Applies to
- cosign v3.1.3; the Sigstore bundle is the default format
- Sigstore policy-controller v0.15.1, ClusterImagePolicy policy.sigstore.dev/v1beta1
Artifact verification against an explicit policy. A valid signature alone does not identify the intended release or trustworthy builder.
- Part of
- Learning path: From source to trusted release (step 9 of 12). Workflows: Fix a finding, Build an AppSec program
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Separate valid signatures from authorized signers.
- Apply exact issuer, identity and digest predicates.
- Prevent a mutable tag from changing the object after verification.
Before you start
Release and platform engineers who sign artifacts and enforce signature policy at promotion or admission
- Understand digests, signatures, trust anchors and signer identity.
A cryptographic signature answers whether particular bytes were signed by a key or authenticated identity. Authorization is the additional question of whether that signer is allowed to approve this artifact for this purpose. Accepting any valid signature allows an attacker to sign their own malicious artifact and pass the test. A deployment policy therefore binds artifact digest, issuer, identity, expected repository/workflow and permitted release context. Certificate identity patterns need careful anchoring; a broad organization match may authorize unrelated repositories. Fulcio certificates record more than the subject alternative name: for GitHub Actions they include the OIDC issuer, the workflow trigger, the source repository, ref and commit, and whether the runner was GitHub-hosted, so a policy can require a push to main of the right repository rather than any run of a similarly named workflow. The SolarWinds Orion and 3CX releases carried valid signatures; the builds behind them were compromised (/domains/supply-chain/builds/case-study-solarwinds/, /domains/supply-chain/builds/case-study-3cx/).
For the fictional Finch service, policy P7 permits a single release workflow identity from the GitHub Actions OIDC issuer. It also requires a trusted verifier's successful cryptographic result and an attestation subject equal to the requested deployment digest. These are conjunctive conditions. A policy file stored beside attacker-controlled input is not independently trusted. Its revision must come from the protected verifier configuration. Revoking a producer changes future authorization decisions; it does not erase the historical fact that an old signature verified under an older policy.
Sigstore's verification interface supports explicit certificate identity and OIDC issuer constraints. For a real container, verify an immutable image reference and keep normal claim checking enabled. A successful command is evidence only for the configured trust roots and options. Disabling claim checks to make a failing artifact pass removes the binding you intended to enforce. For detached files, verify the actual file and its bundle, not a separately supplied checksum string that an attacker can replace.
The small policy below operates only on already authenticated verifier output; it is intentionally not a signature implementation. It demonstrates that a cryptographic success flag cannot replace issuer, identity or digest checks. In production, the flag must arrive through a trusted verifier interface, not from untrusted JSON. Parse the verified statement once, reject malformed or ambiguous claims, and make the policy produce a reason for each failed predicate. A source commit or builder policy can be added as distinct requirements when those claims are part of the release decision, as /domains/supply-chain/provenance/read-and-verify-slsa/ does with build provenance.
EXPECTED = {
"issuer": "https://token.actions.githubusercontent.com",
"identity": "https://github.com/finch/api/.github/workflows/release.yml@refs/heads/main",
"digest": "sha256:" + "a" * 64,
}
def decide(verified):
if verified.get("cryptographic_verification") is not True:
return "deny: signature"
for field, required in EXPECTED.items():
if verified.get(field) != required:
return "deny: " + field
return "allow"The commands below apply policy P7 with real signature verification tools. cosign verify checks the image signature, the certificate chain to the Sigstore root, the transparency-log evidence and every claim you pass; a missing or different claim fails the command. --certificate-identity must equal the certificate identity exactly, while --certificate-github-workflow-trigger and --certificate-github-workflow-repository pin claims that the identity alone does not cover. For files, cosign sign-blob --bundle writes a Sigstore bundle and cosign verify-blob --bundle checks the file against it. For artifacts attested with GitHub, gh attestation verify performs the equivalent check.
# Verify the image digest you will deploy, never a tag.
IMAGE=ghcr.io/finch/api@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
cosign verify "$IMAGE" \
--certificate-identity https://github.com/finch/api/.github/workflows/release.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-trigger push \
--certificate-github-workflow-repository finch/api
# A file signed with cosign sign-blob: check it against its Sigstore bundle.
cosign verify-blob finch-api.tar.gz --bundle finch-api.tar.gz.sigstore.json \
--certificate-identity https://github.com/finch/api/.github/workflows/release.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-trigger pushGitLab.com pipelines can sign without a stored key in the same way. A job requests an ID token with the audience sigstore through id_tokens, Cosign exchanges it for a short-lived Fulcio certificate, and the signature, certificate and transparency-log evidence go into a bundle next to the artifact. GitLab documents this for GitLab.com only; self-managed instances need their own Sigstore deployment, and the id_tokens configuration must live in the project that is built, not in an included file from another repository or a child pipeline.
The certificate identity is the instance URL, the project path, a double slash, the CI configuration path and the ref, for example https://gitlab.com/finch-group/finch//.gitlab-ci.yml@refs/heads/main, and the issuer is https://gitlab.com. Pin both, as policy P7 does for GitHub: an identity that names the ref rejects a signature made by a pipeline on another branch, and a single slash in place of the double one makes every verification fail. Build and sign in the same job, and sign the digest of what you built rather than a tag.
# .gitlab-ci.yml on GitLab.com (Cosign 2.0.1 or later)
build_and_sign:
stage: build
image: alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 # move to each new Alpine release
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
variables:
COSIGN_YES: "true"
id_tokens:
SIGSTORE_ID_TOKEN: # GitLab's OIDC token, exchanged with Fulcio for a short-lived certificate
aud: sigstore
before_script:
- apk add --no-cache cosign make # Cosign 3.0.6 from the community repository
script:
- make dist/finch.tar.gz
- cosign sign-blob dist/finch.tar.gz --bundle dist/finch.tar.gz.sigstore.json
artifacts:
paths:
- dist/
# Consumer: pin project, CI file and ref (note the double slash) and the issuer.
# cosign verify-blob dist/finch.tar.gz --bundle dist/finch.tar.gz.sigstore.json \
# --certificate-identity "https://gitlab.com/finch-group/finch//.gitlab-ci.yml@refs/heads/main" \
# --certificate-oidc-issuer "https://gitlab.com"Keyless artifact signing has three parts. Fulcio issues a certificate valid for 10 minutes that binds a public key to the OIDC identity of the signer, the CI run signs the artifact or an in-toto statement, and the signature is recorded in the Rekor transparency log or timestamped by a timestamp authority. Because the certificate expires almost at once, the log entry or signed timestamp is what shows the signature was made while it was valid. A Sigstore bundle packages the certificate, the signature or DSSE envelope and that log or timestamp evidence, so a verifier can work offline with a pinned trusted root.
Attestations use DSSE envelopes, whose signature covers the payload type as well as the payload, so a signed provenance statement cannot be replayed as a different kind of document. A transparency log entry does not make an artifact trustworthy; it makes the signing event publicly visible, so you can monitor the log for signatures made with your identities that you did not expect.
A promotion job can be bypassed by anyone who can create Pods directly, so enforce the same identity where workloads start. Sigstore policy-controller checks images in namespaces labeled policy.sigstore.dev/include: "true" against ClusterImagePolicy resources and rejects images that match no policy unless no-match-policy is changed. The policy below accepts Finch API images only with a keyless signature from the release workflow on main. Kyverno verifyImages rules provide the same control with optional checks of additional certificate extensions. Admission policies need an availability plan: if the policy service is unavailable, decide in advance whether new Pods fail closed.
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: finch-api-release
spec:
images:
- glob: "ghcr.io/finch/api**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subject: https://github.com/finch/api/.github/workflows/release.yml@refs/heads/main
ctlog:
url: https://rekor.sigstore.devThe promotion job should resolve a digest, verify that exact object, and deploy that digest. Verifying a tag and later pulling the tag reintroduces substitution if the tag changes between those operations. Authorization should be checked at the place that grants deployment, not only in a contributor-controlled test job. Where artifacts move between registries, verify content identity across the copy and preserve the statement or bundle needed by the target verifier.
Define failure behavior explicitly. A missing bundle, unknown identity or unreachable verifier is not a successful policy evaluation. Holding a release during an outage has availability cost, so plan an emergency authorization path in advance. It should record the exact digest, business decision maker, alternative evidence, expiry and follow-up; it must not become an undocumented global bypass. Preserve decision ID, time, full digest, verified identity, issuer, policy revision, verifier version and evidence location. Those fields let incident responders determine which release policy actually admitted an artifact.
A good positive test shows the intended release can pass. Near-miss tests show why others cannot: valid signature from another workflow, correct identity under another issuer, correct signer for different bytes, and missing verification. Keep the expected reason specific. If every failure says only bad artifact, operators may disable the check to diagnose it. Failure categories should expose enough context to repair configuration without logging credentials or treating untrusted text as terminal commands.
The fixture below allows evaluation without a registry, certificate issuance or credentials. All cryptographic results are stipulated training inputs. Running the small function on the corresponding records demonstrates only policy logic. Real integration tests must additionally verify certificate chains, trusted timestamps or log evidence as applicable to the chosen Sigstore mode, and actual image binding. The lab at /labs/artifact-policy/ and the release-verification project on this page provide related practice. A policy-logic unit test is useful evidence about comparisons; it cannot certify the whole supply chain.
Guided practice
Evaluate four authenticated-result records using P7. Use full digest A = sha256 followed by 64 a characters, and B = sha256 followed by 64 b characters.
Supplied scenario material
P7 expected issuer = https://token.actions.githubusercontent.com
P7 expected identity = https://github.com/finch/api/.github/workflows/release.yml@refs/heads/main
P7 expected digest A = sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
R1: verification true, expected issuer, expected identity, digest A.
R2: verification true, expected issuer, identity https://github.com/finch/tools/.github/workflows/release.yml@refs/heads/main, digest A.
R3: verification true, expected issuer, expected identity, digest B.
R4: verification false, expected issuer, expected identity, digest A.
After R1 verifies, tag stable resolves to B. Deployment request still proposes stable.
Fictional policy owner: platform engineer Ren; policy revision P7 is protected.1. Separate authentication and authority
Explain why R2 is a useful negative test although its verification flag is true.
Expected result and reasoning
R2 is authenticated as the tools workflow, not authorized as the api release producer. An organization-wide identity wildcard could admit it incorrectly.
2. Evaluate the policy
Compute the exact result string for each record.
Result string for each record under P7
Expected result and reasoning
R1:
allow. R2:deny: identity. R3:deny: digest. R4:deny: signature. A missing field also denies because it cannot equal the required value.3. Check the deployment selector
Decide whether deploying
stableafter R1 is authorized.Expected result and reasoning
No.
stablenow resolves to B. The verifier authorized only A; deploy the immutable A reference or perform a new approval and verification for B.4. Define a trusted interface
Identify which fixture field would be dangerous to accept directly from uploaded JSON.
Expected result and reasoning
cryptographic_verificationmust be set by the protected verifier, never supplied as authoritative by the artifact producer. Issuer and identity likewise must come from authenticated certificate/statement processing.5. Record the result and limit
Write decision SIG-1 including policy and residual uncertainty.
Expected result and reasoning
SIG-1: Ren, P7, R1, digest A, expected issuer/identity, allow digest A only;
stablerequest rejected. Residual uncertainty: this exercise does not prove a real signature verified or that A contains vulnerability-free code.
All verification flags are stipulated. The Python is a policy demonstration, not a cryptographic verifier, and must never be connected directly to unauthenticated producer input.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Repair the code
Signed artifact provenance lab: Reject a release from the wrong producer
Run a local project
Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.
Build and verify a signed software release
Build a real executable, generate an SBOM and DSSE-signed in-toto provenance, then repair the consuming gate so a valid signature cannot approve the wrong source, builder or release.
In the extracted folder, run:
node build.mjs
node --test exercise.test.mjs20 regression tests · The starter intentionally fails some cases.
Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.
Actual Ed25519 signatures over the DSSE pre-authentication encoding, checked against the DSSE test vector, and executable output; there is no certificate or transparency log, the local producer and consumer are not isolated, and this does not establish a SLSA level.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- Sigstore: verifying signatures and attestations
- SLSA 1.2 specification. Approved, November 2025.
- cosign verify reference
- cosign verify-blob reference
- Fulcio certificate extensions (OID reference)
- Sigstore: certificate authority overview
- Sigstore: policy-controller overview
- GitHub CLI manual: gh attestation verify
- DSSE envelope specification
- GitLab Docs: Use Sigstore for keyless signing and verification
- OWASP Top 10 CI/CD Security Risks: CICD-SEC-9 Improper Artifact Integrity Validation. v1.0 (2022).
Reference information
- SDLC phase
- Release
- CAPEC attack patterns