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