OXYGENHUB
# d7e2c2754807 oxygenhub / docs / transparency.md
9.6 KiB · Markdown · last changed Keyless agents' release tags: log and serve a tag's acceptance and log place 16 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
13b27cb2 Andy McCall ↶ 120
**Built (2026-10-18, oxygen fd97f4c8 and the hub after it):** the hub keeps
121
each cosigned checkpoint by its size (migration 0083,
122
`tlog_cosigned_notes`), lists its witnesses at `GET /log/witnesses` and in
123
every refs answer (key 7), and for each logged bookmark serves the first
124
cosigned checkpoints that hold its entry, with inclusion proofs (ref key 7).
125
oxy pins the witnesses on first use beside the log key, takes the earliest
126
trusted cosignature as the integrated time, and checks a delegate's refs
127
delegation and a short-lived key's certificate at it (Oxygen spec §14.8).
128
Not yet: revocations carry no time today, so they stay absolute rather
129
than "revoked at T" (a decision for Andy, with revocation times); a
130
failure at the integrated time is flagged, not refused; one trusted
131
witness is enough (a k-of-n policy would take the k-th earliest time);
b5c199bd Andy McCall ↶ 132
bookmark and tag attestations get a time so far (tags since
133
andymccall/oxygen#129, so a keyless agent's release tag is checked at it;
134
seals and receipts next).
13b27cb2 Andy McCall ↶ 135
6e006eaf Andy McCall ↶ 136
**RFC 3161 timestamps** from a timestamp authority (TSA) are the other way
137
to get independent time. They are worth adding later for objects that never
138
reach a hub (a local signature, say), but they need ASN.1 and CMS parsing,
139
which none of our crates do today. The log covers everything a hub
140
accepts, and needs no new dependencies.
141
142
## What clients do
143
144
- **`oxy push`:** the response carries each new entry's index and an
145
  inclusion proof against the next checkpoint (or a promise to include it,
146
  fulfilled on the next fetch). oxy keeps it beside the object.
147
- **`oxy fetch`:** for every attestation fetched, check its inclusion proof
148
  against a witnessed checkpoint. Keep the newest checkpoint per remote and
149
  require consistency proofs between checkpoints. Fail closed on an
150
  inconsistency, and say plainly that the hub showed two histories.
151
- **`oxy log verify HUB`:** audit a hub's whole log (or a range): recompute
152
  the tree from the tiles, check every checkpoint and its cosignatures, and
153
  list the entries.
154
- **The hub's pages:** a change, push or receipt shows "logged at #N,
155
  witnessed by A, B at T". A `/log` page shows the latest checkpoint, its
156
  witnesses and recent entries.
157
158
## Monitoring
159
160
Anyone can follow a log and alert on entries that concern them: "a new key
161
was delegated for my identity", "a bookmark of my repository moved outside
162
CI". `oxy log watch` does that for an identity or a repository, and the
163
hub's own notifications can do it for its users. This is how a stolen key
164
or a compromised hub is noticed quickly.
165
166
## Order of work
167
168
1. **The log itself:** Entry, the tree, tiles, signed checkpoints, and
169
   entries added as objects are accepted (hub). Inclusion proofs in push
170
   responses (protocol). About a week.
171
2. **Verification in oxy:** inclusion and consistency checks on fetch,
172
   `oxy log verify`. About a week.
173
3. **Witnesses:** cosignature support on the hub, `oxy witness`, hubs
174
   witnessing each other. About a week.
175
4. **Trusted time (#240):** integrated time in verification; the hub's
176
   pages show it. A few days, once witnesses exist.
177
5. **Monitoring:** `oxy log watch` and notifications. A few days.
178
6. **Later:** RFC 3161 for objects that never reach a hub; registering with
179
   public witness networks.
180
6ba07678 Andy McCall ↶ 181
## Decisions (taken 2026-10-17)
6e006eaf Andy McCall ↶ 182
6ba07678 Andy McCall ↶ 183
Andy chose the recommendation each time: one log, hubs witnessing each
184
other plus an `oxy witness` on a third machine, and warn for a release
185
before refusing.
186
6e006eaf Andy McCall ↶ 187
1. **Public entries for private repositories?** The entries reveal only ids
188
   and hashes. One log for everything is simplest and strongest (a single
189
   history). A separate private log hides even that, but means two logs to
190
   witness. Recommendation: **one log**.
191
2. **Witnesses at launch:** start with hubs witnessing each other (Earth
192
   and GCP already make two) and an `oxy witness` on a third machine;
193
   public witness networks later. Recommendation: **yes**.
194
3. **Enforcement:** should oxy refuse an attestation with no inclusion proof,
195
   or only warn, while the log is new? Recommendation: **warn for a release,
196
   then refuse**, as with unverified repositories today.