Chio/Docs
LOGIN · JOIN

Evidence capture authority

See who may capture, sign, and publish Linux enforcement evidence a merge depends on, and how it is bound to the commit it covers.

Concept chio@5f63e6e

On this page

Before a merge can depend on Linux enforcement evidence, four separately owned workflows fix who selected the source, which runner captured the evidence, and which App published the result. A maintainer can trace any passing check back to all three.

Three commits identify every capture: the reviewed source S, the evidence head E, and the test merge M that carries the checks. Each stage keeps the identities it was permitted to use. Freezing or revoking publication names that original tuple, so a retry cannot select a different head and escape the withdrawal.

Capture tupleChoose a step.
mainselectsrecords≤ 32 linear commits · 3 filestest mergevalidatessignspostschanges source.txtrefusedfails 5 checksfreezes evidence SHA(S, E, M, PR) revokedSeEMκpfσownercapture runnerfinalizersecurity Appmainselectsrecords≤ 32 commits3 filestest mergevalidatessignspostschanges source.txtrefusedfails 5 checksfreezes SHA(S, E, M, PR) revokedSeEMκpfσownercapture runnerfinalizersecurity App

The finalizer signs, and only the security App publishes

enterprise-evidence-finalizer.yml validates and signs the capture, then posts the Security contract check on M as the GitHub App chio-security-authority, its token narrowed to Checks write. The branch ruleset accepts that check only from the App.

One pull request's evidence tuple: the owner's source S, evidence commits ending at E, the test merge M, and the five checks the security App owns on M. The number of evidence commits is illustrative; the ci.yml aggregate, the merge-binding attestation, and the runner's own evidence checks are left out.

Four workflows separate selection, capture, publication, and revocation

Each stage is a separate workflow with its own jobs. The controller runs only for a pull request the repository owner opens from a branch in the same repository, never from a fork.

WorkflowStageJobs
enterprise-evidence-controller.ymlDispatch an isolated capturedispatch-isolated-capture
enterprise-linux-capture.ymlAuthorize, refresh, and capture Linux evidenceauthorize-capture, refresh-linux-evidence, capture-linux-enforcement, dispatch-trusted-finalizer
enterprise-evidence-finalizer.ymlValidate, sign, and publishvalidate-capture, sign-validated-capture, authorize-security-check-publication, publish-security-contract
security-contract-revocation.ymlBind and revoke a published contractbind-revocation, revoke-security-contract

Every step belongs to someone other than the pull request author. The owner selects the candidate, a constrained runner records what it observed for that candidate, and the finalizer checks the retained bindings before it publishes under its own identity. A passing result is therefore attributable to the exact source and workflow under review.

The owner pins the source and the workflow definition before a capture runs

On a configured capture deployment, with its protected runner, pristine default-branch workflow definitions, and dedicated publishing App, the repository owner selects the reviewed public source and workflow-definition commits, then requests a capture. The refresh-linux-evidence label asks the controller for evidence and grants the pull request author no signing key.

Terminalbash
set -euo pipefail
repo=backbay-labs/chio
: "${SOURCE_SHA:?select the reviewed public source commit}"
: "${DEFINITION_SHA:?select the reviewed workflow definition commit}"
: "${PR_NUMBER:?select the same-repository open pull request}"
gh variable set CHIO_AUTHORIZED_SECURITY_SOURCE_SHA --repo "$repo" --body "$SOURCE_SHA"
gh variable set CHIO_ENTERPRISE_SECURITY_DEFINITION_SHA --repo "$repo" --body "$DEFINITION_SHA"
gh pr edit "$PR_NUMBER" --repo "$repo" --add-label refresh-linux-evidence
gh run list --repo "$repo" --workflow enterprise-linux-capture.yml --limit 10

CHIO_ENTERPRISE_SECURITY_DEFINITION_SHA names the reviewed workflow-definition commit. The controller, capture, finalizer, and revocation workflows each require their own workflow bytes to match the bytes at that commit, so neither the capture nor the publisher can silently switch to a different runner policy or definition.

Select the exact dispatched run from the controller's retained dispatch intent, then inspect its source, run, attempt, and original evidence artifact. Before it reconciles the five App-owned merge checks, the finalizer rechecks the live pull request, the source and evidence ancestry, the workflow definition, CI, and the runner contract. A green unrelated run or an author-uploaded JSON file supplies none of that authority.

Only the pinned App's check satisfies the merge ruleset

The finalizer's publish-security-contract job starts only after authorize-security-check-publication and sign-validated-capture both succeed. It runs in the security-check-publisher deployment environment under a non-cancelling, maximum-queue concurrency group keyed to the merge commit.

The job's own GitHub Actions permissions are actions: read, checks: write, contents: read, and pull-requests: read: enough to read run history and pull request context and to post the four Security mirror checks as GitHub Actions. The Security contract check itself is posted under a separate credential.

The publish step authenticates as the dedicated GitHub App chio-security-authority and confirms that the installation and the App identity both resolve to that slug before it requests an installation token. The App's token, otherwise granted metadata read plus checks and commit-status read and write, is narrowed to Checks write for the run (GitHub may attach Metadata read), so the credential that posts the Security contract check cannot write commit statuses. The branch ruleset binds that check to the App's integration ID.

The merge binding and the CI aggregate

On a pull request run, the bind-source job of enterprise-hardening.yml, shown in the Actions UI as "attest exact pull request merge binding", assembles one JSON predicate under the ci-merge-binding/v1 schema. It binds the base, head, and merge commits, the calling and building workflow definitions, the CI run identity, and the pull request number, and checks that the key set is exactly base, builder, caller, ci, head, merge, pull_request_number, repository, and schema, on a canonically serialized, size-bounded file, before anything downstream can consume it.

