# Plan: HTTP and WebSocket (WSS) Listener Modes for n_signer

## Goal

Add two new listener modes to n_signer so that standard HTTP clients (curl) and
WebSocket clients (browsers, relay-style tools) can send JSON-RPC requests
directly, without the custom 4-byte length-prefixed framing.

## Current state

n_signer supports these listener modes:
- `unix` — abstract namespace Unix socket, 4-byte framed JSON
- `stdio` — stdin/stdout, 4-byte framed JSON, one request per invocation
- `qrexec` — same as stdio but with `QREXEC_REMOTE_DOMAIN` caller identity
- `tcp:HOST:PORT` — TCP, 4-byte framed JSON

All modes use the same dispatcher (`dispatcher_handle_request`) which takes a
JSON string and returns a JSON string. The only difference between modes is the
transport framing and caller-identity extraction.

## Proposed modes

### HTTP mode: `--listen http:HOST:PORT`

**Protocol:** HTTP/1.1 POST with JSON body → JSON response.

```
POST / HTTP/1.1
Content-Type: application/json

{"id":"1","method":"otp_encrypt","params":["SGVsbG8=",{"encoding":"ascii"}]}

HTTP/1.1 200 OK
Content-Type: application/json

{"id":"1","result":"{...}"}
```

**curl example:**
```bash
curl -s -X POST http://127.0.0.1:11111/ \
  -H 'Content-Type: application/json' \
  -d '{"id":"1","method":"get_public_key","params":[{"role":"main"}]}'
```

**Implementation:**
- Parse the HTTP request line + headers (minimal parser — just enough for POST)
- Read the Content-Length bytes as the JSON-RPC request
- Call `dispatcher_handle_request()`
- Write the HTTP response with the JSON result

**Difficulty: Low.** The HTTP parser can be minimal (~100 lines). No need for
a full HTTP server — just POST with a JSON body. No chunked encoding, no
keep-alive, no static file serving. One request per connection (connection
close after response), or simple keep-alive loop.

**Caller identity:** `tcp:<peer-addr>` (same as TCP mode). No `QREXEC_REMOTE_DOMAIN`.

**HTTPS variant:** `--listen https:HOST:PORT` wraps the HTTP listener in TLS.
Requires `--tls-cert <path>` and `--tls-key <path>` flags. OpenSSL is already
linked. The HTTP parser stays the same; TLS is layered underneath (accept
connection → TLS handshake → then HTTP). See Phase 3 below.

### WebSocket mode: `--listen wss:HOST:PORT` (or `ws:HOST:PORT`)

**Protocol:** WebSocket with JSON text frames.

```
Client → Server: text frame with JSON-RPC request
Server → Client: text frame with JSON-RPC response
```

**Browser example:**
```javascript
const ws = new WebSocket("ws://127.0.0.1:11111");
ws.onopen = () => {
  ws.send(JSON.stringify({id:"1", method:"get_public_key", params:[{role:"main"}]}));
};
ws.onmessage = (e) => {
  console.log(JSON.parse(e.data));
};
```

**Implementation:**
- HTTP upgrade handshake (Sec-WebSocket-Key → Sec-WebSocket-Accept)
- WebSocket frame parser (opcode, mask, payload length — 7/16/64 bit)
- Text frames (opcode 0x1) carry JSON-RPC requests
- Response as text frames (opcode 0x1, unmasked from server)
- One JSON-RPC request per text frame; one response per request

**Difficulty: Medium.** The WebSocket handshake is straightforward (SHA-1 +
base64 of the key + magic GUID). The frame parser needs to handle:
- 7-bit, 16-bit, and 64-bit payload lengths
- Client-to-server masking (XOR with 4-byte mask)
- Ping/pong frames (opcode 0x9/0xA)
- Close frame (opcode 0x8)
- Fragmentation (continuation frames, opcode 0x0) — can defer/skip for v1

A minimal implementation is ~200-300 lines. No need for a full RFC 6455
compliance — just enough for curl, browsers, and common WebSocket clients.

**WSS (TLS) variant:** `wss:HOST:PORT` wraps the WebSocket in TLS. This
requires linking against OpenSSL (already a dependency) and adding TLS
handshake + certificate handling. Difficulty: Medium-High due to cert
management. For v1, `ws:` (plaintext) is sufficient for localhost/FIPS-mesh
use; `wss:` can be added later.

