verifier

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package verifier implements `aicr evidence verify`: offline verification of a recipe-evidence v1 bundle produced by `aicr validate --emit-attestation`. Five steps run:

  1. Materialize — resolve the input (directory / pointer file / OCI reference) to a bundle root on disk. Pointer and OCI forms pull the artifact via ORAS, then discover the Sigstore Bundle attached as an OCI Referrer and stage it as attestation.intoto.jsonl so the signature step finds it on disk the same way it does for directory input.
  2. Signature verify — when attestation.intoto.jsonl is present, sigstore-go verifies the DSSE-wrapped in-toto Statement against the Sigstore trusted root (Fulcio cert chain, optional Rekor entry). The cryptographically anchored predicate body is extracted from the verified payload.
  3. Predicate parse — use the verified predicate when the signature step produced one; otherwise fall back to the unsigned statement.intoto.json (self-consistency only).
  4. Manifest hash check — sha256(manifest.json) must match predicate.Manifest.Digest, and every file the manifest names must match its recorded sha256. Together these transitively bind every bundled file to the (now signature-anchored) predicate.
  5. Render — Markdown / JSON; surfaces signer identity, fingerprint, phase counts, and BOM info.

The trust chain when a signature is present:

Sigstore trusted root → Fulcio cert (Rekor-logged)
  → DSSE-signed Statement
    → predicate.Manifest.Digest
      → manifest.json
        → every bundled file's sha256

Tampering anywhere below the signature breaks the chain. The OCI input form adds a freshness check: the signed Statement's subject digest is locked to the pulled artifact's OCI manifest digest, so a substituted artifact paired with a stale signature fails too.

See docs/design/007-recipe-evidence.md for the full trust model.

Index

Constants

View Source
const (
	ExitValidPassed        = 0
	ExitValidPhaseFailures = 1
	ExitInvalid            = 2
)

Exit codes returned by Verify in VerifyResult.Exit. The CLI maps these to OS exit codes via pkg/errors error codes.

View Source
const (
	CauseRegistryForbidden = "registry-forbidden" // 401/403 pulling the bundle
	CauseNotFound          = "not-found"          // 404 — bundle/referrer absent at ref
	CauseRegistry          = "registry"           // other registry/transport failure
	CauseSignature         = "signature"          // signature/cert/identity verification failed
	CauseIntegrity         = "integrity"          // manifest hash-chain mismatch
	CauseSchema            = "schema"             // predicate parse / schema / type error
	CauseUnknown           = "unknown"            // unclassified
)

Failure-cause classes recorded in FailureCause.Class. Stable strings so the gate script (and future tooling) can branch on them without parsing human prose. They classify *why* a verification failed, complementing the coarse Exit code.

View Source
const AllowlistFileName = "allowlist.yaml"

AllowlistFileName is the basename of the signer allowlist at the evidence root. It is the one file at the root that is not a recipe directory.

View Source
const EvidenceDirName = "recipes/evidence"

EvidenceDirName is the repo-relative root of the committed evidence tree.

Variables

View Source
var ErrUnsignedBundle = errors.New(errors.ErrCodeNotFound, "no signature attached (unsigned bundle)")

ErrUnsignedBundle is returned by VerifySignature when no Sigstore Bundle file is present in the bundle directory. Callers translate this to a Skipped step row.

Functions

func CrossCheckPointerSigner

func CrossCheckPointerSigner(claimed *attestation.PointerSigner, actual *SignerClaims) error

CrossCheckPointerSigner compares the verified signer claims against what the pointer file claimed. Returns nil when the pointer makes no signer claim, when there's no actual signer to compare against, or when every claimed field matches. A mismatch produces an error that names the specific field and both sides — the verifier surfaces it so a malicious pointer that names a different signer than the actual bundle fails loudly.

func DiscoverPointers added in v0.16.0

func DiscoverPointers(root, recipe string) ([]string, error)

