locqr/server/claude.md

198 lines
7.8 KiB
Markdown

# Backend Server
The backend server is the trust anchor for the entire system. Its signing keys
are the only compile-time constants; all legitimacy within the system traces
back to its ability to sign, and the clients' ability to verify against those
pinned keys. It provides two services: run infrastructure (key bundle
storage, token signing, relay) and account management (domain registration,
certificates, billing).
The server exists in two implementations: a **Node.js stub** used during
development, and the **Java/Spring production server** which is the long-term
target.
---
## Node.js stub (development tool)
A minimal Express server implementing only the parts the extension and companion
actually touch. Its purpose is to allow end-to-end development and testing of
the full run flow without the production infrastructure.
**What it does:**
- Stores key bundles in memory; destroys on fetch (at-most-once)
- Signs key exchange tokens with Ed25519 (Node built-in `crypto` module)
- Returns server-controlled run parameters with each signed token
- Accepts WebSocket connections from the extension for relay
- Forwards relay messages (kem_ciphertext, encrypted credential) from companion
to extension
- Serves a pre-generated test domain certificate (ML-DSA-44); does not
dynamically issue certificates
- Accepts domain status queries; returns valid for the hardcoded test domain
**What it deliberately omits:**
- No persistence — all state is in-memory
- No ML-DSA-44 signing at runtime (test cert pre-generated by the dev setup
script in `../dev.md`; the stub reads it from `dev/certs/test-registration.b64`
at startup)
- No account management
- No billing or registration flows
---
## Production server
Java/Spring. Deferred. The Node stub defines the API contract that production
must honour.
| Concern | Production |
|---|---|
| Key bundle storage | Redis (`GETDEL` for atomic at-most-once delivery) |
| Domain registrations / accounts | PostgreSQL |
| ML-DSA-44 signing | BouncyCastle or liboqs-java |
| WebSocket relay | Spring WebSocket |
| Account management | Full SaaS (registration, billing, domain admin) |
---
## Signing keys
The server holds two secret keys. Neither is ever stored in source code or
committed to version control — injected via environment variables or a secrets
manager at startup.
| Secret key | Algorithm | Corresponding pinned constant | Purpose |
|---|---|---|---|
| `ED25519_SECRET_KEY` | Ed25519 | `ED25519_PUBLIC_KEY_B64` (extension), `ED25519_PUBKEY` (companion) | Signs key exchange tokens |
| `ML_DSA_44_SECRET_KEY` | ML-DSA-44 | `BACKEND_PUBLIC_KEY_B64` (extension) | Signs domain registration certificates |
Key regeneration patches all pinned constants in extension and companion source
and requires redeployment of all three components.
---
## API surface
All request and response body schemas are defined in `../interfaces.md`. This
section describes endpoint behaviour; the interfaces document is the normative
source for field names, types, and encodings.
### Extension endpoints
**`POST /run/bundle`**
Extension uploads key bundle at Phase 1.
Request: `{ runId, origin, x25519_pubkey_b64, kem_pubkey_b64 }`
`origin` is the full HTTPS origin of the site (`https://host:port`), taken from
the registration certificate the extension verified. The server bakes it
verbatim into the signed token as `url`. The extension must reject any origin
whose scheme is not `https:` before the bundle upload is ever attempted.
The server:
1. Stores the key bundle associated with the run ID.
2. Computes `alpha_hash = SHA-256(x25519_pub_bytes || kem_pub_bytes)`.
3. Signs `{ alpha_hash, url, runId, expires_at }` with Ed25519 → signed token.
4. Returns signed token and run parameters.
Response: `{ signed_token, qr_ttl, max_auto_refresh, pin_ttl }`
The server should eventually verify that the origin is registered to an active
account before signing — this ownership verification mechanism is deferred for
the stub. TTL values in run parameters are server-controlled; clients must
not apply local defaults.
**`GET /domain/status?domain=<hostname>`**
Extension queries account status during site verification.
Response: `{ status: "valid" | "rejected" | "suspended" }` or error if
unreachable.
**`POST /security/report`**
Extension or companion reports a security-class error.
Request: `{ runId, error_type, timestamp }`. Server logs and aggregates.
Site owner notification mechanism deferred — see `../extension/claude.md`,
*Error escalation*.
### Companion endpoints
**`GET /run/bundle/:runId`**
Companion fetches key bundle at Phase 2. **At-most-once: the bundle is
atomically destroyed on this fetch.** A second request for the same run ID returns
an error.
Response: `{ x25519_pubkey_b64, kem_pubkey_b64 }` or `404` if already consumed
or expired.
This is a load-bearing security property. Node stub uses `Map.get()` +
`Map.delete()` (safe for single-process development). Production uses Redis
`GETDEL`.
**`GET /run/relay/:runId` (WebSocket upgrade — extension only)**
The extension upgrades this endpoint to a WebSocket connection immediately after
Phase 1 completes. The server maps the run ID to the live socket and uses it to
push relay messages to the extension. If the service worker suspends and the
socket drops, the extension is responsible for reconnecting before any relay
message arrives.
**`POST /run/relay/:runId` (HTTP — companion only)**
Companion sends a relay message (kem_ciphertext in Phase 2, encrypted credential
in Phase 4) addressed to the run ID.
Request: `{ type, payload_b64 }`. Server forwards the complete message object
over the extension's WebSocket for that run ID. If the WebSocket is not currently open (service worker
suspended), the message is buffered briefly. Buffer lifetime is short (seconds);
if the extension does not reconnect within that window the message is discarded
and the run must be restarted.
### Admin / website endpoints
Domain registration and account management. Deferred for the stub; stub serves
a hardcoded test domain certificate. Production endpoints are a full SaaS
concern.
---
## WebSocket relay
The extension opens a WebSocket connection to `/run/relay/:runId` after
completing Phase 1. The server maintains a mapping of run ID → active connection.
**Message flow:**
1. Extension opens WS for run ID (after Phase 1).
2. Companion POSTs kem_ciphertext → server pushes to extension WS (Phase 2).
3. Companion POSTs encrypted credential → server pushes to extension WS (Phase 4).
4. WS closes on run end (delivered, error, or TTL expiry).
The extension is responsible for keeping its WS connection alive across service
worker suspensions using `chrome.alarms` or equivalent. The server-side buffer
provides a short grace window but is not a reliable recovery mechanism.
---
## State model
| Data | Storage | Lifetime |
|---|---|---|
| Key bundles | In-memory / Redis | `qr_ttl`; destroyed on fetch or expiry |
| Active WS connections (runId → socket) | In-memory | Run lifetime |
| Relay message buffer (undelivered) | In-memory | Seconds; discarded if extension does not reconnect |
| Domain registration certificates | Static file / DB | Until expiry or renewal |
| Account records | Hardcoded / DB | Persistent |
| Signing keys | Environment / secrets manager | Permanent |
---
## Security invariants
- Signing keys are never stored in source code or version control.
- The key bundle is atomically destroyed on first fetch. No partial reads; no
second deliveries.
- The server cannot decrypt relay messages. It forwards opaque blobs; the
run key is never transmitted through it.
- The signed token binds `alpha_hash`, `url`, `runId`, and `expires_at` together.
Substituting any field invalidates the signature.
- Security-class error reports are accepted from any client without
authentication — rate-limiting and aggregation are the server's responsibility.