Skip to content

Phase C: evidence readers and stricter verification CI (in progress) - #77

Merged
northcutted merged 11 commits into
mainfrom
feat/phase-c
Oct 27, 2026
Merged

northcutted merged 11 commits into
mainfrom
feat/phase-c

Conversation

@northcutted

@northcutted northcutted commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Adds support for existing image evidence and makes estate verification easier to bootstrap and run: explicit trust, bounded external operations, progress, and an opt-in verification cache. Unknown or unsupported evidence never becomes a pass.

Item Result Evidence
Discovery Notation, BuildKit index attestations, legacy Cosign SBOM tags and raw SBOM referrers are visible. Unsigned evidence remains present. Format, fallback-tag and Referrers API fixtures; raw-SBOM fail-before regression
Trivy Verified schema-v2 OS/language package scans can supply findings. Empty, null, config-only and unsupported mixed coverage remains unknown. Parser regressions and actual Cosign-signed Trivy fixture
Policy bootstrap policy init (--refs FILE or --images FILE) --out DIR writes a starter policy and inactive trust skeleton together. Candidates are unverified comments, never active trust. Incomplete discovery aborts. Scope/platform, hostile claims, output isolation, cancellation and concurrent publication; read-only nine-image showcase run produced zero active signers
Progress Per-image terminal stderr or redirected --verbose; --quiet overrides it. Output isolation, concurrent callbacks and actual-terminal check
Timeouts Global --registry-timeout 1m, --cosign-timeout 2m; registry scan --github-timeout 30s; estate verify --reproduce-timeout 30m, --grype-timeout 10m, --notation-timeout 2m. Stalled headers/bodies, blocked subprocesses, misleading success output and preflight failures; per-image expiry stays unknown/not-checked
Cache estate verify --cache-dir DIR reuses only successful Cosign results bound to pinned evidence/trust bytes and executable identities. Parser, policy, age checks, rescans and rebuilds stay fresh. Rotation, mutable-tag swaps, corruption, unknown retries, FIFO and digest-map regressions; actual Cosign cold/warm parity with 18 warm hits
Notation Optional notation: {trustedIdentities, trustRoots} verifies X.509 image signatures using --notation-path (default notation), Notation 1.3.2+ in the 1.3 series, and its experimental OCI-layout path. Each signature is pinned and checked under a generated strict, isolated policy with no plugins. Actual JWS/COSE signing and verification; forged sibling, retargeted subject, wrong identity/root and missing CLI negatives; isolated environment and timeout tests
VEX OpenVEX 0.0.1 follows Factory semantics. Trusted Factory metadata.vex.suppressed attributes exact existing ignored matches without suppressing anything new or changing counts/rescans. Exact ID/package/version/full-PURL matching, malformed/conflicting/platform attribution tests and actual Cosign metadata preservation
CI Race, staticcheck, govulncheck and baselined zizmor join PR Gate Summary; real crypto matrix also runs pinned checksum-verified Notation. Workflow guards and native crypto integration

The only new estate-report field is optional policy.notation: {trustedIdentities: string[], trustRoots: string[]}. Roots are resolved paths, not embedded certificates. Portal needs the matching additive schema update; northcutted/clearcutt-portal#13 handles trust display and recheck commands (246 local tests passed; optional scale gate runs in CI). JWS signer subjects are reported only for the verified envelope; COSE can have no parsed signer. Factory VEX attribution is the publisher's signed assertion, not independent verification of its VEX document.

Cache scope is deliberately bounded to supported Cosign DSSE/offline-key paths. Unsupported or ambiguous paths verify live. Notation expiry/revocation is always evaluated live; missing/unsupported CLI remains present, and opaque failures/timeouts remain unknown. Use --fail-on unverified to reject them. No new dependencies. Cancellation does not establish that a remote BuildKit task stopped.

Validation: pinned Go 1.26.10; final vet/build/coverage (88.8%), race, staticcheck, govulncheck, docs/workflow guards, offline demo and actual Cosign/Notation local-registry integration. No public image or registry state was changed by these local checks. Fresh PR CI must pass before merge; the prior green head was 6e20431.

