OXYGENHUB

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.