Skip to content

Repository files navigation

cargo-evidence

Certification evidence generation, verification, and traceability for Rust projects.

CI License: MIT OR Apache-2.0

cargo-evidence generates self-describing, offline-verifiable evidence bundles for safety-critical Rust builds. It captures environment fingerprints, source/artifact hashes, command logs, and bidirectional traceability matrices.


Project Status

Work in progress. Not production-ready. Not yet qualified for use on a certification program.

This repository is an early-stage tool and its on-disk formats, CLI surface, schema versions, and tool-qualification story are all still moving. Treat anything below as an implementation sketch, not a contract:

  • No public release yet. There is no crates.io publication and no stable version number. Breaking changes land on main without deprecation cycles.
  • Determinism is a design goal, not a proven property. The tool is built around reproducible bundles (see ADR-001 invariants), but end-to-end cross-platform bit-for-bit determinism has not been measured and is not asserted by the test suite.
  • No DER review. No auditor, DER, or certification authority has evaluated the tool. The "For Auditors" section below documents the intended design, not a qualification claim.
  • Schema versions (0.0.x) signal pre-1.0 instability. Treat them as such.
  • The on-disk schema_version field is informational, not contractual. All schemas are pinned at "0.0.1" until 1.0; breaking shape changes rewrite the format in place without bumping the literal. Two bundles produced by different cargo-evidence releases can both report schema_version: "0.0.1" while being structurally incompatible. Pin a cargo-evidence version (and read index.json's engine_crate_version) instead of pattern-matching on schema_version.

If you are considering this tool on a real program, the honest answer today is: fork it, read every line, and plan to own the delta.


Platform Support

The CI matrix covers three host platforms; the level of confidence in each is intentionally different.

Platform Compiles Unit + integration tests Nix reproducible build Cross-host toolchain parity Toolchain parity under Nix
Linux x86_64 yes yes yes (gated in CI) yes (gated in CI) yes (gated in CI)
macOS (Apple Silicon) yes yes works via devShell (gated in CI) yes (gated in CI) yes (gated in CI)
Windows x86_64 yes yes n/a (no Nix on Windows) yes (gated in CI) n/a (no Nix on Windows)

What's being claimed:

  • Every bundle carries two SHA-256 hashes in index.json: content_hash (the full SHA-256 of SHA256SUMS, covers every recorded byte, necessarily host-specific because env.json records host identity) and recipe_hash (the SHA-256 of a committed deterministic-manifest.json, the canonical recipe projection: toolchain, target triple, profile, features, locked dependency graph, command recipe, source-input digests, resolution policy).
  • Cross-host recipe parity is gated in CI across five flavors (3 native + 2 Nix) via the determinism-compare job: every push runs the generator on Linux, macOS, and Windows native plus Linux and macOS under nix develop, and asserts all five manifests agree on the six-field toolchain projection (rustc, cargo, llvm_version, cargo_lock_hash, rust_toolchain_toml, rustflags). The full recipe_hash also binds host-defining fields (target_triple), so it is a same-target identity and intentionally not compared across hosts. If any flavor's toolchain projection diverges, or if any flavor's artifact is missing, the job fails the PR. The Nix flavors dogfood the cert-build toolchain resolution path (rust-overlay) against the dev path (rustup), so silent drift between dev and cert bundles is caught mechanically. See determinism-compare in .github/workflows/ci.yml.
  • content_hash still differs per host by design, and that's intentional: the full SHA256SUMS-hashed chain records the host operating environment so a DO-330 auditor has cryptographically-bound provenance for the build environment. The cross-host equality channel is the toolchain projection.

What this matrix does not claim:

  • Passing CI tests and a matching toolchain projection prove the tool runs consistently on that platform. Neither proves a bundle is fit for regulatory qualification on that platform — that's a DER's call, not a CI signal.
  • macOS and Windows are kept green primarily so contributors on those hosts can develop the tool. Bundles generated there are still development artifacts pending qualification review.

Recommended posture for anyone evaluating the tool:

  • Final cert builds should run on Linux under the provided Nix flake.
  • For dev work on macOS / Windows, compare the toolchain projection of deterministic-manifest.json against the Linux reference when you need to confirm toolchain parity, and recipe_hash when you need same-target recipe identity; don't compare content_hash across hosts (it will always differ).
  • Pin a commit SHA, because the formats may still change.

Quick Start

Get your first evidence bundle in under 30 seconds:

# Install
cargo install cargo-evidence

# Prerequisite: `generate` runs the workspace tests through cargo-nextest
# to capture per-test execution identity. Install it once:
cargo install cargo-nextest --locked

# Initialize evidence tracking in your project
cargo evidence init

# Generate an evidence bundle — IMPORTANT: --out-dir must point
# OUTSIDE the tracked git tree (see "Choosing --out-dir" below).
cargo evidence generate --out-dir /tmp/evidence

# Verify bundle integrity
cargo evidence verify /tmp/evidence/dev-20260207-*/

That is it. The init command scaffolds a cert/ directory with boundary configuration, floors configuration, profile templates, and schema-valid trace files whose worked examples are commented out (zero live entries — nothing placeholder can enter a bundle). The generate command produces a complete evidence bundle. The verify command checks every SHA-256 hash in the bundle and confirms structural integrity.

Choosing --out-dir

--out-dir should live outside your project's tracked git tree (e.g. /tmp/evidence, $RUNNER_TEMP/evidence in CI, or a sibling directory to your repo). Why: env.json records git_dirty, derived from git status --porcelain. If your bundle directory sits inside the repo root, the first generate leaves an untracked directory there, and every subsequent generate on the same commit observes the tree as dirty — flipping git_dirty from false to true, rotating env.json's hash in SHA256SUMS, and rotating content_hash. Same-commit runs are then no longer byte-reproducible.

If you need to place the bundle inside the repo (e.g. some CI workflows find it convenient), add the output directory to .gitignore before running generate for the first time. git status --porcelain respects .gitignore, so ignored directories do not flip git_dirty. This repo's own .gitignore already excludes /evidence/ as a safety net for the Quick Start example path.


Three-Tier Environment Strategy

cargo-evidence uses a three-tier profile system that auto-detects the appropriate strictness level based on your environment:

Profile When auto-detected Git state Missing tools Timestamps in hash Use case
dev Default (local workstation) Dirty OK Warnings Excluded Local development, iteration
cert Nix shell + CI (IN_NIX_SHELL + CI) Must be clean Hard errors Excluded Certification builds, DER review
record NAV_RECORD env var set Must be clean Hard errors Excluded Hardware-in-the-loop recording

Auto-detection logic:

  1. If NAV_RECORD is set --> record
  2. If IN_NIX_SHELL is set AND CI/GITHUB_ACTIONS is set --> cert
  3. Otherwise --> dev

You can always override with --profile dev|cert|record.

Key differences:

  • dev lets you iterate fast -- dirty git trees and missing tools are tolerated.
  • cert enforces clean git, requires all toolchain components on PATH, and produces bundles suitable for certification data item submission.
  • record is identical to cert in strictness but is triggered by a dedicated environment variable for hardware test recording workflows.

What's in a Bundle?

Every bundle is a self-contained directory with deterministic content:

evidence/cert-20260207-143022Z-a1b2c3d4/
  index.json                   # Bundle metadata, schema versions, content_hash + recipe_hash
  env.json                     # Environment fingerprint (rustc, cargo, LLVM, libc, OS, tools, …)
  deterministic-manifest.json  # Canonical recipe projection (toolchain, target, profile, features, locked graph, command recipe, inputs, resolution policy)
  inputs_hashes.json           # SHA-256 of the source baseline: in-scope crate sources + workspace-control inputs
  outputs_hashes.json          # SHA-256 of the build's workspace deliverables (lib/bin artifacts); host-specific
  commands.json                # Recorded command executions with exit codes
  SHA256SUMS                   # Content-layer integrity manifest
  trace/                       # Traceability matrix outputs (Markdown)
    matrix.md                  # HLR <-> LLR <-> Test bidirectional matrix
  tests/                       # Test execution artifacts (stdout/stderr)

Design invariants:

  • SHA256SUMS covers every content-layer file, including env.json and deterministic-manifest.json. index.json is metadata-layer and excluded from SHA256SUMS, so timestamps do not affect either hash.
  • content_hash in index.json is SHA-256(SHA256SUMS) — the full-fidelity integrity hash. It differs per host because env.json records host identity (host.os, libc, tool availability).
  • recipe_hash in index.json is SHA-256(deterministic-manifest.json) — the recipe identity hash. Two bundles sharing it declare the same recipe: same toolchain, target triple, profile, feature selection, locked dependency graph, command recipe, source-input digests, and resolution policy. Because the recipe binds host-defining fields (target_triple), recipe_hash is a same-target identity; CI's cross-host determinism job gates on the manifest's six-field toolchain projection instead. recipe_hash says nothing about whether the bundles' outputs came out equal — that is the reproduced-output comparison's claim (below).
  • The manifest is a projection of env.json plus the recorded build inputs, not a rewrite. verify_bundle re-projects it from env.json, inputs_hashes.json, commands.json, cargo_metadata.json, and the recorded resolution policy at verification time and asserts byte-equality against the committed manifest; tampering with any input is caught.
  • All paths in SHA256SUMS use forward slashes, regardless of OS.
  • inputs_hashes.json records a defined source baseline, not "every file": the git-tracked sources of each in-scope Cargo package (resolved name → manifest directory via cargo metadata, never by treating the package name as a path) plus the workspace-control inputs (root Cargo.toml/Cargo.lock, rust-toolchain[.toml], and cert/), plus any inputs declared required in boundary.toml's [inputs] section — hashed even when git-ignored (e.g. generated code the build needs). Resolution fails closed: a missing package, a manifest escaping the workspace root, an in-scope crate with zero tracked files, a declared required input absent on disk, or a zero-input total fails cert/record generation, and verify rejects a bundle that declares in-scope packages yet records an empty baseline.
  • Bundle directories are prefixed with the profile name to prevent accidental submission of dev bundles as cert.
  • Existing bundle directories are never overwritten.

The four integrity/identity claims — when to compare which

A bundle makes four distinct, easily-confused claims. Compare the one that matches your question.

1. Bundle content integrity"have these bytes been tampered with since the tool wrote them?" Use content_hash when you need to attest that every recorded byte is unchanged. It is the integrity hash in the classical sense — an integrity re-check, not DO-178C verification independence — and is what sha256sum -c SHA256SUMS inside the bundle will attest to (subject to the index.json exclusion). The check answers "have these files been tampered with since this tool wrote them?", not "did this tool record the right hashes in the first place?" — see the Tool Qualification Level section below for the full independence discussion. Comparing content_hash across hosts is meaningless because the bundles legitimately record different host identities.

2. Recipe identity"do these two bundles declare the same build recipe?" Compare recipe_hash (SHA-256(deterministic-manifest.json)). The recipe covers the toolchain, target triple, profile, feature selection, locked dependency graph, command recipe, source-input digests, and resolution policy. Equal recipe_hash means same declared recipe — it does not prove the outputs reproduce. Because the recipe binds target_triple, this is a same-target comparison.

3. Cross-host recipe parity"did N hosts use the same toolchain?" Compare the six-field toolchain projection of deterministic-manifest.json (rustc, cargo, llvm_version, cargo_lock_hash, rust_toolchain_toml, rustflags). This is what CI's determinism-compare (five flavors, every push) and cross-time-determinism (PR vs. last main-branch build) gate on. Parity proves toolchain sameness across hosts and time — the one comparison that stays meaningful when target artifacts differ.

4. Reproduced-output equality"did the candidate actually reproduce the baseline's outputs?" Use evidence_core::verify::compare_reproduction(baseline, candidate). It compares three planes — canonical input digests, recipe fields, and output digests — and returns typed findings (InputChanged / InputMissing / InputUnverifiable, RecipeFieldChanged { field } / RecipeUnavailable, OutputChanged / OutputMissing / OutputExtra / OutputUnverifiable), deterministically sorted. The bundles are reproduction-equal iff the finding list is empty: same-source, same-recipe, same-target bundles compare equal only when their output digests also compare equal. Missing declared inputs or outputs and unverifiable artifacts surface as explicit non-success findings, never a panic or a silent pass.

Cross-time determinism and the Override-Deterministic-Baseline: protocol

Cross-host determinism (above) pins reproducibility across N hosts at a single commit. The complementary CI job cross-time-determinism pins the other axis: every PR's toolchain projection (rustc, cargo, llvm_version, cargo_lock_hash, rust_toolchain_toml, rustflags) must match the last successful main-branch build — OR the PR must explicitly acknowledge that it intentionally changed a reproducibility-affecting input.

Mechanism: the job downloads the last main-branch xhost-Linux artifact via gh run download, extracts deterministic-manifest.json, and hands both manifests to scripts/deterministic-baseline-override-lint.sh. The lint projects only the six toolchain fields (git_sha / git_branch / git_dirty / schema_version / profile are excluded — they differ between commits by construction, not because of drift) and compares canonicalized JSON.

When the projections match, the job passes silently. When they differ, the job requires a line of the form

Override-Deterministic-Baseline: <one-sentence reason>

Accepted locations (checked in order):

  1. The PR body (any line in the PR description).
  2. Any commit message in the PR's push range — the gate reads github.event.commits[*].message, so the override line in any real-work commit on the branch satisfies the check. This matters for merge-commit workflows (like this repo's) where the merge commit itself typically won't carry the override; for squash-merge workflows the single head commit is the only message and carries it alongside the PR body.

