evidence

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: Apache-2.0 Imports: 35 Imported by: 0

Documentation

Overview

Package evidence implements `canary evidence`: the commands that produce and accumulate the passing-test records `canary verify` consumes.

  • `evidence run-go-test` runs a Go toolchain's own `go test` itself -- the caller supplies only `go test` arguments, never the executable -- and emits evidence records for the tokens that declare the tests it observed pass. It is the only CLI producer allowed to label its output origin "executed", the trusted level `canary verify` requires by default, and only when the toolchain that ran was operator-named via --toolchain-path/evidence.toolchain_path (C6-01): a fixed, canary-resolved executable cannot be a fake event generator (C5-01), but WHICH executable also matters -- a toolchain merely resolved from the caller's own environment (GOROOT, PATH) is still caller-influenced and yields "imported" instead.
  • `evidence run` executes an arbitrary caller-supplied command and emits evidence records the same way, but because the executable is not canary's own, every record's origin is "imported" -- the same trust level as from-go-test.
  • `evidence from-go-test` turns a `go test -json` stream already on stdin into evidence records -- origin "imported", since the stream was produced by something other than this command.
  • `evidence ingest` strictly validates a produced file and merges it into the project's evidence store.

FinalizeGoTestSnapshotContext is a separate protected-host library boundary: it requires an independently authenticated execution receipt and a host-only signer. It is not exposed through these commands and does not upgrade stdin imports into executed evidence.

None of these commands ever invents a record: run, run-go-test, and from-go-test only emit records for tests that actually reported Action="pass", and ingest refuses any file that does not satisfy the record grammar in pkg/evidence.

Index

Constants

View Source
const DefaultStorePath = ".canary/evidence.json"

DefaultStorePath is the evidence store's path relative to the project root.

View Source
const MaxManagedGoTestBytes = 64 << 20

MaxManagedGoTestBytes bounds one authenticated output supplied to finalization. The host executor must enforce its own capture limits before constructing it.

View Source
const MaxStderrBytes = 1 << 20

MaxStderrBytes bounds producer diagnostics at the child pipe, before callers can accumulate them. Overflow cancels the run and cannot produce evidence.

View Source
const MaxStreamBytes = 512 << 20

MaxStreamBytes bounds the total go-test event stream. Beyond this the stream is rejected before any evidence is derived (resource exhaustion and truncation are indistinguishable from tampering at this layer).

Variables

View Source
var ErrStderrLimit = errors.New("evidence: stderr output limit exceeded")
View Source
var ErrStdoutLimit = errors.New("evidence: stdout output limit exceeded")

ErrStdoutLimit identifies a rejected raw event stream before evidence derivation.

View Source
var EvidenceCmd = &cobra.Command{
	Use:   "evidence",
	Short: "Produce and accumulate passing-test evidence records",
	Long: `Produce and accumulate the evidence records that 'canary verify' consumes.

Subcommands:
  run-go-test    Run 'go test' itself; emit evidence (stdout), origin "executed"
  run            Execute an arbitrary command itself; emit evidence (stdout), origin "imported"
  from-go-test   Map a 'go test -json' stream to evidence records (stdout), origin "imported"
  ingest         Validate an evidence file and merge it into the store`,

	PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return nil },
}

EvidenceCmd is the `canary evidence` command group.

Functions

func Merge

func Merge(current, incoming []ev.Record) []ev.Record

Merge appends the records of incoming that the store does not already hold verbatim, preserving the store's existing order. Two records are the same record only when every field matches: a differing digest, timestamp, runner, or argv vector describes a different observation and is kept. Record carries an Argv slice, so it is no longer comparable; dedup keys on string(json.Marshal(rec)) instead, which is deterministic because struct fields always encode in declaration order.

func RunExec added in v0.3.5

func RunExec(opts FromGoTestOptions, argv []string, stdout, stderr io.Writer) int

RunExec executes argv itself (Dir=opts.Root), captures its stdout bounded by MaxStreamBytes, lets its stderr flow through to stderr, derives HEAD/dirty state exactly like RunFromGoTest, then emits evidence records. argv is entirely caller-controlled -- a shell, a fake event generator, anything -- so canary having run it proves nothing about what actually happened; every emitted record's origin is therefore "imported" and no artifact is retained (C5-01). Only 'canary evidence run-go-test' (RunGoTest below), whose executable is fixed to canary's own resolved Go toolchain, may label output "executed".

