diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 00000000..ba5e97f8 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,121 @@ +# Gitlawb Wire Protocol Specification (v1, alpha) + +This document describes the wire behavior a second implementation can interop +with **today**. Anything the node does not ship yet is explicitly marked +*planned*; nothing below is aspirational unless it says so. + +## 1. Identity & DIDs + +- **Key type:** Ed25519. +- **Method (alpha):** `did:key` only. Resolution is deterministic public-key + extraction from the multicodec prefix (`z6Mk…`, multicodec `0xed01`). +- Signed requests presenting any other DID method are rejected; the node's + auth layer answers with a hint that only `did:key` is supported in alpha. +- *Planned:* `did:web` and `did:gitlawb` resolution. Clients MUST NOT assume + either is accepted anywhere yet. + +## 2. Authentication (RFC 9421) + +- **Transport:** production deployments should run the node behind TLS + (typically a reverse proxy). The node itself has no scheme check: it verifies + RFC 9421 headers on whatever transport the request arrives over, so nothing + at the signature layer refuses a cleartext request. +- **HTTP signatures:** `Signature-Input` covers `@method`, `@path`, and + `content-digest`: + `sig1=("@method" "@path" "content-digest");keyid="did:key:…";alg="ed25519";created=` + All three components are mandatory: a `Signature-Input` missing one is + rejected as `incomplete_signature`. +- **`@path` value:** the node binds `@path` to the path *and* query string of + the request, not to the path-only value RFC 9421 defines for `@path`. A + second implementation must sign the request exactly as it goes on the + wire, e.g. `GET /zOwner/myrepo.git/info/refs?service=git-upload-pack` + including the `?service=…` part (see §5 for the owner and `.git` + normalization). +- **Signer identity:** `keyid` is the actor DID (`did:key` only, see §1). +- **Integrity:** `Content-Digest` is SHA-256 over the request body and is + verified against it. Every signed request MUST send the header: a signed + request without it is refused `400 missing_content_digest` before routing. + On a body-less request the header still carries the SHA-256 digest of the + empty body (`sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:`), + it is never omitted. + +## 3. Proof of Intelligence (iCaptcha) + +- **Mode:** controlled by `ICAPTCHA_MODE` = `off` | `shadow` | `enforce`, + **default `off`**. A node with the gate off never demands proof. +- **Scope:** the gate applies to a small set of write endpoints (agent + registration and repo create/fork), not to all writes. +- **Wire shape:** the client-supplied header is `x-icaptcha-proof` — that is + the only iCaptcha header a node reads from a request. When the gate is + `enforce` and a verifier public key is loaded, a missing or invalid proof + answers `403 icaptcha_proof_required` and sets `x-icaptcha-url` and + `x-icaptcha-level` **on the response** (also mirrored as JSON fields) so + the client can go solve a challenge and retry. Fail-safe: `enforce` with + no public key loaded (unset and unfetchable) stays inert and admits the + request rather than rejecting every request. + +## 4. Ref-Update Certificate + +*Planned:* the `gitlawb/ref-update/v1` schema below is defined in code, but no +shipped endpoint serves or verifies these certificates today; nothing in +sections 2-3 produces one on the wire. It is documented here so a second +implementation shares the type vocabulary ahead of the wire surface. + +The hash fields reserve 64 hex characters for the SHA-256 object format; the +repositories the node serves are sha1 with 40-hex OIDs (see §5), so the field +width is a forward reservation, not what a current push produces. + +Two distinct certificate-shaped payloads exist; this section documents the +**core signing schema**, which is what `gitlawb/ref-update/v1` names: + +- **Type string:** `gitlawb/ref-update/v1`. +- **Body (canonical JSON, the signed bytes):** `type`, `repo` (the + repository DID), `ref_name`, `from` (previous hash, 64 hex chars, + all-zeros for a new ref), `to` (target hash, 64 hex chars), `seq` + (monotonically increasing, replay prevention), `timestamp` (RFC 3339), + `nonce`. +- **Signatures:** a list of `{signer: did:key, sig: base64url-unpadded + Ed25519}` entries appended outside the signed body; thresholds are counted + over distinct signers that verify. + +The node's REST API separately serves **push receipts** — a different, +node-issued JSON payload attesting that a push was processed. Receipts are +not `gitlawb/ref-update/v1` documents and are not interchangeable with them; +they are documented with the node's HTTP API, not here. + +## 5. Storage & Git Transport + +- **Object format:** bare repositories are created with + `--object-format=sha1`; production OIDs are 40 hex chars. *Planned:* + SHA-256 object format. +- **Path normalization:** `{repo}` may carry one trailing `.git` suffix, + stripped before lookup (so `myrepo.git` resolves to `myrepo`; a repo + literally named `foo.git` addresses as `foo.git.git`). `{owner}` may be the + full `did:key:…` string or the bare multibase segment the `did:key:` prefix + implies; both resolve to the same owner. None of this changes `@path`: + sign the request path and query exactly as sent. +- **Smart-HTTP endpoints:** + - `GET /{owner}/{repo}/info/refs?service=git-upload-pack` — fetch + advertisement. + - `GET /{owner}/{repo}/info/refs?service=git-receive-pack` — push + advertisement. + The `?service=` parameter is mandatory and takes exactly those two + values; a missing parameter is a `400`, not an omitted-service default. + - `POST /{owner}/{repo}/git-upload-pack` — clone/fetch. Request and + response bodies are pkt-line framed; the result content type is + `application/x-git-upload-pack-result`. + - `POST /{owner}/{repo}/git-receive-pack` — push. **Authentication is + mandatory**: an unsigned push is refused with `401`. Same pkt-line + framing; the result content type is + `application/x-git-receive-pack-result`. + Signed requests on this path follow the RFC 9421 header rules in §2. + §2's transport note applies here too: the verifier has no scheme check, + so nothing at the signature layer requires this push to arrive over TLS. +- **Fetch vs push auth split:** fetch-side requests against a withheld or + private repo answer `404` (existence is not confirmed to unauthorized + callers), while the push path answers `401` for missing/invalid + signatures. A second implementation must not treat the two symmetrically. +- **Content addressing:** the node records IPFS CID metadata per branch and + serves it over REST (the repo `refs` endpoint maps branch heads to CIDs). + *Planned:* IPNS-backed branch pointers; IPNS is not part of the shipped + wire contract.