Without that line in either place, the job fails with the full projection diff + the expected override syntax.

Examples of legitimate override reasons:

Override-Deterministic-Baseline: bumped serde_json to 1.0.130 for CVE-NNNN-NNNN
Override-Deterministic-Baseline: added -C opt-level=3 to RUSTFLAGS
Override-Deterministic-Baseline: upgraded rust-toolchain pin to 1.96

If no prior main-branch artifact is available (fresh repo, or 14-day artifact retention expired), the job logs a warning and passes — the gate is best-effort, never user-hostile.

Known limitation: the live-compare gate detects per-PR drift but not slow cumulative drift across many individually-justified overrides. A committed historical-anchor baseline — pinning {git_sha → recipe_hash} for a curated set of milestone commits — would close that gap and is tracked as a follow-up.

Bundle integrity layers

A bundle's integrity is verifiable along three layers, each requiring strictly less trusted material than the next:

Layer What it covers Key required to verify Tool needed
1. Content Every hashed file in SHA256SUMS (env.json, deterministic-manifest.json, inputs/outputs hashes, commands.json, trace outputs, captured stdout/stderr, compliance reports) None sha256sum -c SHA256SUMS
2. Metadata index.json fields excluded from SHA256SUMS (git_sha, dal_map, test_summary, trace_outputs paths, schema versions, timestamps) The supplier's public ed25519 verifying key (32 bytes, hex) cargo evidence verify --verify-key <pubkey>
3. Provenance Non-repudiation: only the holder of the corresponding private signing key could have produced the matching BUNDLE.sig Same public key as Layer 2; the chain of trust is "did this public key sign this?" Same verify invocation

