ee06a6c5 Andy McCall 1 month ago ↶
1
# Oxygen wire protocol for implementers
2
3
This document describes what a second implementation needs to talk to an
4
Oxygen server or client: the transport, the message encodings, the order
5
of requests, and the checks each side performs. It is derived from
6
[ spec.md ] ( spec.md ) §5.5, §14, §16 and §17; where the two disagree the
7
specification wins, and this document says which section to read.
8
9
Everything on the wire is deterministic CBOR (§5.1): maps have unsigned
10
integer keys in increasing order, integers use the shortest form, no
11
floats, no indefinite lengths, no tags. Objects travel as their canonical
12
bytes, so the receiver hashes exactly what the sender signed.
13
14
## 1. Identifiers and hashing
15
16
- ** ObjectRef** (32 bytes) = BLAKE3 of ` "oxy1 <type> <len>\0" || body` , where
17
` <type>` is one of ` chunk blob change checkpoint op attest identity policy
18
state` and ` <len>` is the decimal body length. A ChangeHash is the
19
ObjectRef of a change; a KeyId is the ObjectRef of an identity.
20
- ** ChangeId** (16 random bytes) survives rewrites of a change.
21
- ** Repository id** (16 random bytes) is chosen at ` init` and returned by
22
` capabilities` ; a client refuses to push to a repository whose id differs
23
from its own.
24
25
Object encodings are in spec §5.5 (conventions), §6.1 (change), §11
26
(checkpoint), §12.1 (op), §14.2 (identity), §14.4 (attestation), §14.5
27
(policy) and §10.1 (state).
28
29
## 2. Transport
30
31
The reference server (` oxy serve` ) exposes one repository per subdirectory
32
of its root at ` <base>/<repo>/oxy/v1/<endpoint>` :
33
34
35
36
| ` GET …/capabilities` | — | capabilities |
37
| ` GET …/refs` | — | refs response |
38
| ` POST …/negotiate` | negotiate request | ` {0: [hashes]}` |
39
| ` POST …/fetch` | fetch request | Bundle |
40
| ` POST …/objects` | objects request | Bundle |
41
| ` POST …/checkpoint` | checkpoint request | Bundle |
42
| ` POST …/shallow` | shallow request | ` {0: [hashes]}` |
43
| ` POST …/push` | push request | push response |
44
45
Rules:
46
47
- Request and response bodies are CBOR with ` Content-Type:
48
application/oxy-cbor` . A body is a single CBOR item (not a sequence).
49
- Repository names are ` [A-Za-z0-9._-]+` and may not start with ` .` ; anything
50
else, and any directory without ` .oxy/` , is ` 404` .
51
- Reads are unauthenticated. ` push` requires ` Authorization: Bearer <token>`
52
when the server was started with tokens; a missing or wrong token is ` 401` .
53
- Errors are plain-text bodies with a 4xx or 5xx status; the client shows the
54
text. A push that is * refused* (policy, compare-and-swap) is a ` 200` with
67a4fe1a Andy McCall 16 days ago ↶
55
` ok = false` in the push response, not an HTTP error; one a host
56
refuses by its content (` vet_push` , spec §16.4) is an HTTP error with
57
the host's reason as its text, and nothing of it is stored.
ee06a6c5 Andy McCall 1 month ago ↶
58
- TLS is ordinary HTTPS. The reference client trusts the system web roots
59
plus an optional PEM file (` remote.<name>.ca` or ` OXY_CA` ); servers with
60
self-signed certificates are expected to hand that file out.
61
- Pushes to one repository are serialised by the server; reads are not.
75167672 Andy McCall 1 month ago
62
- A hosted repository may be ** unbound** : created empty with no id, so an
63
existing repository can be pushed to it and keep its identity. Its
64
` capabilities` reports an all-zero id. A client refuses to clone or fetch
65
from an unbound remote and accepts it for a push, sending its own id in
66
the ` Oxy-Repo-Id` request header (32 hex characters); the server binds
67
the repository to that id under the push lock before validating the
68
push, and refuses a push to an unbound repository that carries no header
69
with ` 400` . Clients send the header on every request when they have an
70
id; servers ignore it on bound repositories.
ee06a6c5 Andy McCall 1 month ago ↶
71
ee130dd9 Andy McCall 15 days ago ↶
72
Client capabilities: the reference client sends ` User-Agent:
73
oxy/<version>` and ` Oxy-Features: <feature>[,<feature>…]` on every request,
74
a comma-separated list (blanks around entries and case are ignored, unknown
75
entries too) of what it understands beyond the oxy 0.5.0 feature set.
76
Servers may tailor their answers to the features a request declares, and a
77
request without the header is a client of that era. Features so far:
78
79
- ` certified-tags` : the client authorizes a tag signed by a short-lived,
80
certified key by a subject grant, its certificate checked at the time
81
the host accepted the tag (spec §14.7; oxygen#129). A client without it
82
authorizes a tag only by a key the policy's rule lists, a delegate of one
83
or a rotation predecessor, and fails the whole clone or fetch on any
84
other. So under a policy, a server leaves out of a ` refs` answer to such
85
a client every tag whose attestation's signer none of those authorizes
86
(` Repo::refs_for_client` , applied by ` oxy serve` and by default by every
87
` oxy_server` host): that client sees the repository as if those tags did
88
not exist. If it holds one of them already and pushes, it sends the tag
89
again (it is not in the answer); the server already has it in the same
90
state and takes it as it is.
91
e02bf0cd Andy McCall 1 month ago ↶
92
Embedding: ` oxy_server::Server` answers any endpoint for a repository name
93
a host's ` Backend` resolves and authorises (` open` , ` authorize` , and an
ee130dd9 Andy McCall 15 days ago ↶
94
` after_push` hook, ` adjust_refs` , which tailors a refs answer to the
95
request's headers and by default applies the rule above, and ` vet_push` , with which a host refuses a push by
67a4fe1a Andy McCall 16 days ago ↶
96
what it adds before anything is stored: an HTTP error, ` 422` say, whose
642fa213 Andy McCall 9 days ago ↶
97
text, several lines perhaps, the client shows as it is; spec §16.4; and ` push_notes` , with which a host adds a note to a reference the push moved, which the client shows in place of "updated", #148), so a hub with ` owner/name` paths and its own accounts
e02bf0cd Andy McCall 1 month ago ↶
98
routes ` /{owner}/{name}/oxy/v1/{endpoint}` to ` Server::handle` ; ` oxy serve`
99
is ` DirBackend` on that same server.
100
ee06a6c5 Andy McCall 1 month ago ↶
101
The spec (§16) also defines framing for Noise and ZCT transports (` u32
102
big-endian length || bytes` ); the reference implementation speaks HTTP only.
103
104
## 3. Messages
105
106
Keys are CBOR unsigned integers. ` [x]` is an array of x; ` bytes(32)` an
107
ObjectRef; "signed X" is ` [X bytes, [Signature]]` .
108
109
110
111
| Signature | ` {0: key id bytes(32), 1: alg (1 = Ed25519 + ML-DSA-65), 2: Ed25519 bytes(64), 3: ML-DSA-65 bytes(3309)}` |
112
| Signed attestation | ` [attest bytes, [Signature]]` |
113
| Signed policy | ` [policy bytes, [Signature]]` (also the policy proposal file format) |
114
| Capabilities | ` {0: [protocol versions], 1: repo id bytes(16), 2: [feature names]}` ; version ` 1` ; features currently ` ["seal"]` |
b63fc7a7 Andy McCall 11 days ago ↶
115
| Refs response | ` {0: [ref], 1: repo id, 2: signed policy or null, 3: [tag], 4: [signed policy], 8: [tag]}` ; key 8 (optional, only when there are any; oxygenhub#301) lists the tags withdrawn there, each a tag whose attestation is its ` untag` attestation and whose key 4 is that attestation's acceptance when a short-lived key signed it; older readers ignore it |
0ed150d6 Andy McCall 18 days ago ↶
116
| ref | ` {0: name, 1: state ref, 2: [heads], 3: signed attestation or null, 4: last serial or null, 5: signed acceptance}` ; the attestation is present only when it describes the current state; ` last serial` is the highest attestation serial ever recorded for the name; key 5 (optional, only when there is one) is the host's ` accepted` attestation of that attestation (spec §14.7); key 6 (optional) is ` [entry number, entry bytes, [proof hash]]` , the attestation's inclusion proof in the host's transparency log against the response's key 6, the latest checkpoint as a signed note (spec §14.8) |
1ec6fdbc Andy McCall 15 days ago ↶
117
| tag | ` {0: name, 1: state ref, 2: [heads], 3: signed attestation or null, 4: signed acceptance, 5: log place, 6: [cosigned checkpoint]}` ; keys 4 to 6 (optional, each only when there is one; oxygen#129) are for the tag's attestation what a ref's keys 5 to 7 are for its own: the issuer's ` accepted` attestation when a short-lived key signed the tag (spec §14.7), which a fetch checks the certificate at, and its place in the host's transparency log with the cosigned checkpoints that first held it (spec §14.8); a push carries key 4 when the client has one; older readers ignore them |
ee06a6c5 Andy McCall 1 month ago ↶
118
| Negotiate request | ` {0: [have], 1: [want]}` ; response ` {0: [hashes]}` in dependency order (no change before its dependencies) |
119
| Fetch request | ` {0: [hashes], 1: max blob size or null}` ; response a Bundle whose blobs above the size (and their chunks) are omitted |
120
| Objects request | ` {0: [refs]}` ; response a Bundle carrying only objects; a blob is accompanied by its chunks |
121
| Checkpoint request | ` {0: [heads]}` ; response a Bundle of objects: the checkpoint of exactly those heads, its payload blob, and the blob's chunks; the server computes one when it has none for those heads |
122
| Shallow request | ` {0: [heads], 1: depth}` ; response ` {0: [hashes]}` : the changes within ` depth` dependency steps of the heads, dependencies first |
123
| Bundle | ` {0: [[change bytes, [Signature]]], 1: [signed attestation], 2: [[type name, object bytes]], 3: [signed policy]}` |
b63fc7a7 Andy McCall 11 days ago ↶
124
| Push request | ` {0: Bundle, 1: [update], 2: signed policy or null, 3: [tag]}` ; a tag whose attestation is an ` untag` attestation withdraws that tag (oxygenhub#301), reported as ` tags/<name>` ; an older server refuses it as an attestation of the wrong kind |
ee06a6c5 Andy McCall 1 month ago ↶
125
| update | ` {0: name, 1: old state ref, 2: new state ref, 3: force (bool), 4: signed attestation or null}` |
126
| Push response | ` {0: [{0: name, 1: ok (bool), 2: reason (text)}]}` ; a result named ` policy` reports the policy update, ` tags/<name>` each tag |
127
128
Bundle details:
129
130
- Changes are in dependency order, each with its individual signatures
131
(possibly none).
132
- Key 1 carries seals (` kind = seal` ) covering the changes.
133
- Key 2 objects are only ` chunk` , ` blob` , ` identity` , ` state` , ` checkpoint` ;
134
a receiver rejects any other type.
135
- Key 3 (optional) carries the policies between the receiver's serial and
136
the one in the push request, ascending, so the receiver can verify each
137
step (§14.5). The refs response carries the same history in its key 4.
138
139
Sizes: a seal lists at most 4096 hashes, sorted and unique. Chunks are
140
32–256 KiB (FastCDC, average 64 KiB, gear table from the BLAKE3 XOF of
141
` "oxy1 fastcdc gear"` , spec §13.2); a receiver re-hashes every object.
142
143
## 4. Sequences
144
145
### 4.1 Clone or fetch
146
147
1. ` capabilities` : check protocol version 1 and record the repository id.
148
2. ` refs` : for each wanted bookmark note ` state` , ` heads` , ` attestation` ;
149
take the policy and history.
150
3. Policy (spec §14.5): with no local policy, accept the remote's if it is
151
for this repository and carries a valid signature by one of its own roots
152
(trust on first use). With a local policy, refuse a lower serial or a
153
different policy at the same serial; otherwise verify each policy in the
154
chain against the one before it (signatures by the previous roots meeting
155
the previous threshold, serial strictly increasing) and adopt them in
156
order. Record the highest serial this remote has ever presented and
157
refuse a remote that later presents none or a lower one.
158
4. ` negotiate` with ` have` = local heads of every bookmark, ` want` = the
159
remote heads. The response lists the missing changes.
160
5. ` fetch` those hashes (with ` max blob size` for a partial clone). Store
161
objects, changes and seals; verify every change's signatures and seals;
162
under a policy, authenticate each new change (§14.4: signer is the author
163
or a delegate of the author) and check it is authorised for the bookmark
164
by the first matching rule (a rule key covers its rotation predecessors,
165
and a delegate's signature counts for the author).
166
6. Verify the bookmark's attestation (` kind = ref` : repo id, name, old
167
state, new state, serial) against the policy and against the serial you
168
last recorded for that reference (rollback protection), then update
169
` remotes/<name>/<bookmark>` .
170
7. Tags: adopt those not yet held, checking their attestations the same way.
171
172
A shallow clone replaces steps 4–5 by ` shallow` (the hashes within the
173
depth) plus ` checkpoint` for the heads, and fetches only the listed changes;
174
their missing dependencies form the boundary. Lazy blobs are fetched later
175
with ` objects` .
176
177
### 4.2 Push
178
179
1. ` capabilities` and ` refs` as above; refuse if the remote's policy is
180
newer than yours (fetch first).
181
2. Compute the changes the remote lacks (` negotiate` with the roles
182
reversed, or locally from the remote heads), bundle them in dependency
183
order with their signatures, seal your unsigned changes with one ` seal`
184
attestation, and attach identities, blobs, chunks and the new state
185
object.
186
3. Attest the reference: ` kind = ref` with ` serial` = the remote's ` last
187
serial + 1` , signed by your key.
188
4. Send ` push` with ` old state` = the state the remote reported (empty
189
state ref when the bookmark is new), the new state, and your policy with
190
its intermediate history when the remote's serial is behind.
191
192
### 4.3 What a server checks on push
193
194
In this order, spec §16.4: adopt the policy chain if present (each step
195
verified against the previous policy); import the bundle, verifying every
196
object's hash and, under a policy, every change's authentication and
197
authorisation; verify the reference attestation (signer authorised for the
198
name, serial above the last recorded); check ` old state` equals the current
199
state (compare-and-swap; ` force` skips this check only); check the rule's
200
` allow_conflicts` and ` require` (signed, sealed, any); check every
201
dependency of every change is present; then update the references
202
atomically and record the attestation. Any failure rejects the whole push
203
with ` ok = false` and a reason.
204
c6aa5bd9 Andy McCall 1 month ago ↶
205
## 4.4 Editors and tools on the same machine
206
207
A tool that drives a local repository does not speak this protocol; it runs
208
` oxy` with ` --json` (spec §24) and reads one JSON document per command. The
209
VS Code extension in ` oxygen-vscode/docs/spec.md` §4 lists the commands it
210
relies on.
211
ee06a6c5 Andy McCall 1 month ago ↶
212
## 5. Minimal implementations
213
214
A ** read-only client** needs: CBOR decode, BLAKE3, the change and state
215
decoders, ` capabilities` /` refs` /` negotiate` /` fetch` , and the ability to
216
materialise a state (spec §7–§9). Signature verification (Ed25519 and
217
ML-DSA-65) is required to honour a policy; without it, treat every
218
repository as unverified and say so.
219
220
A ** server** needs the above plus storage, the push checks of §4.3, per-
221
repository write serialisation, and the policy chain (` refs` key 4, bundle
222
key 3). It never needs a private key of its own: servers store and forward
223
signatures, they do not make them.
224
225
Test material: the encoders and decoders in ` crates/oxy-core/src/protocol.rs`
226
(` wire` module) and the round-trip and corruption tests beside them; the
227
signature vectors in ` crates/oxy-core/src/identity.rs` ; and the chunking
228
bounds test in ` crates/oxy-core/src/chunk.rs` .