func RunFromGoTest

func RunFromGoTest(opts FromGoTestOptions, stdin io.Reader, stdout, stderr io.Writer) int

RunFromGoTest reads a `go test -json` stream from stdin and writes an evidence file to stdout, returning the process exit code. Every record's origin is "imported": the stream was produced by something other than this command, so it is trusted only as far as `canary verify --allow-imported` permits.

func RunGoTest added in v0.3.6

func RunGoTest(opts FromGoTestOptions, testArgs []string, stdout, stderr io.Writer) int

RunGoTest runs a Go toolchain's own 'go test' -- resolved by resolveGoToolchain, never a caller-supplied executable -- and emits evidence records with origin "executed" only when that resolution was TRUSTED: an operator-named toolchain via opts.ToolchainPath (--toolchain-path, overriding config evidence.toolchain_path) or opts.ToolchainDigest (--toolchain-digest, overriding config evidence.toolchain_digest) for integrity. Canary running the real test runner is necessary for "executed" evidence but not sufficient (C5-01); the toolchain that ran must also not have been selectable by the caller (C6-01). When no operator-named toolchain is configured, resolution falls back to GOROOT/bin/go or a PATH lookup for convenience -- but both of those honor values the caller's own environment supplies (runtime.GOROOT()'s GOROOT env, exec.LookPath's PATH), so the emitted records are labeled "imported" instead: a caller-influenced toolchain resolution is exactly the caller-controlled executable this producer otherwise refuses to trust.

The "executed" guarantee rests on three things, not just a fixed argv[0]:

  1. argv is fixed as []string{goBin, "test"} + ensureJSON(testArgs); the caller's testArgs can never occupy argv[0] or argv[1], so no shape of testArgs can substitute a different executable. When testArgs is empty it defaults to ["-count=1", "-json", "./..."]; otherwise "-json" is appended unless already present.
  2. testArgs is scanned and rejected (rejectsUntrustedExecFlags) for -exec/-toolexec/-overlay in any dash or `=`-value form before argv is even built: `go test -exec <wrapper>` runs a caller-chosen wrapper "in place of" the compiled test binary, and `go`'s own -json machinery turns that wrapper's stdout into legitimate-looking pass events for tests that never compiled or ran -- while argv[0]/argv[1] stay "go"/"test". -toolexec substitutes build tools the same way, and -overlay redirects source file paths to attacker-chosen backing content at build time: `go test` genuinely compiles and runs, but the code that ran need not match anything committed -- the same forgery class under a different flag. None of the three may reach `go test` from testArgs.
  3. the child's environment has GOFLAGS and GOENV neutralized -- execCapture's clearGoflags builds the child env with every GOFLAGS= entry from this process's own environment removed and one explicit empty GOFLAGS= appended, so an ambient GOFLAGS=-exec=... that testArgs scanning alone cannot see can't smuggle the same substitution in through go test's own env handling. That empty GOFLAGS= env var does NOT by itself override a GOFLAGS value persisted via `go env -w GOFLAGS=...`: `go` merges argv, the GOFLAGS env var, AND the `go env` config file (located at $GOENV, or a default per-user path) every time it runs, and an empty env var does not erase a value the file itself holds. So the same env construction also sets GOENV=off (see scrubGoenv), which stops the child from reading that config file at all -- closing the gap an empty GOFLAGS= alone leaves open. It also drops any GOROOT= entry (C6-01): argv[0] is already fixed to goBin by the time the child starts, so a caller's GOROOT cannot change WHICH toolchain runs, but scrubbing it keeps the child's own idea of its toolchain root consistent with the resolved goBin rather than whatever the caller's environment claimed.

The raw stream is retained as a digest-named artifact under <root>/.canary/artifacts/ BEFORE being parsed, and the retained artifact's own bytes -- read back from disk, not the in-memory buffer -- are the sole source parsed into records: what 'canary verify' can later re-derive from the artifact is exactly what produced this run's evidence, not a buffer that could have been mutated between capture and parse. Every record also carries the exact argv vector that ran (Argv) and the process's exit status (RunExitStatus).

func RunGoTestContext added in v0.3.9

func RunGoTestContext(ctx context.Context, opts FromGoTestOptions, testArgs []string, stdout, stderr io.Writer) int

RunGoTestContext cancels execution and refuses to publish cancelled evidence.