Why two cryptographic objects (SHA256SUMS and BUNDLE.sig) instead of one? index.json is deliberately excluded from SHA256SUMS because index.json records content_hash, which is SHA-256 (SHA256SUMS) — a self-referential cycle. The detached BUNDLE.sig covers a length-prefixed (SHA256SUMS, index.json) envelope, closing the metadata-layer tampering gap.

What BUNDLE.sig is not:

  • Not an HMAC. The verifier needs only the public key, not a shared secret. (Pre-0.1.5 versions used HMAC-SHA256 and called it a "signature" — that was a misnomer; it's now a real signature.)
  • Not an X.509 / PKI signature. The 32-byte public key is raw — distribute it however you like (commit to repo, attach to release, publish to a transparency log).
  • Not a SLSA L3 attestation. SLSA build-provenance integration is a separate roadmap item; the current signature covers integrity and non-repudiation only.

Generating and verifying:

# One-time: generate the project's ed25519 keypair. Refuses if a
# pair already exists; use `keygen --rotate --reason <text>` to
# replace one (rotation is appended to cert/KEY-ROTATION-LOG).
cargo evidence keygen
# → writes cert/signing.key (gitignored, mode 0600 on Unix)
# → writes cert/signing.pub (commit this; it's the public anchor)

# Sign during generate. Without --signing-key, the tool falls back
# to $EVIDENCE_SIGNING_KEY_PATH and then cert/signing.key in that
# order. Cert/Record profiles fail loud if no key resolves.
cargo evidence generate --out-dir /tmp/evidence

# Verify with public key. Without --verify-key, the tool falls
# back to $EVIDENCE_VERIFY_KEY_PATH and then cert/signing.pub.
cargo evidence verify /tmp/evidence/<bundle>

Anchor consistency. When cert/signing.pub is present, the public key derived from the signing key being used must match it byte-for-byte. Mismatch (SIGN_PUBKEY_ANCHOR_MISMATCH) refuses the generate run — the silent re-key defense: if a developer regenerates a keypair without going through cargo evidence keygen --rotate, the next generate catches the drift before the bundle ships.

Rotation audit trail. cargo evidence keygen --rotate --reason "<text>" appends one line to cert/KEY-ROTATION-LOG carrying the timestamp, new public key hex, and the reason. That file is the project's keypair-history-of-record and should be committed alongside the new cert/signing.pub.

Captured Output Normalization

Every file written by cargo evidence generate under the capture directory (tests/) has its line endings normalized to LF (\n) before being written to disk and hashed. This applies uniformly to cargo test stdout and stderr on every host.

Why: a Windows host running cargo test emits output with CRLF (\r\n) line endings; a Linux host emits LF. Without normalization, the same logical test run on two different hosts would produce different bytes on disk, different SHA256SUMS entries, and therefore different content_hash values — a cross-platform determinism leak that would defeat the evidence chain the tool is built around.

What's normalized: strict \r\n pairs collapse to a single \n. Lone \r bytes (e.g. cargo's Compiling foo\r progress spinners) are preserved, so legitimate carriage-return use is not corrupted. Lone \n bytes pass through unchanged.

