# Verify It Yourself

Witbitz's claim is **verifiable, not promised** — so this page is where you *check* it, in your own terminal, instead of taking our word. It's the honest counterpart to every "enforcement-proven" line in the other docs: here are the exact commands, and here — just as plainly — is what they do **not** prove yet.

> See also: [The Verified Room](./verified-rooms.md) (the model you're checking) · [the Space link is the auth](./room-link-auth.md) (how a real member passes) · [the double blind](./the-double-blind.md) (the privacy claim + its north-star).

---

## What this page proves — and what it doesn't

Be exact about the scope, because "verify it yourself" is itself a claim that can overreach:

| | Status |
|---|---|
| **The admission gate enforces** — reads *and* writes are refused without an allow-listed identity, even when you hold the room key | ✅ **checkable now** (Tests 1–3) |
| **The app's egress is locked** — the app you loaded can reach only a published allowlist of hosts; the browser blocks everything else | ✅ **checkable now** (Test 4) |
| The platform is **content-blind at rest** — it stores only ciphertext, sealed to *your* key, not the operator's | ✅ **checkable now** (Test 5) |
| The render runs **only the declared program** in use (the one place plaintext opens) | 🔶 **not independently checkable yet** — needs the attested-inference tier |

So today you can verify **that the gate enforces** — the load-bearing "who may read/write" layer — **and** that the front-end's data footprint is locked to a published allowlist (Test 4). You **cannot** yet independently verify content-blindness-at-rest or declared-program-in-use in full; those need the *signed* certificate (build hash) and the attested tier ([the double blind §4–6](./the-double-blind.md)) — the certificate's **egress-allowlist half is now live**, its build-hash half is not. Honest position: **the gate and the egress are verifiable; the render is still trusted in use.**

---

## Setup (30 seconds)

1. In the [Spaces app](https://witbitz-spaces.pages.dev), create a Space with **"Who can post?" → Only specific Google accounts**, and add any email to the allow-list. This is an `admission:'email'` Space.
2. From its link `…/space?room=sp-XXXX#mk=YYYY&gate=email`, copy the **`room`** (`sp-XXXX`) and the **`mk`** (`YYYY`) — the room key that *everyone with the link holds*.
3. Discover the API endpoint (it's public; the app fetches it the same way):

```bash
SPACE=$(curl -s https://witbitz-spaces.pages.dev/space-config | grep -oE 'https[^"]+/space')
echo "$SPACE"   # the /space Lambda URL the client posts to
```

You now hold exactly what a link-holder holds: the room and its key. The tests below show that **is not enough**.

---

## Test 1 — the read gate: the link alone reveals nothing

A token-less `poll` — the read op — with a *valid* room key:

```bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -w '\n%{http_code}\n' \
  -d '{"op":"poll","room":"<ROOM>","mk":"<MK>","sinceEtag":"","sinceCount":0}'
# → {"error":"not_authorized","reason":"unsigned"}
#   403
```

Holding `mk` returns **no history**. Reads require an allow-listed Google sign-in — the server won't decrypt-and-return without one.

## Test 2 — the write gate: you can't post without an allowed identity

A token-less `turn` — the write op:

```bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -w '\n%{http_code}\n' \
  -d '{"op":"turn","room":"<ROOM>","mk":"<MK>","message":{"text":"hi"}}'
# → {"error":"not_authorized","reason":"unsigned"}
#   403
```

Rejected before anything is written — nothing lands in the Space.

## Test 3 — positive control: the key is valid; the 403 is the *gate*, not a bad key

Create a second Space with **"Who can post?" → Anyone with the link** (an `open` Space), and `poll` it the same way with **its** room/key:

```bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -w '\n%{http_code}\n' \
  -d '{"op":"poll","room":"<OPEN_ROOM>","mk":"<OPEN_MK>","sinceEtag":"","sinceCount":0}'
# → 200  (returns entries)
```

Same request shape, a valid key — but **no admission policy**, so it reads. This is the control that proves Tests 1–2 are the **admission gate** refusing you, not a malformed request or a wrong key. An open Space reads by the link; an email Space does not.

## Test 4 — the egress footprint: the app can only talk to a published allowlist

The other three tests check the *server*. This one checks the *app itself*: a static page can `fetch()` anywhere, so "the app doesn't leak your data to third parties" is a real claim — and it's now **browser-enforced**, not promised. Read the enforced Content-Security-Policy straight off the response header — it is the *exhaustive* list of hosts the loaded app is permitted to reach:

```bash
curl -sI https://witbitz-spaces.pages.dev/space | grep -i '^content-security-policy:'
# default-src 'none'; script-src 'self' 'sha256-…' 'wasm-unsafe-eval' https://accounts.google.com; …
#   connect-src 'self' blob: https://api.witbitz.chat https://accounts.google.com; … frame-ancestors 'self'
```

Read it directly:

- **`connect-src`** is the data path — where the app may send anything. It is exactly **`'self'`** (the same-origin app + its map/tile proxy), **`https://api.witbitz.chat`** (the Space backend), and **`https://accounts.google.com`** (Sign-in). No analytics host, no CDN, no third party. Your data has nowhere else to go — the browser refuses it.
- **`script-src`** allows only same-origin files **and each inline block by its `sha256`** — **no `'unsafe-inline'`**. So even injected script can't run, and can't exfiltrate.
- **`default-src 'none'`** means anything not named above is denied by default; `form-action` and `object-src` are shut; `frame-ancestors 'self'` blocks cross-origin framing.

This is the **egress-allowlist half of the signed certificate** ([the double blind §4–6](./the-double-blind.md)) — now shipped and enforced. You are reading the app's true data footprint off ground truth. (What it does **not** yet prove: that the *server-side* render binary is the published one — that's the build-hash + attested tier, still pending. This test bounds the **front-end**; the gate tests bound **access**; the render remains trusted in use.)

## Test 5 — content-blind at rest: the store holds only ciphertext, sealed to YOUR key

The other tests check access and egress. This one checks *what the operator actually holds*. The `sealed` op returns the **exact bytes the server stores** for a room's chat — no key required, because reading ciphertext reveals nothing:

```bash
curl -sX POST "$SPACE" -H 'content-type: application/json' -d '{"op":"sealed","room":"<ROOM>"}' | python3 -m json.tool
# {
#   "alg": "AES-256-GCM · multi-recipient envelope v1",
#   "bytes": 14554,
#   "sha256": "ad7c09…",                                     ← hash of the exact stored bytes
#   "recipients": ["room"],                                  ← WHO can decrypt: only the room key. NOT the operator.
#   "sealed": "{\"v\":1,\"iv\":\"…\",\"ct\":\"…\",\"recipients\":{\"room\":{…}}}"   ← the opaque envelope itself
# }
```

Two things to read straight off it:

- **`recipients` is `["room"]`.** The envelope is sealed to exactly one key — the room key (`mk`), which lives in your link's `#fragment` and *never reached the server* (you didn't send it in the request above). The operator would be a recipient **only if** `"beta"` or `"admin"` appeared in that list. It doesn't. So the operator holds no key that opens this blob — an `aws s3 cp`, a backup, or a subpoena returns exactly these bytes: noise.
- **`sealed` is opaque.** It's an AES-256-GCM ciphertext; without `mk` it is indistinguishable from random. Confirm it's genuinely *your* chat, not a decoy, by decrypting it locally with the `mk` from your link — the same key the server never saw.

This is **content-blindness at rest, made checkable**: what the operator stores is ciphertext sealed to a key it does not have. (What it still can't prove: that the **render** — the one transient moment plaintext exists to run the model — doesn't secretly keep `mk`. That's declared-program-in-use, the attested tier, below.)

---

## Reading the result codes

The gate is fail-closed; every rejection is a `403 not_authorized` with a precise `reason`:

| `reason` | Meaning |
|---|---|
| `unsigned` | no identity presented at all (no `idToken` + `spk`) |
| `token: <…>` | a Google token was presented but failed — e.g. `token: expired`, `token: bad_aud`, `token: bad_signature` (RS256-vs-JWKS) |
| `nonce_mismatch` | a valid token, but its `nonce` isn't bound to the signing key it was presented with |
| `not_on_list` | a valid, key-bound token, but the email isn't on the Space's allow-list |
| `revoked` | a valid, allowed identity that has since been removed from the Space |
| *(none)* → `200` | accepted |

## How a real member passes (the signed path)

An allow-listed member's client sends, with each request, an **`idToken`** (a fresh Google ID token) and **`spk`** (its device signing public key). The server checks, in order: the token verifies **RS256 against Google's JWKS**; its `nonce` equals **`spkNonce(spk) = base64url(SHA-256(canonical-JWK(spk)))`** (so the token is bound to *this* device's key); and the email is on the **sealed allow-list**. Only then does it decrypt-and-return, or accept the write. Full mechanism: [the Space link is the auth §4](./room-link-auth.md). This is the async analog of Kibitz binding an OIDC token to a live DTLS cert — "signed by the keyholder who authenticated as this account, now."

---

## What you can't check here yet — and why

Being explicit, so this page doesn't become the overclaim it's meant to prevent:

- **Declared-program-in-use.** The one thing left. To run the model, the render decrypts with `mk` for a transient instant — the single door plaintext opens. Tests 1–5 prove the operator can't read you at *rest*, in *transit*, or by *egress*; what they can't prove is that the render code doesn't *secretly keep* `mk` during that instant. That it runs **only** the declared program needs the **attested tier**: a pinned build hash of the render artifacts (Lambda bundle + inference image), signed from ground truth, ideally on confidential/on-device compute. Designed, not yet issued.

This page grew **"verify the gate" → "verify the footprint" → "verify the store" → and now only "verify the render" remains.** The gate is live (Tests 1–3), the footprint is live (Test 4), and content-blindness at rest is live (Test 5). What's left is the attestation — binding the running render binary to a published, reproducible build. When it ships, its check lands **here**.