func RunIngest

func RunIngest(in, out string, stderr io.Writer) int

RunIngest validates the evidence file at in and merges it into the store at out, returning the process exit code (0 accepted, 1 rejected). Nothing is written when the input is rejected. Equivalent to RunIngestWithOptions(in, out, IngestOptions{}, stderr) -- no attestation gating, the pre-Task-6 behavior every existing caller still gets by default.

func RunIngestContext added in v0.3.9

func RunIngestContext(ctx context.Context, in, out string, opts IngestOptions, stderr io.Writer) int

RunIngestContext checks cancellation during validation and before publication.

func RunIngestWithOptions added in v0.3.7

func RunIngestWithOptions(in, out string, opts IngestOptions, stderr io.Writer) int

RunIngestWithOptions is RunIngest with the attestation gate (Plan-8 Task 6, C6-01b) available: when opts.requireAttestation() is true, an origin:"executed" record is demoted to "imported" not only when it fails the existing artifactProvesRecord check, but ALSO when it fails gotest.AttestationValid against opts.TrustedKeys -- closing the gap a forgeable local artifact alone left open (a workspace-writer can fabricate both a hand-authored "executed" record AND a matching local artifact; it cannot fabricate a signature from a key it does not hold).

func RunKeygen added in v0.3.7

func RunKeygen(privPath, pubPath string, stdout, stderr io.Writer) int

RunKeygen generates a fresh ed25519 keypair and writes the private key (PEM PKCS8) to privPath and the public key (PEM PKIX) to pubPath -- defaulting pubPath to privPath with its extension replaced by ".pub" when empty. Returns the process exit code.

func WriteStore

func WriteStore(path string, records []ev.Record) error

WriteStore writes records to path atomically (staged in the same directory, fsynced, then renamed), creating the directory when needed. A crash mid-write therefore leaves the previous store intact rather than a truncated one. The store is this command's own output, so replacing it is intended; only a partial write would be a defect.

func WriteStoreContext added in v0.3.9

func WriteStoreContext(ctx context.Context, path string, records []ev.Record) error

WriteStoreContext preserves the old store if cancellation is observed before atomic publication. Cancellation after publication cannot undo a receipt.

Types

type FromGoTestOptions

type FromGoTestOptions struct {
	Root      string
	ProjectID string
	// Commit, when non-empty, is an optional cross-check against Root's
	// derived HEAD -- the actual commit every record binds to is always
	// derived, never taken from this field on trust.
	Commit string
	// AllowDirty accepts a working tree that does not match HEAD. Without
	// it, RunFromGoTest refuses: evidence stamped with HEAD would otherwise
	// describe a tree that exists at no commit.
	AllowDirty bool
	Runner     string
	ObservedAt string
	// ToolchainPath and ToolchainDigest are consumed only by RunGoTest
	// (C6-01): an operator-named toolchain -- ToolchainPath (the
	// --toolchain-path flag, overriding config evidence.toolchain_path) -- is
	// the ONLY source of trusted, origin:"executed" evidence; see
	// resolveGoToolchain. ToolchainDigest (the --toolchain-digest flag,
	// overriding config evidence.toolchain_digest) is an integrity check on
	// whichever toolchain resolves. RunFromGoTest and RunExec ignore both.
	ToolchainPath   string
	ToolchainDigest string
	// SignKeyPath is consumed only by RunGoTest (Plan-8 Task 6, --sign-key
	// flag, falling back to config evidence.sign_key when empty): when
	// non-empty AND the run resolved a trusted (operator-named) toolchain
	// (origin "executed"), RunGoTest signs an attest.Attestation describing
	// the run, writes it as a sidecar alongside the retained artifact, and
	// stamps every emitted record's ProducerIdentity. A run whose toolchain
	// resolution was untrusted (origin "imported") is never signed --
	// signing something already labeled "imported" would misleadingly
	// suggest cryptographic backing for evidence this producer itself does
	// not trust as executed. Relative signing-key paths resolve against Root,
	// matching verification's trusted-key profile path semantics.
	SignKeyPath string
}

FromGoTestOptions are the resolved inputs of one from-go-test run.

type GoTestPreflight added in v0.3.9

