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:
- 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.
- 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.
- Predicate parse — use the verified predicate when the signature step produced one; otherwise fall back to the unsigned statement.intoto.json (self-consistency only).
- 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.
- 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
- Variables
- func CrossCheckPointerSigner(claimed *attestation.PointerSigner, actual *SignerClaims) error
- func DiscoverPointers(root, recipe string) ([]string, error)
- func LoadAndValidatePointer(path string) (*attestation.Pointer, error)
- func RenderJSON(r *VerifyResult) ([]byte, error)
- func RenderMarkdown(r *VerifyResult) string
- type FailureCause
- type InputForm
- type KV
- type MaterializedBundle
- type SignatureResult
- type SignerClaims
- type StepResult
- type StepStatus
- type TreeProblem
- type VerifyOptions
- type VerifyResult
Constants ¶
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.
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.
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.
const EvidenceDirName = "recipes/evidence"
EvidenceDirName is the repo-relative root of the committed evidence tree.
Variables ¶
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
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.
func DetectInputForm ¶
DetectInputForm classifies a user-supplied input string into one of the three supported transport forms. Detection precedence:
- URL prefix: oci:// → OCI; http(s):// is rejected.
- Filesystem: directory → dir; .yaml/.yml file → pointer.
- Bare OCI ref shape ("registry/repo[:tag][@digest]") → OCI.
type KV ¶
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:
- 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.
- Every file the manifest names exists, has the expected size, and hashes to the recorded sha256.
- 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
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.