Files
nx9-chronoseal-rs/docs/API.md
T
thakares 4b27a342d1 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)
2026-05-09 18:04:13 +05:30

217 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.