The job holds attestations: write and id-token: write, so it attests the predicate against the run's own GitHub Actions OIDC identity with no stored key, the keyless pattern that also signs a shipped archive in release integrity.

The security-contract-required job of ci.yml, also displayed as "Security contract", is the GitHub Actions aggregate the finalizer requires to have succeeded before it publishes. It runs with if: always() after ten jobs and fails unless every one reports success:

  • the main build and test job, the public Kani run, the formal proof contract, and the Apalache contract;
  • threat model coverage, MSRV, cargo-vet, and cargo-deny;
  • the enterprise security contract and the nonce FIPS contract.

Inside that job, the one committed-evidence variable read is CHIO_COMMITTED_LINUX_EVIDENCE_SHA, which must be a 40-character hex commit SHA; CHIO_ENTERPRISE_SECURITY_DEFINITION_SHA does not appear in ci.yml. The finalizer re-reads the definition variable immediately before publication and refuses to publish unless it still names the definition commit the capture was authorized under. The merge-gating check is the one the App posts.

Evidence-only commits after the source pass, and a changed source is refused

check-committed-linux-evidence.py fixes where an evidence commit sits relative to the source it covers. It requires exactly three files under audits/evidence/enterprise-linux/: a migration binding digest, a migration canary, and the canary's SHA-256 sidecar. An evidence commit identical to the source commit is refused outright.

From the evidence commit, the validator walks back toward the source one parent at a time and refuses three shapes:

  1. A long walk. The walk fails when it passes 32 commits without reaching the source.
  2. A merge in the chain. A commit with more than one parent fails the walk, because the evidence must sit on a single line of commits descended from the source.
  3. A commit outside the evidence files. An empty commit, or one that changes any other path, is refused, so the commits between source and evidence carry no code.

Every Chio release passes the capture-ancestry gate before it is tagged. chio release verify --evidence qualification shows its result for the release you run. The gate exercises the ancestry rule on temporary commits: one linear evidence commit is accepted, and evidence on the source commit itself or a descendant that changes source are refused with the validator's own message. The contributor qualification page runs it against a source tree.

Passing the ancestry rule is one check. Full evidence validation also pins the verifier bytes, the runner identity and labels, the policy, configuration, and inventory digests, the generation interval, and the key-log, broker, cage, authenticated-caller, and migration controls.

Revocation fails all five checks on the original tuple, permanently

To withdraw a published contract, the repository owner completes three steps before repairing or replacing its authority:

  1. Freeze publication. Set CHIO_COMMITTED_LINUX_EVIDENCE_SHA to the reserved all-zero SHA.
  2. Revoke the tuple. While the pull request is still open with the evidence commit as its head, dispatch security-contract-revocation.yml from main, naming the original source, evidence, and merge commits and the pull request.
  3. Replace the authority. Withdraw or replace the failed source, policy, App installation or key, signing environment, or ruleset. Revocation fails the checks and leaves key, App, and policy rotation to this separate owner operation.
Terminalbash
set -euo pipefail
repo=backbay-labs/chio
: "${SOURCE_SHA:?original authorized source}"
: "${EVIDENCE_SHA:?original evidence head}"
: "${MERGE_SHA:?original test merge carrying authority checks}"
: "${PR_NUMBER:?original open pull request}"
gh variable set CHIO_COMMITTED_LINUX_EVIDENCE_SHA --repo "$repo" \
  --body 0000000000000000000000000000000000000000
gh workflow run security-contract-revocation.yml --repo "$repo" --ref main \
  -f authorized_source_sha="$SOURCE_SHA" -f evidence_sha="$EVIDENCE_SHA" \
  -f merge_commit_sha="$MERGE_SHA" -f pr_number="$PR_NUMBER" \
  -f reason=policy-authority-withdrawn
gh api "repos/$repo/commits/$MERGE_SHA/check-runs?filter=all&per_page=100" \
  --jq '.check_runs[] | {name,head_sha,status,conclusion,app:.app.slug,external_id}'

The reason takes one of six values: ci-regression, source-authority-withdrawn, evidence-authority-withdrawn, policy-authority-withdrawn, app-authority-withdrawn, or operator-security-revocation. The dispatch marks all five merge-authority checks failed, creating a failed check where one is missing, and keeps the original workflow and App identity checks. CI or finalizer failure triggers the same revocation automatically.

You should see five completed failure checks owned by the configured security App on the original M. Check names alone are insufficient: confirm the App namespace, the exact head, and the external authority binding, paginating if needed.

Revocation is idempotent and permanent for that tuple, which stays failed through repeats and later finalization. Publication returns only through a newly reviewed source, evidence, or merge tuple.

Evidence that does not descend from the reviewed source stops before signing

check-committed-linux-evidence.py raises the first two as plain Python exception messages. The controller prints the third as a GitHub Actions error annotation in the run's summary, then exits 1.

RefusalConditionRaised by
"Linux evidence must be committed after the source commit"
The evidence commit is the source commit
check-committed-linux-evidence.py
"Linux evidence descendant chain is not linear"
A commit between source and evidence has more than one parent
check-committed-linux-evidence.py
"Enterprise security definition is not configured"
No pinned definition commit
enterprise-evidence-controller.yml

Each outcome has one continuation. An accepted ancestry still needs the other evidence checks, and the owner retains the exact source, evidence, run, and attempt. An implementation change after S calls for a newly reviewed source. A runner or definition that differs from the retained authority cannot publish the original claim, so provision and review the intended authority explicitly.

Next: verify a signed artifact against your own pin