SLSA L3 build provenance for Rust crates — three-job publish/provenance/release pipeline
From gh-guardnpx claudepluginhub anthropics/claude-plugins-community --plugin gh-guardThis skill is limited to using the following tools:
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
SLSA (Supply-chain Levels for Software Artifacts) Level 3 provides verifiable build provenance — a signed attestation of what was built, from what source, by which builder. This skill covers the three-job pipeline architecture for Rust crates.
The publish workflow uses three sequential jobs:
publish ──→ provenance ──→ release
│ │ │
├─ Tests ├─ SLSA L3 ├─ Download provenance
├─ Verify tag │ generator ├─ Create GitHub Release
├─ Package │ (reusable) └─ Attach .intoto.jsonl
├─ Hash .crate │
├─ Auth OIDC │
└─ Publish │
│
Generates signed
.intoto.jsonl
attestation
publishfetch-depth: 0 (needed for ancestry verification)Cargo.toml versionorigin/maincargo package --locked).crate fileOutput: hashes — base64-encoded SHA-256 subject hashes
provenance.intoto.jsonl attestation (DSSE envelope)Critical constraints:
@tag reference (e.g., @v2.1.0), not SHA — the reusable workflow requires thisupload-assets: false because we attach provenance to the release ourselves (needed for immutable releases)release.intoto.jsonl file attached--verify-tag to ensure the tag existsCritical constraint for immutable releases:
If your GitHub org has "immutable releases" enabled, you CANNOT upload assets after a release is created. The provenance file must be attached AT creation time, which is why this job downloads the artifact first, then creates the release with it in a single gh release create call.
The .crate file hash is the subject of the SLSA attestation:
- name: Build crate archive for provenance
run: cargo package --locked
- name: Generate subject hashes
id: hash
run: |
echo "hashes=$(sha256sum target/package/my-crate-*.crate | base64 -w0)" >> "$GITHUB_OUTPUT"
The wildcard * matches the version in the filename (e.g., my-crate-0.1.5.crate).
provenance:
needs: [publish]
permissions:
actions: read
id-token: write
contents: write
# CRITICAL: Must use @tag, not SHA
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: "${{ needs.publish.outputs.hashes }}"
upload-assets: false
provenance-name: my-crate.intoto.jsonl
@tag and not SHA?The SLSA generator is a reusable workflow. GitHub's security model for reusable workflows verifies their identity differently than regular actions. The generator's verification requires a tag reference to validate the signing identity. SHA pinning here would break the attestation.
Users can verify provenance with slsa-verifier:
# Install
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
# Verify
slsa-verifier verify-artifact my-crate-0.1.5.crate \
--provenance-path my-crate.intoto.jsonl \
--source-uri github.com/owner/repo \
--source-tag v0.1.5
| Pitfall | Symptom | Fix |
|---|---|---|
| SHA reference for generator | Workflow error: "invalid ref" | Use @v2.1.0 tag reference |
fetch-depth: 1 (default) | Ancestry check fails: "not on origin/main" | Use fetch-depth: 0 |
upload-assets: true with immutable releases | Release exists but provenance upload fails | Set upload-assets: false, download artifact in release job |
Missing id-token: write | OIDC token exchange fails | Add to provenance job permissions |
Missing contents: write on provenance | Cannot write attestation | Add to provenance job permissions |
| Wrong provenance filename | Download artifact step fails | Ensure provenance-name matches download-artifact name: |
| Shallow clone for ancestry | merge-base --is-ancestor fails | fetch-depth: 0 on checkout |
| Failed publish, need retrigger | gh run rerun uses old workflow file | Use workflow_dispatch with the tag input to retrigger the current workflow |
The publish workflow supports workflow_dispatch with a tag input for manual retrigger. This is safer than gh run rerun (which uses the original workflow file, not the current one):
gh workflow run publish.yml -f tag=v0.1.5
The PUBLISH_TAG env var resolves the tag from either trigger type, so all steps work correctly with both push and workflow_dispatch.
See templates/workflows/publish.yml for the complete implementation.