# Sign an AI Model and Attest Its Provenance > A worked example: sign model files with the OpenSSF model_signing CLI, see real verify failures, add SLSA provenance in CI and link both from the AIBOM. Source: https://playciso.com/blog/sign-ai-model-provenance-attestation-example · Published: 2026-10-29 · Publisher: PlayCISO (https://playciso.com) --- To sign an AI model, you hash every file in the model folder and sign that list. The OpenSSF `model_signing` CLI does both and writes a Sigstore bundle, `model.sig`. Add SLSA build provenance from your CI platform, verify both before the model loads, and link both from the AIBOM’s `externalReferences`. Below, Northwind Health’s Triage Assistant goes through each step, including what a failed verification looks like. **Who this is for:** AppSec and AI-security engineers who own the model release pipeline, and SOC analysts who need to check that what is running is what was released. **What we ran and what we did not.** We installed `model_signing` 1.1.1 from PyPI in a clean Python 3.13 environment and ran the key-based sign and verify commands on three placeholder files. Every “Signing succeeded”, “Verification succeeded” and “Verification failed” line below is real output from that run. The Sigstore keyless signing, the GitHub Actions workflow and the `gh attestation verify` commands need a real OIDC identity and repository, so they are marked **illustrative**: the syntax is checked against the tools’ own documentation and help, but we did not run them. ## What you will produce ArtefactMade byFormatLinked from the AIBOM as `model.sig`model_signing 1.1.1Sigstore bundle (`application/vnd.dev.sigstore.bundle.v0.3+json`) holding a DSSE envelope with an in-toto statement, predicate `https://model_signing/signature/v1.0``externalReferences` type `digital-signature` `triage-assistant-2.3.0.intoto.jsonl``actions/attest@v4`Sigstore bundle with an in-toto statement, predicate `https://slsa.dev/provenance/v1`type `attestation` The AIBOMNorthwind’s release jobCycloneDX 1.7 ML-BOM, itself signedPublished with the release ## Step 1: install the CLI, and two traps `python3 -m venv .venv && . .venv/bin/activate pip install model-signing==1.1.1 model_signing --version # model_signing, version 1.1.1` **Trap 1: a missing dependency.** In our clean environment the CLI did not start until we added one package. The first run failed with `ModuleNotFoundError: No module named 'OpenSSL'`; `pip install pyopenssl` fixed it. **Trap 2: flag spelling.** Version 1.1.1, released on 10 October 2025 and still the latest on PyPI, spells multi-word flags with underscores: `--private_key`, `--public_key`, `--identity_provider`, `--use_ambient_credentials`. The README on the project’s main branch already uses hyphens for a release that is not out yet. With 1.1.1, the README’s spelling fails: `Error: No such option '--public-key'. Did you mean '--public_key'?` Pin the version you install, and check `model_signing verify sigstore --help` before you copy a command. ## Step 2: sign the model folder For a run you can repeat offline, sign with a local P-256 key. In CI you will sign keyless with Sigstore instead (step 4); the file layout and the verification behaviour are the same. `openssl ecparam -name prime256v1 -genkey -noout -out key.priv openssl ec -in key.priv -pubout -out key.pub model_signing sign key triage-assistant --private_key key.priv --signature model.sig # Signing succeeded` Our `triage-assistant/` folder held three placeholder files: `model.safetensors`, `config.json` and `tokenizer.json`. A real Northwind release would hold the merged weights. The digests below are of our placeholder bytes, not of a model. ### What is inside model.sig The file is a Sigstore bundle with three keys: `mediaType` (`application/vnd.dev.sigstore.bundle.v0.3+json`), `verificationMaterial` and `dsseEnvelope`. The envelope’s payload type is `application/vnd.in-toto+json`. Decoded, the payload is the statement that was signed: `{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "triage-assistant", "digest": { "sha256": "4350cc606319ffb504ff4478854632e8c668bdb2ff8bc0de71570e58be053105" } } ], "predicateType": "https://model_signing/signature/v1.0", "predicate": { "serialization": { "method": "files", "hash_type": "sha256", "ignore_paths": [".gitattributes", ".git", ".github", ".gitignore"], "allow_symlinks": false }, "resources": [ { "algorithm": "sha256", "name": "config.json", "digest": "3dfc3eaf456e1e5c96160026f7291f77292db3f165356bc5539c0f0174b17428" }, { "algorithm": "sha256", "name": "model.safetensors", "digest": "c2e224cdf0193cde5ce821bdd728331643e614e6bd8987d2f01f8079b122976b" }, { "algorithm": "sha256", "name": "tokenizer.json", "digest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" } ] } }` Three things to notice: - The subject is the folder name and one digest. The per-file SHA-256 digests are in predicate.resources. (The project README describes the subjects as file and digest pairs; the 1.1.1 output we got puts them in resources.) - Git files are ignored by default, and the signature file itself is left out, so model.sig can live inside the folder it signs. We checked: signing with --signature triage-assistant/model.sig and verifying the same way succeeds. - The model.safetensors digest here should equal the model’s hashes value in the AIBOM. That comparison ties the signature to the AIBOM. (Northwind’s published AIBOM uses a placeholder hash, so it will not match our placeholder file.) ## Step 3: verify, and what failure looks like `model_signing verify key triage-assistant --public_key key.pub --signature model.sig # Verification succeeded (exit status 0)` Then we broke it on purpose, one change at a time, and restored the folder between tests: What we changedOutput (real; long digests shortened with …)What it tells you Appended one byte to model.safetensors`Verification failed with error: Signature mismatch: ['Hash mismatch for \'model.safetensors\': Expected \'Digest(algorithm=\'sha256\', digest_value=b\'\\xc2\\xe2$…\')\', Actual \'Digest(algorithm=\'sha256\', digest_value=b\'aH\\xfb1…\')\'']`The weights were changed after signing Dropped adapter_model.bin into the folder`Verification failed with error: Signature mismatch: ["Extra files found in model 'triage-assistant': adapter_model.bin"]`Something was added: here, a pickle file a loader might fall back to Deleted tokenizer.json`Verification failed with error: Signature mismatch: ["Missing files in model 'triage-assistant': tokenizer.json"]`The release is incomplete Verified with a different public key`Verification failed with error: Key mismatch: The public key hash in the signature's verification material does not match the provided public key.`Someone else signed it, or you have the wrong key Used the README’s --public-key spelling`Error: No such option '--public-key'. Did you mean '--public_key'?`Flag spelling of a newer, unreleased version (exit status 2) Every failure exits with status 1 (2 for the usage error), so a script can stop on it. The extra-file case matters most. A `.bin` file next to safetensors weights is exactly what a loader that falls back to pickle would pick up, as the [AIBOM security and runtime section](/blog/aibom-security-runtime-section) explains for vLLM. Version 1.1.1 also has `--ignore_unsigned_files`; leave it off in a deploy gate, or this check disappears. ## Step 4: sign in CI with Sigstore, and add SLSA provenance In the release pipeline, nobody holds a key. The job signs with its own workload identity, and the same job asks GitHub for SLSA build provenance. This workflow is **illustrative**; the permissions and inputs follow the `actions/attest` README and the `model_signing` 1.1.1 help. `# .github/workflows/release-model.yml (illustrative: not run) name: release-model on: push: tags: ['v*'] permissions: contents: read id-token: write # OIDC token: Sigstore signing and the attestation attestations: write # store the provenance with GitHub artifact-metadata: write # required by actions/attest v4 jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' # Northwind merges the LoRA adapter into the base weights here and writes # model.safetensors, config.json and the tokenizer files to triage-assistant/. - name: Build release folder run: ./scripts/build-release.sh triage-assistant - name: Sign the model files (OpenSSF model signing, Sigstore keyless) run: | pip install model-signing==1.1.1 pyopenssl model_signing sign sigstore triage-assistant \ --signature triage-assistant/model.sig \ --use_ambient_credentials - name: SLSA build provenance id: provenance uses: actions/attest@v4 with: subject-path: 'triage-assistant/*' - name: Keep the provenance next to the release run: cp "${{ steps.provenance.outputs.bundle-path }}" triage-assistant-2.3.0.intoto.jsonl` - --use_ambient_credentials tells model_signing 1.1.1 to use the workflow’s OIDC token instead of opening a browser login. The CLI flag is off by default. - The signer identity becomes the workflow file at the ref that ran, in the form the project documents for GitHub Actions: https://github.com/northwind-health-example/triage-assistant/.github/workflows/release-model.yml@refs/tags/v2.3.0, with identity provider https://token.actions.githubusercontent.com. A verifier needs both. - Privacy. Keyless signing against the public Sigstore instance records the signing certificate, which names the repository and workflow, in the public Rekor transparency log. For a private repository, that may publish names you would rather keep. The alternatives are key or certificate signing, or a private Sigstore instance with --trust_config. - actions/attest@v4 with only subject-path generates SLSA build provenance, signs it and stores it with GitHub. The glob makes every file in the folder a subject, model.sig included. The bundle-path output is the Sigstore bundle; we copy it to triage-assistant-2.3.0.intoto.jsonl, the name Northwind’s AIBOM links to. GitHub says this is SLSA Build Level 2 on its own, and Build Level 3 when the build runs in a reusable workflow that isolates it. - What the provenance covers. This job merges and packages the weights. The provenance says this workflow, at this commit, produced these digests. It does not cover fine-tuning on another machine. The guide to who can attest to what explains that boundary. ## Step 5: verify both before the model loads Verification happens where the model is deployed, not only in CI. These commands are **illustrative**: they need the real signature and attestation from step 4. `# Illustrative: needs a real Sigstore signature from the workflow above. model_signing verify sigstore triage-assistant \ --signature triage-assistant/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" gh attestation verify triage-assistant/model.safetensors \ --repo northwind-health-example/triage-assistant \ --signer-workflow northwind-health-example/triage-assistant/.github/workflows/release-model.yml \ --bundle triage-assistant-2.3.0.intoto.jsonl` - model_signing verify sigstore checks the bundle against Sigstore’s trust root, checks that the certificate names the expected identity and provider, then re-hashes every file. If the identity or provider does not match, verification fails and the command exits non-zero, just as the key-based failures above did. - gh attestation verify checks the predicate type https://slsa.dev/provenance/v1 by default. --signer-workflow pins which workflow must have produced it. --bundle verifies from the file published with the release, without asking GitHub’s API for it. - We did not run either, so we do not quote their failure messages. Then make it a gate. The same check in the container entrypoint, so vLLM never starts on unverified weights: `#!/bin/sh # Illustrative deploy gate: start vLLM only if the signature verifies. set -eu model_signing verify sigstore /models/triage-assistant \ --signature /models/triage-assistant/model.sig \ --identity "$EXPECTED_SIGNER" \ --identity_provider "https://token.actions.githubusercontent.com" exec python -m vllm.entrypoints.openai.api_server \ --model /models/triage-assistant --load-format safetensors` The `--load-format safetensors` setting (vLLM 0.6.3 accepts it) closes the fallback the extra-file test showed. ## Step 6: link the signature and provenance from the AIBOM Northwind’s CycloneDX 1.7 AIBOM already links both from the model component: `[ { "type": "model-card", "url": "https://models.northwind-health.example/northwind/triage-assistant/2.3.0/MODEL_CARD.md" }, { "type": "digital-signature", "url": "https://models.northwind-health.example/northwind/triage-assistant/2.3.0/model.sig", "comment": "Sigstore bundle from OpenSSF model signing, produced in CI." }, { "type": "attestation", "url": "https://ci.northwind-health.example/attestations/triage-assistant-2.3.0.intoto.jsonl", "comment": "SLSA build provenance as an in-toto attestation." }, { "type": "distribution", "url": "https://models.northwind-health.example/northwind/triage-assistant/2.3.0/model.safetensors" } ]` _Excerpt: the model component’s externalReferences, exactly as in the full file (519 lines pretty-printed), which is in the Pro [AIBOM Template Library](/tools/aibom-template-library)._ One improvement: a Sigstore signature cannot be verified without the expected identity and identity provider, so write them where the verifier will look. The same two references with the verification inputs in the comment: `[ { "type": "digital-signature", "url": "https://models.northwind-health.example/northwind/triage-assistant/2.3.0/model.sig", "comment": "OMS signature (Sigstore bundle) made with model_signing 1.1.1 in CI. Verify with identity https://github.com/northwind-health-example/triage-assistant/.github/workflows/release-model.yml@refs/tags/v2.3.0 and identity provider https://token.actions.githubusercontent.com." }, { "type": "attestation", "url": "https://ci.northwind-health.example/attestations/triage-assistant-2.3.0.intoto.jsonl", "comment": "SLSA provenance v1 from GitHub artifact attestations (Build L2), signer workflow northwind-health-example/triage-assistant/.github/workflows/release-model.yml. Covers the release job, not fine-tuning." } ]` _We validated this against the CycloneDX 1.7 JSON schema inside the full Northwind BOM._ - SPDX 3.0.1 has no dedicated reference type, so Northwind’s SPDX file uses externalRefType: other with contentType: application/vnd.dev.sigstore.bundle.v0.3+json. That is the same media type our real bundle declared. The SPDX 3.0 AI profile example shows the full element. - Sign the AIBOM too. Otherwise anyone who can edit it can point it at a different signature. - Check it with a linter. The AIBOM Validator (Pro) flags a model component without these links. Every field of the file is explained in the CycloneDX ML-BOM worked example. ## Northwind’s release checks CheckHowFails when Model files match the signature`model_signing verify sigstore …`A file changed, appeared or disappeared, or the signer identity differs Signer is the release workflow`--identity` and `--identity_provider`Someone else signed, for example a developer’s personal account Files came from the attested build`gh attestation verify … --signer-workflow …`No SLSA provenance from that workflow covers this digest AIBOM matches the filesCompare the `model.safetensors` digest in the signature with the model `hashes` in the AIBOMThe AIBOM describes different weights Loader cannot fall back to pickle`--load-format safetensors`A swapped folder with a .bin file would still load _Fictional example. Northwind Health, Triage Assistant and its datasets, services, scores and URLs are invented for illustration; the base model and open-source libraries are real._ The example GitHub organisation `northwind-health-example` and its workflow are part of the fictional example. ## What signing and provenance do not tell you A valid signature and valid provenance say the files are the ones a known workflow produced. They say nothing about whether the training data was lawful, whether the evaluation numbers are honest, or whether the model is fit for its use. Those claims live in the AIBOM, the model card and, where they exist, third-party certificates. The [guide to who can attest to what](/blog/who-can-attest-ai-model) sets out which party can vouch for which. When something goes wrong, the signature and provenance are how you prove which model was running; [using an AIBOM in incident response](/blog/aibom-incident-response-which-model-was-running) covers that side. All guides by role are on the [AIBOM Library hub](/aibom). ## Frequently asked questions ### How do I sign a machine learning model with Sigstore? Install the OpenSSF model signing package (pip install model-signing) and run model_signing sign on the model folder. By default it signs with Sigstore: it opens an OIDC login, gets a short-lived certificate for your identity and writes a Sigstore bundle, model.sig, that lists the SHA-256 of every file. In CI, pass --use_ambient_credentials (version 1.1.1 spelling) so it uses the workflow’s identity token instead of a browser login. ### How do I verify a signed model? Run model_signing verify on the model folder with --signature pointing at the bundle. For a Sigstore signature, version 1.1.1 also requires --identity and --identity_provider: the identity you expect to have signed it and the OIDC provider that vouched for it. For a key signature, pass --public_key. The command prints “Verification succeeded” and exits 0, or prints the mismatch and exits 1. ### What does a failed model signature verification look like? In our test of model_signing 1.1.1, a changed file gave “Signature mismatch” with a hash mismatch for that file, an added file gave “Extra files found in model”, a deleted file gave “Missing files in model”, and the wrong public key gave “Key mismatch”. Each exits with status 1, so a deploy script can stop on it. ### How do I generate SLSA provenance for a model in GitHub Actions? Add the actions/attest@v4 step after the model files are produced, with subject-path pointing at them, and grant the job id-token: write, attestations: write and artifact-metadata: write. With no predicate input it generates SLSA build provenance, signs it with Sigstore and stores it with GitHub. Verify it with gh attestation verify. GitHub says this alone is SLSA Build Level 2. ### How do I link a model signature and provenance from a CycloneDX AIBOM? In the model component’s externalReferences, add an entry of type digital-signature pointing at the signature bundle and one of type attestation pointing at the provenance. Put the identity and identity provider a verifier should expect in the comment, because the signature cannot be checked without them. In SPDX 3.0.1 use externalRef type other with a contentType instead. ### Why do model_signing flags fail with “No such option”? Version 1.1.1, the latest on PyPI as of October 2026, spells multi-word flags with underscores: --private_key, --public_key, --identity_provider, --use_ambient_credentials. The README on the project’s main branch already uses hyphens (--public-key) for a release that is not out yet. Pin the version you install and check model_signing verify sigstore --help. ## Sources - sigstore/model-transparency: OpenSSF model signing (model_signing), README on main - sigstore/model-transparency README at tag v1.1.1 - model-signing on PyPI (1.1.1 released 10 October 2025) - ossf/model-signing-spec: the OpenSSF Model Signing (OMS) specification - Sigstore documentation (Fulcio, Rekor, keyless signing) - in-toto Attestation Framework v1 (Statement, subject, predicate) - actions/attest (v4) README - actions/attest-build-provenance README (plans and repositories) - GitHub Docs: artifact attestations (Build L2, L3 with reusable workflows) - GitHub CLI manual: gh attestation verify - SLSA build provenance (predicateType https://slsa.dev/provenance/v1) - SLSA v1.2: security levels (Build L0 to L3) - CycloneDX bom-1.7 JSON schema (declarations, externalReference types) - vLLM v0.6.3 engine/arg_utils.py (--load-format) _Checked on 29 October 2026. PlayCISO is not affiliated with any standards body, accreditation body, vendor or project named here. This is general information, not legal advice._