Release Signing And Offline Verification
July 24, 2026 ยท View on GitHub
This document defines the release handoff to NVIDIA. It is a publication
contract, not evidence that the current checkout is runnable. The existing
v0.1.0 annotated tag is unsigned and remains review-only. Do not move that tag
or add sidecars later and relabel it runnable. Publish a new version after every
release gate is accepted.
The normal MindLab public snapshot path is separate: the private -alpha
repository's Release to Public workflow delegates v* tags to
mindlab-bot. Use docs/public_release.md for that path;
this document applies only when an NVIDIA offline signed bundle is required.
Trust boundary
The release bundle is untrusted input. NVIDIA must receive these two files by an out-of-band channel controlled by NVIDIA or the release owner:
- a policy conforming to
environment/release-signing-policy.schema.json; and - the Sigstore trusted root whose raw SHA-256 is fixed by that policy.
The checked-in environment/release-signing-policy.example.json is an
UNTRUSTED TEMPLATE. The verifier rejects a policy or trusted root located
inside either the release bundle or source checkout. It also rejects wildcard
certificate identities, mutable Git refs, an unpinned Cosign version, and an
OCI tag without a registry digest.
The policy also pins the raw SHA-256 of the approved Cosign executable; the
CLI requires its absolute path and never resolves a verifier from PATH.
Immutable release identity
A release has three different Git identities:
source_commit(A) contains all implementation, tests, and evidence.release_commit(B) hasAas its only parent and changes onlyevidence/manifest.jsonto bind that source tree.tag_object(T) is an annotated tag object that peels toB.
The signed manifest records the repository, version, tag, T, B, A, exact
linux/amd64 platform, and digest-pinned OCI image. A tag name, branch name,
GitHub-generated tag archive, mutable tag, or OCI tag alone is never a release
identity. A signed Git tag may be additional protection, but publisher identity
for this contract comes from the signed release manifest and attestation.
Bundle layout
The bundle contains exactly five metadata files plus the nine manifest subjects. Files, parent directories, JSON keys, subject roles, and paths are strictly validated; symlinks, non-regular files, unbound extras, path traversal, duplicate JSON keys, and non-finite JSON numbers are rejected.
release-manifest.json
release-manifest.sigstore.json
SHA256SUMS
release-attestation.json
release-attestation.sigstore.json
artifacts/... exactly one subject for each required role
The required roles are source archive, wheel, sdist, runtime lock, runtime
identity, SPDX SBOM, image provenance, evidence manifest, and validation
evidence. SHA256SUMS contains only these nine subjects in bytewise path order,
with two spaces between each digest and path. It does not list either signature
bundle, manifest, or attestation, so the graph has no hash cycle.
The authentication chain is:
Cosign(release-manifest.json bytes)
-> checksum_manifest raw SHA-256 and size
-> SHA256SUMS
-> nine subject files
Cosign(release-attestation.json bytes)
-> release-manifest.json + SHA256SUMS + nine subjects + OCI digest
Running sha256sum -c SHA256SUMS alone checks integrity but not publisher
identity.
Signing
Stage exactly the nine subject files under one new bundle directory, then build the unsigned metadata from a clean checkout of the annotated version tag:
python3 scripts/build_release_bundle.py \
--bundle /release/staging \
--checkout "$PWD" \
--version 0.2.0 \
--image registry.example/mint-2m@sha256:<64hex> \
--subject evidence_manifest=artifacts/evidence-manifest.json \
--subject git_source_archive=artifacts/mint-2m-source.tar.gz \
--subject image_provenance=artifacts/image-provenance.json \
--subject python_sdist=artifacts/mint_2m-0.2.0.tar.gz \
--subject runtime_lock=artifacts/runtime.lock.json \
--subject runtime_identity=artifacts/runtime-identity.json \
--subject sbom=artifacts/sbom.spdx.json \
--subject validation_evidence=artifacts/validation-evidence.tar.gz \
--subject wheel=artifacts/mint_2m-0.2.0-py3-none-any.whl
The builder refuses an unclean checkout, lightweight or mismatched tag, non-binding commit, unexpected staged file, duplicate role/path, unsafe path, or mutable image reference. It never creates a signature placeholder.
The baseline uses Cosign v3.1.1. Install that exact version from the official
Sigstore release, verify its official release checksum or signed bundle, and
never resolve latest in a release job. After generating and independently
inspecting the manifest and in-toto statement, sign both raw files:
cosign sign-blob --yes \
--bundle release-manifest.sigstore.json \
release-manifest.json
cosign sign-blob --yes \
--bundle release-attestation.sigstore.json \
release-attestation.json
The protected release workflow must sign only an annotated version tag whose
object and peeled commit match the manifest. It builds the wheel and sdist
twice, requires byte-identical results, publishes one result, signs those
bytes, verifies them before upload, refuses an existing release, and never
overwrites an asset. GitHub immutable releases are required online
defense-in-depth, but they do not replace offline verification.
The checked-in .github/workflows/release.yml enforces the source-side path and calls
scripts/check_release_tree.py --require-runnable before obtaining an OIDC
token, so the current review-only tree cannot enter the signing job.
Before enabling that workflow, repository administrators must configure all of these GitHub-side controls. They are external state and cannot be proved by the checked-in YAML:
- a
v*tag ruleset that prevents tag update and deletion and limits creation to approved release owners; release-signingandrelease-publicationenvironments with required reviewers, self-review prevention, and a deployment rule restricted to protected version tags;- immutable releases enabled for the repository; and
- branch protection that prevents unreviewed changes to this workflow and its release scripts.
The release approver must inspect those settings before each publication. A named environment without these settings is not an approval boundary.
NVIDIA verification
Place the bundle, checkout, policy, and trusted root in separate directories.
Check out the exact release_commit, retain the annotated tag object locally,
and run:
python3 scripts/verify_release_bundle.py \
--archive /nvidia/input/mint2m-v0.2.0-release-bundle.tar.gz \
--checkout /nvidia/src/MinT-2M \
--policy /nvidia/trust/mint2m-policy.json \
--trusted-root /nvidia/trust/sigstore-trusted-root.json \
--cosign /nvidia/tools/cosign-v3.1.1 \
--verified-output /nvidia/verified/mint2m-v0.2.0
Cosign v3.1.1 has no --offline option. Verification is offline when the
signature bundles and trusted root are already local. The command verifies the
exact certificate identity, OIDC issuer, workflow repository/ref/SHA, both
signatures, all hashes and sizes, in-toto subjects, OCI digest, clean checkout,
annotated T -> B -> A relationship, and evidence manifest binding. Exit 0
emits "verified": true; any ambiguity or mismatch exits 2 with one JSON
error report.
The CLI safely parses the untrusted outer tar before authentication. It rejects
absolute or parent paths, links, duplicate entries, non-regular members, excess
member counts, and more than 64 GiB of declared uncompressed data. All checks
then run on a private snapshot; only a successful run leaves the requested
read-only --verified-output. Use that directory for subsequent inspection.
After signature verification, run the CPU, checkout doctor, runtime, licensing,
input, and current-tree GPU validation gates documented in docs/runbook.md.
A verified bundle is necessary but does not turn blocked actor, image, grant,
fixture, or 32x H20 gates into accepted ones.
Private repository decision
Public keyless Sigstore records certificate identity, issuer, signing event, and artifact digest in a public transparency log. It does not publish artifact contents, but it can reveal a private repository/workflow name, release timing, and hashes. The repository owner must approve that exposure before signing. If it is not acceptable, use an approved private Fulcio/Rekor deployment or organization-managed key-backed Cosign and deliver the public key/root through the same out-of-band NVIDIA trust channel. Never put a private key or a newly self-declared trust root in the repository, workflow artifact, or release bundle.