What Windows users should expect: opening tests/cargo_test_stdout.txt in Notepad may render as one long line. Use VS Code, Notepad++, or any editor that handles Unix line endings. This is a deliberate, tool-wide invariant — there is no flag to opt out, and bundles from all three supported hosts are byte-comparable as a result.

What's not normalized: this rule applies only to captured subprocess text output. JSON files (index.json, env.json, *_hashes.json, commands.json) and SHA256SUMS are written by the tool itself and are LF-only by construction. Binary outputs recorded into outputs_hashes are hashed as-is — normalization would corrupt them and would not apply.


Commands Reference

cargo evidence check — the agent-facing verb

Agents and humans should call check as the default. It auto-detects whether the argument is a source tree or a bundle and dispatches:

cargo evidence check                 # source mode on current dir
cargo evidence check .               # same, explicit
cargo evidence check path/to/bundle  # bundle mode (auto-detected via SHA256SUMS)
cargo evidence check --mode=source . # force source mode
cargo evidence check --mode=bundle path/to/bundle  # force bundle mode
cargo evidence --format=jsonl check .              # streaming per-requirement diags

In source mode, check runs cargo test --workspace, parses outcomes, and emits one REQ_PASS / REQ_GAP / REQ_SKIP diagnostic per requirement in the discovered trace (cert/trace/ or cert/trace/). REQ_GAP events carry a FixHint for mechanically-fixable cases (missing UUID, empty traces_to under policy, dangling test_selector), and derived GAPs at higher layers carry root_cause_uid pointing at the primary failure.

