198 lines
7.8 KiB
Markdown
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.
|