feat: Signing & Trust v1 (milestone 2)#203
Open
michael-herwig wants to merge 9 commits into
Open
Conversation
Keyless Sigstore signing and verification surfaced as OCI Referrers artifacts. Adds oci::sign / oci::verify / oci::referrer pipelines (Fulcio cert, OIDC ambient + browser flows, Rekor log, trust root, identity policy), package_manager sign/verify tasks, the ocx package sign / ocx package verify CLI commands, a typed error taxonomy with dedicated exit codes (RekorUnavailable 83, ReferrersUnsupported 84), and OCX_IDENTITY_TOKEN handling that is never forwarded to subprocess children.
Planning artifacts (ADRs, PRDs, PR-FAQs, research, swarm review rounds, Codex cross-model gates), exit-code rule additions (RekorUnavailable 83, ReferrersUnsupported 84), subsystem-cli command rows, and a structural test asserting every command file has a table row.
Upgrade the primary acceptance registry from registry:2 to zot v2.1.18, which serves the OCI 1.1 Referrers API natively (GET /v2/<name>/referrers/ <digest> + OCI-Subject on push). distribution (BOTH registry:2 and registry:3) does NOT implement the Referrers API — verified empirically: registry:3 (distribution 3.1.1) returns Go's "404 page not found" on the referrers route and omits OCI-Subject. OCX is referrers-only with no tag-schema fallback (#106), so the supply-chain sign/verify path (#194) and the smoke test require a real referrers endpoint. zot is pinned to a multi-arch tag (linux/amd64 + linux/arm64) with a mounted read-only config. Keep the single registry:2 (mirror-registry, port 5001) as the permanent referrers-NEGATIVE fixture, exposed via a new `legacy_registry` fixture for test_referrers_capability.py (#106); it still backs the mirror tests. Add raw-HTTP referrers helpers (push_blob / push_manifest / push_minimal_ image / push_referrer / list_referrers) to src/registry.py and a smoke test (test_referrers_smoke.py) that pushes a subject + referrer to zot and lists it back, and asserts the registry:2 negative fixture does not serve referrers. Refs #195.
Fill the slice-1 scaffold with a working keyless Sigstore sign + verify pipeline over OCI 1.1 Referrers: - Sign: ephemeral P-256 key -> Fulcio cert -> ECDSA sign of the subject digest -> Rekor hashedrekord upload -> Sigstore bundle v0.3 -> push bundle blob + referrer manifest. OIDC dispatch (override -> GHA/GitLab/ CircleCI ambient -> browser/no-tty). Capability cache wired (exit 84). - Verify: list referrers -> fetch bundle -> validate Fulcio chain vs injected trust root -> bind message signature to subject digest -> ECDSA verify -> Rekor SET verify (Ed25519, key fetched from --rekor-url) -> byte-equal identity + issuer match. - NativeTransport list_referrers/push_referrer_manifest over the OCI 1.1 Referrers API incl. server-side ?artifactType= filter. - Trust-root seam: --trust-root flag + OCX_SIGSTORE_TRUST_ROOT env (Fulcio CA PEM); embedded/TUF default is network-gated. - Endpoint lift to oci::endpoint (ADR Amendment 2). - deps: sigstore 0.14 + sigstore_protobuf_specs + p256/x509-cert/ ed25519-dalek + reqwest (rustls). rsa RUSTSEC-2023-0071 ignored in deny.toml (transitive via openidconnect; OCX is ECDSA-only). Spike + plan: research_sigstore_rs_spike.md, plan_issue194_sigstore_pipeline.md. Hand-rolls the HTTP because sigstore-rs's high-level clients mandate an SCT and a different wire shape than the offline fake stack.
Extend `ocx package verify` with trust-policy identity pinning so a typosquat carrying a valid-but-wrong Sigstore identity is rejected. - New leaf module `crate::trust`: `[[trust.policy]]` schema (scope + identity XOR identity_regexp + oidc_issuer), literal-prefix scope match, most-specific-wins + ANY-of-among-equal resolution, anchored full-match regex identities. - Cross-tier precedence (security): operator `config.toml` tiers array-append into one authoritative set; `resolve_tiered` consults the project `ocx.toml` only for scopes no operator policy governs, so a project file can never override or weaken an operator pin. - CLI: `--certificate-identity`/`--certificate-oidc-issuer` become optional (both-or-neither via clap `requires`); both given override policy (flag-only verify unchanged); neither uses the matched policy set. New `VerifyErrorKind::NoIdentityProvided` (exit 64) and `TrustPolicyInvalid` (exit 78). - `trust` field on `Config` + `ProjectConfig` (excluded from the declaration hash so a policy edit never invalidates `ocx.lock`). Verify reading `ocx.toml` for trust policy is a documented OCI-tier carve-out, scoped solely to `[[trust.policy]]`.
Make `ocx package verify` offline-capable and close #194's Rekor-key trust-on-first-use hole. - Trust-root cache in `$OCX_HOME/state/trust_root/<rekor-authority>.json` (mirrors the referrers capability cache): a successful online verify caches the Fulcio CA + Rekor key it used; a later verify reuses it. - `--tuf-root` / `OCX_SIGSTORE_TUF_ROOT`: load a Sigstore trusted-root JSON (Fulcio CA + pinned Rekor key) for air-gapped verify. No TUF network fetch — deferred, documented. - Rekor key pinning: verify prefers the trust-root Rekor key (TUF/cache) over the network TOFU fetch, falling back to fetch only when online and unpinned. - Offline semantics: for verify, `--offline`/`OCX_OFFLINE` scopes to the Sigstore trust services (Rekor-key fetch, TUF), not the artifact registry. Offline verify requires cached/supplied trust material with a pinned Rekor key; with none it fails exit 78 naming the remedy — never silently skips. `sign` stays online-only. The cache read + into_trust_root is the reusable offline decision seam for #99. Docs: environment.md (OCX_SIGSTORE_TUF_ROOT, OCX_OFFLINE x verify), signing.md (offline section + Rekor-pinning limitation update), user-guide air-gapped verify. ADR: adr_offline_verify_trust_cache.md.
When an operator [[trust.policy]] covers a package, `ocx package install` and `ocx package pull` now verify its keyless Sigstore signature at the metadata-first seam — after the manifest resolves, before any layer download — and abort fail-closed on a covered-but-invalid signature (no package-store or symlink state). No matching policy -> no verify (opt-in trust). `--no-verify` / `OCX_NO_VERIFY` opt out with a single WARN; the flag wins over the env. - PackageManager::verify_one: single lib verify facade over VerifyPipeline::run (#194); wraps VerifyError so the exit code survives the batch classifier. - AutoVerify + maybe_auto_verify hook (leader-only in setup_impl): gates on trust::resolve_tiered (#98), resolves trust material via the shared offline-aware ladder (#196), verifies against the resolved leaf digest. - oci::verify::resolve_trust_root: shared trust-root ladder (env/flag override -> cache -> embedded, offline gate) — one source of the offline gate for package verify and auto-verify. - OCX_NO_VERIFY env + OcxConfigView forwarding; --verify/--no-verify reused from options/verify.rs (flag > env). - Docs: environment.md, command-line.md, user-guide.md. - test/tests/test_auto_verify.py: 8 acceptance tests.
… v3 interop (#197) payload format and a single-hop certificate chain, not the public Sigstore wire format. That makes both #107 and #197 infeasible to ship in this milestone regardless of scheduling — Rekor v2 has no sigstore-rs client yet, and cosign cannot validate a bundle that isn't real-Sigstore-shaped no matter how the trust root is supplied. Ties the whole production-hardening cluster (Fulcio chain walking, Rekor SET format, TUF refresh, Rekor v2, cosign interop) into one roadmap section, and fixes an overclaim in the `package sign` reference that implied cosign could already verify OCX signatures.
Rebasing the milestone onto main (v0.4.2) required adapting to changes that landed after the branch was cut: - oci-client submodule advanced to upstream 7d27d20e (CA-root seeding, chunked push, referrers-with-artifactType). The branch's fork became redundant and is dropped; native_transport now uses upstream's ImageIndexEntry. Upstream's pull_referrers spec-fallback silently swallows a native 404 into an empty list, which would downgrade ReferrersUnsupported (84) to NoSignaturesFound (79); native_transport now calls the new pull_referrers_native (submodule) so the exit-84 contract survives. - SelectResult gained a FeatureMismatch variant (libc differentiation); sign/verify pipelines fold it into the no-target-manifest arm. - OciTransport::pull_blob_to_file slimmed to 4 params; the referrer capability test mock follows. - The managed-config [managed] tier and [trust] policy now coexist on Config (both merged); config_trust is cloned so the full config still moves into Context. - New 'ocx config' command group (managed-config) documented in subsystem-cli-commands.md; the branch's own CLI-coverage test learns the config group.
This was referenced Jul 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Milestone 2 — Signing & Trust v1
Delivers keyless Sigstore sign + verify over the OCI 1.1 Referrers API, identity-pinned trust policy, offline/air-gapped verify, and default-on auto-verify. Built on a long-living branch via per-issue swarm-loops; each feature is one commit, rebased cleanly onto current
main(v0.4.2).Closes #194 #195 #196 #98 #99 #106
What shipped (one commit per issue)
registry:2negative fixture; confirmedocx.shserves referrers.--trust-root/OCX_SIGSTORE_TRUST_ROOTseam; flat--format jsonverify report.ReferrersUnsupported(exit 84) error + capability-cache wiring (no re-probe within TTL).[trust.policy]inocx.toml#98 — identity-pinned verify via[[trust.policy]](config.tomloperator-authoritative over projectocx.toml; segment-boundary scope;identityXORidentity_regexp).ocx install/ocx pull(policy-gated) #99 — policy-gated auto-verify on every auto-install surface (install/pull/exec/env/run/patch), fail-closed no-partial-state,OCX_NO_VERIFYflag>env.Plus one integration commit adapting the milestone onto the current
main: origin/main'sSelectResult::FeatureMismatchvariant, the slimmed 4-argOciTransport::pull_blob_to_file, the newconfigcommand group (doc + coverage-test sync), and the upstreamoci-clientadoption (see below).Submodule (
external/rust-oci-client) — CI prerequisiteThe branch adopts upstream
oci-client7d27d20e(already onmain) plus one commit that addspull_referrers_native— a native-only referrers lookup that returnsOk(None)on a/v2/<name>/referrers/<digest>404 instead of the spec tag-schema fallback. Upstream'spull_referrerssilently swallows that 404 into an empty list, which would downgradeReferrersUnsupported(84) toNoSignaturesFound(79). The commit is on branchocx/referrers-native-detectionofocx-sh/rust-oci-client— must be pushed before CI can build.Deferred (stay open, tracked separately)
sigstore-rsadding a Rekor v2 client).Known scope (documented in
signing.md § Deferred to Future Work)v1 is keyless sign/verify against operator-supplied trust material + the in-repo fake Sigstore stack. Production public-good Sigstore (live Fulcio chain validation, standard Rekor wire SET, TUF trust root, cosign interop) is deferred and honestly documented — not claimed.
Verification
task verify(basic gate): green — 3016 Rust unit + ~1576 acceptance, clippy-D warnings, cargo-deny, hawkeye, actionlint, AI-config structural, link-check all clean.verify-deep.yml): zot +registry:2fixtures wired.