All approved Verify Phase C implementation items are complete. Merge waits for fresh all-job CI and the Portal compatibility update. Factory Phase C and the second automated showcase drill remain separate follow-up work.

northcutted and others added 6 commits October 26, 2026 17:33
Each runs as its own job the PR Gate Summary requires: go test -race
(Build verifies images concurrently), staticcheck v0.8.2, govulncheck
v1.9.0, and zizmor 1.31.1 over the workflows and the action. The findings
zizmor reported when it was added are kept in .github/zizmor.yml, each
with its reason; the three pr-gate.yml checkouts it flagged now set
persist-credentials: false instead. make analysis runs the four locally.

A test now covers evidence discovery through the referrers tag
(sha256-<hex>, what cosign and oras write on registries without the
Referrers API): found and verified like referrers, without asking the
API; a tag that can't be read (429, 500, 401) or holds an image instead
of an index leaves evidence unknown, never missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cosign vuln attestation holding Trivy's report (trivy image --format
cosign-vuln) verified, but its findings weren't read, so the image's
vulnerabilities were unknown. They are now read like grype's, from
verified attestations only: converted to matches, so they are counted,
merged across platforms, and gated on by failOn and maxScanAgeDays, with
vulnerabilities.scanner "trivy <version>".

It is read only when the predicate's scanner.uri names Trivy and its
result is Trivy's JSON report of schema version 2 with a Results list. A
report without Results (what Trivy writes when it found nothing it could
scan) stays unknown, never clean, as does an unnamed scanner, grype's
shape under Trivy's URI, a finding with no ID or package, or another
schema version. Every finding counts, whatever its Status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bom tags

A team with existing evidence saw it as missing: estate verify read only
cosign's bundles and tags and unsigned SBOM referrers. It now also finds:

- Notation signatures: referrers of artifact type
  application/vnd.cncf.notary.signature (the OCI 1.1 form, or the config
  media type), source notation-signature, with the x509.subject the JWS
  envelope's certificate claims as the signer;
- BuildKit's attestations in the image index (docker buildx build --sbom
  --provenance): the SPDX SBOM and SLSA provenance in each platform's
  attestation manifest, source buildx-attestation;
- the SBOM in cosign's sha256-<digest>.sbom tag (cosign attach sbom),
  source cosign-sbom-tag.

None is verified: each is present, nothing in it is read, none is handed
to cosign, and none crowds out evidence cosign verified. Notation
signatures aren't checked because no ClearCutt trust policy can name a
Notation signer yet (the trust policy is shared with clearcutt-factory).

A platform image with no SBOM or scan of its own is now judged by any
attached to the image index (present: such a copy doesn't say which
platform it describes), and is unknown rather than missing when the
index's evidence, or BuildKit's attestation manifests, can't be read.
Fixtures are built by hand from each format's specification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
govulncheck's first CI run found GO-2026-6809 (crypto/x509, fixed in Go
1.26.10) in the standard library of the Go 1.26.9 the runners had cached:
setup-go took the cached 1.26 patch release, for the CI jobs and for the
release build alike. cli/go.mod now names toolchain go1.26.10, which
setup-go (go-version-file) installs for every job, the release workflow,
and the action's source build; the minimum Go stays 1.26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A policy's vex: document in OpenVEX 0.0.1 was refused (its vulnerability
is a string, which didn't unmarshal), while clearcutt-factory converts
and applies it, so one file meant different things to the two tools.
Statements are now converted on reading the way factory converts them: a
string vulnerability is its name, and the subcomponents a 0.0.1 statement
names beside its products are those of each product that names none of
its own. The scoping rule is unchanged: subcomponents none of which can
be read never widen a statement to every package, and grype is handed
v0.2.0 statements. A test checks a 0.0.1 document scopes exactly like
its v0.2.0 equivalent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@northcutted northcutted changed the title Phase C: read existing evidence, policy init, timeouts and cache, stricter CI Phase C: evidence readers and stricter verification CI (in progress) Oct 27, 2026
@northcutted
northcutted marked this pull request as draft October 27, 2026 03:13
@northcutted
northcutted marked this pull request as ready for review October 27, 2026 21:37
@northcutted
northcutted merged commit 644ff22 into main Oct 27, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant