Release integrity
Know how a Chio release is staged as a draft, gated to the exact reviewed source, signed, and verified by the person installing it.
On this page
Anyone installing Chio can confirm, before running it, that an archive was built from the reviewed source by the release workflow. Each archive carries a signature from that workflow's own identity, and one provenance bundle binds all five release targets to the tagged commit and the run that built them.
Verification starts from expectations you choose before you open the evidence: the repository, the version tag, the source commit, the target, and the workflow identity. Each check then answers one question. A checksum compares archive bytes, a signature establishes the signer, and provenance connects the build to its complete output set.
You accept the archive for the identity you pinned
cosign verify-blob checks the signature against the archive bytes, the certificate chain to the Sigstore trust root, the Rekor inclusion proof, and the certificate identity and OIDC issuer you chose before downloading.
A release stays a hidden draft until its exact commit passes four workflows
The release job of release-binaries.yml cuts every native binary release. Before it touches an existing GitHub Release for the tag, check-release-draft-assets.py stage refuses to proceed unless that release is still a draft. The job then stages the archives, checksums, and cosign signatures with draft: true, so the release stays hidden while its provenance is assembled.
check-release-source-gates.py decides which commit may ship. For each of four workflows, the latest run on the exact commit, on main in the same repository, must have completed successfully: ci.yml (its main-branch push run, including the build and test, MSRV, cargo-vet, and cargo-deny jobs), release-qualification.yml, cve-monitor.yml, and cargo-vet.yml. A run against another commit, a fork, another branch, or a partly successful run leaves the gate closed. The release job runs the gate before it stages the draft.
For a stable tag, publish-stable starts once the build matrix, the provenance job, and the checksum-index job finish. It lifts the draft in four steps:
- Re-check the source. It runs
check-release-source-gates.pyagain. - Download the staged assets. It fetches them back from the draft.
- Verify provenance. It runs
verify-release-provenance.pyagainst the downloaded assets. - Compare manifests. It records a manifest of the assets before and after that verification, and publishes only when the two compare byte for byte.
A prerelease tag never runs publish-stable, so a candidate release is still a draft when the workflow finishes. The same source gates run before the npm and PyPI release workflows publish, so an SDK release clears the checks a native binary release clears. npm then publishes with npm publish --provenance, binding the published version to the workflow identity at the registry; PyPI publishes through the pypa/gh-action-pypi-publish action.
Every archive is signed by the run's own identity, with no stored key
Each per-target archive is signed the moment it is staged, with keyless Sigstore cosign sign-blob. Fulcio issues a short-lived certificate against the workflow run's GitHub Actions OIDC token, so no long-lived signing key sits in repository secrets. The detached signature and the certificate ship beside the archive as its .sig and .pem siblings.
After the release matrix succeeds, release-binaries.yml calls the repository's own slsa.yml workflow. It runs the upstream generator_generic_slsa3 reusable workflow, which produces build provenance binding the released archives to the exact tagged commit and the run that built them. The sidecar OCI image goes through the same keyless signing, covered in container images.
Every released binary also carries a software bill of materials generated by Syft. Before a target's archive uploads, check-release-binary-sbom.py checks the recorded Syft version, requires the SBOM to be bound to the executable's SHA-256, and requires every cataloged Rust crate to come from the binary's embedded cargo-auditable data and to appear in the selected Cargo.lock.
A release verifies only against the workflow, issuer, and five archives you pinned
Download the archive with its .sig and .pem siblings, then set the version and target from the release you reviewed. The certificate identity pins the public Chio workflow at exactly that tag, and the issuer pins GitHub Actions. A certificate cannot choose its own acceptable identity, and renaming a local file cannot make another tag's certificate pass.
set -euo pipefail
: "${VERSION:?select the independently reviewed release version without v}"
: "${TARGET:?select the reviewed Unix archive target}"
ARCHIVE="chio-${VERSION}-${TARGET}.tar.gz"
WORKFLOW="https://github.com/backbay-labs/chio/.github/workflows/release-binaries.yml"
cosign verify-blob \
--certificate "${ARCHIVE}.pem" --signature "${ARCHIVE}.sig" \
--certificate-identity "${WORKFLOW}@refs/tags/v${VERSION}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$ARCHIVE"cosign verify-blob exits non-zero when the signature does not match the archive bytes, the certificate does not chain to the embedded Sigstore trust root, the certificate's Subject Alternative Name or OIDC issuer differs from your pins, or the Rekor inclusion proof is missing. Windows releases ship a .zip archive; select that filename instead. The cosign verification reference documents the identity and issuer flags.
The installer verifies the archive's checksum before it installs; these identity checks are what a reviewer adds to confirm the archive came from the expected workflow. In Rust, chio_attest_verify pairs a certificate identity regex with the OIDC issuer as an ExpectedIdentity, and callers reach it through the AttestVerifier trait. The same contract backs bundle-based verification elsewhere, such as guard images in the guard registry.
In Rust: the ExpectedIdentity declaration
pub struct ExpectedIdentity {
/// Regex matched against the Fulcio cert SAN. Example:
/// `https://github\.com/owner/chio/\.github/workflows/release-binaries\.yml@refs/tags/v.*`
pub certificate_identity_regexp: String,
/// Fulcio cert OIDC issuer. For GitHub-hosted runners this is exactly
/// `https://token.actions.githubusercontent.com`.
pub certificate_oidc_issuer: String,
}Provenance verification then accepts the complete set of five archives, or none. chio release verify carries the signing identity and the Sigstore trust root pinned in the CLI, so it checks every archive's signature and the one provenance bundle in a single run:
chio release verify release/chio-0.2.0-*.tar.gz release/chio-0.2.0-*.ziparchive chio-0.2.0-x86_64-unknown-linux-gnu.tar.gz checksum ok archive chio-0.2.0-aarch64-unknown-linux-gnu.tar.gz checksum ok archive chio-0.2.0-x86_64-apple-darwin.tar.gz checksum ok archive chio-0.2.0-aarch64-apple-darwin.tar.gz checksum ok archive chio-0.2.0-x86_64-pc-windows-msvc.zip checksum ok signed by release.yml on backbay-labs/chio, issuer token.actions.githubusercontent.com provenance SLSA v1, 5 subjects, tag v0.2.0, all present verified
The command pins the immutable SLSA builder workflow and the generic build type, refuses a duplicate, missing, or extra subject, compares each archive's hash, verifies the bundle cryptographically, and checks that no file changed during verification. A bundle that names four targets, or a sibling archive substituted for one of the five, fails the whole set. With no archive argument, it verifies the chio binary you are running.
The Linux x86_64 binary rebuilds byte for byte from its tag
reproducible-build.yml rebuilds the Linux x86_64 chio binary from each version tag on two separate builders and compares the outputs byte for byte. macOS and Windows archives are checksummed and signed; codesign and PE timestamps make their bytes platform-dependent. The release workflow signs the result of that comparison and attaches it to the release, and chio release verify --evidence reproducible checks that attestation for the release you run.
chio release verify --evidence reproduciblebinary /usr/local/bin/chio chio 0.2.0 x86_64-unknown-linux-gnu checksum matches SHA256SUMS for v0.2.0 signed by release.yml on backbay-labs/chio, issuer token.actions.githubusercontent.com provenance SLSA v1, 5 subjects, tag v0.2.0 reproducible two builders produced identical bytes from tag v0.2.0 verified
To rebuild the binary yourself, the reproducible-build procedure runs the same two-directory comparison from the tag.
The archive review refuses a changed copy before any signature is trusted
The review script checks one public archive against a tag, target, and source commit you selected. It peels the tag to its commit and refuses a mismatch, refuses a draft release, writes the release's asset inventory, and downloads the archive with its checksum sidecar and the SHA256SUMS index.
The complete archive review script
"""Check selected public archive bytes; require separate signer/provenance evidence."""
import argparse
import hashlib
import json
import os
import re
import subprocess
from pathlib import Path
REPOSITORY = "backbay-labs/chio"
TARGETS = ("x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu",
"x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-pc-windows-msvc")
def review_archive(tag, target, expected_source, output):
if not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?", tag):
raise ValueError("select an explicit version tag")
if not re.fullmatch(r"[0-9a-f]{40}", expected_source):
raise ValueError("select the independently reviewed public commit")
output.mkdir(mode=0o700)
def gh(*arguments):
return subprocess.check_output(["gh", *arguments], text=True, timeout=120)
reference = json.loads(gh("api", f"repos/{REPOSITORY}/git/ref/tags/{tag}"))["object"]
while reference["type"] == "tag":
reference = json.loads(gh("api", f"repos/{REPOSITORY}/git/tags/{reference['sha']}"))["object"]
if reference["type"] != "commit" or reference["sha"] != expected_source:
raise ValueError("public tag differs from the independently selected source commit")
release = json.loads(gh("release", "view", tag, "--repo", REPOSITORY,
"--json", "tagName,isDraft,isPrerelease,assets"))
assert release["tagName"] == tag and not release["isDraft"]
with (output / "asset-inventory.json").open("x") as stream:
json.dump(release, stream, indent=2)
stream.write("\n")
suffix = "zip" if target.endswith("msvc") else "tar.gz"
archive = f"chio-{tag[1:]}-{target}.{suffix}"
available = {asset["name"] for asset in release["assets"]}
for name in (archive, archive + ".sha256", "SHA256SUMS"):
if name not in available:
raise ValueError(f"required checksum input is absent: {name}")
gh("release", "download", tag, "--repo", REPOSITORY,
"--pattern", name, "--dir", str(output))
original = (output / archive).read_bytes()
digest = hashlib.sha256(original).hexdigest()
def selected_digest(text):
matches = []
for line in text.splitlines():
columns = line.split()
if len(columns) == 2 and columns[1].lstrip("*") == archive:
if re.fullmatch(r"[0-9a-f]{64}", columns[0]):
matches.append(columns[0])
if len(matches) != 1:
raise ValueError("checksum file must contain exactly one selected archive entry")
return matches[0]
sidecar = selected_digest((output / (archive + ".sha256")).read_text())
index = selected_digest((output / "SHA256SUMS").read_text())
assert digest == sidecar == index, "archive differs from selected checksum records"
print(f"CHECKSUM {archive}: selected sidecar and index match original bytes")
changed = original[:-1] + bytes([original[-1] ^ 1])
with (output / (archive + ".tampered")).open("xb") as stream:
stream.write(changed)
assert hashlib.sha256(changed).hexdigest() != sidecar
print("REFUSE changed archive: selected checksum no longer matches")
assert (output / archive).read_bytes() == original
signed_inputs = {archive + ".sig", archive + ".pem",
f"chio-{expected_source}.intoto.jsonl"}
missing = sorted(signed_inputs - available)
if missing:
print("INCOMPLETE signed-release acceptance: missing " + ", ".join(missing))
print("SCOPE checksum comparison only; workflow identity and provenance remain unverified")
return 4
print("AVAILABLE signing inputs; independently pinned signature and five-subject provenance verification still required")
return 0
if __name__ == "__main__":
os.umask(0o077)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--tag", required=True)
parser.add_argument("--target", choices=TARGETS, required=True)
parser.add_argument("--source-sha", required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
raise SystemExit(review_archive(args.tag, args.target, args.source_sha, args.output))Download the complete source and run it against the release you reviewed. For v0.2.0 on Apple silicon:
python3 review_public_archive.py \
--tag "v${VERSION}" --target aarch64-apple-darwin \
--source-sha "$REVIEWED_SOURCE_SHA" \
--output ./release-reviewCHECKSUM chio-0.2.0-aarch64-apple-darwin.tar.gz: selected sidecar and index match original bytes REFUSE changed archive: selected checksum no longer matches AVAILABLE signing inputs; independently pinned signature and five-subject provenance verification still required
You should see the original bytes match both checksum records, then a refusal for the copy the script writes beside them with one bit flipped. The original archive stays byte-identical. The last line hands off to the signature and provenance checks above: the checksum identifies bytes, and those checks identify the signer and the build.
When the archive's .sig, its .pem, or the source-named .intoto.jsonl bundle is absent from the release, the script prints INCOMPLETE signed-release acceptance with the missing names and exits 4. Signed-release acceptance needs all three files, and an installer's checksum success is reported as a checksum success.
Identity, issuer, and transparency mismatches stop verification with a non-zero exit
chio release verify exits 1 when an archive fails its checks and prints the reason in the standard error format; check-release-draft-assets.py, on the release side, prints its refusal before it exits non-zero.
| Refusal | Condition | Raised by |
|---|---|---|
error[CHIO- | Certificate SAN is not the expected workflow identity | chio release verify |
error[CHIO- | Certificate OIDC issuer is not the expected issuer | chio release verify |
error[CHIO- | Transparency- | chio release verify |
error[CHIO- | The bundle omits or substitutes one of the five archives | chio release verify |
release asset check refused: ValueError: existing release is not the expected draft; never reopen a published release | The tag's release is already published, or its prerelease flag does not match the tag | check- |
Every refusal fails closed, and each one has its own evidence to retain:
- Changed archive. The original, both checksum records, and the changed copy.
- Wrong workflow, tag, or issuer pin. The expectation you selected and the verifier's full stderr and exit status.
- Missing or substituted target. The original bundle and the complete target set.
- Missing signatures or provenance. The asset inventory, with the acceptance reported as incomplete and kept apart from any failed cryptographic check.
A corrupt download calls for reacquisition; an unexpected signer calls for rejection or an explicit policy review. Verification leaves publishing, installing, and launching the archive to separate acts. A target rebuilt and launched later is a new executable, so review its helper and its measured launch contract on their own.