**Caller identity:** `ws:<peer-addr>` or `wss:<peer-addr>`.

## Security concerns

### 1. Network exposure

**Unix sockets** are only accessible from the same host/qube — no network
exposure. **HTTP/WS** listeners bind to a TCP port, which is potentially
reachable from other hosts/qubes on the network.

**Mitigation:**
- Default bind address: `127.0.0.1` (localhost only), not `[::]` (all
  interfaces). The user must explicitly pass `--listen http:0.0.0.0:11111` to
  expose it.
- The existing policy/approval system is the real gate — network access alone
  doesn't grant signing. Unknown callers get `PROMPT_EVERY_REQUEST` by default.

### 2. No authentication in HTTP/WS mode

The current TCP mode has no authentication either — it relies on the
policy/approval prompt. HTTP/WS would be the same. Anyone who can reach the
port can send requests, but they still need approval for sensitive operations.

**Mitigation:**
- The `--auth required` mode (auth envelopes) could be extended to HTTP/WS.
  The client would include a signed auth envelope in an HTTP header
  (e.g. `X-Nsigner-Auth: <envelope>`) or as a WebSocket subprotocol header.
- For v1, rely on localhost binding + policy/approval prompts, same as TCP.

### 3. CORS for browser access

If the WebSocket listener is accessed from a browser running on a different
origin, CORS headers are needed for the HTTP upgrade handshake.

**Mitigation:**
- Add `Access-Control-Allow-Origin: *` to the WebSocket upgrade response
  (the policy/approval system is the real security boundary, not CORS).
- Or restrict to same-origin only (no CORS header → browser blocks
  cross-origin).

### 4. Request size limits — MUST be raised for OTP blob encryption

The current `SERVER_MAX_MSG_SIZE` is 65536 bytes (64KB). This is too small
for OTP encryption of images or blobs:

- A 48KB image → base64 in JSON param = ~64KB → hits the limit
- A 1MB image → base64 = ~1.33MB → way over
- OTP Padmé padding rounds up to the next power of 2, and the response
  contains the base64-encoded ciphertext, which is even larger

**This is a pre-existing limitation that affects ALL transport modes**, not
just HTTP/WS. It should be fixed before or alongside the HTTP/WS work.

**Proposed change:** raise `SERVER_MAX_MSG_SIZE` to 16MB (16777216). This
accommodates:
- Up to ~12MB plaintext (base64 = ~16MB in the JSON param)
- The OTP response (base64-encoded padded ciphertext) for the same
- The `stdin_buf[SERVER_MAX_MSG_SIZE + 1]` stack buffer in `client_main`
  should be changed to a heap allocation to avoid a 16MB stack frame

The `transport_recv_framed` function already takes a max-size parameter, so
the change is: update the constant, change the stack buffer to malloc, and
verify the dispatcher doesn't have other hardcoded size assumptions.

**For HTTP/WS:** enforce the same `SERVER_MAX_MSG_SIZE` limit via
`Content-Length` checking in the HTTP parser.

### 5. TLS for WSS

For `wss:` mode, TLS is required. This means:
- Certificate management (self-signed for local/FIPS mesh, or CA-signed for
  public-facing).
- The signer would need a `--tls-cert` and `--tls-key` flag.
- Pinning: clients should verify the cert fingerprint, not just trust the CA.

**For v1:** skip WSS/TLS. Provide `ws:` (plaintext) only, suitable for
localhost and FIPS mesh (which has its own network-level isolation). Add
`wss:` later when there's a real public-facing use case.

### 6. Connection flooding

HTTP/WS listeners are susceptible to connection flooding (many open
connections, slowloris-style attacks). The current single-threaded poll loop
handles one connection at a time, which naturally limits flood impact but
also limits throughput.

**Mitigation:**
- Connection timeout (close idle connections after N seconds).
- Max connections limit.
- For v1, the single-threaded model is fine — it's a signer, not a web server.

## Implementation plan

### Phase 0: Raise SERVER_MAX_MSG_SIZE (prerequisite)