type GoTestPreflight struct {
	Status          string `json:"status"`
	Reason          string `json:"reason"`
	NextAction      string `json:"next_action"`
	ToolchainPath   string `json:"toolchain_path,omitempty"`
	ToolchainDigest string `json:"toolchain_digest,omitempty"`
	TrustQualified  bool   `json:"trust_qualified"`
}

GoTestPreflight is an observation in the caller's execution environment, not an authorization, attestation or promise that a later invocation works. In particular, a workspace configuration is not proof of operator authority.

func PreflightGoTestContext added in v0.3.9

func PreflightGoTestContext(ctx context.Context, opts FromGoTestOptions) (GoTestPreflight, error)

PreflightGoTestContext inspects configured producer availability without spawning a process, creating state, or resolving an ambient fallback as a trusted producer. RunGoTestContext rechecks resolution when actually invoked. Managed callers must perform this inside the admitted execution environment; a host-side observation says nothing about a separate realm filesystem.

type GoTestResult added in v0.3.9

type GoTestResult struct {
	// CaptureError preserves a launch, cancellation, I/O or output-budget failure
	// from child capture. It is nil before capture and after a completed capture;
	// it does not classify later evidence derivation or verification failures.
	CaptureError  error
	ExitCode      int
	RunExitStatus *int
	// Run is nil until a completed capture has a stable source identity and
	// its retained artifact has been read back. It can survive a later
	// derivation, signing or output failure; it never implies command success.
	Run *GoTestRunIdentity
}

GoTestResult separates evidence-command completion from the child test run. RunExitStatus is nil when no completed run was observed, including launch refusal and cancelled or output-limited capture. It is available even when the retained stream yields no evidence records. Neither status is a verdict.

func RunGoTestResultContext added in v0.3.9

func RunGoTestResultContext(ctx context.Context, opts FromGoTestOptions, testArgs []string, stdout, stderr io.Writer) GoTestResult

RunGoTestResultContext runs the same producer as RunGoTestContext and also returns its observed process outcome independently of record cardinality.

type GoTestRunIdentity added in v0.4.2

type GoTestRunIdentity struct {
	ProjectID        string   `json:"project_id"`
	CommitSHA        string   `json:"commit_sha"`
	SourceDigest     string   `json:"source_digest"`
	ArtifactDigest   string   `json:"artifact_digest"`
	ToolchainPath    string   `json:"toolchain_path"`
	ToolchainDigest  string   `json:"toolchain_digest"`
	Argv             []string `json:"argv"`
	Origin           string   `json:"origin"`
	Dirty            bool     `json:"dirty"`
	ObservedAt       string   `json:"observed_at"`
	ProducerIdentity string   `json:"producer_identity"`
}

GoTestRunIdentity describes an observed producer run independently of how many requirement records it yields. It is metadata, not a verification verdict or proof that ingest/publication completed. SourceDigest uses the canonical scan identity; ArtifactDigest names the captured test stream. Origin retains the normal executed/imported distinction. ProducerIdentity is empty unless this run actually signed evidence; a configured signer alone does not populate it. Dirty retains the evidence producer's conservative dirty marking.

type IngestOptions added in v0.3.7

type IngestOptions struct {
	// RequireAttestation, OR a non-empty TrustedKeys (even without the flag
	// -- config evidence.trusted_keys being configured at all is itself a
	// signal the operator wants attestation-backed trust), turns on the
	// attestation gate below.
	RequireAttestation bool
	TrustedKeys        string
}

IngestOptions are the attestation-related inputs of one ingest run (Plan-8 Task 6, C6-01b). The zero value reproduces pre-Task-6 behavior exactly: RequireAttestation false and TrustedKeys empty never evaluate attestations at all.

type IngestResult added in v0.3.9

type IngestResult struct {
	ExitCode           int
	RecordsIngested    int
	EligibleCandidates int
	DroppedDirty       int
	DroppedImported    int
	DemotedArtifact    int
	DemotedUnattested  int
	Records            []ev.Record
}

IngestResult reports this input's post-policy records after successful publication, excluding existing history. RecordsIngested includes duplicates already retained. Eligibility here is not a current source verdict.

func RunIngestResultContext added in v0.3.9

func RunIngestResultContext(ctx context.Context, in, out string, opts IngestOptions, stderr io.Writer) IngestResult

RunIngestResultContext is the typed form of RunIngestContext. Unsuccessful publication returns no successful-ingest counts or records.

type ManagedGoTestEvidence added in v0.3.23

