---
key: "artifact-signature-policy"
title: "Verify artifact signatures against an explicit trust policy"
url: "https://softwaresecurity.com/domains/supply-chain/provenance/artifact-signature-policy/"
api: "https://softwaresecurity.com/api/v1/articles/artifact-signature-policy.json"
domain: "supply-chain"
topic: "Software supply chain"
type: "Guide"
identifiers: ["CWE-347","A03:2025","A08:2025","CAPEC-206"]
tags: ["Build integrity and provenance","Signing and software updates","Kubernetes and containers"]
stages: ["release"]
roles: ["developer","platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Verify artifact signatures against an explicit trust policy

Require the expected artifact, signer and release context before allowing a signed package or image to proceed.

Canonical URL: https://softwaresecurity.com/domains/supply-chain/provenance/artifact-signature-policy/

## In short

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.

## Recommended defaults

| 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 |

## Verify it

1. Run `cosign verify <image>@sha256:<digest>` with the exact identity, issuer, trigger and repository flags and expect exit code 0.
2. Change one expected value, such as `--certificate-github-workflow-trigger`, and confirm that verification fails.
3. Test the `ClusterImagePolicy` with the policy-controller tester (`-policy` and `-image`) before applying it to the cluster.
4. Label a test namespace with `policy.sigstore.dev/include=true` and confirm that an unsigned image and an image signed by another workflow are both rejected.
5. Compare the digest in the running Pod status with the verified digest.

## 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.

Audience: Release and platform engineers who sign artifacts and enforce signature policy at promotion or admission

Before you start:

- Understand digests, signatures, trust anchors and signer identity.

Time: About 31 minutes: 8 to read and 23 for the exercise

## Write down what a valid signature must authorize

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.

## Authenticate first, then compare exact subject and identity

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.

```python
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"
```

## Verify with cosign: identity, issuer and claims

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.

```bash
# 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 push
```

## Sign and verify keylessly on GitLab

GitLab.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.

```yaml
# .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"

```

## Know what the bundle and the transparency log prove

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.

## Enforce the policy at admission

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.

```yaml
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.dev
```

## Make verification and deployment one identity-preserving operation

The 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.

## Use near misses to prove that the rule is discriminating

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 material

```text
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.

Worked answer: 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.

Worked answer: 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 `stable` after R1 is authorized.

Worked answer: No. `stable` now 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.

Worked answer: `cryptographic_verification` must 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.

Worked answer: SIG-1: Ren, P7, R1, digest A, expected issuer/identity, allow digest A only; `stable` request rejected. Residual uncertainty: this exercise does not prove a real signature verified or that A contains vulnerability-free code.

Deliverable: Four exact policy outcomes and an identity-preserving promotion decision.

Scope: 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.

## Executable practice

- [Signed artifact provenance lab: Reject a release from the wrong producer](https://softwaresecurity.com/labs/artifact-policy/)
- [Build and verify a signed software release: project ZIP](https://softwaresecurity.com/downloads/projects/release-verification-bd914fcbede61c32.zip) — Node.js 22+ · no dependencies

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [Sigstore: verifying signatures and attestations](https://docs.sigstore.dev/cosign/verifying/verify/)
- [SLSA 1.2 specification](https://slsa.dev/spec/v1.2/). Approved, November 2025.
- [cosign verify reference](https://github.com/sigstore/cosign/blob/main/doc/cosign_verify.md)
- [cosign verify-blob reference](https://github.com/sigstore/cosign/blob/main/doc/cosign_verify-blob.md)
- [Fulcio certificate extensions (OID reference)](https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md)
- [Sigstore: certificate authority overview](https://docs.sigstore.dev/certificate_authority/overview/)
- [Sigstore: policy-controller overview](https://docs.sigstore.dev/policy-controller/overview/)
- [GitHub CLI manual: gh attestation verify](https://cli.github.com/manual/gh_attestation_verify)
- [DSSE envelope specification](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md)
- [GitLab Docs: Use Sigstore for keyless signing and verification](https://docs.gitlab.com/ci/yaml/signing_examples/)
- [OWASP Top 10 CI/CD Security Risks: CICD-SEC-9 Improper Artifact Integrity Validation](https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-09-Improper-Artifact-Integrity-Validation). v1.0 (2022).