docs/transparency.md: the transparency log and trusted time, proposed 6e006eaf1da4
A design for #238 (an append-only, witnessed log, so a hub cannot show two histories or drop what it accepted) and #240 (independent time for signatures): an entry per accepted attestation, policy and identity; an RFC 6962 tree served as C2SP tlog-tiles with signed-note checkpoints (Ed25519, plus the composite in an extra line); C2SP tlog-witness cosigning by other hubs and an oxy witness; integrated time from witness cosignatures for checking delegations, expiry and revocation; what oxy checks on push and fetch; monitoring; the order of work and three decisions for Andy. RFC 3161 kept for later (it needs ASN.1/CMS crates we don't have). Refs #238, #240
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
- When
- · 2026-10-17 01:26 UTC
- Signature
- ✔ signed by Claude Code
- Hash
6e006eaf1da4b032d6589cd214e0137ce99b5ad837bcc2be405adead773b3691- Depends on
- 93be9ce4653e
- Depended on by
- b5c199bd6b1a 13b27cb2200c 965b950f9b96 6ba07678ae86
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 1 atom, 1 tree operation, 1 file
Browse the tree at this change
added docs/transparency.md +172 −0
| 1 | + | # A transparency log for OxygenHub |
|
| 2 | + | ||
| 3 | + | Design for oxygenhub#238 (an append-only, witnessed log) and #240 (trusted |
|
| 4 | + | time for signatures), written 2026-10-17. **Status: proposed.** Nothing here |
|
| 5 | + | is built yet; it is for Andy to read and decide. |
|
| 6 | + | ||
| 7 | + | ## Why |
|
| 8 | + | ||
| 9 | + | Every change, seal, push, receipt and approval on a hub is signed, and |
|
| 10 | + | anyone can check the signatures. What they cannot check is **completeness**: |
|
| 11 | + | that the hub showed them everything, and showed everyone the same thing. A |
|
| 12 | + | hub could hide an attestation (a revocation, say), show one history to one |
|
| 13 | + | client and another to someone else, or backdate a signature to before a |
|
| 14 | + | key was revoked. Sigstore answers this with Rekor, a transparency log. |
|
| 15 | + | Oxygen has nothing like it yet. That is the main gap the 2026-10-16 |
|
| 16 | + | comparison found. |
|
| 17 | + | ||
| 18 | + | The aim: **a hub cannot show two different histories, or drop or reorder |
|
| 19 | + | anything it accepted, without being caught**. As a by-product, every |
|
| 20 | + | signature gets an **independent upper bound on when it was made** (#240). |
|
| 21 | + | ||
| 22 | + | ## What goes in |
|
| 23 | + | ||
| 24 | + | One entry per signed object the hub accepts, in the order it accepts them: |
|
| 25 | + | ||
| 26 | + | | Kind | Entry made when | |
|
| 27 | + | |---|---| |
|
| 28 | + | | `ref` attestation | a push moves a bookmark | |
|
| 29 | + | | `seal` | a push brings one | |
|
| 30 | + | | `tag` | a tag is created | |
|
| 31 | + | | `rotate`, `revoke` | an identity rotates or revokes a key | |
|
| 32 | + | | `receipt` | a Flow job's signed receipt is kept | |
|
| 33 | + | | `approval` | an approval is sealed | |
|
| 34 | + | | `policy` | a new trust policy is accepted | |
|
| 35 | + | | identity | a new identity, or new delegates, is published | |
|
| 36 | + | ||
| 37 | + | An entry is small and fixed-shape, never the payload itself: |
|
| 38 | + | ||
| 39 | + | ``` |
|
| 40 | + | Entry { |
|
| 41 | + | version [0]: 1 |
|
| 42 | + | kind [1]: text // as above |
|
| 43 | + | repo [2]: bytes(16) // repository id; zero for identities |
|
| 44 | + | object [3]: ObjectRef // the attestation, policy or identity |
|
| 45 | + | signers [4]: [KeyId] // the keys whose signatures were checked |
|
| 46 | + | received [5]: u64 // the hub's clock, milliseconds (a claim) |
|
| 47 | + | } |
|
| 48 | + | ``` |
|
| 49 | + | ||
| 50 | + | The leaf hash is `BLAKE3("oxygen-tlog-leaf\0" || cbor(Entry))`, so the log |
|
| 51 | + | commits to the exact object without holding it. Private repositories' |
|
| 52 | + | entries reveal only an id and hashes, nothing else. A hub may keep a separate |
|
| 53 | + | log for them if even that is too much (a setting; off by default). |
|
| 54 | + | ||
| 55 | + | ## The log |
|
| 56 | + | ||
| 57 | + | - **Structure:** an RFC 6962 Merkle tree, served as **C2SP tlog-tiles**: |
|
| 58 | + | static, cacheable tiles of hashes at `/log/tile/...` and entry bundles at |
|
| 59 | + | `/log/tile/entries/...`. This is the format Sigstore's Rekor v2 and the Go |
|
| 60 | + | checksum database use, so their tools (and anyone's) can read it. Tiles |
|
| 61 | + | are immutable once full, so they sit behind Cloudflare's cache for free. |
|
| 62 | + | - **Checkpoints:** a C2SP signed note at `/log/checkpoint`: the origin (the |
|
| 63 | + | hub's host plus `/log`), the tree size and the root hash, signed with the |
|
| 64 | + | hub's key. The key is Ed25519 for the note format, and a composite |
|
| 65 | + | Ed25519 + ML-DSA-65 signature goes in an extra note line, so the log stays |
|
| 66 | + | post-quantum like the rest of Oxygen. |
|
| 67 | + | - **Sequencing:** the hub adds entries in the same transaction that accepts |
|
| 68 | + | the object (a push is accepted only if its entries are logged). It |
|
| 69 | + | publishes a new checkpoint at most every few seconds, batching entries. |
|
| 70 | + | - **Storage:** the tiles and entries in the hub's data directory, or |
|
| 71 | + | through Strata in a bucket. They are append-only files: backups copy |
|
| 72 | + | them, and nothing ever rewrites them. |
|
| 73 | + | ||
| 74 | + | ## Witnesses |
|
| 75 | + | ||
| 76 | + | A log alone only stops a hub showing two different histories if someone |
|
| 77 | + | compares. **Witnesses** do that: |
|
| 78 | + | ||
| 79 | + | - A witness (C2SP tlog-witness) fetches each new checkpoint, checks it is |
|
| 80 | + | consistent with the last one it signed (a consistency proof), and |
|
| 81 | + | cosigns it with **its own key and its own clock**. |
|
| 82 | + | - The hub serves the latest checkpoint with every cosignature it has. |
|
| 83 | + | - **Who witnesses:** |
|
| 84 | + | - another OxygenHub (each hub can witness others: a setting, with |
|
| 85 | + | peers); |
|
| 86 | + | - a small `oxy witness` daemon anyone can run; |
|
| 87 | + | - public witness networks that speak tlog-witness, such as the ones |
|
| 88 | + | Sigstore and the Go checksum database use, once we register there. |
|
| 89 | + | - **A client** (oxy, the VS Code extension, a verifier) trusts a checkpoint |
|
| 90 | + | only when it carries enough cosignatures (policy: for example 2 of these |
|
| 91 | + | N witnesses). It remembers the largest checkpoint it has seen per hub, |
|
| 92 | + | and asks for a consistency proof whenever it sees a new one. A hub that |
|
| 93 | + | forks its log gets caught the first time a client or witness sees both |
|
| 94 | + | sides. |
|
| 95 | + | ||
| 96 | + | ## Trusted time (#240) |
|
| 97 | + | ||
| 98 | + | Today a signature's time is the signer's own claim (its HLC). So "was this |
|
| 99 | + | signed before the key was revoked?" can only take the signer's word. |
|
| 100 | + | ||
| 101 | + | With the log, every object has an **integrated time**: the earliest |
|
| 102 | + | witness cosignature over a checkpoint that includes it. That time comes |
|
| 103 | + | from the witnesses' clocks, not the signer's or the hub's. It gives an upper |
|
| 104 | + | bound: the signature existed by then. |
|
| 105 | + | ||
| 106 | + | **Verification then uses it:** |
|
| 107 | + | - A delegation, expiry or revocation is checked at the integrated time, |
|
| 108 | + | not the claimed one. A key revoked at T invalidates signatures integrated |
|
| 109 | + | after T. Ones integrated before T stay valid, whatever they claim. |
|
| 110 | + | - A signature whose claimed time is far from its integrated time (beyond a |
|
| 111 | + | tolerance, for example 10 minutes) is flagged, though not refused: clocks |
|
| 112 | + | drift, but backdating shows. |
|
| 113 | + | - **Before the log exists for a hub,** or for objects older than it, |
|
| 114 | + | verification falls back to today's behaviour and says so. |
|
| 115 | + | ||
| 116 | + | **RFC 3161 timestamps** from a timestamp authority (TSA) are the other way |
|
| 117 | + | to get independent time. They are worth adding later for objects that never |
|
| 118 | + | reach a hub (a local signature, say), but they need ASN.1 and CMS parsing, |
|
| 119 | + | which none of our crates do today. The log covers everything a hub |
|
| 120 | + | accepts, and needs no new dependencies. |
|
| 121 | + | ||
| 122 | + | ## What clients do |
|
| 123 | + | ||
| 124 | + | - **`oxy push`:** the response carries each new entry's index and an |
|
| 125 | + | inclusion proof against the next checkpoint (or a promise to include it, |
|
| 126 | + | fulfilled on the next fetch). oxy keeps it beside the object. |
|
| 127 | + | - **`oxy fetch`:** for every attestation fetched, check its inclusion proof |
|
| 128 | + | against a witnessed checkpoint. Keep the newest checkpoint per remote and |
|
| 129 | + | require consistency proofs between checkpoints. Fail closed on an |
|
| 130 | + | inconsistency, and say plainly that the hub showed two histories. |
|
| 131 | + | - **`oxy log verify HUB`:** audit a hub's whole log (or a range): recompute |
|
| 132 | + | the tree from the tiles, check every checkpoint and its cosignatures, and |
|
| 133 | + | list the entries. |
|
| 134 | + | - **The hub's pages:** a change, push or receipt shows "logged at #N, |
|
| 135 | + | witnessed by A, B at T". A `/log` page shows the latest checkpoint, its |
|
| 136 | + | witnesses and recent entries. |
|
| 137 | + | ||
| 138 | + | ## Monitoring |
|
| 139 | + | ||
| 140 | + | Anyone can follow a log and alert on entries that concern them: "a new key |
|
| 141 | + | was delegated for my identity", "a bookmark of my repository moved outside |
|
| 142 | + | CI". `oxy log watch` does that for an identity or a repository, and the |
|
| 143 | + | hub's own notifications can do it for its users. This is how a stolen key |
|
| 144 | + | or a compromised hub is noticed quickly. |
|
| 145 | + | ||
| 146 | + | ## Order of work |
|
| 147 | + | ||
| 148 | + | 1. **The log itself:** Entry, the tree, tiles, signed checkpoints, and |
|
| 149 | + | entries added as objects are accepted (hub). Inclusion proofs in push |
|
| 150 | + | responses (protocol). About a week. |
|
| 151 | + | 2. **Verification in oxy:** inclusion and consistency checks on fetch, |
|
| 152 | + | `oxy log verify`. About a week. |
|
| 153 | + | 3. **Witnesses:** cosignature support on the hub, `oxy witness`, hubs |
|
| 154 | + | witnessing each other. About a week. |
|
| 155 | + | 4. **Trusted time (#240):** integrated time in verification; the hub's |
|
| 156 | + | pages show it. A few days, once witnesses exist. |
|
| 157 | + | 5. **Monitoring:** `oxy log watch` and notifications. A few days. |
|
| 158 | + | 6. **Later:** RFC 3161 for objects that never reach a hub; registering with |
|
| 159 | + | public witness networks. |
|
| 160 | + | ||
| 161 | + | ## Decisions for Andy |
|
| 162 | + | ||
| 163 | + | 1. **Public entries for private repositories?** The entries reveal only ids |
|
| 164 | + | and hashes. One log for everything is simplest and strongest (a single |
|
| 165 | + | history). A separate private log hides even that, but means two logs to |
|
| 166 | + | witness. Recommendation: **one log**. |
|
| 167 | + | 2. **Witnesses at launch:** start with hubs witnessing each other (Earth |
|
| 168 | + | and GCP already make two) and an `oxy witness` on a third machine; |
|
| 169 | + | public witness networks later. Recommendation: **yes**. |
|
| 170 | + | 3. **Enforcement:** should oxy refuse an attestation with no inclusion proof, |
|
| 171 | + | or only warn, while the log is new? Recommendation: **warn for a release, |
|
| 172 | + | then refuse**, as with unverified repositories today. |