type ManagedGoTestEvidence struct {
	Store, Artifact, Attestation []byte
	ArtifactDigest               string
	SourceDigest                 string
	RunExitStatus                int
	RecordCount                  int
	Run                          *GoTestRunIdentity
}

ManagedGoTestEvidence owns the complete publication inputs. A caller must recheck its grant and current target generation before publishing these bytes. Store uses the normal v4 evidence format; Artifact and Attestation use the normal digest-derived paths. No filesystem publication occurs in finalization.

func FinalizeGoTestSnapshotContext added in v0.3.23

func FinalizeGoTestSnapshotContext(ctx context.Context, rootLabel string, snapshot *canaryscan.WorkspaceSnapshot, raw []byte,
	signed SignedManagedGoTestReceipt, trust ManagedGoTestTrust, signer ManagedGoTestSigner, allowDirty bool,
) (ManagedGoTestEvidence, error)

FinalizeGoTestSnapshotContext authenticates a protected execution receipt, derives evidence exclusively from its immutable snapshot and exact captured output, then requests host-only signing. It has no CLI/MCP entry point and no environment, Git, live workspace, toolchain or key-file fallback. rootLabel is only a logical canonical scanner path. It neither executes nor authenticates a realm itself: the protected receipt adapter must establish those facts.

Raw stdout is copied before callbacks run and must not be concurrently mutated during this call. Failure returns no publication bytes, including when a grant is revoked by the signer. Zero records remain a typed observed process result, not an inference of test success. Dirty evidence requires explicit admission and remains dirty under the existing canonical eligibility rules.

type ManagedGoTestReceipt added in v0.3.23

type ManagedGoTestReceipt struct {
	Version         int      `json:"version"`
	Scope           string   `json:"scope"`
	ArchiveDigest   string   `json:"archive_digest"`
	ArchivePrefix   string   `json:"archive_prefix"`
	CommitSHA       string   `json:"commit_sha"`
	Clean           bool     `json:"clean"`
	Argv            []string `json:"argv"`
	ToolchainDigest string   `json:"toolchain_digest"`
	StdoutDigest    string   `json:"stdout_digest"`
	StdoutBytes     int64    `json:"stdout_bytes"`
	RunExitStatus   int      `json:"run_exit_status"`
	IssuedAt        int64    `json:"issued_at_unix_milli"`
	ExpiresAt       int64    `json:"expires_at_unix_milli"`
}

ManagedGoTestReceipt is a protected host's assertion about a completed run. Signing it requires independently verifying execution, toolchain/image pins, exact immutable execution input, process outcome, output and teardown. Merely receiving JSON from a worker does not establish any of those facts. CommitSHA and Clean must come from comparison with a protected pinned Git baseline. Scope identifies one separately authorized invocation, not a workspace name.

type ManagedGoTestSigner added in v0.3.23

type ManagedGoTestSigner struct {
	PublicKey ed25519.PublicKey
	Sign      func(context.Context, attest.Attestation) (string, error)
}

ManagedGoTestSigner holds host authority, never a workspace key-file path. Sign must honor cancellation and recheck any deployment/key revocation. The returned signature is verified against the separately protected PublicKey.

type ManagedGoTestTrust added in v0.3.23

type ManagedGoTestTrust struct {
	Scope           string
	PublicKey       ed25519.PublicKey
	Argv            []string
	ToolchainDigest string
	Authorize       func(context.Context) error
}

ManagedGoTestTrust is protected invocation state. None of it may be selected by the receipt, a tool argument, workspace configuration, or worker output. Argv and ToolchainDigest are the exact admitted command and toolchain pin. Authorize rechecks the live grant, including any one-invocation replay rules; this stateless library does not keep a used-receipt database.

type SignedManagedGoTestReceipt added in v0.3.23

type SignedManagedGoTestReceipt struct {
	Receipt   ManagedGoTestReceipt `json:"receipt"`
	Signature []byte               `json:"signature"`
}

func SignManagedGoTestReceipt added in v0.3.23

func SignManagedGoTestReceipt(receipt ManagedGoTestReceipt, private ed25519.PrivateKey) (SignedManagedGoTestReceipt, error)

SignManagedGoTestReceipt is only for a protected execution adapter after all receipt assertions have been verified. It does not run tests or confer trust. Receipt authority and the final producer signing key are independent; neither private key may be made available to workspace code or the test process.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL