feat(evidence): add aicr evidence publish for off-network signing#1140
Conversation
Decouple the cluster-bound validate step from the Fulcio/Rekor-bound signing step so they can run on different networks. `validate --emit-attestation` (without --push) now leaves an unsigned bundle on disk; `aicr evidence publish <bundle-dir> --push <ref>` then signs, pushes, and writes pointer.yaml from a host with Sigstore egress. The signed artifact is content-addressable: the predicate (including its baked-in attestedAt timestamp) is read verbatim from the bundle's statement.intoto.json, so the result is identical to the one-shot `validate --emit-attestation --push` output regardless of which host ran which leg. Refactor the sign+push pipeline out of EmitOptions into a shared signPushOptions so both Emit (in-memory bundle) and Publish (on-disk bundle) drive the identical path. OIDC token resolution is shared via oidcResolveOptionsFromFlags across validate --push and evidence publish.
5335ea0 to
3d687d2
Compare
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Enterprise Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR introduces a decoupled evidence publishing workflow. Users can now run Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/cli/evidence_publish_test.go`:
- Around line 54-89: The test TestEvidencePublishCmd_RejectsInvalidInvocations
currently only asserts err != nil; update each subtest to assert the returned
error is the expected invalid-request error by using errors.Is(err,
ErrCodeInvalidRequest) (or the package-specific sentinel/typed error) after
calling root.Run, and optionally also assert a case-specific substring in
err.Error() to ensure the failure reason matches the test's `why`; keep
references to root.Run and ErrCodeInvalidRequest so reviewers can find and
change the assertions in the subtests.
In `@pkg/cli/evidence_publish.go`:
- Around line 72-90: Repeated string literals for CLI flag names ("plain-http",
"insecure-tls", "identity-token", "oidc-device-flow", etc.) are causing a
goconst lint failure; define constants (e.g., PlainHTTPFlag, InsecureTLSFlag,
IdentityTokenFlag, OIDCDeviceFlowFlag) near the top of the
pkg/cli/evidence_publish.go file and replace all literal usages in the flag
definitions and any reads/refs with those constants so the name is declared once
and reused everywhere.
- Around line 113-121: Replace the bare return of err from attestation.Publish
with errors.PropagateOrWrap so structured errors are preserved; specifically,
where attestation.Publish(...) currently returns err, return
errors.PropagateOrWrap(err, errors.ErrUnknown, "failed to publish attestation")
(or the appropriate project error code constant instead of errors.ErrUnknown) so
uncoded errors are wrapped and coded errors propagate unchanged.
In `@pkg/cli/evidence.go`:
- Around line 36-38: Update the parent help/description for the "evidence"
command group to remove the phrase implying it's "offline-only" since the group
now includes networked operations like publish; locate the evidence
command/group declaration (e.g., the evidence command variable or function that
sets its Use/Short/Long text such as evidenceCmd or NewEvidenceCmd) and change
the Short/Long/help strings to neutral wording that reflects both offline and
online subcommands (for example "Manage evidence-related commands: digest,
publish, verify") and make the same replacement for the related help string
referenced around the symbol that sets subcommand help on line ~43.
In `@pkg/evidence/attestation/publish.go`:
- Around line 205-210: The readBundlePredicate function currently uses
os.ReadFile to load StatementFilename which is unbounded; change it to open the
file with os.Open, wrap the file reader with io.LimitReader using a named size
constant from pkg/defaults (e.g., DefaultMaxStatementSize) to enforce a maximum
read, and read the limited data (handle EOF vs limit hits as an error); also
ensure the file is closed and propagate errors using the existing errors.Wrap
pattern.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: f9475121-80b4-46ae-9ba2-4b92b14c255e
📒 Files selected for processing (12)
demos/evidence.mddocs/design/007-recipe-evidence.mddocs/user/cli-reference.mdpkg/cli/evidence.gopkg/cli/evidence_publish.gopkg/cli/evidence_publish_test.gopkg/cli/validate_evidence.gopkg/evidence/attestation/emit.gopkg/evidence/attestation/emit_test.gopkg/evidence/attestation/publish.gopkg/evidence/attestation/publish_test.gopkg/evidence/verifier/fetch.go
mchmarny
left a comment
There was a problem hiding this comment.
Clean architectural split — splits the one-shot validate --emit-attestation --push into a cluster-bound leg and a Sigstore-bound leg without changing the signed artifact, which is exactly the right shape for the corporate-VPN-vs-Sigstore-egress problem. The signPushOptions extraction (so Emit and Publish drive the same pipeline without leaking build-only fields), the oidcResolveOptionsFromFlags consolidation, the shared HasBundleMarkers, and the flag-constant extraction are all good DRY moves that the new command motivated rather than adding incidentally. Up-front push-ref validation (saving a wasted Fulcio cert + Rekor inclusion proof on a malformed ref) and the bounded statement read against MaxAttestationFileBytes match the project's established patterns. CI is green and the description documents a live EKS GB200 end-to-end run.
Two nits inline, both optional — neither blocks merge.
Summary
Add
aicr evidence publish <bundle-dir> --push <ref>, which signs, pushes, and writes the pointer for an evidence bundle thataicr validate --emit-attestation(without--push) left on disk — splitting the one-shot path into a cluster-bound leg and a Sigstore-bound leg.Motivation / Context
The one-shot
validate --emit-attestation --pushrequires one host to reach both the target cluster and Fulcio/Rekor. In practice those live on different networks: validation needs the cluster (often behind a corporate VPN), while keyless signing needs public Sigstore egress (frequently blocked on that same VPN). This change lets the two legs run on different hosts/networks without changing the signed artifact.Because the predicate — including its baked-in
attestedAttimestamp — is read verbatim from the bundle'sstatement.intoto.json, the published result is byte-for-byte identical to the one-shot output regardless of which host ran which leg.Fixes: #1130
Related: ADR-007 (
docs/design/007-recipe-evidence.md)Type of Change
Component(s) Affected
cmd/aicr,pkg/cli)cmd/aicrd,pkg/server)pkg/recipe)pkg/bundler,pkg/component/*)pkg/collector,pkg/snapshotter)pkg/validator)pkg/errors,pkg/k8s)docs/,examples/)pkg/evidence/attestation,pkg/evidence/verifier)Implementation Notes
aicr evidence publishand newattestation.Publishentry point. Publish loads a pre-built on-disk bundle, validates the--pushref up front (so a malformed ref doesn't waste a Fulcio cert + Rekor inclusion proof), signs the unsignedstatement.intoto.jsonverbatim, pushes as an OCI artifact, attaches the Sigstore bundle as an OCI referrer, and writespointer.yaml.EmitOptionsinto asignPushOptionsstruct so bothEmit(in-memory bundle) andPublish(on-disk bundle) drive the identical pipeline without sharing build-only fields (Recipe/Snapshot/PhaseResults).oidcResolveOptionsFromFlagscentralizes the--identity-token/COSIGN_IDENTITY_TOKEN/ ambient-GHA /--oidc-device-flowprecedence chain acrossvalidate --pushandevidence publish. Token resolution stays deferred until adjacent to signing so Fulcio's nonce-binding window is respected.HasBundleMarkers(recipe.yaml + manifest.json co-occurrence), accepting either the parent dir orsummary-bundle/itself, mirroring the verifier's directory handling.Testing
Plus a full live end-to-end run against a real EKS GB200 (Ubuntu, Dynamo) cluster:
validate --emit-attestation ./out(no--push) — all phases passed (deployment / performance / conformance), unsigned bundle written.evidence publish ./out --push ttl.sh/…:72h— pushed, Fulcio keyless-signed (Rekor log index recorded), Sigstore bundle attached as referrer,pointer.yamlpopulated.evidence verifyconfirmed exit 0 across all three input modes (OCI digest ref, pointer file); the tag-only ref was correctly rejected.Verified the cross-host content-addressability guarantee held:
attestedAtfrom leg 1 was preserved verbatim in the signed output from leg 2.Risk Assessment
Rollout notes: Purely additive — new subcommand + new exported
Publish. The existing one-shotvalidate --emit-attestation --pushpath is unchanged (the refactor is internal and output-preserving). No migration required.Checklist
make testwith-race)make lint)git commit -S)