Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
599 changes: 599 additions & 0 deletions .claude/artifacts/adr_deps_interpolation_syntax.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions .claude/artifacts/adr_oci_artifact_enrichment.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,37 @@ Extended existing commands:
- [AWS ECR OCI 1.1 Support](https://aws.amazon.com/blogs/opensource/diving-into-oci-image-and-distribution-1-1-support-in-amazon-ecr/)
- [Red Hat Quay Referrers API](https://www.redhat.com/en/blog/announcing-open-container-initiativereferrers-api-quayio-step-towards-enhanced-security-and-compliance)
- [ORAS Compatible Registries](https://oras.land/docs/compatible_oci_registries/)

---

## Amendment 2026-04-19 — Phase 5 (Signature Support) pulled into v1

**Status:** Proposed
**Trigger:** `/swarm-plan max 24` (issue #24) post-handoff user feedback — verify-only scope ships a half-product because users must still use `cosign` to produce the signatures that `ocx verify` discovers. A feature-complete v1 requires signing and verification to land together so each iteration delivers a standalone usable loop.

### What changes

Phase 5 (Signature Support) as originally scoped ("design signing scheme", "keyless Sigstore consideration") is no longer deferred. Cosign keyless signing (`ocx package sign`) and enforcing verification (`ocx verify`) land as the **first shippable slice** of the referrer infrastructure, ahead of or alongside the read-only SBOM / external-signature discovery work.

The five-phase rollout in the original body of this ADR is superseded on the signature axis by a slice-based roadmap defined in `adr_oci_referrers_discovery.md`. Other phases (annotations, `_desc` tag, SBOM read-only discovery, external-referrer discovery) keep their existing scope and order — only the signing half-product is being promoted.

### Why this amendment rather than a new ADR

- The original Phase 5 decision — "use cosign compatibility first; add Notation later" — still holds; only its scheduling is changing.
- No design decision in the parent ADR is being reversed. Media types (`application/vnd.oci.empty.v1+json` config + cosign bundle layer), subject-targeting semantics (per-platform ImageManifest digest), registry capability detection, and "no tag fallback on push" all stand.
- The child ADR `adr_oci_referrers_discovery.md` carries the detailed signing + verification contracts and the Decisions A/B/C/D already recorded there.

### What stays deferred explicitly

- **Notation signatures** — no production Rust library exists; key-based cosign compatibility may land before Notation support.
- **DSSE / in-toto attestations** — sigstore-rs v0.13.0 does not support signing DSSE envelopes.
- **Trust-policy files** — v1 uses `--certificate-identity` / `--certificate-oidc-issuer` CLI flags for enforcement; a policy-file shape is a v2 concern captured in the child ADR's deferred findings.
- **Write-side fallback tags for GHCR/Docker Hub** — read-side fallback is the split-stance decision (child ADR Decision A3); push-side fallback remains deferred, matching this parent ADR's original stance for the push path.

### Scope impact on existing architecture

Additions to `oci::Client` that Phase 3 of this ADR already planned (`push_referrer`, `list_referrers`, `pull_referrer`) are brought forward; the OIDC/Fulcio/Rekor orchestration lives above the transport layer in a new `ocx_lib::signing` module wired through `Context`. No change to the `_desc` tag design, no change to annotation usage, no change to the registry compat matrix.

### Next step

Child ADR `adr_oci_referrers_discovery.md` and its associated plan artifacts (`plan_oci_referrers_*`) are being re-scoped into two slices under the orchestrator's follow-up `/swarm-plan`. This amendment exists so that any future reviewer sees the scheduling change at the parent level.
934 changes: 934 additions & 0 deletions .claude/artifacts/adr_oci_referrers_discovery.md

Large diffs are not rendered by default.

611 changes: 611 additions & 0 deletions .claude/artifacts/adr_oci_referrers_discovery_v2.md

Large diffs are not rendered by default.

1,039 changes: 1,039 additions & 0 deletions .claude/artifacts/adr_oci_referrers_signing_v1.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions .claude/artifacts/adr_offline_verify_trust_cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# ADR — Offline / air-gapped verify + trust-root cache (#196)

Status: Accepted (2026-07-09). Depends on #194. Gates #99.

## Context

`ocx package verify` is online-only today: `--offline`/`OCX_OFFLINE` fails at
`Context::online_context()` → exit 81 before any work. Two #194 weaknesses block
offline verify:

1. The Rekor public key is **TOFU-fetched** from `--rekor-url/api/v1/log/publicKey`
at verify time — a network dependency AND a trust-on-first-use hole.
2. The embedded TUF trust root is stubbed (`load_embedded` → `TrustRootUnavailable`),
so trust material only ever comes from a supplied `--trust-root` PEM.

Product principle #2 is offline-first; auto-verify on install (#99) collides
verify's online-only stance with install's offline-first stance. This ADR
resolves that contradiction.

## Decision

### What "offline" means for verify (the contradiction, resolved)

For `ocx package verify`, `--offline`/`OCX_OFFLINE` governs the **Sigstore
trust-services network** (the Rekor-public-key fetch and any TUF fetch/refresh) —
NOT the artifact registry. Verifying an artifact inherently means reading it, and
its signature referrer, from the registry where it lives; in an air-gapped
deployment that registry is a local mirror the operator runs. So offline verify:

- still fetches the referrer + bundle from the configured registry (a live client
is available in every mode — see "Registry client in all modes");
- MUST NOT contact Sigstore trust services — the Fulcio CA **and** the Rekor
public key must come from a supplied override or the fresh trust-root cache;
- FAILS with an actionable error when trust material is absent/stale — never
silently skips verification.

`sign` stays online-only, unchanged (it needs Fulcio + Rekor round-trips).

The bundle-is-local-too concern (true no-registry air-gap) is #99's install-time
job: install already downloads the artifact, and the reusable offline-trust
decision below lets install-time auto-verify make the same fail-vs-verify call.

### Trust-root cache (`$OCX_HOME/state/trust_root/<rekor-authority-slug>.json`)

Mirrors the referrers capability cache (`oci/referrer/capability.rs`): atomic
tempfile+rename write, TTL-gated fail-open read, host-scoped key. Caches the
trust MATERIAL needed for offline verify:

- Fulcio CA certificate(s) — DER (the certs the online verify chained against);
- the Rekor public key PEM (whether pinned from a trust root or TOFU-fetched).

Populated on a **successful online verify**. Read on a later verify when no
explicit override is supplied. TTL = 24h (`TTL_SECS`); honoring real TUF metadata
expiry is deferred with the real TUF client. Keyed by the Rekor URL authority so
public and private Sigstore instances never collide. The cache is per-`OCX_HOME`.

### `OCX_SIGSTORE_TUF_ROOT` override (+ `--tuf-root` flag)

Points verify at a Sigstore `TrustedRoot` JSON (a file, or a directory containing
`trusted_root.json`). Parsed leniently (serde_json walk) to extract Fulcio CA
certs (`certificateAuthorities[].certChain.certificates[].rawBytes`) and Rekor
public keys (`tlogs[].publicKey.rawBytes`, DER SPKI → PEM). No TUF **network**
fetch/refresh — that stays deferred; this is the air-gapped local-mirror seam.

### Rekor key pinning (security fix for #194 weakness 1)

`verify_rekor_set` now prefers the Rekor key from the trust root (supplied via TUF
root, or cached) when present — no network, and it closes the TOFU hole. It falls
back to the online `--rekor-url` fetch ONLY when no trust-root Rekor key exists
AND the run is online. Offline + no pinned Rekor key → actionable failure.

### Registry client in all modes

`Context` now builds the registry client unconditionally (cheap; no network on
build) and exposes it to verify via `verify_context()`. `remote_client()` /
`online_context()` keep their offline gating (sign etc. unchanged); only verify
reads the always-present client, because verify's offline semantics scope to
trust services, not the registry.

### Trust-material precedence (verify)

1. `--tuf-root` / `OCX_SIGSTORE_TUF_ROOT` (Fulcio + pinned Rekor key)
2. `--trust-root` / `OCX_SIGSTORE_TRUST_ROOT` PEM (Fulcio only; Rekor via cache/TOFU)
3. fresh trust-root cache (Fulcio + Rekor key)
4. embedded root (stubbed → exit 78)

Offline additionally requires the resolved material to carry a Rekor key (only
1 and 3 do); offline + only a bare PEM, or offline + empty cache → exit 78 with a
remedy naming `--tuf-root` / "run an online verify first".

## Reusable seam for #99

The offline decision is a library primitive: `TrustRootCache::from_cache(...)` →
`filter(is_fresh)` → `into_trust_root()` (has a Rekor key ⟺ offline-verifiable).
`#99`'s install-time auto-verify composes the same primitive: fresh cached
material ⇒ verify offline; none ⇒ the documented fail-vs-skip policy.

## Consequences

- Offline verify is genuinely no-Sigstore-network (proved by the acceptance suite
returning 503 from fake Rekor and still passing offline).
- The TOFU Rekor-key hole is closed whenever trust material provides the key.
- Real TUF fetch/refresh + bundle-local-CAS air-gap remain honestly deferred.
Loading
Loading