Model Signing and Provenance Lab
A signed model is only as good as the checks you run before loading it. This lab gives you 10 model releases to verify: the files and their hashes, the Sigstore signature, the SLSA build provenance and the AIBOM entry. Trust or reject each one, name the reason, and get the command that would have caught it.
Who this is for: AppSec and AI-security engineers who own the deploy gate for models, and SOC analysts who need to confirm that the model running is the one that was signed. Every release is from Northwind Health, a fictional company.
What this lab is
- 10 fictional model releases to verify before they reach production. 8 hide a planted problem; 2 are clean.
- Each release gives you what you would really have: the files and their SHA-256, the model.sig Sigstore bundle from OpenSSF model signing, SLSA v1 build provenance, and the CycloneDX ML-BOM entry that links them.
- You trust or reject each release and name the reason: a tampered file, an unexpected signer, a missing log entry, a file the signature leaves out, an unsigned pickle, an unapproved builder, self-signed provenance, or a base model that differs from the BOM.
- Every hash is a real SHA-256 and every signature payload has the exact model_signing 1.1.1 format, reproduced with the real tool. Two releases pass every standard verify command and are still wrong; a third got through only because of one flag in the deploy job.
- The debrief scores each of the seven checks, gives the command that catches each problem, and exports a verification report as PDF, CSV or JPG, plus an illustrative verify script.
The 10 releases
Rookie gives you four releases, Pro seven, Elite all ten. Each is a version of Triage Assistant, Northwind’s fictional support model, fine-tuned from Mistral-7B-Instruct-v0.3. The same release policy applies to all of them: one signer identity, one OIDC issuer, a builder allow-list and the base model revision the AIBOM pins. The set-up goes one step past the worked signing example: the release workflow still signs the model files, but fine-tuning runs in a separate reusable workflow that records the base model revision and dataset hashes in its provenance, so that provenance describes training rather than only packaging.
- The 2.3.0 release, from the registry (v2.3.0, Rookie): The current production release, pulled again from Northwind’s model registry for a new region. The deploy job wants a yes or no before vLLM loads it.
- The same 2.3.0, from a partner mirror (v2.3.0, Rookie): A partner’s team could not reach the registry and fetched 2.3.0 from their own mirror. They sent the directory with model.sig and the provenance file. The signature and provenance are byte-for-byte the ones from the registry.
- 2.4.0: the quick retrain (v2.4.0, Pro): A data-drift fix that had to ship this week. The release workflow signed it as usual. The training ran somewhere faster.
- 2.3.1: one more file (v2.3.1, Rookie): A patch release. The deploy job’s verify step passed. It runs model_signing verify with --ignore_unsigned_files "because the training job sometimes leaves logs behind".
- 2.5.0: verification passed (v2.5.0, Elite): Verification passed with the standard command, no extra flags. A reviewer asks you to sign off anyway.
- 2.4.1: re-run and re-built (v2.4.1, Pro): The first build of 2.4.1 timed out and was re-run. The platform team also moved the fine-tuning workflow to a new tag last week.
- 2.3.2: signed by hand (v2.3.2, Rookie): The release workflow failed on a runner outage. The release engineer “signed it the same way, just locally” to keep the date.
- 2.5.1: provenance attached (v2.5.1, Elite): The fine-tuning job ran on a laptop GPU during an outage. The engineer wrote the provenance file from the build log so the release “has everything the policy asks for”, then signed it.
- 2.4.2: same base, apparently (v2.4.2, Pro): A routine refresh on new tickets. Signature and provenance both verify with the standard commands.
- 2.5.2: the new signing set-up (v2.5.2, Elite): Last month the release job was moved to a new signing configuration. This is the first release from it.
The seven checks
Four checks are about the signature, three about the provenance. The standard verify commands cover most of them. The rest are reading and comparing: two of the lab’s releases pass every verify command and still fail one of these.
| Check | Where to look | Passes when | How (illustrative) |
|---|---|---|---|
| 1. Every file matches the signed manifest | model.sig → dsseEnvelope.payload → predicate.resources[] (name, algorithm, digest) | Each file on disk hashes to its resources[] digest. No file is missing and none is extra. | model_signing verify re-hashes every file and fails on "Hash mismatch", "Extra files found" or "Missing files". |
| 2. The signature covers everything that will load | predicate.serialization.ignore_paths; the flags your deploy job passes to verify | ignore_paths holds only model.sig and git files. The deploy gate never runs with --ignore_unsigned_files. | jq -r '.dsseEnvelope.payload' model.sig | base64 -d | jq '.predicate.serialization.ignore_paths' |
| 3. Signed by the expected identity and OIDC issuer | verificationMaterial.certificate: SAN and Fulcio extension 1.3.6.1.4.1.57264.1.8 (Issuer V2) | SAN is the release workflow at this version's tag; issuer is https://token.actions.githubusercontent.com. | model_signing verify sigstore … --identity <release workflow URL> --identity_provider https://token.actions.githubusercontent.com |
| 4. The signing event is in the transparency log | verificationMaterial.tlogEntries[] (logIndex, integratedTime, inclusionProof) | A Rekor entry with an inclusion proof, integrated inside the certificate's 10-minute validity. | model_signing verify (Sigstore mode) rejects a bundle with no log entry or timestamp for the signature. |
| 5. The provenance is signed by the build platform | The attestation bundle's certificate SAN and issuer, not the statement text | The attestation is signed by the builder workflow's own CI identity, the same URL as runDetails.builder.id. | gh attestation verify <file> --bundle <provenance>.intoto.jsonl --repo northwind-health-example/triage-assistant --signer-workflow northwind-health-example/ml-build/.github/workflows/finetune.yml |
| 6. builder.id is on the allow-list | predicate.runDetails.builder.id; internalParameters.github.runner_environment | An allow-listed builder at a pinned tag, on a GitHub-hosted runner. | gh attestation verify … --signer-workflow <allowed builder> --deny-self-hosted-runners |
| 7. The provenance agrees with the AIBOM | resolvedDependencies[] base model digest.gitCommit vs pedigree.ancestors[].purl; subject digests vs component.hashes[] | The base model commit the build fetched is the revision the BOM pins, and the weights hash is the same in all three documents. | jq over the decoded statement and the BOM (no verifier checks this for you) |
The verify commands
Illustrative, with the fictional Northwind values. Flag names are those of model_signing 1.1.1, the release on PyPI as of 29 October 2026; its main branch renames the underscore flags to hyphens (--identity-provider, --ignore-unsigned-files) for the next release, so check model_signing verify --help for the version you install.
Model signature: files, signer, issuer and log entry
model_signing verify sigstore triage-assistant-2.3.0 \
--signature model.sig \
--identity "https://github.com/northwind-health-example/triage-assistant/.github/workflows/release-model.yml@refs/tags/v2.3.0" \
--identity_provider "https://token.actions.githubusercontent.com"What the signature covers, and whether it was logged
jq -r '.dsseEnvelope.payload' model.sig | base64 -d \
| jq '{signed: [.predicate.resources[].name], ignore_paths: .predicate.serialization.ignore_paths}'
jq '.verificationMaterial.tlogEntries | length' model.sig # 0 = never loggedProvenance: signed by the allow-listed builder
gh attestation verify triage-assistant-2.3.0/model.safetensors \
--bundle triage-assistant-2.3.0.intoto.jsonl \
--repo northwind-health-example/triage-assistant \
--signer-workflow northwind-health-example/ml-build/.github/workflows/finetune.yml \
--source-ref refs/tags/v2.3.0 \
--deny-self-hosted-runnersFor builds made with the slsa-github-generator builders, slsa-verifier does the same job. Its README says the project is no longer actively maintained and recommends GitHub artifact attestations with gh attestation verify for GitHub Actions.
# Only for builds made with slsa-github-generator builders:
slsa-verifier verify-artifact triage-assistant-2.3.0/model.safetensors \
--provenance-path triage-assistant-2.3.0.intoto.jsonl \
--source-uri github.com/northwind-health-example/triage-assistant \
--source-tag v2.3.0 \
--builder-id <the generator workflow you allow>Provenance against the AIBOM: no tool does this for you
jq -r '.dsseEnvelope.payload' triage-assistant-2.3.0.intoto.jsonl | base64 -d \
| jq -r '.predicate.buildDefinition.resolvedDependencies[] | select(.name=="base-model") | .digest.gitCommit'
jq -r '.components[] | select(.type=="machine-learning-model" and .name=="triage-assistant") | .pedigree.ancestors[].purl' bom.cdx.json
# The commit after @ in the purl must equal the gitCommit above.The full walk-through, from signing the model in CI to linking the bundle and the attestation from the ML-BOM, is in Sign a model and attach provenance: a worked example. Who is allowed to sign or attest what, and why self-attestation is weaker, is in Who can attest to an AI model, and to what. Where the links sit in the BOM is covered in the AIBOM security and runtime section and AIBOM identity, provenance and lineage.
How scoring works
- 2 points for the right call. On a release that should be rejected, 1 more for naming the planted problem.
- A clean release you trust scores the full 3 points. Rejecting it scores nothing: a gate that blocks good releases gets switched off.
- Your percentage feeds the same S to D grade as every PlayCISO lab, with a small bonus for speed. 75% clears the lab.
- The report shows, for each check, how many of the releases that fail it you rejected.
The debrief sends you on: missed provenance problems to the AI Model Attestation Planner, missed signature problems to the worked example, and everyone to the AI Model Intake and Approval Checklist, where these checks become a gate. Everything else in the series is on the AIBOM Library hub.
How the values were made
Each file’s SHA-256 is the hash of a fixed placeholder string. The signature payloads use the model_signing 1.1.1 format exactly: an in-toto Statement v1 with predicate type https://model_signing/signature/v1.0, the per-file digests in predicate.resources, and a subject digest computed over them. They were reproduced with the real model_signing 1.1.1 CLI on files holding those strings, and the hash mismatch, the extra unsigned file and the ignored tokenizer were replayed with it. The provenance uses the SLSA v1.0 field names; its buildType is a fictional Northwind one, which SLSA allows. Every ML-BOM entry validates against the CycloneDX 1.7 JSON schema. Certificates, log indexes and run ids are summaries, not real Sigstore data.
Sources
Every format, field name and command was checked against these on 29 October 2026.
- sigstore/model-transparency: OpenSSF model signing (model_signing CLI and library)
- model_signing payload format: in-toto statement, predicate resources and serialization (signing.py)
- model-signing on PyPI (1.1.1, the release the commands use)
- Sigstore bundle: Bundle and VerificationMaterial (protobuf-specs)
- Rekor TransparencyLogEntry (protobuf-specs)
- Fulcio certificate extensions (OID 1.3.6.1.4.1.57264.1.8, Issuer V2)
- Fulcio: short-lived certificates valid for 10 minutes
- SLSA Provenance v1.0
- in-toto Attestation Framework: Statement v1
- GitHub Actions workflow buildType v1 (builder.id is the reusable workflow)
- gh attestation verify (GitHub CLI manual)
- slsa-verifier (README: supported builders and maintenance status)
- CycloneDX 1.7 JSON reference (hashes, pedigree, externalReferences)
- Python pickle documentation: security warning
Frequently asked questions
How do I verify a signed AI model?
Run model_signing verify on the model directory with the signature and the identity and OIDC issuer you expect: model_signing verify sigstore <dir> --signature model.sig --identity <signer> --identity_provider <issuer> (version 1.1.1 flag names). It checks the Sigstore signature, the certificate identity and the transparency log entry, then re-hashes every file against the signed manifest. Then check what the signature covers (ignore_paths), and verify the build provenance separately.
What is the difference between a model signature and SLSA provenance?
The signature proves who published these exact files: the release identity signed a manifest of file hashes. SLSA provenance describes how the files were built: which builder ran, from which source commit, with which inputs such as the base model revision and the datasets. A release can carry a perfect signature over weights trained on an unapproved builder, so you need both.
Can a model pass signature verification and still be unsafe to load?
Yes. model_signing applies the ignore_paths recorded in the signature again at verification, so a file the signer chose to ignore, such as a tokenizer, is unsigned but verify still passes. A deploy step that adds --ignore_unsigned_files lets an extra file, such as a pickle written after signing, through. Read the signed resources and ignore_paths, and never use that flag in a deploy gate.
How do I verify SLSA provenance for a model built on GitHub Actions?
For provenance signed in GitHub Actions, gh attestation verify checks the attestation bundle, the source repository and the workflow that signed it: gh attestation verify <file> --bundle <provenance> --repo <owner/repo> --signer-workflow <builder workflow> --deny-self-hosted-runners. slsa-verifier covers builds made with the slsa-github-generator builders; its README says it is no longer actively maintained and points GitHub users to artifact attestations. Neither tool compares the provenance with your AIBOM, so do that check yourself.
Where does the signature and provenance go in an AIBOM?
Link them from the model component. In CycloneDX, externalReferences type digital-signature points at the Sigstore bundle and type attestation at the provenance, next to the component hashes and the pedigree that names the base model. The BOM only points at the evidence: the links are claims until someone verifies them.
Are the releases in the lab real?
No. Northwind Health, its GitHub organisation, people, hosts, commits, run ids and Rekor log indexes are invented. The SHA-256 values are real hashes of fixed placeholder strings, so the files, signature, provenance and BOM agree exactly. The base model, mistralai/Mistral-7B-Instruct-v0.3 at revision c170c708, is a real public model.
More free security tools
The tools people use most on PlayCISO.
Catch injected instructions hiding in content your AI agent should only read.
Score your readiness against the first international standard for AI security threats.
Critical CVEs across 21 major vendors, month by month — and what the 2026 surge means.
Build an ordered ruleset, replay labelled packets and catch shadowed and any-to-any rules.
Self-assess all six Functions, from Govern to Recover, and see your weakest one.
Search 172 threat actors across Microsoft, CrowdStrike, Mandiant and community names.