DiscoverPointers returns, sorted, the per-source pointer files for one recipe under root, i.e. the glob <root>/<recipe>/<source>/*.yaml (issue #1347 Option A). Each match is an immutable single-attestation V1 pointer; callers iterate over the set and aggregate across sources rather than assuming a single fixed-path file. A recipe with no committed evidence yields an empty slice and no error.

func LoadAndValidatePointer

func LoadAndValidatePointer(path string) (*attestation.Pointer, error)

LoadAndValidatePointer reads and validates the pointer file at path. V1 enforces schema 1.0.x with exactly one attestation entry — schema 2.0 (multi-instance pointers) is reserved.

func RenderJSON

func RenderJSON(r *VerifyResult) ([]byte, error)

RenderJSON serializes the VerifyResult deterministically.

func RenderMarkdown

func RenderMarkdown(r *VerifyResult) string

RenderMarkdown produces the PR-comment-shaped summary. Signed predicate fields (fingerprint, phase counts, BOM info) are surfaced directly — when the signature step passed, the predicate body is cryptographically anchored to the Fulcio cert claims shown on the Signer line.

Types

type FailureCause added in v0.16.0

type FailureCause struct {
	// Class is one of the Cause* constants — a stable identifier.
	Class string `json:"class" yaml:"class"`
	// Detail is the underlying error message (sanitized).
	Detail string `json:"detail" yaml:"detail"`
	// HTTPStatus is the registry HTTP status when the failure was a
	// registry response (0 otherwise).
	HTTPStatus int `json:"httpStatus,omitempty" yaml:"httpStatus,omitempty"`
	// Hint is an actionable, human-facing remediation when one is known.
	Hint string `json:"hint,omitempty" yaml:"hint,omitempty"`
}

FailureCause is the structured, machine-readable reason a verification produced a non-zero Exit. The gate renders Class/Hint into the PR comment so a contributor can self-serve the fix (e.g. a 403 → "make the fork's aicr-evidence package public") instead of seeing a bare "invalid".

type InputForm

type InputForm string

InputForm enumerates supported bundle transport shapes.

const (
	InputFormDir     InputForm = "dir"
	InputFormPointer InputForm = "pointer"
	InputFormOCI     InputForm = "oci"
)

func DetectInputForm

func DetectInputForm(input string) (InputForm, error)

DetectInputForm classifies a user-supplied input string into one of the three supported transport forms. Detection precedence:

  1. URL prefix: oci:// → OCI; http(s):// is rejected.
  2. Filesystem: directory → dir; .yaml/.yml file → pointer.
  3. Bare OCI ref shape ("registry/repo[:tag][@digest]") → OCI.

type KV

type KV struct {
	Key   string `json:"key" yaml:"key"`
	Value string `json:"value" yaml:"value"`
}

KV is a flat key-value pair for StepResult.SubRows.

func CheckInventory

func CheckInventory(ctx context.Context, mat *MaterializedBundle, expectedManifestDigest string) ([]KV, error)

CheckInventory verifies the bundle's integrity chain:

  1. sha256(manifest.json) matches expectedManifestDigest (the predicate's Manifest.Digest field). This is what binds the unsigned manifest to the predicate — without it, a tampered bundle could rewrite manifest.json to match its own contents and pass file-by-file hash checks.
  2. Every file the manifest names exists, has the expected size, and hashes to the recorded sha256.
  3. No file in the bundle is unmanaged (i.e., not in the manifest).

expectedManifestDigest must be the "sha256:<hex>" form from pred.Manifest.Digest. An empty value is rejected — the verifier refuses to operate without a predicate-side digest to compare against.

ctx is honored between files (large bundles, hostile manifests with many entries) and during the bundle walk for stray-file detection.

Returns per-file mismatch rows and an error summarizing the failure; both nil on success.

func CheckPhaseDigests added in v0.16.0

func CheckPhaseDigests(mat *MaterializedBundle, pred *attestation.Predicate) ([]KV, error)

CheckPhaseDigests verifies that each phase summary's CTRFDigest recorded in the (signed) predicate matches the sha256 of the corresponding on-disk ctrf/<phase>.json file.

CheckInventory already binds every bundle file (including the CTRF reports) to predicate.Manifest.Digest, but the predicate's per-phase CTRFDigest is an independent claim that nothing else cross-checks. Without this step a predicate could carry a CTRFDigest that disagrees with the committed report and still pass verification. Fails closed on any mismatch or unreadable phase file. Returns per-phase mismatch rows and a summarizing error; both nil on success.

type MaterializedBundle

type MaterializedBundle struct {
	// BundleDir is the local directory containing recipe.yaml,
	// manifest.json, ctrf/*, etc. Always populated.
	BundleDir string

	// Reference and Digest are populated when the bundle came from an
	// OCI source. Reference is the canonical registry/repo:tag string;
	// Digest is the resolved OCI manifest digest ("sha256:...").
	Reference string
	Digest    string

	// MediaType and Size are the pulled manifest's descriptor fields,
	// populated for OCI sources (empty/zero for a local directory). Together
	// with Digest they form the subject descriptor needed to attach a
	// Sigstore referrer to the already-pushed artifact — the input the
	// sign-existing path (`aicr evidence sign`) needs that a pointer alone
	// (digest only) cannot supply.
	MediaType string
	Size      int64
	// contains filtered or unexported fields
}

MaterializedBundle is the verifier's view of a bundle on local disk.

func MaterializeBundle

func MaterializeBundle(
	ctx context.Context,
	opts VerifyOptions,
	form InputForm,
	pointer *attestation.Pointer,
) (*MaterializedBundle, error)

MaterializeBundle dispatches on InputForm. Returns a directory the rest of the verifier reads from, plus optional OCI provenance.

func (*MaterializedBundle) Cleanup

func (m *MaterializedBundle) Cleanup()

Cleanup releases any temporary directories the verifier created.

type SignatureResult

type SignatureResult struct {
	// Signer holds OIDC claims extracted from the verifying cert.
	Signer *SignerClaims

	// Predicate is the cryptographically anchored predicate body
	// extracted from the verified DSSE payload. Callers should prefer
	// this over the unsigned statement.intoto.json when present —
	// THIS is the value the signer attested to.
	Predicate *attestation.Predicate
}

SignatureResult is what a successful VerifySignature returns.

func VerifySignature

func VerifySignature(ctx context.Context, mat *MaterializedBundle, opts VerifyOptions) (*SignatureResult, error)

VerifySignature performs sigstore-go verification of the bundle's in-toto Statement signature. Returns ErrUnsignedBundle when no attestation.intoto.jsonl is present.

For OCI inputs the subject digest in the signed Statement is locked to the actual pulled artifact digest — a mismatch means someone substituted the bundle and re-pointed at a different signature.

type SignerClaims

type SignerClaims struct {
	Identity      string `json:"identity" yaml:"identity"`
	Issuer        string `json:"issuer" yaml:"issuer"`
	RekorLogIndex *int64 `json:"rekorLogIndex,omitempty" yaml:"rekorLogIndex,omitempty"`
}

SignerClaims records the OIDC identity from the signing certificate. nil on unsigned bundles.

type StepResult

type StepResult struct {
	Step    int        `json:"step" yaml:"step"`
	Name    string     `json:"name" yaml:"name"`
	Status  StepStatus `json:"status" yaml:"status"`
	Detail  string     `json:"detail,omitempty" yaml:"detail,omitempty"`
	SubRows []KV       `json:"subRows,omitempty" yaml:"subRows,omitempty"`
}

StepResult is the recorded outcome of one verification step.

type StepStatus

type StepStatus string

StepStatus is the per-step verdict.

const (
	StepPassed        StepStatus = "passed"
	StepFailed        StepStatus = "failed"
	StepSkipped       StepStatus = "skipped"
	StepInformational StepStatus = "informational"
)

type TreeProblem added in v0.16.0

type TreeProblem struct {
	Path    string
	Message string
}

TreeProblem is one path-ownership / allowlist violation found by CheckEvidenceTree. Path is the offending pointer file (relative to the process working directory, as walked).

func CheckEvidenceTree added in v0.16.0

func CheckEvidenceTree(root, allowlistPath string, allowPending bool) ([]TreeProblem, error)

CheckEvidenceTree enforces the per-source pointer contract over every committed pointer under root, using the allowlist at allowlistPath. It is the anti-squat gate (issue #1401): a pointer is rejected unless

  • it parses and validates as a single-attestation V1 pointer;
  • its attestation carries a signer with identity + issuer;
  • the <recipe> path segment equals the pointer's recipe;
  • the <source> path segment equals SourceSlug(signer.issuer, signer.identity) — so a party cannot write under another party's directory; and
  • that claimed signer is allowlisted as community or partner (first-party ingests directly and must not commit per-run pointers).

IMPORTANT — this gate is structural, not cryptographic (issue #1535). It establishes signer identity from the issuer/identity fields the pointer itself supplies; it does NOT pull the referenced bundle or verify a Fulcio/Rekor signature actually binds that identity to the bundle digest. So a committed pointer can *claim* an allowlisted signer it does not control and pass here. That is by design: the gate runs offline in CI on every PR (no registry/Sigstore egress) and only proves the on-disk tree is internally consistent (path ownership + allowlist membership of the claimed signer). The cryptographic binding is enforced separately at ingest time (.github/workflows/evidence-ingest.yaml → VerifySignature + CrossCheckPointerSigner): each pointer's signature is verified pinned to the signer it claims, and a pointer that lied about who signed fails ingest and is never counted in corroboration. Trust therefore derives from that ingest step, not from passing this gate.

allowPending controls the flat root-level <recipe>.yaml *pending* pointer (unsigned, single-attestation, bundle-referencing) — the transient commit-flat state of the two-phase publish flow (#1530), which the fork-based CI leg signs and relocates under <recipe>/<source>/. When true it is accepted as a valid intermediate; when false (the merge gate's posture) a flat root file is rejected just like any other unexpected root file, so an unsigned pointer cannot land on a protected branch — the relocation must have run first. See checkPendingPointer.

It returns the list of problems (empty when the tree is clean) plus a non-nil error only for an operational failure (unreadable allowlist, etc.), keeping policy violations distinct from infrastructure errors.

func (TreeProblem) String added in v0.16.0

func (p TreeProblem) String() string

type VerifyOptions

type VerifyOptions struct {
	// Input is the user-supplied positional argument: pointer path,
	// OCI reference (with or without oci:// prefix), or unpacked
	// bundle directory. Required.
	Input string

	// BundleRef overrides the OCI reference when the input does not
	// embed one — e.g., a pointer file whose bundle.oci is empty.
	BundleRef string

	// ExpectedIssuer pins the OIDC issuer URL recorded on the signing
	// certificate. Empty allows any issuer.
	ExpectedIssuer string

	// ExpectedIdentityRegexp pins the signer's SubjectAlternativeName
	// via regex. Empty allows any identity.
	ExpectedIdentityRegexp string

	// PlainHTTP forces HTTP for registry traffic (local-registry tests
	// only).
	PlainHTTP bool

	// InsecureTLS disables TLS verification for the registry
	// (self-signed certificates).
	InsecureTLS bool

	// AllowUnpinnedTag opts into accepting OCI references that resolve
	// to a tag rather than a digest. By default the verifier refuses
	// unpinned refs because tags can be rewritten by the registry, so
	// "verify this artifact at this tag" is not content-addressable.
	// Pointer-driven flows ignore this flag when the pointer carries a
	// non-empty bundle.digest (the pointer's digest claim becomes the
	// pin and is cross-checked against the actual pulled digest).
	AllowUnpinnedTag bool
}

VerifyOptions configures one Verify run.

type VerifyResult

type VerifyResult struct {
	Input        InputForm              `json:"input" yaml:"input"`
	Pointer      *attestation.Pointer   `json:"pointer,omitempty" yaml:"pointer,omitempty"`
	Predicate    *attestation.Predicate `json:"predicate,omitempty" yaml:"predicate,omitempty"`
	Signer       *SignerClaims          `json:"signer,omitempty" yaml:"signer,omitempty"`
	RecipeName   string                 `json:"recipeName,omitempty" yaml:"recipeName,omitempty"`
	BundleDigest string                 `json:"bundleDigest,omitempty" yaml:"bundleDigest,omitempty"`
	Steps        []StepResult           `json:"steps" yaml:"steps"`
	Exit         int                    `json:"exit" yaml:"exit"`

	// Pending is true when the bundle is unsigned — a "pending signature"
	// state, not a failure. An in-flight PR that committed an unsigned
	// pointer (via `--no-sign`) verifies with Exit 0 and Pending true, so
	// the gate can render "pending signature" instead of a misleading
	// "all checks passed" or a false "invalid".
	Pending bool `json:"pending,omitempty" yaml:"pending,omitempty"`

	// FailureCause classifies why the bundle was rejected. Set only when
	// Exit is ExitInvalid (2); nil for Exit 0 (valid, possibly pending) and
	// Exit 1 (valid bundle with recorded phase failures), which are not
	// bundle-invalid outcomes.
	FailureCause *FailureCause `json:"failureCause,omitempty" yaml:"failureCause,omitempty"`
}

VerifyResult is what Verify returns to its caller.

func Verify

func Verify(ctx context.Context, opts VerifyOptions) (*VerifyResult, error)

Verify runs the verification pipeline. Returns a non-nil error only when verification could not begin (bad input, etc.); step-level failures are recorded in VerifyResult.Steps and reflected in Exit.

Jump to

Keyboard shortcuts

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