In bundle mode, check is a passthrough to verify — same wire shape, same exit codes.

verify remains supported as the low-level primitive for CI scripts and bash pipelines that want a stable bundle-only surface without argument-shape inference. The evidence-mcp wrapper exposes check as one of six MCP tools (alongside rules, doctor, floors, diff, and ping); it does not expose verify because check in bundle mode already delegates to it.

cargo evidence generate

Generate a new evidence bundle.

cargo evidence generate --out-dir /tmp/evidence
cargo evidence generate --out-dir /tmp/evidence --profile cert
cargo evidence generate --out-dir /tmp/evidence --boundary cert/boundary.toml
cargo evidence generate --out-dir /tmp/evidence --trace-roots cert/trace
cargo evidence generate --out-dir /tmp/evidence --json --quiet
Flag Description
--out-dir <DIR> Output directory — must be outside tracked tree (required unless --write-workspace)
--profile <PROF> Override auto-detected profile (dev/cert/record)
--boundary <FILE> Path to boundary.toml (default: cert/boundary.toml)
--trace-roots <D> Comma-separated trace root directories
--write-workspace Write to evidence/ in workspace (xtask integration)
--quiet, -q Suppress non-error output
--json Output results as JSON

cargo evidence verify

Verify an existing evidence bundle.

cargo evidence verify /tmp/evidence/cert-20260207-143022Z-a1b2c3d4/
cargo evidence verify /tmp/evidence/cert-20260207-*/  --strict
cargo evidence verify /tmp/evidence/cert-20260207-*/  --json

Checks performed:

  1. Bundle directory exists and contains all required files
  2. index.json parses correctly; bundle-completeness cross-check:
    • bundle_complete is true iff tool_command_failures == [] (tamper signal: VERIFY_BUNDLE_INCOMPLETELY_CLAIMED);
    • on cert / record profile, tool_command_failures must be empty (VERIFY_TOOL_COMMANDS_FAILED_SILENTLY);
    • on dev profile, bundle_complete: false is allowed and surfaces as VERIFY_BUNDLE_INCOMPLETE (Warning, non-blocking) so snapshots of half-broken local builds remain inspectable.
  3. All trace outputs referenced in the index exist
  4. Every entry in SHA256SUMS matches the actual file hash
  5. index.json is NOT listed in SHA256SUMS (metadata-layer invariant)
  6. content_hash matches the SHA-256 of SHA256SUMS

Exit codes: 0 = pass, 1 = error, 2 = verification failure.

When cargo test (or any captured subprocess) exits non-zero during generate, the builder records a ToolCommandFailure { command_name, exit_code, stderr_tail } entry on the bundle's index.json, and bundle_complete flips to false. On cert / record profile this also propagates as a non-zero exit from generate itself (EXIT_VERIFICATION_FAILURE, 2), so automation sees the signal without parsing the bundle.

bundle_complete is the legacy single-bit rollup. Every bundle also records a completeness object on its index.json — one independently derived state (complete / incomplete / not_applicable / unverifiable) per assurance area: capture, graph_validity, verification, objective_mapping, review_approval, integrity, reproducibility, and tool_qualification. Each state derives only from generation-time facts; verification reflects the immediate post-generation verification outcome recorded before the signature seals the envelope, so a bundle the verifier rejects never reports verification completeness, and a state the generator cannot substantiate is never optimistically complete. verify --json reports the recorded states alongside the bundle's profile — the named claim context they were derived under — while verify itself checks integrity and internal consistency, which is a different claim than any single completeness state.### cargo evidence diff

Compare two evidence bundles across every assurance-relevant category.

cargo evidence diff ./evidence/bundle-a ./evidence/bundle-b
cargo evidence diff ./evidence/bundle-a ./evidence/bundle-b --json

