# The Verified Room

One definition of *who is in a room and who said what* — and the reason it is interesting: **the same definition
works with no server at all (true peer-to-peer) and, now, with a server that verifies but cannot read.** It is
inherited from the open **Kibitz** engine, where it secures live peer-to-peer calls, and Witbitz has carried it —
mechanism for mechanism — to **asynchronous, end-to-end-encrypted Spaces**, where a certified, content-blind
function does the verifying when no peer is present.

This page is the deep, precise treatment. For the membership/key-possession and double-blind basics see
[The Space link is the auth](https://docs.witbitz.chat/docs/room-link-auth.md); for cross-Space governance see
[the Bridge](https://docs.witbitz.chat/docs/the-bridge.md).

---

## 1. The definition

A verified room has **no account server and no stored access-control list.** Instead:

> **The link carries a *verifier*, never a secret. A member holds a *credential*. Anyone can check
> credential-against-verifier using only the link's *public* material — so trust is rooted in the link, not in any
> server.**

Three consequences fall out, and together they are the whole model:

1. **The link is a capability.** Public verifier material — a room-invite *public* key, a creator *public* key, a
   name list, an OAuth client id — rides the link openly. Secrets — the room key, a member's *private* signing key —
   ride the **URL fragment**, which a browser never transmits. (Witbitz adds one thing to Kibitz's link: the
   fragment also carries the **decryption key** for the room's content, so a Witbitz link literally holds *everything*.)
2. **Verification is cryptographic, not a lookup.** "Is this a member?" is answered by verifying a signature against
   a public key from the link — never by asking a database *who* someone is. The verifier learns *that a valid
   credential was presented*, not an identity it stores.
3. **The verifier can be anyone.** Because the check needs only public material, the party that runs it is a free
   choice — **a peer, a reader, or a certified function** — without changing the credential. This is the hinge on
   which the rest of the page turns.

---

## 2. The primitive

Every credential in the model is the same ECDSA P-256 token, and it is tiny (`kibitz/src/core/inviteToken.ts`,
reimplemented byte-for-byte in `agent/spaceEntrySig.mjs`):

```
token = base64url(JSON.stringify(payload)) + "." + base64url( ECDSA-P256-SHA256 sig over the first part )
```

- **Bearer + unforgeable.** Minting needs the *private* key; verifying needs only the *public* key. Forging one is
  forging ECDSA, not guessing a short code — a brute force is a non-starter.
- **Room-bound + expiring.** Every payload carries `room` and `exp`, so a token cannot be replayed into another room
  or used forever.
- **The link holds only the public half**, so *any* authority — even one that took over after the creator left —
  can verify without ever holding a private key.

`signPayload` / `verifyPayload` are the shared floor; **invite grants, agent assertions, and host commands are all
this token with a different payload and a wire tag**, so they can never be confused for one another.

---

## 3. The credential families (all of Kibitz)

The link can carry any of these verifiers; a room composes them (e.g. a human gate *and* an agent allow-list).

### 3.1 Signed invites — `inviteToken.ts`
The creator holds an invite keypair; each guest gets a grant token `{name, room, exp}`; the link carries the invite
**public** key (`gk`). A holder joins as `name`; any authority verifies the grant against `gk`. A signed invite
link, cryptographically — no server, no account.

### 3.2 The signed manifest — `roomManifest.ts`
The room's committed roster: `{room, mode, exp, members?, domains?, agentKeys?}`, **signed by the creator key** and
verified against the creator **public** key (`gm`) in the link. So every peer — not just the coordinator — can
check a joiner against the *same committed* roster, and the creator's signing key is discarded after minting. A
manifest can be **passphrase-encrypted** (`gateSecret.ts`), so a mere link-holder can't even read *who* is allowed
until they hold the passphrase.

### 3.3 Join-gate modes — `joinGate.ts`
How a member proves themselves: `open` · `names` (pick from an allow-list) · `code` (a mailed secret, constant-time
compared) · `email` / `google` (OIDC — §3.4) · `invite` (§3.1). All but `email` are **serverless** — a peer runs
them. Codes are compared in constant time; brute force forges ECDSA, not 30 bits.

### 3.4 OIDC + cert-binding — `oidcVerify.ts` · `oidcBinding.ts` · `identityCert.ts`
"Sign in with Google," done without a backend and without trusting the transport:

- **`oidcVerify`** — hand-rolled **RS256-only** ID-token verification over WebCrypto. `alg:"none"`, HS\* (the
  classic algorithm-confusion attack) and ES\* are rejected outright — the algorithm is *never* taken from the
  token to pick the verifier. The key is chosen by `kid` from the provider's published **JWKS**; an unknown `kid`
  fails closed. It validates `iss`/`aud`/`exp`/`nbf` and surfaces `email_verified` + `nonce` (policy is the
  caller's).
- **`oidcBinding`** — the serverless heart. At sign-in the user sets the token's **`nonce`** to
  `base64url(SHA-256(canonicalFingerprint(fp) [+ "|" + room]))` — a hash of *their* WebRTC **DTLS certificate
  fingerprint**. Every peer recomputes that hash from the cert it **actually handshook with** and requires a match.
  So a token is valid **only for the peer holding that cert's private key** — it cannot be replayed by anyone else,
  because their connection presents a different cert.
- **`identityCert`** — one **pinned** cert, reused for every connection in the mesh, private key never leaving the
  browser → the binding is **non-transferable**.

The payoff: cert-binding composes with the emoji safety code (both read the same cert), so **"this is really Dana"**
and **"there is no man-in-the-middle on this connection"** collapse into **one** guarantee.

### 3.5 Agent keys — `agentKey.ts`
An AI agent's identity *is* an ECDSA keypair. To enter it signs a **cert-bound assertion**
`{k:"kbz-agent-key.v1", room, fp, iat}`; the authority admits it iff the signing key is on the room's committed
`agentKeys` and the assertion is bound to **this** connection (`fp`) and fresh. The room stores only the agent's
**public** key. `admitAgentByManifest` (`roomManifest.ts`) resolves the matched entry's capability policy.

### 3.6 Capabilities — `capabilities.ts`
A per-participant **`Grant`** = what it may **perceive** (`read-chat`, `read-roster`, `read-media`, `read-files`,
`see-screen`, `hear-audio`, `receive-directed`) and **act** (`send-chat`, `speak`, `act`), plus agent disclosures
(`backend`, `egress`). `defaultGrant('agent')` is **least-privilege** — an agent perceives the conversation but
acts on nothing until explicitly granted. `sanitizeGrant` clamps a grant at the trust boundary; `intersectGrant`
gives *requested ∩ allowed*.

### 3.7 Verified host — `hostKey.ts`
Admin is bound to a **password, not to who holds the room id.** The link commits the host **public** key (`gh`) and
the **private** key **sealed under a host password** (`ghk`). To moderate, a peer enters the password → unseals the
key → signs a **cert-bound command** `{room, fp, op, target?, iat}`; the coordinator verifies it against the
committed public key. It **survives a coordinator migration** — every peer already holds the committed key from its
own link.

---

## 4. The leap — one definition, three verifiers

§1.3 said the verifier can be anyone. That is what lets the *same* credential model span three very different
runtimes. The only thing that changes is **who runs the check** and **what fresh value the credential is bound to**
so it can't be replayed.

| | **Live P2P call** (Kibitz) | **Async P2P read** (Witbitz) | **Async server verify** (Witbitz) |
|---|---|---|---|
| Who verifies | a **peer** (the authority/coordinator) — no server | **any reader**, at read time — no server | the **certified-ephemeral function** — the only party present |
| Freshness / anti-replay binding | the live **DTLS cert fingerprint** (`oidcBinding`) | the per-entry **signature** over a canonical core | a **server-issued challenge**, folded into the signed core |
| Presence | co-present over the connection | none — verify what's written | none — a fresh challenge stands in for presence |
| Trust root | the link's public key | the link's public key | the link's public key **+** a certified deployment |
| Content secrecy | peer-to-peer E2EE (no server sees media) | sealed ledger, content-blind platform + owner | same — the double blind |

The unifying trick is a **nonce/challenge binding**: bind the credential to a *fresh value only the right holder
could have used*, so a captured credential can't be replayed.

- **Live:** the fresh value is the **DTLS fingerprint** — the credential is welded to *this connection*.
- **Async:** there is no connection, so the fresh value becomes the member's **signing key** (identity binding) and
  a **server-issued challenge** (presence binding). Same idea, different anchor.

The important claim: **async did not need a weaker model. It needed a different anchor for the same model.**

---

## 5. The async realization (Witbitz) — mechanism for mechanism

Every Kibitz method above has a Witbitz async counterpart. The credential *formats* are identical (a Kibitz-minted
artifact validates in Witbitz and vice-versa where it makes sense); only the **verifier** and the **binding anchor**
change.

| Kibitz (live P2P) | Witbitz (async) | What's the same / what changed |
|---|---|---|
| `inviteToken` signed invite | `spaceEntrySig` grant `{name, room, exp, spk, caps?}` | **Byte-identical** token wire format (no import). Adds `spk` (the member's signing key) + optional `caps`. |
| DTLS-bound message attribution | **per-entry signature** over `{room, author, ts, kind, id, text, chal?}` | Attribution moves from the connection to a signature verified **at read time** by any reader / the render. |
| `oidcVerify` + `oidcBinding` (Google, cert-bound) | `spaceOidc` — RS256-only vs Google JWKS, bound to the **signing key** via `nonce = base64url(SHA-256(canon(spk)))` | Same RS256/JWKS/`email_verified` discipline. The nonce binds to the **signing key** instead of a DTLS cert. Verified inside the certified render and **discarded** — never stored. |
| cert-binding = presence proof | `spaceChallenge` — a **server-issued HMAC challenge** `{r,t,j}.mac`, folded into the **signed** entry core | The member's *existing* signature also covers the fresh challenge (no separate signature). Single-use via ledger de-dup → replay is rejected. The async stand-in for the DTLS binding. |
| `agentKey` cert-bound assertion + `agentKeys` | `spaceAgentKeys` — the **same `AgentEntry {key, caps?, label?, boxPub?}`**, admitted by key-possession + per-entry signature | One authored `AgentEntry` admits an agent **live *and* async** — author once, valid everywhere. |
| `capabilities.Grant` | `spaceCaps` — **identical** perceive/act vocabulary | A Grant crosses live ↔ async unchanged; enforced in the turn op (`send-chat`, `act`). |
| mesh perceive-gating (withhold per peer) | `spaceScopedView` — seal the entitled slice to the agent's **box key**, never `mk` | An external agent reads *only* what its caps allow, sealed to its own ECDH key; the runtime never hands out the room key. |
| the signed `roomManifest` roster | the **mk-sealed config** (`admission`, allow-list, `agentKeys`, `revoked`) | The room's policy is **ciphertext at rest** — the platform can't read *who* is allowed; it surfaces only inside the decrypt-once render. |

**Where the server sits, precisely.** In async there is no peer to run the gate, so the **turn function** does —
but it is a *certified, ephemeral, content-blind* verifier, not a trusted ACL store:

- it checks signatures against the link's **public** keys and a **public** OIDC provider's JWKS — it roots no trust
  of its own;
- the allow-list it consults is **sealed under `mk`**, decrypted only for the length of one turn;
- an OIDC email it verifies is **transient** — checked and dropped, never persisted or logged;
- reads it serves an agent are **sealed to that agent's key** — it relays ciphertext.

So the server *verifies* without being *trusted with content* — the same trust boundary the AI already lives behind
to run a turn at all. (See [the double blind](https://docs.witbitz.chat/docs/the-double-blind.md).)

---

## 6. Why one definition matters

- **Serverless when it can be.** In a live call or an async *read*, the credential is checked by a peer or a
  reader — **no server is in the trust path at all.** The room is genuinely account-free and self-authenticating.
- **Server-verified when it must be.** When everyone is away, async *writes* still need a gate. The same credential
  is checked by a certified function — the locus moved, the definition did not, and the function is blind to
  content by construction. You get server enforcement *without* a server that can read you.
- **Author once, honored everywhere.** An `AgentEntry`, an invite grant, an OIDC allow-list authored for one
  context is honored in all of them — live (peer), async-read (reader), async-write (server), and across a Bridge.
  Authorization becomes **portable across the topology** instead of re-implemented per transport.
- **Least-privilege + revocable.** Capabilities travel *inside* the signed credential (identity and authorization
  in one tamper-proof object). Revocation is a `revoked` line, an expiry, or — for reads — an **epoch re-key** that
  simply stops sealing new content to a removed party (forward secrecy).

---

## 7. Security properties

- **Unforgeable** — every credential is an ECDSA P-256 signature; minting needs a private key held only by the
  authority / the member.
- **No cross-room, no replay** — `room` + `exp` in every payload; plus a per-connection fingerprint (live) or a
  single-use, TTL-bounded server challenge (async).
- **No algorithm confusion** — OIDC verification is RS256-only; `alg:"none"`/HS\*/ES\* are rejected; the verifier
  algorithm is never taken from the token.
- **Fail-closed everywhere** — a malformed grant, an unknown `kid`, a missing signature, an empty allow-list → *no
  access*, never a default-open.
- **No server trust root for the credential** — verification uses only the link's public keys and a public JWKS; a
  compromised or curious platform cannot mint membership, only (in the async-write locus) *decline to run*.
- **Blind at rest** — the policy (allow-list, admission mode) is sealed under `mk`; identities the server verifies
  are transient; content sealed to a party is opaque to the platform.

---

## 8. Code map

| Concern | Kibitz (live P2P) | Witbitz (async) |
|---|---|---|
| Signing primitive (`payloadB64.sigB64`) | `src/core/inviteToken.ts` (`signPayload`/`verifyPayload`) | `agent/spaceEntrySig.mjs` (byte-compatible) |
| Signed invites / grants | `src/core/inviteToken.ts` | `agent/spaceEntrySig.mjs` (`mintGrant`/`verifyGrant`) + `spaces/public/entrySig.js` |
| Signed roster / policy | `src/core/roomManifest.ts` | the **mk-sealed config** (`agent/spaceHandler.mjs`, `agent/spaceService.mjs`) |
| Join-gate modes | `src/core/joinGate.ts` | admission `open`/`invite`/`email` in `agent/spaceService.mjs` |
| OIDC verify + cert-binding | `src/core/oidcVerify.ts` · `oidcBinding.ts` · `identityCert.ts` | `agent/spaceOidc.mjs` (RS256/JWKS + `spkNonce` binding) |
| Presence / anti-replay | cert fingerprint (`oidcBinding`/`agentKey`) | `agent/spaceChallenge.mjs` (server-issued HMAC challenge) |
| Agent admission | `src/core/agentKey.ts` (+ `admitAgentByManifest`) | `agent/spaceAgentKeys.mjs` (same `AgentEntry`) |
| Capabilities | `src/core/capabilities.ts` | `agent/spaceCaps.mjs` (identical vocabulary) |
| Scoped read | mesh perceive-gating (`mesh.ts`) | `agent/spaceScopedView.mjs` (seal-to-agent-key + `view`/`fetchview`) |
| Verified host | `src/core/hostKey.ts` | (delegated authority — `agent/delegatedAuthority.mjs`) |
| Recipient sealing (reads) | — | `agent/envelope.mjs` (`sealTo`/`openBox`, ECDH P-256 box) |

---

## 9. Status

| Piece | Live P2P (Kibitz) | Async (Witbitz) |
|---|---|---|
| Signed-invite grants + per-entry signatures | shipped | **deployed** |
| Capabilities in the grant (perceive/act) | shipped | **deployed** |
| OIDC / Google admission | shipped (cert-bound) | server-verify **deployed**; browser Sign-in UI pending a Client id |
| Presence proof / anti-replay | shipped (cert-binding) | **deployed** (server-issued challenge) |
| Agent admission by allow-list (`AgentEntry`) | shipped | **deployed** (same shape, live + async) |
| Scoped read (least-privilege perceive) | shipped (mesh) | **deployed** (`view` + member-absent `fetchview`) |
| Verified host / moderation | shipped | delegated-authority (propose → approve → execute) |

The through-line: **a room's authority lives in its link, and the check that enforces it can run wherever the room
happens to be — on a peer, in a reader, or inside a function that verifies but cannot read.**
