Skip to content

feat: Signing & Trust v1 (milestone 2)#203

Open
michael-herwig wants to merge 9 commits into
mainfrom
feat/signing-and-trust
Open

feat: Signing & Trust v1 (milestone 2)#203
michael-herwig wants to merge 9 commits into
mainfrom
feat/signing-and-trust

Conversation

@michael-herwig

@michael-herwig michael-herwig commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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)

Plus one integration commit adapting the milestone onto the current main: origin/main's SelectResult::FeatureMismatch variant, the slimmed 4-arg OciTransport::pull_blob_to_file, the new config command group (doc + coverage-test sync), and the upstream oci-client adoption (see below).

Submodule (external/rust-oci-client) — CI prerequisite

The branch adopts upstream oci-client 7d27d20e (already on main) plus one commit that adds pull_referrers_native — a native-only referrers lookup that returns Ok(None) on a /v2/<name>/referrers/<digest> 404 instead of the spec tag-schema fallback. Upstream's pull_referrers silently swallows that 404 into an empty list, which would downgrade ReferrersUnsupported (84) to NoSignaturesFound (79). The commit is on branch ocx/referrers-native-detection of ocx-sh/rust-oci-clientmust be pushed before CI can build.

Deferred (stay open, tracked separately)

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.
  • Deep gate (verify-deep.yml): zot + registry:2 fixtures wired.

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.
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Unit Test Results

3 015 tests  +184   3 015 ✅ +184   33s ⏱️ +2s
    5 suites ±  0       0 💤 ±  0 
    1 files   ±  0       0 ❌ ±  0 

Results for commit d31be9d. ± Comparison against base commit 6214aec.

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.

Implement sign/verify pipeline via sigstore-rs (slice 2)

1 participant