The comparison walks the full assurance surface in a fixed order — scope (profile, DAL map, boundary/resolution policy, schema versions, git identity), trace_graph (entry-level by uid), tests (summary + per-test outcome rows), coverage (aggregates + capture-file presence), commands, recipe, inputs, outputs, objective_mappings (per-crate statuses + standards-pack identity), reviews_approvals, anomalies (recorded command failures), tool_identity, integrity, completeness_states, and content_hash — and marks each equal, added, removed, changed, or unverifiable with deterministic detail lines. Categories whose artifacts are missing report unverifiable with the reason (reviews/approvals are always unverifiable: they are workspace-corpus state, not bundle artifacts). The human report prints (no changes) only when every category compares equal, so unexamined content is never implied unchanged. Exit code stays 0 whether or not differences exist — diff reports, it does not judge.

cargo evidence init

Initialize evidence tracking for a new project.

cargo evidence init
cargo evidence init --force   # rewrite the managed template files (discards edits to them)

Creates the managed template set:

  • cert/boundary.toml — certification boundary configuration
  • cert/floors.toml — rigor-ratchet floors (zero baseline; ratchets up as you adopt)
  • cert/profiles/dev.toml, cert.toml, record.toml — profile configs
  • cert/trace/sys.toml, hlr.toml, llr.toml, tests.toml, derived.toml — schema-valid trace files with zero live entries; worked examples are commented out in each file so placeholder content can never enter a bundle

A fresh scaffold is adoption-incomplete by design: it parses against every schema immediately, cargo evidence doctor exits 0 (reporting DOCTOR_TRACE_NO_EVIDENCE as a warning), and cargo evidence trace --validate reports TRACE_EVIDENCE_EMPTY until you add real requirements — the intended pre-adoption signal, not a scaffold error. Re-running init preserves existing files (user edits included) and exits 0; --force rewrites exactly the managed set and nothing outside it. The printed next steps — edit the boundary, add real requirements, trace --backfill-uuids, trace --validate, doctor, generate — are executable as printed.

cargo evidence schema show

Print a JSON schema to stdout.

cargo evidence schema show index
cargo evidence schema show env
cargo evidence schema show commands
cargo evidence schema show hashes

cargo evidence schema validate

Validate a JSON file against its schema.

cargo evidence schema validate ./evidence/bundle/index.json
cargo evidence schema validate ./evidence/bundle/env.json

Auto-detects the schema type from the filename or file content.

cargo evidence floors — the ratchet

Enforce "rigor only goes up" across every dimension the tool ratchets. Reads cert/floors.toml, measures the current state, and reports per-dimension pass/fail:

cargo evidence floors         # human table, exit 0 if all ✓
cargo evidence floors --json  # machine-readable, deterministic

Dimensions currently tracked: diagnostic code count, terminal code count, per-layer trace entry counts (SYS/HLR/LLR/Test), #[test] fn count, library panics. Adding a dimension is a PR that lands the measurement helper in evidence_core::floors and the initial floor in cert/floors.toml; CI keeps the floor from falling.

Lowering a floor requires a Lower-Floor: <dimension> <reason> line in the PR body (or direct-push commit message). Without it, scripts/floors-lower-lint.sh fires in CI with FLOORS_LOWERED_WITHOUT_JUSTIFICATION. The friction is intentional: the ratchet only moves up.

Squash-merge caveat. GitHub's default "squash and merge" button DROPS the original PR body unless the committer hand-copies it into the squash commit message. If your PR lowers a floor, paste the Lower-Floor: line into the squash commit's extended description before merging — otherwise a post-merge dogfood run of the lint on the main branch would fail against the squashed commit's body. Projects that use merge-commits or rebase-and-merge preserve the PR body and are unaffected.

Using floors in your own project (no manual setup required for the default case). If your project has no cert/floors.toml, the subcommand emits a friendly "not configured" info line on stderr and exits 0 — non-adopters aren't forced into the gate. To opt in, drop a minimal cert/floors.toml in your repo:

# cert/floors.toml — pin whichever dimensions matter for your project
[floors]
test_count = 42          # `#[test]` fn count across crates/
diagnostic_codes = 10    # evidence_core::RULES.len() if you use it

[delta_ceilings]
# Reserved for delta-based gates (new dead-code allows, new library
# panics). Parsed today, enforced via a follow-up.