- [ ] Change `SERVER_MAX_MSG_SIZE` from 65536 to 16777216 (16MB) in all
      `.c` files where it's defined (the headerless-decls pattern means it's
      redefined in every file).
- [ ] Change the `stdin_buf[SERVER_MAX_MSG_SIZE + 1]` stack buffer in
      `client_main` to a heap allocation (malloc/free) to avoid a 16MB stack
      frame.
- [ ] Verify `transport_recv_framed` handles the larger size correctly (it
      already takes max-size as a parameter).
- [ ] Test with a large OTP encrypt/decrypt round-trip (e.g. 1MB plaintext).

### Phase 1: HTTP listener mode

- [ ] Add `NSIGNER_LISTEN_HTTP` to the listen mode enum.
- [ ] Parse `--listen http:HOST:PORT` in the CLI flag parser.
- [ ] Implement a minimal HTTP/1.1 parser in `src/http_listener.c`:
  - Read request line (method, path, HTTP version)
  - Read headers (only need Content-Length)
  - Read body (Content-Length bytes)
  - Reject non-POST methods with 405
  - Reject oversized bodies with 413
- [ ] Call `dispatcher_handle_request()` with the body, write JSON response
  with `200 OK` + `Content-Type: application/json`.
- [ ] Caller identity: `http:<peer-ip>:<peer-port>`.
- [ ] Default bind: `127.0.0.1` (not `[::]`).
- [ ] Add to interactive transport menu as option 4.
- [ ] Test with curl.

### Phase 2: WebSocket listener mode

- [ ] Add `NSIGNER_LISTEN_WS` to the listen mode enum.
- [ ] Parse `--listen ws:HOST:PORT` in the CLI flag parser.
- [ ] Implement WebSocket handshake + frame parser in `src/ws_listener.c`:
  - HTTP upgrade handshake (Sec-WebSocket-Key → Accept)
  - Frame parser (opcode, mask, payload length)
  - Text frames → JSON-RPC request → dispatcher → text frame response
  - Ping/pong handling
  - Close frame handling
- [ ] Caller identity: `ws:<peer-ip>:<peer-port>`.
- [ ] Default bind: `127.0.0.1`.
- [ ] Add to interactive transport menu as option 5.
- [ ] Test with a browser or `websocat`.

### Phase 3: TLS mode (HTTPS + WSS)

- [ ] Add `--tls-cert <path>` and `--tls-key <path>` CLI flags.
- [ ] Add `--listen https:HOST:PORT` — HTTP listener wrapped in TLS.
- [ ] Add `--listen wss:HOST:PORT` — WebSocket listener wrapped in TLS.
- [ ] TLS handshake via OpenSSL (already a dependency):
  - Load cert/key from files
  - `SSL_CTX_new(TLS_server_method())`
  - `SSL_CTX_use_certificate_file()` / `SSL_CTX_use_PrivateKey_file()`
  - Per-connection: `SSL_new()` → `SSL_set_fd()` → `SSL_accept()`
  - Replace read/write calls with `SSL_read()` / `SSL_write()`
- [ ] Self-signed cert generation helper (or document `openssl req` command).
- [ ] Certificate pinning guidance for clients (verify fingerprint, not just CA).
- [ ] curl usage: `curl --cacert <cert.pem>` or `curl -k` (insecure, for testing).
- [ ] Not strictly needed for localhost/FIPS mesh use, but good for defense in depth.

## Difficulty assessment

| Mode | Difficulty | New code | Dependencies | Risk |
|---|---|---|---|---|
| HTTP | Low | ~150 lines | None (minimal parser) | Low — simple POST/JSON |
| WS | Medium | ~300 lines | None (minimal frame parser) | Medium — frame parsing edge cases |
| HTTPS | Medium | ~150 + ~80 TLS | OpenSSL (already linked) | Medium — cert management |
| WSS | Medium-High | ~300 + ~80 TLS | OpenSSL (already linked) | Higher — cert + WS edge cases |

## Recommendation

Start with HTTP mode (Phase 1) — it's the simplest and immediately enables
curl access. Add WebSocket (Phase 2) if browser access is needed. Defer WSS
(Phase 3) until there's a real public-facing use case.
