| 1 | # v0.5 - Sigstore Rekor v2 Migration Plan |
| 2 | |
| 3 | > **STATUS: Shipped.** v0.5 (Sigstore Rekor v2 integration) is live in |
| 4 | > `oversight_core/rekor.py` and the `oversight-rekor` Rust crate, with |
| 5 | > cross-language conformance enforced by `oversight-rust/tests/conformance_rekor.sh`. |
| 6 | > This document is the original migration plan, kept for design context. |
| 7 | |
| 8 | Drafted 2026-04-19. Approved scope: public Rekor v2 only (no self-host). |
| 9 | USENIX Cycle 2 strategy: v0.4.1 frozen as paper artifact safety net; |
| 10 | v0.5 lands as a stretch goal if evaluation work comes together first. |
| 11 | |
| 12 | --- |
| 13 | |
| 14 | ## 0. Source-of-truth facts (verified 2026-04-19 via web) |
| 15 | |
| 16 | - **Rekor v2 GA: 2025-10-10.** Tile-backed log following C2SP `tlog-tiles`. |
| 17 | - **Entry types:** ONLY `hashedrekord` (artifact) and `dsse` (attestation). |
| 18 | intoto, rekord, helm, tuf, rfc3161, jar, rpm, cose, alpine are removed. |
| 19 | Custom types are **not** accepted - "additional types may be added if there is |
| 20 | demand, but this requires updating the client specification." |
| 21 | - **Write API:** single endpoint `POST /api/v2/log/entries` (HTTP + gRPC). |
| 22 | Returns `TransparencyLogEntry` (protobuf) which clients persist in bundles. |
| 23 | Minimum client write timeout: 20s. |
| 24 | - **Reads:** no online proof API. Clients fetch tiles per the tlog-tiles spec |
| 25 | and compute inclusion proofs locally. Inclusion proofs are bundled into the |
| 26 | `TransparencyLogEntry` returned at write time. |
| 27 | - **Signed timestamps removed from Rekor** - clients fetch from a separate TSA. |
| 28 | (Oversight already uses FreeTSA RFC 3161; no change needed.) |
| 29 | - **Search indexing removed** - Rekor will not answer "what entries did issuer X |
| 30 | register?". A separate verifiable-index service is planned. Oversight registry |
| 31 | must keep its own local index (it does: `registry/server.py` SQLite). |
| 32 | - **Public log URL pattern:** `https://logYEAR-N.rekor.sigstore.dev/api/v2/`, |
| 33 | rotated about every 6 months. Current: `log2025-1`. **Do NOT hardcode.** |
| 34 | Discover via Sigstore TUF trusted root. |
| 35 | - **Client coverage:** Python, Go, Java GA. JS + Ruby pending. |
| 36 | |
| 37 | ## 1. Goals (in order) |
| 38 | |
| 39 | 1. Replace `oversight_core/tlog.py` calls in the issuer's registration path with |
| 40 | a Rekor v2 DSSE upload, while keeping the local tlog as a verifier fallback |
| 41 | for v0.4-era `.sealed` files. |
| 42 | 2. Embed the returned `TransparencyLogEntry` in the Oversight evidence bundle. |
| 43 | 3. Add a `verify_rekor_inclusion()` helper auditors can run with no Oversight |
| 44 | code at all - only the standard `sigstore-python` library. |
| 45 | 4. Maintain bit-identical Python โ Rust output. New conformance test: |
| 46 | `seal-then-register` round trip across both languages must produce the same |
| 47 | DSSE envelope bytes (signatures aside, since they're nondeterministic). |
| 48 | |
| 49 | ## 2. Non-goals for v0.5 |
| 50 | |
| 51 | - No self-hosted Rekor for the reference deployment. Recorded as out-of-scope (revisit point 3). |
| 52 | - No removal of legacy `oversight_core/tlog.py`. It stays as fallback verifier. |
| 53 | - No Hardware KeyProvider work - that's v0.6 alongside format adapters. |
| 54 | - No new entry-type negotiation with Sigstore. We use vanilla DSSE. |
| 55 | |
| 56 | ## 3. Entry-type design: DSSE, not hashedrekord |
| 57 | |
| 58 | `hashedrekord` proves "key K signed digest D." We need more: "issuer K asserts |
| 59 | that mark_id M maps to file_id F with content_hash H, recipient R, suite S, |
| 60 | registered at time T, with optional policy bounds." That's an attestation, not |
| 61 | a signature primitive. Use **DSSE** with a custom predicate type. |
| 62 | |
| 63 | **Predicate type:** `https://oversight.dev/registration/v1` |
| 64 | |
| 65 | **Statement payload (canonical JSON, JCS):** |
| 66 | |
| 67 | ```json |
| 68 | { |
| 69 | "_type": "https://in-toto.io/Statement/v1", |
| 70 | "subject": [{ |
| 71 | "name": "mark:<mark_id>", |
| 72 | "digest": {"sha256": "<content_hash_hex>"} |
| 73 | }], |
| 74 | "predicateType": "https://oversight.dev/registration/v1", |
| 75 | "predicate": { |
| 76 | "file_id": "<uuid>", |
| 77 | "issuer_pubkey_ed25519": "<base64>", |
| 78 | "recipient_id": "<string>", |
| 79 | "recipient_pubkey_x25519": "<base64>", |
| 80 | "suite": "OSGT-CLASSIC-v1 | OSGT-PQ-HYBRID-v1 | OSGT-HW-P256-v1", |
| 81 | "policy": { "not_after": "<iso>?", "max_opens": <int>?, "jurisdiction": [...]? }, |
| 82 | "watermarks": { "L1": true, "L2": true, "L3": true }, |
| 83 | "registered_at": "<iso>", |
| 84 | "rfc3161_tsa": "<TSA URL used>", |
| 85 | "rfc3161_token_b64": "<base64 of TimeStampToken>" |
| 86 | } |
| 87 | } |
| 88 | ``` |
| 89 | |
| 90 | DSSE envelope: signed by the issuer's Ed25519 key (the same key already in the |
| 91 | manifest). Sigstore Fulcio/OIDC is **not** required for v0.5; we use |
| 92 | "self-managed key" mode of the Rekor v2 write API. |
| 93 | |
| 94 | ## 4. Bundle format change |
| 95 | |
| 96 | Today (`v0.4`): |
| 97 | ```json |
| 98 | { "manifest": {...}, "manifest_sig": "...", "tlog_proof": {...}, "rfc3161_token": "..." } |
| 99 | ``` |
| 100 | |
| 101 | After v0.5: |
| 102 | ```json |
| 103 | { |
| 104 | "manifest": {...}, |
| 105 | "manifest_sig": "...", |
| 106 | "tlog_kind": "rekor-v2-dsse", |
| 107 | "rekor": { |
| 108 | "log_url": "https://log2025-1.rekor.sigstore.dev/api/v2/", |
| 109 | "log_entry_b64": "<protobuf TransparencyLogEntry>", |
| 110 | "dsse_envelope_b64": "<DSSE we uploaded>" |
| 111 | }, |
| 112 | "rfc3161_token": "..." |
| 113 | } |
| 114 | ``` |
| 115 | |
| 116 | For v0.4 backward compat, the verifier reads `tlog_kind`. Default |
| 117 | (omitted/`oversight-self-merkle-v1`) โ use `oversight_core/tlog.py`. |
| 118 | `rekor-v2-dsse` โ use Rekor verifier. |
| 119 | |
| 120 | ## 5. Code surface |
| 121 | |
| 122 | ### New files |
| 123 | - `oversight_core/rekor.py` (~250 LOC) |
| 124 | - `build_oversight_dsse(manifest, ed25519_priv) -> dsse_envelope_bytes` |
| 125 | - `upload_to_rekor(envelope, log_url) -> TransparencyLogEntry` |
| 126 | - `verify_rekor_inclusion(entry, dsse_envelope, issuer_pubkey) -> bool` |
| 127 | - Pure-stdlib HTTP client; no `sigstore-python` runtime dep (we use it only in |
| 128 | the auditor helper, which lives in a separate file). |
| 129 | - `oversight_core/auditor_helper.py` (~80 LOC) |
| 130 | - Thin wrapper over `sigstore-python` so an external auditor can verify a |
| 131 | bundle with one import. |
| 132 | - `oversight-rust/oversight-rekor/` (new crate, ~400 LOC) |
| 133 | - Mirrors Python rekor.py exactly; uses `sigstore` crate for verify only. |
| 134 | - Async (tokio) for upload; sync verify path for use from CLI. |
| 135 | |
| 136 | ### Modified files |
| 137 | - `oversight_core/manifest.py`: add optional `tlog_kind` field (default-omit |
| 138 | for back-compat). |
| 139 | - `registry/server.py`: replace inline tlog append with `rekor.upload_to_rekor`. |
| 140 | Keep the SQLite event index - that is now the only way to answer "list marks |
| 141 | for issuer X" queries. |
| 142 | - `oversight_core/tlog.py`: mark module-docstring as "fallback verifier for |
| 143 | pre-v0.5 bundles only." No new writes against it. |
| 144 | - `oversight-rust/oversight-cli/`: `inspect` learns to print Rekor entry info. |
| 145 | |
| 146 | ### New tests (must add at least 3 to keep "additions only" promise) |
| 147 | - `tests/test_rekor_e2e.py` - register a mark, upload to Rekor, fetch back, |
| 148 | verify locally without Oversight code (uses `sigstore-python` only). |
| 149 | - `tests/test_rekor_backcompat.py` - open a v0.4-era `.sealed` file and |
| 150 | confirm verifier falls back to local tlog. |
| 151 | - `oversight-rust/tests/conformance_rekor.sh` - Python uploads, Rust |
| 152 | downloads-and-verifies. Skip when offline; mark as "online conformance." |
| 153 | |
| 154 | Target test count after v0.5: **79+** (76 existing + 3 new minimum). |
| 155 | |
| 156 | ## 6. Backward compatibility rules (do not break) |
| 157 | |
| 158 | 1. Every existing v0.4.1 `.sealed` file must still parse, open, and verify |
| 159 | exactly as it does today. The cross-language conformance script must keep |
| 160 | passing without modification on those files. |
| 161 | 2. Bundle format must accept missing `tlog_kind` and behave as |
| 162 | `oversight-self-merkle-v1` (the v0.4 path). |
| 163 | 3. Python and Rust must agree on every new field's canonical JSON ordering |
| 164 | (JCS already enforces this; just make sure the new fields are added to both |
| 165 | sides in the same commit). |
| 166 | |
| 167 | ## 7. Risks / gotchas |
| 168 | |
| 169 | - **Log shard rotation.** `log2025-1` will freeze and `log2026-1` (or similar) |
| 170 | will replace it. Bundles registered against a frozen shard are still |
| 171 | verifiable - the shard URL stays read-only. We must record the URL we used |
| 172 | in the bundle and never assume "current" log. |
| 173 | - **No online inclusion proof API.** Old habit dies hard: there is no |
| 174 | `GET /api/v2/log/entries/{uuid}/proof`. The proof is bundled at write time. |
| 175 | If a verifier is missing one, they have to compute from tiles. |
| 176 | - **20s write timeout minimum.** Set urllib3/reqwest accordingly. Don't fail |
| 177 | fast on registration. |
| 178 | - **Rekor v2 won't accept custom predicate types via metadata** - the predicate |
| 179 | type lives inside the DSSE statement payload, which Rekor doesn't inspect. |
| 180 | This is fine; we just need to be unambiguous in our own predicate URI so |
| 181 | third parties don't collide. |
| 182 | - **No Oversight code on the auditor's side.** This is a feature, not a risk. |
| 183 | The whole point of migrating is that any Sigstore-compatible client can |
| 184 | audit Oversight bundles. Don't compromise this by leaking proprietary |
| 185 | helpers into the verify path. |
| 186 | |
| 187 | ## 8. Sequencing (3 sessions) |
| 188 | |
| 189 | **Session A (this one or next):** |
| 190 | - Approve plan with Zion (this document). |
| 191 | - Add `tlog_kind` field, keep default behavior unchanged. Land + tests. |
| 192 | - Build `oversight_core/rekor.py` skeleton with the DSSE construction, |
| 193 | unit-tested against a fixture envelope (no network). |
| 194 | |
| 195 | **Session B:** |
| 196 | - Wire `registry/server.py` to call Rekor for new registrations. |
| 197 | - `tests/test_rekor_e2e.py` against `log2025-1.rekor.sigstore.dev`. |
| 198 | - Backward compat test against v0.4-era fixtures. |
| 199 | |
| 200 | **Session C:** |
| 201 | - Rust `oversight-rekor` crate. |
| 202 | - Cross-language Rekor conformance. |
| 203 | - Update `docs/SPEC.md`, bump version to 0.5.0, ship. |
| 204 | |
| 205 | ## 8b. Desktop review fixes applied 2026-04-19 |
| 206 | |
| 207 | Independent review by desktop session caught six issues; all addressed before |
| 208 | Session A landed: |
| 209 | |
| 210 | 1. **DSSE choice confirmed** - hashedrekord cannot carry structured |
| 211 | attestations; Rekor v2 forces this choice. |
| 212 | 2. **Predicate URI pinned** to git-tagged GitHub path |
| 213 | `https://github.com/oversight-protocol/oversight/blob/v0.5.0/docs/predicates/registration-v1.md` |
| 214 | instead of `oversight.dev` (which Zion may not own / could be squatted). |
| 215 | Predicate body now also carries `predicate_version: 1` for cheap |
| 216 | version gating without URI parsing. |
| 217 | 3. **Bundle gained four 5-year-replay fields:** |
| 218 | `rekor.log_pubkey_pem` (raw key at write time, lets verifiers skip TUF), |
| 219 | `rekor.checkpoint` (signed tree-head promoted out of the protobuf so a |
| 220 | strip-happy serializer can't drop it), |
| 221 | `rekor.log_entry_schema = "rekor/v1.TransparencyLogEntry"` (schema URI for |
| 222 | the opaque base64 blob), and the optional |
| 223 | `rfc3161_chain` (full TSA cert chain so 2031 verifiers can validate the |
| 224 | token after the TSA cert has expired). |
| 225 | 4. **`bundle_schema: 2` integer** added so pre-v0.5 verifiers fail fast with |
| 226 | "unknown schema, upgrade" instead of mis-routing on `tlog_kind`. |
| 227 | 5. **`sigstore-python>=4.1,<5` pin** for the auditor helper. Rekor v2 support |
| 228 | is stable since v4.0.0 (2025-09-19). No beta risk. |
| 229 | 6. **Privacy fix (critical):** the on-log predicate now carries |
| 230 | `recipient_pubkey_sha256` instead of the raw X25519 public key. Otherwise |
| 231 | anyone could enumerate recipients by pubkey or correlate marks across |
| 232 | issuers. The raw key stays in the local `.sealed` bundle. New unit test |
| 233 | `t8_recipient_pubkey_never_appears_raw` enforces this. |
| 234 | |
| 235 | ## 9. Open questions to surface to Zion before Session B |
| 236 | |
| 237 | 1. Predicate URI: `https://oversight.dev/registration/v1` - does he own |
| 238 | oversight.dev? If not, use `https://github.com/oversight-protocol/spec/registration/v1` |
| 239 | so the URI resolves to public spec docs. |
| 240 | 2. Auditor helper: ship inside `oversight_core/` or as a separate |
| 241 | `oversight-auditor` PyPI package so non-issuers can `pip install` it |
| 242 | without pulling Oversight's full crypto stack? |
| 243 | 3. Should v0.5 also write a tiny `verify-bundle` standalone Rust binary |
| 244 | (~200 LOC, depends only on the `sigstore` crate) for distribution to |
| 245 | journalists / lawyers / non-technical leak responders? |