OXYGENHUB
# 205d8f863e5c oxygenhub / docs / transparency.md
8.3 KiB · Markdown · last changed transparency.md and keyless.md: accepted, with Andy's decisions 19 days ago renderedsource blame history raw
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: accepted** (Andy, 2026-10-17, the three
5
decisions below as recommended). Nothing here is built yet.
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 (taken 2026-10-17)
162
163
Andy chose the recommendation each time: one log, hubs witnessing each
164
other plus an `oxy witness` on a third machine, and warn for a release
165
before refusing.
166
167
1. **Public entries for private repositories?** The entries reveal only ids
168
   and hashes. One log for everything is simplest and strongest (a single
169
   history). A separate private log hides even that, but means two logs to
170
   witness. Recommendation: **one log**.
171
2. **Witnesses at launch:** start with hubs witnessing each other (Earth
172
   and GCP already make two) and an `oxy witness` on a third machine;
173
   public witness networks later. Recommendation: **yes**.
174
3. **Enforcement:** should oxy refuse an attestation with no inclusion proof,
175
   or only warn, while the log is new? Recommendation: **warn for a release,
176
   then refuse**, as with unverified repositories today.