Only the dimensions you list are enforced — missing ones are skipped, not assumed-zero. Point at a custom path via cargo evidence floors --config path/to/floors.toml if your layout differs. Measurement helpers that need workspace subdirs (cert/trace/, crates/*/src/) gracefully degrade to 0 when the dirs are absent, so a single-crate project without a cert/trace/ directory can still enforce test_count and diagnostic_codes without configuring the other dimensions.

cargo evidence rules — what can the tool say?

Dump every diagnostic code the tool can emit as a deterministic JSON array (for agents / MCP) or a human-readable table:

cargo evidence rules --json  # machine-readable, stable shape
cargo evidence rules         # human table

Each entry carries code, severity, domain, has_fix_hint, and terminal. This is the self-describe endpoint evidence-mcp consumes; every code here is (a) backed by a DiagnosticCode::code() impl or by the TERMINAL_CODES / HAND_EMITTED_CLI_CODES sets, and (b) claimed by at least one LLR's emits list in cert/trace/llr.toml. Four bijection invariants in diagnostic_codes_locked fail CI if those relationships ever drift — adding a code without updating RULES or writing an owning LLR is not possible silently.

The --json wire shape is byte-locked against a committed fixture at crates/cargo-evidence/tests/fixtures/golden_rules.json. Intentional regeneration: tools/regen-golden-fixtures.sh.


For Auditors

This section is for certification auditors and Designated Engineering Representatives (DERs) evaluating cargo-evidence as a tool qualification candidate.

Certification Data Item Coverage

Data Item Status Notes
Software Configuration Index (SCI) Partial Git SHA + env fingerprint captured
Software Environment Config Index (SECI) Partial rustc, cargo, LLVM, libc, OS; missing Cargo.lock hash, RUSTFLAGS
Software Verification Results (SVR) Partial Per-test {name, module_path, passed, ignored, failure_message?} in tests/test_outcomes.jsonl; duration missing (libtest stable limitation)
Traceability Data Yes Bidirectional HLR <-> LLR <-> Test
SCM Records No Not yet implemented

Tool Qualification Level

  • Classification: Development tool + Verification tool.
  • Qualification status: Not qualified. The tool has not undergone DO-330 qualification; no Tool Qualification Plan, Tool Qualification Data, or independent assessor review exists. The TQL ceiling discussed in older internal notes (TQL-3 / TQL-5) was an aspirational target, not a current claim — it is retired here to avoid downstream confusion. The minimum viable qualification package (PSAC + SVVP + SVR + SCM Plan + SQA Plan + Qualification Report, signed by an independent DER) does not exist; a template-shaped placeholder for projects to fill in is in the 1.0 backlog (cert/DO-330-TEMPLATE/).
  • No independent-verification path today. The tool generates AND verifies its own bundles — they share the same binary. sha256sum -c SHA256SUMS from a separate utility re-checks integrity of the recorded hashes, but it cannot answer the auditor's actual question: did the tool-under-qualification record the correct hashes in the first place? That answer requires either (a) re-running this tool with the same inputs and comparing the resulting content_hash byte-for-byte (which is what the cross-host CI gate does internally — see .github/workflows/ci.yml), or (b) a separately-developed verifier with its own qualification story. Option (b) does not exist yet and is in the 1.0 backlog.

MC/DC coverage at DAL-A — currently unavailable

DO-178C DAL-A requires Modified Condition/Decision Coverage at the source- code level. Stable Rust does not currently expose MC/DC instrumentation: the -Zcoverage-options=mcdc nightly flag was removed upstream by rust-lang/rust#144999 (merged 2025-08-08). The tracking issue rust-lang/rust#124144 remains open with no active reimplementation.

Practical consequences for projects using this tool today:

  • A DAL-A project running cargo evidence generate --profile cert will produce a bundle whose compliance/<crate>.json reports DO-178C objective A7-10 (MC/DC coverage) as NotMet. The bundle's terminal can still be VERIFY_OK because the terminal does not read compliance verdicts — and branch coverage, an LLVM approximation, caps A7-9 at manual_review_required even at 100%. A careful auditor reads the A7-10 line and rejects the submission; a careless one signs off. This asymmetry is a known sharp edge — see the 0.2 backlog item "DAL-A fail-loud on missing MC/DC."
  • Projects pursuing actual DAL-A certification today need an auxiliary qualified MC/DC tool (LDRA, VectorCAST, Rapita) and must record its output by reference in their own qualification submission. This tool's bundle does not yet have a schema hook for that reference; that's also in the 0.2 backlog.

The CoverageLevel::Mcdc enum variant + the decisions: Vec<DecisionCoverage> and conditions: Vec<ConditionCoverage> per-file vectors in the bundle's coverage schema are forward-looking placeholders so the wire format can absorb rustc's future re-implementation without a breaking schema bump. They emit empty arrays today.

ADR-001: Evidence Engine Invariants

The six invariants that govern this tool's design:

# Invariant Status How enforced
1 Non-mutating PASS --out-dir required; never modifies source tree
2 Self-describing PASS index.json + SHA256SUMS in every bundle
3 Deterministic PASS content_hash excludes timestamps; BTreeMap ordering; two back-to-back runs on the same commit produce identical content_hash (gated by the dogfood Evidence (self) CI job)
4 Data-driven PASS TOML policy files in cert/
5 Offline-capable PASS Zero network calls; all operations are local
6 Cross-platform PASS Toolchain-projection parity across Linux/macOS/Windows is gated by the evidence-cross-host + determinism-compare CI jobs; content_hash differs by host by design (it binds env.json's host identity fields)

Known Limitations (P1 Items)

These items are tracked and not yet resolved:

  1. No derived requirements safety report -- derived LLRs are validated but no summary report is generated for safety analysis.

Resolved tracked items (kept for the record):

  • No structured SVR capture → per-test outcome atoms in tests/test_outcomes.jsonl via the enriched libtest parser (captures panic/assertion text from ---- <test> stdout ---- failure blocks). A-7 Obj-3/Obj-4 upgrade from Partial → Met when present + aggregate tests_passed == true.
  • No cryptographic signing → ed25519 detached signature over the (SHA256SUMS, index.json) envelope, hex-encoded as BUNDLE.sig. The verifying party needs only the supplier's 32-byte public key — no shared secret. See "Bundle integrity layers" below for the three-layer model (SHA-256 content / ed25519 metadata / non-repudiation).
  • No extra-file detectionverify.rs walks bundle and flags unexpected files
  • Incomplete SCI/SECICargo.lock hash, RUSTFLAGS, rust-toolchain.toml captured
  • engine_git_sha conflation / "unknown" fallbackbuild.rs captures the engine's own SHA via EVIDENCE_ENGINE_GIT_SHA env override (CI publish path uses ${GITHUB_SHA}) with git rev-parse HEAD as the second choice; a release-v<version> string is embedded when neither is available (crates.io tarball builds). engine_build_source in index.json records which branch fired, and verify rejects cert/record bundles whose provenance is "release" or "unknown".

Library Usage

The evidence-core crate can be used as a library for custom integration:

use evidence_core::{
    EvidenceBuilder, EvidenceBuildConfig, verify_bundle, VerifyResult,
    EnvFingerprint, Profile,
};
use std::path::PathBuf;

// Build configuration
let config = EvidenceBuildConfig {
    output_root: PathBuf::from("./evidence"),
    profile: "dev".to_string(),
    in_scope_crates: vec!["my-crate".to_string()],
    trace_roots: vec!["cert/trace".to_string()],
    require_clean_git: false,
    fail_on_dirty: false,
};

// Create a builder and generate a bundle
let builder = EvidenceBuilder::new(config)?;
builder.write_inputs()?;
builder.write_outputs()?;
builder.write_commands()?;
let bundle_path = builder.finalize("0.0.1", "0.0.3", vec![])?;

// Verify a bundle
match verify_bundle(&bundle_path)? {
    VerifyResult::Pass => println!("Bundle verified."),
    VerifyResult::Fail(reason) => eprintln!("Verification failed: {}", reason),
    VerifyResult::Skipped(reason) => println!("Skipped: {}", reason),
}

// Capture environment fingerprint
let env = EnvFingerprint::capture("dev", false)?;
println!("rustc: {}", env.rustc);
println!("LLVM: {:?}", env.llvm_version);

Traceability

use evidence_core::trace::{read_all_trace_files, validate_trace_links, generate_traceability_matrix};

// Read trace files from cert/trace/
let (hlr, llr, tests) = read_all_trace_files("cert/trace")?;

// Validate all links (UID format, ownership, derived rationale, orphans)
validate_trace_links(&hlr.requirements, &llr.requirements, &tests.tests)?;

// Generate a Markdown traceability matrix
let matrix = generate_traceability_matrix(&hlr, &llr, &tests, "TM-001")?;
std::fs::write("trace/matrix.md", matrix)?;

Reproducible Builds with Nix

A flake.nix is provided for fully reproducible build environments:

nix develop          # Enter the development shell
cargo evidence generate --out-dir ./evidence --profile cert

The Nix flake pins the exact Rust toolchain, ensuring that cert profile bundles are reproducible across machines.


Contributing

  1. Fork the repository and create a feature branch.
  2. First-time setup: run ./tools/install-hooks.sh once. This points the repo at .githooks/ so the pre-push hook runs scripts/local-ci.sh automatically.
  3. Before pushing, run ./scripts/local-ci.sh. It mirrors every cargo gate run by .github/workflows/ci.yml — including the full RUSTDOCFLAGS doc gate (-D rustdoc::broken_intra_doc_links -D warnings). Subset commands (cargo test or cargo clippy alone) are not a substitute; the local_ci_mirrors_workflow integration test pins the script's coverage against CI.
  4. Run cargo evidence generate --out-dir /tmp/evidence to verify the tool works end-to-end.
  5. Submit a pull request with a clear description of the change.

Project Structure

crates/
  evidence-core/     # Core library (types, trace, verify, compliance)
  cargo-evidence/    # Cargo subcommand binary
  evidence-mcp/      # MCP (Model Context Protocol) server binary
schemas/             # JSON schemas for bundle files
cert/                # Certification configuration (boundary, profiles, floors)
cert/trace/          # SYS / HLR / LLR / Test chain (this project's own trace)
tools/               # Repo utilities (install-hooks.sh, regen-golden-fixtures.sh)
scripts/             # CI mirror (local-ci.sh)

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages