docs: comprehensive ARCHITECTURE, DEPLOYMENT, API, and THREAT_MODEL

ARCHITECTURE.md
- Full component map with ASCII diagram
- Complete session lifecycle (init + heartbeat + failure path)
- Cryptographic protocol spec (hash chain formula, canonical JSON)
- Stack machine instruction set table with stack effects
- Behavioral validation thresholds
- SQLite schema, threat model summary, module reference

DEPLOYMENT.md
- Build instructions (WASM + server + convenience script)
- native binary, systemd (with hardened sandbox notes), Docker
- nginx, Nginx Proxy Manager, and HAProxy reverse proxy configs
- Integration options (sidecar vs proxy-only)
- Full configuration table with all constants
- Observability (RUST_LOG levels), health check, security checklist

API.md
- Full /init and /hb request/response schemas with field tables
- Canonical signing payload specification
- Complete validation rules table (all 13 rejection conditions)
- Hash chain byte-level specification
- WASM exported function reference

THREAT_MODEL.md
- Four attacker profiles (script kiddie → sophisticated adversary)
- Eight attack vectors with mitigations (replay, forgery, hijack, DoS…)
- Explicit out-of-scope limitations
- Operational security notes (CORS, TLS, log level, SQLite)
This commit is contained in:
thakares committed 2026-05-09 18:04:13 +05:30
1 parent 851d3b4876
commit 4b27a342d1
4 files changed
+1017 -256

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
# ChronoSeal — API Reference
## Base URL
All endpoints are relative to the server root. In development: `http://localhost:3000`.
In production: your HTTPS domain via reverse proxy.
---
## Endpoints
### `POST /init`
Initialise a new session. Called once per page load, immediately after the
WASM module generates an Ed25519 keypair.
#### Request
```http
POST /init
Content-Type: application/json
```
```json
{
"public_key": "hex-encoded 32-byte Ed25519 verifying key"
}
```
| Field | Type | Description |
|---|---|---|
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 verifying key generated by the WASM module |
#### Response `200 OK`
```json
{
"session_id": "64-char hex string (32 bytes)",
"salt": "32-char hex string (16 bytes)",
"opcodes_b64": "base64-encoded VM program (8–16 opcodes)",
"initial_hash": "64-char hex string (32 bytes Blake3)",
"expires_at": 1234567890123
}
```
| Field | Type | Description |
|---|---|---|
| `session_id` | `string` | Opaque session identifier; include in every heartbeat |
| `salt` | `string` | Initial salt; used to compute `H(0)` and first `H(1)` |
| `opcodes_b64` | `string` | Base64 VM program; execute with `run_program()` on every heartbeat |
| `initial_hash` | `string` | `H(0) = Blake3(session_id ║ pub_key ║ salt)`; the first `prev_hash` |
| `expires_at` | `number` | Unix timestamp in milliseconds; session expires after 30 minutes of inactivity |
#### Error
Returns `500 Internal Server Error` only on server-side failures (DB errors,
invalid public key length). No meaningful error body is returned.
---
### `POST /hb`
Submit a heartbeat. Called every 12–25 seconds with uniform random jitter.
#### Request
```http
POST /hb
Content-Type: application/json
```
```json
{
"session_id": "64-char hex",
"prev_hash": "64-char hex",
"timestamp": 1234567890123,
"entropy_data": {
"events": [
{ "x": 412.0, "y": 308.5, "t": 1234.567 },
{ "x": 415.2, "y": 310.1, "t": 1285.123 }
]
},
"stack_state": {
"stack": [2971406957, 1234567890],
"ip": 42
},
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"signature": "128-char hex Ed25519 signature"
}
```
| Field | Type | Description |
|---|---|---|
| `session_id` | `string` | Session ID from `/init` |
| `prev_hash` | `string` | Hash chain head from previous heartbeat (or `initial_hash` for the first) |
| `timestamp` | `number` | `Date.now()` in milliseconds; must be within ±30s of server time |
| `entropy_data.events` | `array` | Mouse events since previous heartbeat; each has `x`, `y` (px), `t` (performance.now ms) |
| `stack_state.stack` | `array` | `u32[]` result of executing the VM program |
| `stack_state.ip` | `number` | Instruction pointer after execution |
| `fingerprint.aspectRatio` | `string` | `(screen.width / screen.height).toFixed(10)` |
| `fingerprint.devicePixelRatio` | `string` | `String(window.devicePixelRatio)` |
| `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency \|\| 1` |
| `signature` | `string` | Hex-encoded 64-byte Ed25519 signature over the canonical payload |
#### Canonical Signing Payload
The client signs the following JSON object. Top-level keys must be sorted
alphabetically. Nested object keys follow their natural serialisation order.
```json
{
"entropyData": { "events": [{ "t": …, "x": …, "y": … }] },
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
"prevHash": "…",
"sessionId": "…",
"stackState": { "ip": …, "stack": […] },
"timestamp": …
}
```
Note: field names in the signing payload use camelCase (`sessionId`,
`prevHash`, `entropyData`, `stackState`) while the request body uses
snake_case (`session_id`, `prev_hash`, `entropy_data`, `stack_state`).
#### Response `200 OK` — Accepted
```json
{
"status": "ok",
"next_salt": "32-char hex string (16 bytes)"
}
```
The client must:
1. Capture `sentSalt = currentSalt` before updating.
2. Set `currentSalt = next_salt`.
3. Compute `prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt)`.
#### Response `200 OK` — Rejected
```json
{
"status": "ok"
}
```
`next_salt` is absent. The response body is intentionally identical in
structure. Rejections are silent — the caller cannot distinguish a validation
failure from a rate limit hit or an expired session.
The client should log a warning and continue scheduling heartbeats (they will
continue to fail until the page is reloaded and a new session is established).
---
## Validation Rules (Server-Side)
Heartbeats are rejected (silently) if any of the following checks fail:
| Check | Condition for rejection |
|---|---|
| Rate limit | > 5 requests per 10-second window for this `session_id` |
| Session not found | `session_id` not in SQLite |
| Session expired | `current_time_ms > expires_at` |
| Signature invalid | Ed25519 verification fails against stored public key |
| Hash chain broken | `hex(prev_hash) ≠ stored last_hash` |
| Timestamp drift | `\|server_now_ms - timestamp\| > 30 000` |
| Insufficient mouse events | `events.len() < 3` |
| Insufficient mouse distance | `total_dist < 10.0 px` |
| Mouse speed too high | `total_dist / total_time_ms > 2.0 px/ms` |
| No mouse pauses | `pause_count < 1` |
| Invalid aspect ratio | `ar < 0.5` or `ar > 3.0` |
| Invalid devicePixelRatio | `dpr ≤ 0.0` or `dpr > 5.0` |
| Zero hardwareConcurrency | `hardware_concurrency == 0` |
---
## Hash Chain Specification
```
H(0) = Blake3( session_id_bytes ║ pub_key_bytes ║ salt₀_bytes )
H(n) = Blake3(
saltₙ₋₁_bytes
║ H(n-1)_bytes
║ timestamp_u64_le_bytes
║ Blake3( UTF-8( JSON(entropy_data) ) )
║ Blake3( UTF-8( JSON(stack_state) ) )
)
```
All inputs are concatenated in the order shown. `timestamp` is encoded as a
64-bit unsigned integer in little-endian byte order. JSON serialisation of
`entropy_data` and `stack_state` uses the field order defined by the shared
Rust types (serde derive, no custom ordering).
---
## WASM API
The WASM module (`antibot_wasm`) exports the following functions to JavaScript:
| Function | Signature | Description |
|---|---|---|
| `generate_keypair()` | `() → string` | Generate Ed25519 keypair; return hex public key. Private key stored in WASM memory. |
| `get_public_key()` | `() → string` | Return hex public key, or `""` if not initialised. |
| `sign_message(msg)` | `(string) → string` | Sign UTF-8 string; return hex signature, or `""` if not initialised. |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) → string` | Compute next Blake3 chain hash; all inputs/output hex or JSON strings. |
| `run_program(b64)` | `(string) → JsValue` | Execute base64 VM program; return `{ stack: u32[], ip: number }`. |
All functions return empty strings on error rather than panicking.
Callers must check for empty return values before using the result.