Certification evidence generation, verification, and traceability for Rust projects.
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.
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
mainwithout 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_versionfield 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 differentcargo-evidencereleases can both reportschema_version: "0.0.1"while being structurally incompatible. Pin acargo-evidenceversion (and readindex.json'sengine_crate_version) instead of pattern-matching onschema_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.
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 deterministic_hash parity |
deterministic_hash 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 ofSHA256SUMS, covers every recorded byte, necessarily host-specific becauseenv.jsonrecords host identity) anddeterministic_hash(the SHA-256 of a committeddeterministic-manifest.json, which is a projection ofenv.jsondown to toolchain + target + source identity). deterministic_hashparity is gated in CI across five flavors (3 native + 2 Nix) via thedeterminism-comparejob: every push runs the generator on Linux, macOS, and Windows native plus Linux and macOS undernix develop, and asserts all fivedeterministic_hashvalues are byte-equal. If any flavor 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. Seedeterminism-comparein.github/workflows/ci.yml.content_hashstill 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 isdeterministic_hash.
What this matrix does not claim:
- Passing CI tests and matching
deterministic_hashprove 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
deterministic_hashagainst the Linux reference when you need to confirm reproducibility; don't comparecontent_hashacross hosts (it will always differ). - Pin a commit SHA, because the formats may still change.
Get your first evidence bundle in under 30 seconds:
# Install
cargo install cargo-evidence
# 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, profile templates, and example trace files. The generate
command produces a complete evidence bundle. The verify command checks every
SHA-256 hash in the bundle and confirms structural integrity.
--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.
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:
- If
NAV_RECORDis set -->record - If
IN_NIX_SHELLis set ANDCI/GITHUB_ACTIONSis set -->cert - Otherwise -->
dev
You can always override with --profile dev|cert|record.
Key differences:
devlets you iterate fast -- dirty git trees and missing tools are tolerated.certenforces clean git, requires all toolchain components on PATH, and produces bundles suitable for certification data item submission.recordis identical tocertin strictness but is triggered by a dedicated environment variable for hardware test recording workflows.
Every bundle is a self-contained directory with deterministic content:
evidence/cert-20260207-143022Z-a1b2c3d4/
index.json # Bundle metadata, schema versions, content_hash + deterministic_hash
env.json # Environment fingerprint (rustc, cargo, LLVM, libc, OS, tools, …)
deterministic-manifest.json # Cross-host-stable projection of env.json (toolchain + target + source)
inputs_hashes.json # SHA-256 hashes of all source inputs
outputs_hashes.json # SHA-256 hashes of all build outputs
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:
SHA256SUMScovers every content-layer file, includingenv.jsonanddeterministic-manifest.json.index.jsonis metadata-layer and excluded fromSHA256SUMS, so timestamps do not affect either hash.content_hashinindex.jsonisSHA-256(SHA256SUMS)— the full-fidelity integrity hash. It differs per host becauseenv.jsonrecords host identity (host.os, libc, tool availability).deterministic_hashinindex.jsonisSHA-256(deterministic-manifest.json)— the cross-host reproducibility contract. Two runs on the same commit with the samerust-toolchain.tomland the same--targetproduce the samedeterministic_hashregardless of host. This is what CI's cross-host determinism job gates on.- The manifest is a projection of
env.json, not a rewrite.verify_bundlere-projectsenv.jsonat verification time and asserts byte-equality against the committed manifest; tampering with either side is caught. - All paths in
SHA256SUMSuse forward slashes, regardless of OS. - Bundle directories are prefixed with the profile name to prevent
accidental submission of
devbundles ascert. - Existing bundle directories are never overwritten.
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.
Use deterministic_hash when you need to confirm that two bundles
represent the same logical build (same commit, same toolchain,
same target). It is the cross-host reproducibility channel. Our CI
cross-host job asserts deterministic_hash parity across Linux,
macOS, and Windows on every push.
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):
- The PR body (any line in the PR description).
- 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 → deterministic_hash} for a curated set of milestone
commits — would close that gap and is tracked as a follow-up.
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.
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.
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 diagsIn 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.
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 |
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-*/ --jsonChecks performed:
- Bundle directory exists and contains all required files
index.jsonparses correctly; bundle-completeness cross-check:bundle_completeistrueifftool_command_failures == [](tamper signal:VERIFY_BUNDLE_INCOMPLETELY_CLAIMED);- on
cert/recordprofile,tool_command_failuresmust be empty (VERIFY_TOOL_COMMANDS_FAILED_SILENTLY); - on
devprofile,bundle_complete: falseis allowed and surfaces asVERIFY_BUNDLE_INCOMPLETE(Warning, non-blocking) so snapshots of half-broken local builds remain inspectable.
- All trace outputs referenced in the index exist
- Every entry in
SHA256SUMSmatches the actual file hash index.jsonis NOT listed inSHA256SUMS(metadata-layer invariant)content_hashmatches the SHA-256 ofSHA256SUMS
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.
Compare two evidence bundles.
cargo evidence diff ./evidence/bundle-a ./evidence/bundle-b
cargo evidence diff ./evidence/bundle-a ./evidence/bundle-b --jsonShows added, removed, and changed files in both input and output hash sets, plus metadata changes (profile, git SHA, branch, dirty state).
Initialize evidence tracking for a new project.
cargo evidence init
cargo evidence init --force # overwrite existing cert/ directoryCreates:
cert/boundary.toml-- certification boundary configurationcert/profiles/dev.toml,cert.toml,record.toml-- profile configscert/trace/hlr.toml,llr.toml-- example trace files
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 hashesValidate a JSON file against its schema.
cargo evidence schema validate ./evidence/bundle/index.json
cargo evidence schema validate ./evidence/bundle/env.jsonAuto-detects the schema type from the filename or file content.
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, deterministicDimensions 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.
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 tableEach 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.
This section is for certification auditors and Designated Engineering
Representatives (DERs) evaluating cargo-evidence as a tool qualification
candidate.
| 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 |
- 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 SHA256SUMSfrom 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 resultingcontent_hashbyte-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.
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 certwill produce a bundle whosecompliance/<crate>.jsonreports DO-178C objective A7-10 (MC/DC coverage) asNotMet. The bundle's terminal can still beVERIFY_OKbecause branch coverage was met. 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.
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 | deterministic_hash parity across Linux/macOS/Windows is gated by the evidence-cross-host CI job; content_hash differs by host by design (it binds env.json's host identity fields) |
These items are tracked and not yet resolved:
- No derived requirements safety report -- derived LLRs are validated but no summary report is generated for safety analysis.
Previously tracked items now resolved:
No structured SVR capture→ per-test outcome atoms intests/test_outcomes.jsonlvia 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 + aggregatetests_passed == true.No cryptographic signing→ ed25519 detached signature over the(SHA256SUMS, index.json)envelope, hex-encoded asBUNDLE.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 detection→verify.rswalks bundle and flags unexpected filesIncomplete SCI/SECI→Cargo.lockhash,RUSTFLAGS,rust-toolchain.tomlcaptured→engine_git_shaconflation /"unknown"fallbackbuild.rscaptures the engine's own SHA viaEVIDENCE_ENGINE_GIT_SHAenv override (CI publish path uses${GITHUB_SHA}) withgit rev-parse HEADas the second choice; arelease-v<version>string is embedded when neither is available (crates.io tarball builds).engine_build_sourceinindex.jsonrecords which branch fired, andverifyrejects cert/record bundles whose provenance is"release"or"unknown".
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);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)?;A flake.nix is provided for fully reproducible build environments:
nix develop # Enter the development shell
cargo evidence generate --out-dir ./evidence --profile certThe Nix flake pins the exact Rust toolchain, ensuring that cert profile
bundles are reproducible across machines.
- Fork the repository and create a feature branch.
- First-time setup: run
./tools/install-hooks.shonce. This points the repo at.githooks/so thepre-pushhook runsscripts/local-ci.shautomatically. - Before pushing, run
./scripts/local-ci.sh. It mirrors every cargo gate run by.github/workflows/ci.yml— including the fullRUSTDOCFLAGSdoc gate (-D rustdoc::broken_intra_doc_links -D warnings). Subset commands (cargo testorcargo clippyalone) are not a substitute; thelocal_ci_mirrors_workflowintegration test pins the script's coverage against CI. - Run
cargo evidence generate --out-dir /tmp/evidenceto verify the tool works end-to-end. - Submit a pull request with a clear description of the change.
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)
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
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.