docs: update README and docs for v0.6.0 architecture and deployment, preserve current server/shared/wasm updates

This commit is contained in:
thakares committed 2026-05-29 21:27:11 +05:30
1 parent 6067746898
commit 2b8afd54e0
27 files changed
+1413 -2567

No files matched your search

+100 -137
View File
@@ -1,20 +1,18 @@
# ChronoSeal — API Reference
# ChronoSeal API Reference
ChronoSeal defines a small, deterministic API surface for browser attestation and heartbeat verification.
## Base URL
All endpoints are relative to the server root. In development: `http://localhost:3000`.
In production: your HTTPS domain via reverse proxy.
All endpoints are relative to the server root. In development: `http://localhost:3000`. In production: the HTTPS origin of the protected site.
---
## Endpoints
## POST /init
### `POST /init`
Initialise a new browser session.
Initialise a new session. Called once per page load, immediately after the
WASM module generates an Ed25519 keypair.
#### Request
### Request
```http
POST /init
@@ -29,17 +27,17 @@ Content-Type: application/json
| Field | Type | Description |
|---|---|---|
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 verifying key generated by the WASM module |
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 public key generated by the WASM runtime |
#### Response `200 OK`
### 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,
"session_id": "64-char hex string",
"salt": "32-char hex string",
"opcodes_b64": "base64-encoded VM program",
"initial_hash": "64-char hex string",
"expires_at": 1234567890123,
"heartbeat_min_interval_ms": 12000,
"heartbeat_max_interval_ms": 25000,
"gene_size": 512,
@@ -50,29 +48,28 @@ Content-Type: application/json
| 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 |
| `heartbeat_min_interval_ms` | `number` | Lower bound for randomized heartbeat scheduling |
| `heartbeat_max_interval_ms` | `number` | Upper bound for randomized heartbeat scheduling |
| `gene_size` | `number` | Initial synthetic gene size used by server and WASM (default 512) |
| `mutation_step` | `number` | Server-issued mutation order step expected on next heartbeat |
| `mutation_order_b64` | `string` | Base64-encoded mutation opcode program for the current step |
| `session_id` | `string` | Opaque session identifier for the current browser session |
| `salt` | `string` | Random 16-byte salt used to seed the hash chain |
| `opcodes_b64` | `string` | Base64-encoded randomized VM program executed on every heartbeat |
| `initial_hash` | `string` | Initial chain hash `H(0)` used as `prev_hash` for the first heartbeat |
| `expires_at` | `number` | Unix timestamp in milliseconds after which the session expires |
| `heartbeat_min_interval_ms` | `number` | Minimum heartbeat interval in milliseconds |
| `heartbeat_max_interval_ms` | `number` | Maximum heartbeat interval in milliseconds |
| `gene_size` | `number` | Size of the initial synthetic gene buffer |
| `mutation_step` | `number` | Initial mutation step expected on the first heartbeat |
| `mutation_order_b64` | `string` | Base64-encoded mutation order for gene commitment preview |
#### Error
### Error
Returns `500 Internal Server Error` only on server-side failures (DB errors,
invalid public key length). No meaningful error body is returned.
`POST /init` returns `500 Internal Server Error` only for server-side failures such as invalid public key length or persistence errors. No detailed error information is exposed to callers.
---
### `POST /hb`
## POST /hb
Submit a heartbeat. Called every 12–25 seconds with uniform random jitter.
Submit a heartbeat to continue the session.
#### Request
### Request
```http
POST /hb
@@ -81,13 +78,12 @@ Content-Type: application/json
```json
{
"session_id": "64-char hex",
"prev_hash": "64-char hex",
"timestamp": 1234567890123,
"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 }
{ "x": 412.0, "y": 308.5, "t": 1234.567 }
]
},
"stack_state": {
@@ -95,74 +91,73 @@ Content-Type: application/json
"ip": 42
},
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"aspectRatio": "1.7777777778",
"devicePixelRatio": 2,
"hardwareConcurrency": 8
},
"mutation_step": 1,
"gene_commitment": "64-char hex Blake3 commitment",
"signature": "128-char hex Ed25519 signature"
"gene_commitment": "64-char hex",
"signature": "128-char hex"
}
```
| 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` |
| `mutation_step` | `number` | Must match server-side pending mutation step |
| `gene_commitment` | `string` | Commitment of the locally previewed candidate gene after applying `mutation_order_b64` |
| `prev_hash` | `string` | Previous hash chain head (`initial_hash` on first heartbeat) |
| `timestamp` | `number` | `Date.now()` in milliseconds |
| `entropy_data.events` | `array` | Mouse event list since the previous heartbeat |
| `stack_state.stack` | `array` | VM stack contents after program execution |
| `stack_state.ip` | `number` | VM instruction pointer after execution |
| `fingerprint.aspectRatio` | `string` | `screen.width / screen.height` to 10 decimal places |
| `fingerprint.devicePixelRatio` | `number` | `window.devicePixelRatio` |
| `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency || 1` |
| `mutation_step` | `number` | Current mutation step sent by the client |
| `gene_commitment` | `string` | Gene commitment produced by the WASM preview mutation engine |
| `signature` | `string` | Hex-encoded 64-byte Ed25519 signature over the canonical payload |
#### Canonical Signing 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.
The client signs a canonical JSON object with top-level keys sorted alphabetically:
```json
{
"entropyData": { "events": [{ "t": …, "x": …, "y": … }] },
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
"geneCommitment":"…",
"mutationStep": …,
"prevHash": "…",
"sessionId": "…",
"stackState": { "ip": …, "stack": […] },
"timestamp": …
"entropyData": { "events": [{ "t": ..., "x": ..., "y": ... }] },
"fingerprint": {
"aspectRatio": "...",
"devicePixelRatio": ...,
"hardwareConcurrency": ...
},
"geneCommitment": "...",
"mutationStep": ...,
"prevHash": "...",
"sessionId": "...",
"stackState": { "ip": ..., "stack": [...] },
"timestamp": ...
}
```
Note: field names in the signing payload use camelCase (`sessionId`,
`prevHash`, `entropyData`, `stackState`, `mutationStep`, `geneCommitment`)
while the request body uses snake_case (`session_id`, `prev_hash`,
`entropy_data`, `stack_state`, `mutation_step`, `gene_commitment`).
Note: the signed payload uses camelCase while the transport request uses snake_case.
#### Response `200 OK` — Accepted
### Response `200 OK` — Accepted
```json
{
"status": "ok",
"next_salt": "32-char hex string (16 bytes)",
"status": "ok",
"next_salt": "32-char hex string",
"next_mutation_step": 2,
"next_mutation_order_b64": "base64-encoded mutation program"
}
```
The client must:
1. Preview commitment locally from `mutation_order_b64` and send it in the heartbeat.
2. Capture `sentSalt = currentSalt` before updating.
3. Set `currentSalt = next_salt`.
4. Compute `prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt)`.
5. Commit the previewed gene state.
6. Replace pending mutation values with `next_mutation_step` and `next_mutation_order_b64`.
| Field | Type | Description |
|---|---|---|
| `status` | `string` | Always `ok` |
| `next_salt` | `string` | Next server salt for the following heartbeat |
| `next_mutation_step` | `number` | Next mutation step to apply after acceptance |
| `next_mutation_order_b64` | `string` | Base64-encoded next mutation program |
#### Response `200 OK` — Rejected
### Response `200 OK` — Rejected
```json
{
@@ -170,77 +165,45 @@ The client must:
}
```
`next_salt`, `next_mutation_step`, and `next_mutation_order_b64` are 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.
A rejected heartbeat omits `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
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).
This silent rejection model avoids giving attackers distinct failure signals.
---
## Validation Rules (Server-Side)
## Validation Rules
Heartbeats are rejected (silently) if any of the following checks fail:
Heartbeats are rejected silently when any validation step fails:
| 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` |
| Mutation step mismatch | `mutation_step ≠ pending_mutation_step` |
| Mutation commitment mismatch | `gene_commitment` does not match server-computed candidate commitment |
| 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` |
* session missing or expired
* signature invalid
* hash chain mismatch
* mutation step mismatch
* gene commitment mismatch
* timestamp outside ±30 seconds
* insufficient mouse events
* insufficient mouse movement
* unrealistic speed profile
* missing pause intervals
* invalid fingerprint values
---
## Hash Chain Specification
## WASM Runtime Exports
```
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 (`chronoseal_wasm`) exports the following functions to JavaScript:
The WASM module 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 }`. |
| `init_gene_state(gene_size)` | `(u32) → bool` | Initialise synthetic gene state in WASM memory. |
| `preview_gene_commitment(order_b64)` | `(string) → string` | Apply mutation order on preview state and return commitment hex. |
| `commit_gene_preview()` | `() → bool` | Commit previewed mutation state after accepted heartbeat. |
| `discard_gene_preview()` | `() → void` | Discard previewed mutation state after rejection/error. |
| `current_gene_commitment()` | `() → string` | Return current committed gene commitment hex. |
| `generate_keypair()` | `() -> string` | Generate a new Ed25519 keypair and return the public key hex |
| `get_public_key()` | `() -> string` | Return the current public key hex |
| `sign_message(msg)` | `(string) -> string` | Sign a UTF-8 string payload and return the hex signature |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | Compute the next Blake3 hash chain value |
| `run_program(b64)` | `(string) -> JsValue` | Execute a base64 VM program and return stack state |
| `init_gene_state(gene_size)` | `(u32) -> bool` | Initialise the synthetic gene buffer in WASM memory |
| `preview_gene_commitment(order_b64)` | `(string) -> string` | Preview the next gene commitment from a mutation order |
| `commit_gene_preview()` | `() -> bool` | Commit the previewed mutation after an accepted heartbeat |
| `discard_gene_preview()` | `() -> void` | Discard the previewed mutation after rejection or error |
| `current_gene_commitment()` | `() -> string` | Return the current committed gene commitment |
String-returning functions return `""` on error rather than panicking. Callers
must check for empty strings and boolean return values before use.
String-returning functions return `""` on error. Callers must handle empty values and boolean failures gracefully.
+108 -489
View File
@@ -1,526 +1,145 @@
# ChronoSeal — Architecture
# ChronoSeal Architecture
> Note (v0.6.0): Synthetic Gene Mutation flow and mutation handshake updates are documented in [REFRACTORING-v0.6.0.md](https://github.com/thakares/chronoseal-rs/blob/main/docs/REFRACTORING-v0.6.0.md) and [API.md](https://github.com/thakares/chronoseal-rs/blob/main/docs/API.md).
ChronoSeal is a Unix-native cryptographic attestation daemon that validates browser session continuity through deterministic VM execution, chained cryptographic state, and a shared Synthetic Gene Mutation Engine.
## Overview
ChronoSeal is a stateless, cryptographic browser attestation framework. Its
purpose is to make automated clients (headless browsers, AI scrapers, API
harvesters) computationally expensive and operationally complex to operate,
while remaining completely invisible to real human users.
ChronoSeal is designed as a production-grade infrastructure component, not as a consumer-facing widget. It is a lightweight daemon that can be operated, monitored, and integrated like any other native Linux service.
The design is inspired by the heartbeat model used in embedded IoT firmware:
a device that stops sending signed, chained attestations is assumed to be
offline or compromised. ChronoSeal applies the same principle to browser
sessions.
Key characteristics:
---
* Unix-native daemon with systemd-compatible lifecycle
* CLI-first control plane and configuration
* Shared Rust/WASM runtime for server/client parity
* Modular storage backend abstraction (`sqlite-in-memory`, `sqlite-disk`, `valkey`)
* Silent rejection semantics for attacker resilience
* Privacy-preserving ephemeral session state
## Design Principles
## Core Components
**Stateless per request.** The server carries no per-request state beyond what
is stored in SQLite keyed on `session_id`. Every HTTP request is independently
verifiable.
### `shared/`
**Silent failure.** Validation failures never return an error status or an
error body. The server always responds `{"status":"ok"}` and simply omits `next_salt`. The client degrades gracefully. Attackers cannot enumerate
validation rules by probing error responses.
Shared protocol and runtime primitives used by both the server and the browser runtime:
**Private key isolation.** The Ed25519 signing key is generated inside the
WASM module and never serialised, never exposed to the JavaScript environment,
and never transmitted. It exists only in WASM linear memory for the lifetime
of the page.
* Cryptographic primitives: Blake3, Ed25519
* Hash chain logic and session commitment handling
* Synthetic gene model and deterministic mutation engine
* Serialization, encoding, and canonical signing helpers
**Layered validation.** A heartbeat must pass five independent checks: session
existence, expiry, signature, hash chain, and behavioral signals. Bypassing
one layer is not sufficient.
### `server/`
**Cost asymmetry.** Each heartbeat requires a real browser environment, mouse
activity, correct WASM execution, chain state synchronisation, and a valid
Ed25519 signature over a time-windowed payload. For an automated client, the
synchronisation burden alone makes scaled operation expensive.
The server crate implements the runtime daemon:
**Deterministic mutation parity (v0.6.0).** Each heartbeat additionally carries
a `mutation_step` and `gene_commitment` derived from a server-issued mutation
program. Server and WASM execute the same shared opcode engine
(`shared/src/vm_extensions.rs`), and the server rejects any heartbeat where
the recomputed commitment does not match the client-supplied value.
* `routes/init.rs` — session initialization API
* `routes/heartbeat.rs` — heartbeat verification API
* `session.rs` — session lifecycle, mutation parity, and heartbeat validation
* `storage.rs` — backend abstraction and persistence
* `crypto.rs` — signature verification and key handling
* `trust.rs` — behavioral entropy and sanity validation
* `ratelimit.rs` — per-session request throttling
* `cleanup.rs` — expiration and eviction tasks
* `runtime.rs` — daemon bootstrap, metrics, and state management
### High-Level Design
### `wasm/`
- **Core**: Rust + Axum (async web framework)
- **Storage**: `db_type` selectable (`sqlite-in-memory`, `sqlite-in-disk`, `valkey` compatibility mode)
- **Client**: WASM + Rust (runs in browser for proof generation)
- **Security Model**: Behavioral analysis + hash chaining + entropy scoring + deterministic gene mutation parity
- **Deployment**: Static musl binary, systemd service, optional Docker
The client runtime crate compiles to WebAssembly and powers attestation in the browser.
### Key Components
* `crypto.rs` — in-WASM signing and hash computation
* `vm.rs` — randomized opcode VM execution
* `vm_extensions.rs` — synthetic gene mutation preview and commit lifecycle
- `shared/` — Types, constants, crypto primitives, gene model, and mutation engine used by server and WASM
- `server/` — Axum routes, session management, trust engine, rate limiting, cleanup tasks
- `wasm/` — Client-side proof generation, mutation preview/commit lifecycle
- `frontend/` — Static assets served by the application
### `frontend/`
### Unix-Native Design Decisions
Static browser integration code that loads the WASM module, orchestrates init/heartbeat flow, and collects browser entropy.
- Runs as a proper systemd service with strict sandboxing
- All state is either in-memory or in standard locations (`/run/`, `/var/log/`, `/etc/`)
- Graceful shutdown and reload support via signals
- Logging designed for `journalctl` and structured parsing
- Configuration is fully runtime (no recompile needed)
## v0.6.0 Innovation
### Design Goal
The primary innovation in v0.6.0 is the **Synthetic Gene Mutation Engine**.
## ChronoSeal should feel as natural to use as `nginx` or `redis-server` on a Linux system.
This layer adds a deterministic, shared server/WASM mutation handshake to the existing heartbeat continuity model.
## Component Map
Key v0.6.0 behavior:
* `mutation_order_b64` is issued at session initialization and after every accepted heartbeat
* `mutation_step` is tracked on both client and server
* `gene_commitment` is computed locally in WASM and validated by the server
* mutation state is persisted per session and advanced only on accepted heartbeats
* scalar mutation programs are deterministic and bounded in cost
This makes replay and tampering attacks significantly more expensive while preserving the existing privacy-first and silent-failure semantics.
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ entropy.js │ │ heartbeat.js │ │ transport.js│ │
│ │ │ │ │ │ │ │
│ │ mousemove │──►│ orchestrates │──►│ fetch POST │ │
│ │ event ring │ │ init + HB │ │ /init /hb │ │
│ └─────────────┘ └──────┬───────┘ └─────────────┘ │
│ │ │
│ ┌──────▼───────────────────────┐ │
│ │ WASM Module (antibot_wasm) │ │
│ │ │ │
│ │ crypto.rs vm.rs │ │
│ │ ├ generate_keypair() │ │
│ │ ├ sign_message() │ │
│ │ ├ compute_next_hash() │ │
│ │ ├ run_program() │ │
│ │ vm_extensions.rs │ │
│ │ ├ preview_mutation() │ │
│ │ └ commit_mutation() │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ HTTPS
┌─────────────────────────▼───────────────────────────────┐
│ Server (Axum) │
│ │
│ routes/init.rs routes/heartbeat.rs │
│ │ │ │
│ └──────────┬───────────────┘ │
│ ▼ │
│ session.rs │
│ ├ create_session() │
│ └ verify_heartbeat() │
│ └ validate_mutation_parity() [v0.6.0] │
│ │ │
│ ┌──────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ crypto.rs trust.rs fingerprint.rs │
│ (sig verify) (mouse (aspect ratio, │
│ speed) DPR, HW conc.) │
│ │ │
│ ▼ │
│ shared::hashing (Blake3 hash chain) │
│ shared::gene (gene model + commitment) [v0.6.0] │
│ shared::vm_extensions (mutation opcodes) [v0.6.0] │
│ │ │
│ ▼ │
│ storage.rs (SQLite: in-memory / in-disk / valkey) │
│ │
│ ratelimit.rs cleanup.rs vm.rs middleware.rs │
└─────────────────────────────────────────────────────────┘
Browser Server
┌──────────────────────────────────────────────────────────────┐
│ frontend/ + WASM runtime │
│ - generate_keypair() │
│ - sign_message() │
│ - compute_next_hash() │
│ - run_program() │
│ - preview_gene_commitment() │
│ - commit_gene_preview() │
│ │
│ POST /init -> │
│ POST /hb -> │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ server/ │
│ - signature validation │
│ - hash chain continuity │
│ - rate limiting │
│ - behavioral trust checks │
│ - mutation step validation │
│ - gene commitment verification │
│ - session persistence │
│ - metrics and health │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ storage backends │
│ - sqlite-in-memory │
│ - sqlite-disk │
│ - valkey compatibility mode │
└──────────────────────────────────────────────────────────────┘
```
---
## Storage Backends
## Session Lifecycle
ChronoSeal supports pluggable backend modes using the `db_type` configuration option.
### 1. Initialisation — `POST /init`
* `sqlite-in-memory` — default ephemeral session storage. No persistence across restarts.
* `sqlite-disk` — persisted SQLite database on disk through `db_path`.
* `valkey` — compatibility mode for alternative storage backends, currently supported alongside SQLite compatibility semantics.
```
Client Server
│ │
│ generate Ed25519 keypair (in WASM) │
│ pub_key = verifying_key.to_bytes() │
│ │
├─── { public_key: hex(pub_key) } ──────►│
│ │ session_id = rand::random::<[u8;32]>()
│ │ salt₀ = rand::random::<[u8;16]>()
│ │ H(0) = Blake3(session_id║pub_key║salt₀)
│ │ opcodes = generate_random_program(8..=16)
│ │ gene = initial gene buffer [v0.6.0]
│ │ mutation_order = generate_mutation_program() [v0.6.0]
│ │ INSERT INTO sessions …
│ │
│◄── { session_id, salt, opcodes_b64, │
│ initial_hash, expires_at, │
│ mutation_step, │ [v0.6.0]
│ mutation_order_b64 } ─────────────┤ [v0.6.0]
│ │
│ prevHash = initial_hash │
│ currentSalt = salt │
│ opcodesB64 = opcodes_b64 │
│ mutationStep = mutation_step │ [v0.6.0]
│ mutationOrderB64 = mutation_order_b64 │ [v0.6.0]
```
## Runtime Philosophy
### 2. Heartbeat — `POST /hb`
ChronoSeal is intentionally designed to behave like traditional Unix infrastructure software:
Fired every 12–25 seconds with uniform random jitter.
* explicit CLI operations (`run`, `status`, `health`, `config`, `metrics`, `stats`, `db-type`)
* structured logging for `journalctl`
* PID file management and graceful shutdown
* systemd sandbox support
* runtime configuration via TOML and CLI overrides
* clear separation of protocol, persistence, and runtime concerns
```
Client Server
│ │
│ stackState = run_program(opcodesB64) │
│ commitment = preview_mutation( │ [v0.6.0]
│ mutationOrderB64, mutationStep) │
│ events = collectEntropy(lastTime)│
│ ts = Date.now() │
│ │
│ signable = { │
│ entropyData, fingerprint, │ ← keys sorted alphabetically
│ prevHash, sessionId, │
│ stackState, timestamp, │
│ mutation_step, gene_commitment │ [v0.6.0]
│ } │
│ sig = sign_message( │
│ JSON.stringify(signable, keys.sort))│
│ │
├─── { session_id, prev_hash, timestamp, │
│ entropy_data, stack_state, │
│ fingerprint, signature, │
│ mutation_step, gene_commitment }─►│ [v0.6.0]
│ │ 1. Rate limit check
│ │ 2. Lookup session, check expiry
│ │ 3. Verify Ed25519 signature
│ │ 4. Verify hash chain continuity
│ │ 5. Validate timestamp window ±30s
│ │ 6. Validate mouse behavior
│ │ 7. Validate fingerprint signals
│ │ 8. Validate mutation step parity [v0.6.0]
│ │ 9. Validate gene commitment [v0.6.0]
│ │ 10. Compute H(n), rotate salt
│ │ 11. Advance gene state [v0.6.0]
│ │ 12. UPDATE sessions …
│ │
│◄── { status: "ok", next_salt, │
│ next_mutation_step, │ [v0.6.0]
│ next_mutation_order_b64 } ────────┤ [v0.6.0]
│ │
│ sentSalt = currentSalt ◄── captured BEFORE rotation
│ currentSalt = next_salt │
│ mutationStep = next_mutation_step │ [v0.6.0]
│ mutationOrderB64 = next_mutation_order_b64 [v0.6.0]
│ prevHash = compute_next_hash( │
│ prevHash, ts, entropy, │
│ stackState, sentSalt) │
```
## Integration Points
### 3. Failure Path
* Browser clients consume the WASM module and call `/init` and `/hb`
* Existing sites can proxy these API routes through their own web server
* Frontend assets can be served by ChronoSeal directly or mounted in a sidecar deployment
* TLS termination should be handled by a reverse proxy in production
On any validation failure the server returns `{"status":"ok"}` with no `next_salt`. The client logs a warning and continues scheduling heartbeats.
The chain is broken — subsequent heartbeats will also fail silently.
No error is surfaced to the page or its visitors.
## Operating Assumptions
---
ChronoSeal is not a general-purpose authentication service. It is a cryptographic attestation and anti-automation layer intended to be integrated with existing site logic.
## Cryptographic Protocol
It assumes:
### Key Generation
```
Ed25519 keypair generated via ed25519-dalek + rand::thread_rng (OS-seeded)
Private key: stored in WASM thread_local, never leaves WASM memory
Public key: 32 bytes, hex-encoded, sent to server at init
```
### Hash Chain
```
H(0) = Blake3( session_id ║ pub_key ║ salt₀ )
H(n) = Blake3(
saltₙ₋₁ ← server-side only, rotated each heartbeat
║ H(n-1) ← must match stored last_hash
║ timestamp_u64_le
║ Blake3( JSON(entropy_data) )
║ Blake3( JSON(stack_state) )
)
```
Salt rotation means an attacker who intercepts a heartbeat cannot compute
future chain links without also intercepting every subsequent server response.
### Gene Commitment (v0.6.0)
A domain-separated BLAKE3 commitment binds both the gene buffer and the sorted
environment records into a single 32-byte value that is included in the signed
heartbeat payload and validated server-side:
```
gene_commitment = BLAKE3(
"chronoseal/gene/v1" ← domain separator
║ gene_bytes ← Vec<u8> gene buffer
║ for each (symbol, qty) sorted by symbol:
symbol_u16_le ║ qty_u32_le
)
```
The server recomputes the candidate gene state from its authoritative
`pending_mutation` program and rejects any heartbeat where
`recomputed_commitment != client_gene_commitment`.
### Canonical Signing Payload
The signed message is a JSON object with top-level keys sorted alphabetically,
serialised with no extra whitespace:
```
{
"entropyData": { "events": [{"t":…,"x":…,"y":…}] },
"fingerprint": { "aspectRatio":"…","devicePixelRatio":"…","hardwareConcurrency":… },
"gene_commitment": "hex…",
"mutation_step": N,
"prevHash": "hex…",
"sessionId": "hex…",
"stackState": { "ip":…,"stack":[…] },
"timestamp": 1234567890123
}
```
The server reconstructs this using `std::collections::BTreeMap` (alphabetical
key order) before calling `VerifyingKey::verify_strict`. Any field mismatch,
key order difference, or whitespace difference causes a signature failure.
### Hashing Algorithm
Blake3 is used throughout: hash chain links, entropy data digest, stack state
digest, gene commitment, and the VM HASH opcode. Blake3 is chosen for speed
in WASM, resistance to length-extension attacks, and a clean Rust API.
---
## Stack Machine
The server generates a random program on session init. The client executes it
on every heartbeat and includes the resulting `StackState { stack, ip }` in
the signed payload. This ensures each heartbeat carries unique, verifiable
computation without additional round-trips.
### Core Instruction Set
| Opcode | Mnemonic | Operand | Stack effect | Description |
| ------ | -------- | ----------- | ------------ | -------------------------------------- |
| `0x00` | PUSH | u32 (4B LE) | +1 | Push literal |
| `0x01` | ADD | — | −1 | `a + b` wrapping |
| `0x02` | SUB | — | −1 | `a - b` wrapping |
| `0x03` | MUL | — | −1 | `a * b` wrapping |
| `0x04` | XOR | — | −1 | `a ^ b` |
| `0x05` | AND | — | −1 | `a & b` |
| `0x06` | OR | — | −1 | `a \| b` |
| `0x07` | ROT | — | −1 | `a.rotate_left(b % 32)` |
| `0x08` | NOT | — | 0 | `!a` (unary) |
| `0x09` | HASH | — | -(depth-1) | Blake3 of all stack items → single u32 |
The generator ensures ≥ 2 items on the stack before any binary opcode.
NOT (0x08) does not change depth. HASH resets depth to 1.
### Mutation Opcodes (v0.6.0)
The gene mutation extension operates on a separate `Vec<u8>` gene buffer and a
bounded environment map `Vec<(u16 symbol, u32 quantity)>`. These opcodes are
defined in `shared/src/vm_extensions.rs` and executed identically by both
server and WASM to guarantee deterministic parity.
| Opcode | Mnemonic | Effect |
| ------ | ------------------ | ------------------------------------------------------------- |
| `0x23` | GENE_LOAD | Push `gene[idx]` onto the stack |
| `0x24` | GENE_STORE | Pop stack top and store at `gene[idx]` |
| `0x25` | MUTATE_POINT | Apply wrapping byte delta at index |
| `0x26` | INSERT | Insert popped byte at index |
| `0x27` | DELETE | Delete byte at index and push the removed value |
| `0x28` | TRANSCRIBE | Push deterministic transcription hash of current gene state |
| `0x29` | APPLY_MUTAGEN | Mix environment symbol quantity into gene byte at index |
| `0x2A` | FINALIZE_GENE_HASH | Push commitment-derived `u32` onto the stack |
| `0x2B` | CONSUME | Pop amount, subtract from environment symbol quantity |
| `0x2C` | PRODUCE | Pop amount, add to environment symbol quantity |
**Constraints enforced at runtime:**
- Mutation program length capped at `MAX_MUTATION_PROGRAM_BYTES`
- Environment record count capped at `MAX_ENV_RECORDS`
- Environment records validated for sortedness, uniqueness, non-zero quantity
- Stack underflow and unknown opcodes cause deterministic, symmetric failures on both server and WASM paths
---
## Behavioral Validation
### Mouse Entropy
Every heartbeat includes the mouse events collected since the previous
heartbeat. Server checks:
| Check | Threshold |
| --------------------------- | ------------------------------------ |
| Minimum event count | ≥ 3 |
| Minimum cumulative distance | ≥ 10 px |
| Maximum average speed | ≤ 2.0 px/ms (distance / elapsed ms) |
| Minimum pause count | ≥ 1 (movement < 0.2 px over > 50 ms) |
### Browser Fingerprint
| Signal | Valid range |
| ------------------------------ | ------------- |
| `aspectRatio` (width / height) | 0.5 – 3.0 |
| `devicePixelRatio` | 0 < dpr ≤ 5.0 |
| `hardwareConcurrency` | ≥ 1 |
---
## Rate Limiting
Token bucket per `session_id`: 5 requests / 10-second window.
Stale entries evicted every 60 seconds by the cleanup task.
Rate-limited responses are indistinguishable from validation failures.
---
## SQLite Schema
The schema is extended in v0.6.0 with four new columns to persist per-session
gene mutation state. Migration is additive — columns are created when missing,
preserving compatibility with existing deployments.
```sql
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
public_key BLOB NOT NULL, -- 32-byte Ed25519 verifying key
salt BLOB NOT NULL, -- 16-byte current salt
last_hash BLOB NOT NULL, -- 32-byte Blake3 chain head
chain_length INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL, -- Unix ms
last_seen INTEGER NOT NULL, -- Unix ms
expires_at INTEGER NOT NULL, -- Unix ms
-- v0.6.0: gene mutation state
gene BLOB NOT NULL DEFAULT X'', -- Vec<u8> gene buffer
environment BLOB NOT NULL DEFAULT X'', -- Vec<(u16, u32)> env records
pending_mutation BLOB NOT NULL DEFAULT X'', -- server-issued mutation program
pending_mutation_step INTEGER NOT NULL DEFAULT 0 -- current mutation step counter
);
```
In-memory SQLite (`sqlite-in-memory`) — all sessions lost on server restart by
design. Clients re-initialise transparently on the next page load. Use
`sqlite-in-disk` for persistent sessions across restarts.
---
## Storage Backend (v0.6.0)
ChronoSeal v0.6.0 introduces selectable database backends via the `db_type`
configuration option. The default remains in-memory to preserve ephemeral
session behavior.
### Backend Options
| `db_type` | Behavior |
| ------------------ | -------------------------------------------------------------------- |
| `sqlite-in-memory` | Default. All sessions ephemeral; lost on restart. Zero disk I/O. |
| `sqlite-in-disk` | Persistent sessions. Requires `db_path`. Survives restarts. |
| `valkey` | Compatibility mode. Currently falls back to in-memory. CLI contract preserved. |
### Configuration
**Config file** (`/etc/chronoseal/config.toml`):
```toml
db_type = "sqlite-in-disk"
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
```
**Environment variable**:
```bash
CHRONOSEAL_DB_TYPE=sqlite-in-disk
CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
```
**CLI flag**:
```bash
chronoseal run --db-type sqlite-in-disk --db-path /var/lib/chronoseal/chronoseal.sqlite
```
**Inspect active backend**:
```bash
chronoseal db-type --format text
```
### Precedence
```
CLI flags > CHRONOSEAL_* environment variables > config file > defaults
```
### Migration Notes
- Schema migration is additive; new columns are created when missing on startup.
- Switching from `sqlite-in-memory` to `sqlite-in-disk` requires no code changes — only config.
- `valkey` is available in the CLI contract today; full backend support is tracked for a future release.
- Existing deployments without the v0.6.0 mutation columns will have those columns added automatically on first start.
---
## Threat Model
### In Scope
| Threat | Mitigation |
| ------------------------------------------ | --------------------------------------------------------------- |
| Playwright / Puppeteer / Selenium | Mouse entropy + behavioral validation |
| Puppeteer Stealth, undetected-chromedriver | Signature over VM execution state |
| Heartbeat replay | Hash chain + ±30s timestamp window |
| Mutation replay | mutation_step + gene_commitment parity check (v0.6.0) |
| Mutation tampering | Server recomputes candidate gene from authoritative program (v0.6.0) |
| Signature forgery | Private key isolated in WASM memory |
| Parallel session sharing | Each session bound to a unique keypair |
| Brute-forced session IDs | 256-bit random entropy |
| Flooding with fake session IDs | Rate limiter + periodic HashMap eviction |
| Traffic analysis | Uniform `{"status":"ok"}` on all failure paths |
| Malformed mutation programs | Strict parsing, length caps, underflow/unknown-opcode errors |
### Out of Scope
| Threat | Reason |
| ---------------------------------- | ---------------------------------------- |
| Real browser with real human input | Indistinguishable from a legitimate user |
| WASM reverse engineering | Obfuscation is not a security primitive |
| Server-side compromise | Outside the scope of client attestation |
ChronoSeal raises cost and complexity of automated access. It is not a
cryptographic proof of humanity and does not claim to be.
---
## Module Reference
| Path | Purpose |
| ------------------------------------- | -------------------------------------------------------------------- |
| `shared/src/protocol.rs` | Shared types: `InitRequest`, `HeartbeatRequest`, `StackState`, … |
| `shared/src/hashing.rs` | `initial_hash`, `next_chain_hash`, `hash_stack` |
| `shared/src/constants.rs` | All tunable parameters |
| `shared/src/gene.rs` | Gene model, deterministic commitment (`chronoseal/gene/v1`) [v0.6.0] |
| `shared/src/vm_extensions.rs` | Mutation opcode set, shared engine for server/WASM parity [v0.6.0] |
| `server/src/routes/init.rs` | `POST /init` handler |
| `server/src/routes/heartbeat.rs` | `POST /hb` handler |
| `server/src/session.rs` | `create_session`, `verify_heartbeat`, `validate_mutation_parity` |
| `server/src/crypto.rs` | `verify_signature` — BTreeMap canonical JSON |
| `server/src/trust.rs` | `validate_mouse` — speed, distance, pauses |
| `server/src/fingerprint.rs` | `validate` — aspect ratio, DPR, HW concurrency |
| `server/src/vm.rs` | `generate_random_program` |
| `server/src/ratelimit.rs` | `RateLimiter::check`, `evict_stale` |
| `server/src/cleanup.rs` | Background loop: expire sessions + evict rate limiter |
| `server/src/storage.rs` | SQLite init, backend selection, `current_time_ms` |
| `wasm/src/crypto.rs` | `generate_keypair`, `sign_message`, `compute_next_hash` |
| `wasm/src/vm.rs` | `run_program` — stack machine executor |
| `wasm/src/vm_extensions.rs` | `preview_mutation`, `commit_mutation` [v0.6.0] |
| `frontend/heartbeat.js` | Session init, heartbeat loop, chain advancement |
| `frontend/entropy.js` | Mouse event ring buffer, `collectEntropy` |
| `frontend/transport.js` | `sendRequest` fetch wrapper |
* browser clients can execute WASM
* heartbeats will arrive every 12–25 seconds
* session state can be safely persisted in SQLite or Valkey
* service operators want Unix-native systemd deployment and observability
+107 -245
View File
@@ -1,32 +1,37 @@
# ChronoSeal — Deployment Guide
# ChronoSeal Deployment Guide
ChronoSeal v0.6.0 is designed for production deployment as a Unix-native daemon with hardened systemd support, lightweight WASM client runtime, and flexible backend storage.
## Prerequisites
| Tool | Minimum version | Purpose |
|---|---|---|
| Rust | 1.87 stable | Server + WASM compilation |
| Rust | 1.87 stable | Server and WASM compilation |
| wasm-pack | 0.13 | WASM build and packaging |
| Docker + Compose | 24 / 2.x | Container deployment |
| nginx / NPM / HAProxy | any | TLS termination, reverse proxy |
| Docker | 24.x | Optional container deployment |
| docker-compose | 2.x | Optional local orchestration |
| systemd | 248+ | Service management |
Install Rust: https://rustup.rs
Install Rust: [https://rustup.rs](https://rustup.rs)
Install wasm-pack: `cargo install wasm-pack`
---
## Build
## Build Steps
### 1. Build the WASM module
```bash
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
wasm-pack build wasm --target web --release
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
This produces `frontend/pkg/antibot_wasm.js` and `frontend/pkg/antibot_wasm_bg.wasm`,
which are loaded by `frontend/main.js` at runtime.
This produces the browser runtime assets required by the frontend and the server static file handler.
### 2. Build the server
### 2. Build the server binary
```bash
cargo build -p server --release
@@ -34,158 +39,109 @@ cargo build -p server --release
Binary output: `target/release/server`
### 3. Build both (convenience script)
### 3. Convenience script
```bash
bash scripts/build.sh
```
---
## Running
### Development
```bash
bash scripts/dev.sh
```
Runs the server with `cargo run --release`. The server serves the `frontend/`
directory statically at `/` via tower-http `ServeDir`.
Open `http://localhost:3000` in a browser. Open DevTools console — heartbeats
should appear every 12–25 seconds. No visible UI is rendered; the protection
is entirely silent.
### Production (native binary)
```bash
cargo build -p server --release
sudo cp target/release/server /usr/local/bin/chronoseal
```
Set environment variables before running:
```bash
export RUST_LOG=info # or warn for quieter output
chronoseal
```
The server binds to `0.0.0.0:3000` by default. Place behind a reverse proxy
for TLS — do not expose port 3000 directly.
This script builds the WASM module, moves the generated package into `frontend/pkg`, and builds the server binary.
---
## systemd
## Deploying as a Native Service
### Service file
The provided `chronoseal.service` includes hardened systemd sandboxing:
```
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
SystemCallArchitectures=native
```
ChronoSeal is intended to run as a proper Unix daemon managed by systemd.
### Install
```bash
# Create a dedicated system user
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chronoseal
# Install binary and frontend
sudo cp target/release/server /usr/local/bin/chronoseal
sudo mkdir -p /opt/chronoseal/frontend
sudo cp -r frontend/ /opt/chronoseal/frontend/
sudo chown -R chronoseal:chronoseal /opt/chronoseal
# Install and enable service
sudo cp chronoseal.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now chronoseal
sudo bash scripts/install.sh
```
### Verify
This installer should perform the following tasks:
* create a system user for `chronoseal`
* install the server binary into `/usr/local/bin/chronoseal`
* install static frontend assets into `/opt/chronoseal/frontend`
* install `chronoseal.service` into `/etc/systemd/system/`
* enable and start the service
### Verify the service
```bash
sudo systemctl status chronoseal
journalctl -u chronoseal -f
sudo journalctl -u chronoseal -f
```
### Recommended runtime options
Use structured info-level logging in production:
```bash
export RUST_LOG=info
sudo systemctl restart chronoseal
```
Avoid `RUST_LOG=debug` in production because debug logs can expose internal session identifiers.
---
## Docker
## systemd Integration
### Build and run
The supplied `chronoseal.service` is designed for hardened Unix-native operation.
```bash
docker compose up -d --build
```
Recommended service options:
### docker-compose.yml overview
* `NoNewPrivileges=true`
* `PrivateTmp=true`
* `ProtectSystem=strict`
* `ProtectHome=true`
* `ProtectKernelTunables=true`
* `ProtectKernelModules=true`
* `ProtectControlGroups=true`
* `MemoryDenyWriteExecute=true`
* `RestrictRealtime=true`
* `RestrictSUIDSGID=true`
* `SystemCallArchitectures=native`
```yaml
services:
chronoseal:
build: .
restart: unless-stopped
ports:
- "3000:3000"
environment:
RUST_LOG: info
tmpfs:
- /tmp
```
The `tmpfs` mount ensures the in-memory SQLite database is never written to
disk, even if Docker's storage driver were to flush the container filesystem.
### Dockerfile stages
The Dockerfile uses a two-stage build:
1. `rust:1.87-bookworm` — compiles the server binary
2. `debian:bookworm-slim` — minimal runtime image with only `ca-certificates`
The WASM module and frontend must be built separately (wasm-pack requires a
browser toolchain not present in the server image) and mounted or copied into
the container at `/opt/chronoseal/frontend/`.
```bash
# Build WASM first
wasm-pack build wasm --target web --release
mv wasm/pkg frontend/pkg
# Then build and run the container
docker compose up -d --build
```
Or mount the pre-built frontend as a volume:
```yaml
volumes:
- ./frontend:/opt/chronoseal/frontend:ro
```
These options reduce the host attack surface and keep the daemon constrained to its required runtime privileges.
---
## Reverse Proxy
## Configuration
ChronoSeal must be served over HTTPS. The heartbeat payload contains a
timestamp; if traffic is observable in plaintext, timing attacks become
easier. TLS 1.3 is strongly recommended.
ChronoSeal reads configuration from a TOML file, environment variables, and CLI overrides. Use `chronoseal config` to validate the effective configuration.
### nginx
Example runtime configuration options:
```toml
bind = "0.0.0.0:3000"
pid_file = "/run/chronoseal.pid"
log_level = "info"
db_type = "sqlite-in-memory"
db_path = "/var/lib/chronoseal/chronoseal.db"
```
### Supported `db_type`
* `sqlite-in-memory`
* `sqlite-disk`
* `valkey`
`sqlite-in-memory` is the default and preserves ephemeral session semantics.
`sqlite-disk` will persist session state to a file specified by `db_path`.
`valkey` selects the Valkey-compatible backend mode and may be useful for future deployment scenarios.
---
## Reverse Proxy and TLS
ChronoSeal should be served over HTTPS in production. The heartbeat protocol includes timestamps and entropy data; serving that traffic in plaintext weakens security and allows easier traffic analysis.
### nginx example
```nginx
server {
@@ -197,7 +153,6 @@ server {
ssl_protocols TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Tight timeouts — heartbeat interval is 12–25s
proxy_read_timeout 35s;
proxy_send_timeout 10s;
@@ -218,129 +173,36 @@ server {
}
```
### Nginx Proxy Manager
1. Add a new Proxy Host pointing to `http://chronoseal:3000`
2. Enable SSL, Request Let's Encrypt certificate
3. Enable HTTP/2, Force SSL
4. Under Advanced, add:
```
proxy_read_timeout 35s;
proxy_send_timeout 10s;
```
### HAProxy
```haproxy
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/your.domain.pem alpn h2,http/1.1
default_backend chronoseal_back
backend chronoseal_back
server chronoseal 127.0.0.1:3000 check
timeout connect 5s
timeout server 35s
```
---
## Integration into an Existing Site
ChronoSeal is designed to run as a sidecar — its `/init` and `/hb` endpoints
can be proxied from any existing web server. The frontend assets (`pkg/`) need
to be served from the same origin as the protected page (or CORS must be
configured).
### Option A — Serve everything from ChronoSeal
ChronoSeal serves `frontend/` statically. Put your protected HTML inside
`frontend/` and let ChronoSeal serve it directly.
### Option B — Proxy only the API endpoints
Keep your existing server. Proxy `/init` and `/hb` to ChronoSeal, and serve
the WASM and JS assets from your CDN or existing static file server.
```nginx
# On your existing server:
location ~ ^/(init|hb)$ {
proxy_pass http://127.0.0.1:3000;
}
```
Add to your protected pages:
```html
<script type="module" src="/pkg/antibot_wasm.js"></script>
<script type="module" src="/main.js"></script>
```
---
## Configuration
All parameters are in `shared/src/constants.rs`. Recompile after changes.
| Constant | Default | Notes |
|---|---|---|
| `SESSION_ID_LEN` | 32 bytes | 256-bit entropy — do not reduce |
| `SALT_LEN` | 16 bytes | Per-heartbeat salt |
| `HEARTBEAT_MIN_INTERVAL_MS` | 12 000 ms | Increase to reduce server load |
| `HEARTBEAT_MAX_INTERVAL_MS` | 25 000 ms | Jitter upper bound |
| `EXPIRATION_MINUTES` | 30 min | Session TTL after last heartbeat |
| `RATE_LIMIT_COUNT` | 5 | Max heartbeats per window per session |
| `RATE_LIMIT_WINDOW_SECS` | 10 s | Rate limit window |
| `MAX_TIMESTAMP_DRIFT_MS` | 30 000 ms | Anti-replay window; account for NTP skew |
| `MIN_MOUSE_TOTAL_DIST` | 10.0 px | Lower for low-activity pages |
| `MAX_MOUSE_AVG_SPEED` | 2.0 px/ms | Raise if legitimate users are rejected |
| `MIN_PAUSE_COUNT` | 1 | Minimum natural pause events |
---
## Observability
ChronoSeal uses `tracing` with `tracing-subscriber`. Log levels:
| Level | Events |
|---|---|
| `INFO` | Server start, request method + path + status |
| `WARN` | Heartbeat validation failures (with session ID and reason) |
| `DEBUG` | Rate limit hits |
### Docker deployment
```bash
RUST_LOG=info chronoseal # production
RUST_LOG=debug chronoseal # development
RUST_LOG=warn chronoseal # minimal output
docker compose up -d --build
```
Log format is plain text to stdout. Pipe to `journald`, `fluentd`, or any
log aggregator via stdout capture.
The supplied `docker-compose.yml` is intended for local evaluation and development. It mounts `frontend/` and exposes port `3000`.
Note: build the WASM package before container startup, or mount a pre-built `frontend/pkg/` volume.
---
## Health Check
## Production Best Practices
The server has no dedicated `/health` endpoint. Use a TCP check on port 3000,
or a lightweight HTTP check on `GET /` (which serves `index.html`).
```bash
# Docker health check (add to docker-compose.yml if needed)
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3000/"]
interval: 30s
timeout: 5s
retries: 3
```
* Use TLS termination at the perimeter
* Run ChronoSeal behind a reverse proxy or firewall
* Keep `RUST_LOG` at `info` or `warn`
* Use `systemctl` for lifecycle management
* Monitor `chronoseal` metrics with Prometheus
* Place the frontend under the same origin as the protected pages or configure CORS carefully
---
## Security Checklist
## Health and Metrics
- [ ] TLS 1.3 enabled, TLS 1.0/1.1 disabled
- [ ] HTTP/2 enabled
- [ ] Port 3000 not exposed to the public internet (only via reverse proxy)
- [ ] `RUST_LOG=warn` or `info` in production (not `debug` — session IDs appear in logs)
- [ ] systemd service running as `chronoseal` user with hardened sandbox
- [ ] `MemoryDenyWriteExecute=true` in service file (prevents JIT in process)
- [ ] CORS `CorsLayer::permissive()` replaced with origin-restricted policy for production
- [ ] Frontend assets served over the same HTTPS origin as protected pages
ChronoSeal exposes runtime endpoints for health and metrics.
* `chronoseal health` — health probe
* `chronoseal metrics` — Prometheus metrics output
* `chronoseal status` — runtime status report
* `chronoseal stats` — runtime statistics
These endpoints are accessible locally from the daemon and may be proxied or scraped by monitoring infrastructure.
+44 -27
View File
@@ -1,41 +1,58 @@
# ChronoSeal Design Philosophy
**"Everything is a File" — Unix-Native Software Design**
ChronoSeal is built for operators who value clarity, stability, and Unix-native infrastructure.
ChronoSeal is intentionally built as a **first-class citizen of Linux**. The entire application is designed to behave like a well-engineered native file within the Unix filesystem.
## Core Philosophy
### Why This Philosophy Matters
ChronoSeal is a Unix-first, CLI-first cryptographic attestation daemon. It is intentionally designed to feel like infrastructure software such as `nginx`, `redis-server`, or `systemd` itself.
ChronoSeal is designed so that administrators can operate, monitor, configure, and integrate it using the same reliable, transparent, and trusted tools and patterns they already use on Linux systems — without fighting the operating environment.
### Design priorities
### Core Principles
* **Unix-native operation** — systemd integration, PID files, structured logs, and predictable lifecycle semantics.
* **CLI as source of truth** — all runtime operations available through the command line.
* **Minimal opacity** — no hidden telemetry, no opaque fingerprinting database.
* **Privacy-first** — ephemeral session state and no persistent user profiling.
* **Deterministic runtime behavior** — shared Rust/WASM implementation for the mutation engine and heartbeat protocol.
* **Incremental cost escalation** — make automation painful to scale without claiming impossible security.
* **Operational transparency** — expose health, metrics, status, and config as first-class artifacts.
- **Everything is a File**: The application must be controllable, inspectable, and composable through standard Unix interfaces (CLI, files, signals, pipes, and environment).
- **CLI as Source of Truth**: All operations — starting, stopping, configuring, monitoring, and debugging — must be possible from the command line with excellent discoverability.
- **Behave Like a Native File**: Predictable lifecycle management through commands, signals (`SIGHUP`, `SIGTERM`, `SIGUSR1`), logs, configuration files, and standard process semantics.
- **Composability**: Must work naturally with pipes, redirection, scripts, systemd, Ansible, Docker, and orchestration tools.
- **Observability by Default**: All important state and metrics should be accessible as text or structured data.
- **Minimal Friction, Maximum Durability**: One-line installer, world-class `--help`, proper man pages, and decades-long maintainability are non-negotiable.
- **Respect for the OS**: Follows Linux Filesystem Hierarchy Standard (FHS), XDG Base Directory specification, and hardened systemd practices.
## Execution Model
### Non-Goals
ChronoSeal emphasizes deterministic, stateless request validation with a lightweight server-side session store.
ChronoSeal is **not** designed to be:
- Cloud-first or vendor-specific
- Browser-first or JavaScript-heavy
- Dependency-heavy or framework-driven
- GUI-centric (any graphical interface must be a thin wrapper)
- Telemetry-oriented or privacy-invasive
- Optimized for rapid prototyping at the cost of long-term reliability
* The server persists only the small session state required for continuity.
* The client executes a deterministic WASM runtime for every heartbeat.
* The protocol is intentionally ambiguous on rejection to avoid leaking validation rules.
These non-goals help keep the project focused on stability, simplicity, security, and deep Unix integration.
## Non-Goals
### Development Mindset
ChronoSeal does not aim to be:
- Production robustness, security, and long-term sustainability take clear precedence over development speed.
- Every design decision is evaluated against one question:
**“Does this make ChronoSeal feel like it naturally belongs in `/usr/bin/`?”**
* a tracking platform
* a browser fingerprinting database
* a long-term behavioral analytics engine
* a platform for user profiling
* a SaaS or cloud-first service
This philosophy guided the complete refactoring of ChronoSeal and continues to drive all future development.
Instead, ChronoSeal aims to be an infrastructure layer that raises attacker cost while leaving legitimate users unobstructed.
**Status**: Core architecture and systemd integration completed. Rich CLI, runtime configuration system, and one-line installer are in active development.
## Operational Assumptions
ChronoSeal assumes:
* the host environment is Linux
* systemd is available for service management
* TLS is used in production
* browser clients can execute WASM
* operators can manage native binaries and configuration files
## Privacy and Trust
The project is designed so that the verification mechanism is:
* ephemeral
* difficult to reverse-engineer at scale
* not based on personal identifiers
* not dependent on long-term user history
These choices reflect the belief that the best anti-automation system is one that can be operated without becoming a surveillance platform.
+40 -252
View File
@@ -1,278 +1,66 @@
# ChronoSeal Privacy & Design Principles
# ChronoSeal Privacy Policy
## Privacy-First Browser Attestation Framework
ChronoSeal is a privacy-first cryptographic attestation system. It is intentionally designed to avoid long-term profiling, tracking, and persistent identity storage.
ChronoSeal is a lightweight, privacy-first browser attestation framework designed to resist:
## What ChronoSeal Collects
- automated bots
- AI-driven browser automation
- scripted abuse
- browser surveillance ecosystems
ChronoSeal only collects the minimum ephemeral data required to validate a live browser session:
Unlike conventional anti-bot systems, ChronoSeal is intentionally designed to operate **without collecting or storing client identity data**.
* `session_id` — ephemeral session identifier
* `prev_hash` / `initial_hash` — cryptographic chain state
* `timestamp` — heartbeat timing information
* `entropy_data` — recent mouse event samples for behavioral plausibility
* `stack_state` — VM execution result for heartbeat uniqueness
* `fingerprint` signals — basic browser sanity values such as aspect ratio, DPR, and hardware concurrency
* `mutation_step` / `gene_commitment` — synthetic mutation parity values for protocol continuity
---
## What ChronoSeal Does Not Store
# Core Philosophy
ChronoSeal does not store or persist:
ChronoSeal verifies:
* IP addresses as a core artifact
* browser history
* user identifiers
* personal data
* device fingerprint databases
* long-term behavioral profiles
* cross-session tracking records
- session continuity
- runtime coherence
- cryptographic synchronization
If you need browser telemetry or user profiling, ChronoSeal is not the right tool.
It does **not** verify:
## Session Ephemerality
- personal identity
- browsing history
- behavioral profiles
- long-term reputation
By default, ChronoSeal uses `sqlite-in-memory` storage. Sessions are ephemeral and are expected to be recreated after process restarts.
The framework is built around one principle:
Persistent state is only stored when the operator explicitly configures `sqlite-disk` or `valkey`.
> Verify live browser participation without turning users into telemetry.
## Client-Side Key Handling
---
The Ed25519 signing keypair is generated inside the WASM runtime and is never serialized or transmitted in full.
# Privacy-First By Architecture
* Private key: stays inside WASM linear memory
* Public key: transmitted once during session initialization
ChronoSeal is intentionally engineered to avoid becoming:
This design minimizes the amount of sensitive material exposed outside the browser runtime.
- a tracking platform
- a fingerprinting database
- a telemetry pipeline
- a surveillance system
## Intentional Silent Rejection
## ChronoSeal Does NOT Store
ChronoSeal intentionally returns a uniform `{"status":"ok"}` response for invalid heartbeats.
- IP addresses
- Browser history
- Persistent fingerprints
- User profiles
- Behavioral telemetry
- Tracking identifiers
- Device databases
- Long-term session history
- Cross-site correlation data
This is a privacy-preserving decision: it avoids emitting detailed rejection reasons that could be used to fingerprint or probe clients.
No client-side personal information is persisted.
## Data Retention
---
Session state is retained only as long as it is needed for heartbeat continuity.
# Stateless Trust Model
Expired sessions are purged automatically by cleanup tasks. Ephemeral backend modes do not write state to disk beyond the current process lifetime.
ChronoSeal focuses on:
## Transparency
- ephemeral runtime verification
- cryptographic continuity
- synchronized challenge progression
- live execution integrity
The source code is open and the verification model is documented. Operators can inspect exactly what ChronoSeal stores and validates.
The server only validates:
## Summary
- whether the current browser session behaves like a coherent participant *right now*
ChronoSeal is designed to provide anti-automation defense without becoming a tracking or surveillance platform.
ChronoSeal does not maintain:
- user identity databases
- reputation systems
- persistent surveillance records
---
# Anti-Bot Without Surveillance
Most modern anti-bot systems rely heavily on:
- fingerprinting
- behavioral tracking
- telemetry aggregation
- centralized analytics
ChronoSeal deliberately rejects this model.
Instead, ChronoSeal uses:
- synchronized cryptographic chains
- WASM-isolated signing
- protocol continuity
- transient verification state
This provides bot resistance while preserving user privacy.
---
# Lightweight By Design
ChronoSeal is intentionally engineered to remain:
- compact
- dependency-light
- operationally simple
- Unix-native
## Current Footprint
### Server Binary
Compiled x86_64 Linux server binary:
- ~8.4 MB
### WASM Runtime
`chronoseal_wasm_bg.wasm`
- ~218 KB
### Full WASM Package
Entire generated WASM package:
- ~720 KB
Includes:
- WASM runtime
- JavaScript glue code
- Type definitions
---
# No Frontend Framework Dependency
ChronoSeal does not depend on:
- React
- Angular
- Vue
- Electron
- Node.js runtime
- Browser bundler ecosystems
The browser runtime uses:
- native ES modules
- direct WebAssembly loading
- lightweight JavaScript glue
This minimizes:
- dependency complexity
- supply-chain risk
- build fragility
- browser overhead
---
# Clean Repository Philosophy
ChronoSeal keeps generated artefacts out of version control.
## What Is NOT Stored In The Repository
| Path | Reason |
|---|---|
| `wasm/pkg/` | Generated build output |
| `frontend/pkg/` | Generated serve-time artefacts |
| `target/` | Standard Rust build artefacts |
Generated binaries change frequently and are reproducible from source.
The repository intentionally stores:
- source code
- architecture
- reproducible build logic only
---
# Unix-Native Operational Model
ChronoSeal is designed as:
- infrastructure software
- not browser-centric SaaS
Core operational principles:
- CLI-first operation
- systemd-native deployment
- structured logs
- explicit configuration
- inspectable runtime behavior
- minimal hidden state
ChronoSeal should feel natural on Linux systems:
- simple to deploy
- easy to audit
- understandable years later
---
# Security Through Operational Asymmetry
ChronoSeal increases attacker cost through:
- synchronization burden
- runtime continuity requirements
- WASM-isolated cryptographic execution
- chained session progression
It does not attempt:
- invasive tracking
- permanent identification
- surveillance-driven scoring
---
# Design Goals
ChronoSeal prioritizes:
- Privacy
- Simplicity
- Transparency
- Operational clarity
- Long-term maintainability
- Minimalism
- Unix-native behavior
- Low deployment friction
---
# Non-Goals
ChronoSeal is intentionally NOT:
- A surveillance platform
- A telemetry collection system
- A browser fingerprinting database
- An analytics engine
- A cloud lock-in service
- A JavaScript-heavy frontend platform
- An advertising or tracking framework
---
# Summary
ChronoSeal is designed to prove:
> “A live browser session is coherently participating right now.”
without storing:
- who the user is
- where they came from
- what they previously did
It is a lightweight, privacy-preserving, Unix-native browser attestation framework focused on:
- anti-bot resistance
- anti-automation
- operational simplicity
without compromising user privacy.
It is a privacy-aware, ephemeral attestation layer with strong operational guardrails.
+92 -89
View File
@@ -1,115 +1,118 @@
# ChronoSeal v0.6.0 — Synthetic Gene Mutation System
# ChronoSeal v0.6.0 — Refactoring and System Upgrade
## Overview & Motivation
ChronoSeal v0.6.0 introduces a synthetic mutation chain model to strengthen attestation liveness and anti-replay guarantees while preserving privacy-first behavior. The core model combines:
- a primary byte-oriented gene buffer (`Vec<u8>`), and
- a bounded secondary environment map (`Vec<(u16 symbol, u32 quantity)>`).
ChronoSeal v0.6.0 is a major architecture and protocol update that transforms the project from a lightweight heartbeat service into a mature Unix-native attestation daemon with deterministic mutation parity and pluggable storage backends.
Each heartbeat now carries deterministic mutation progression evidence (`mutation_step`, `gene_commitment`) that is validated server-side against the exact server-issued mutation order. This design increases attacker workload by coupling cryptographic chain continuity with stateful deterministic mutation parity.
## Summary of Changes
## Architectural Goals
1. Keep runtime behavior deterministic across server and WASM execution.
2. Preserve ephemerality and low operational complexity.
3. Minimize additional latency on the heartbeat path.
4. Improve protocol resistance against replay and mutation tampering.
5. Maintain a maintainable codebase with explicit invariants and focused modules.
* Introduced the **Synthetic Gene Mutation Engine** for deterministic mutation parity across server and WASM.
* Added server-side validation of `mutation_step` and `gene_commitment`.
* Centralized shared protocol logic in `shared/` for server/WASM parity.
* Added support for multiple storage backend modes: `sqlite-in-memory`, `sqlite-disk`, and `valkey` compatibility.
* Hardened runtime architecture with `systemd` readiness, graceful shutdown, PID file support, and structured logging.
* Expanded CLI with rich subcommands and effective runtime configuration.
* Preserved silent rejection semantics while improving anti-replay and liveness guarantees.
## Design Decisions
1. **Shared mutation engine**
Mutation opcode semantics live in `shared/src/vm_extensions.rs` to guarantee server/client parity from one implementation.
## Why This Refactor?
2. **Deterministic gene commitment**
A domain-separated BLAKE3 commitment (`chronoseal/gene/v1`) binds both gene bytes and sorted environment records.
The previous model relied on heartbeat continuity and behavioral entropy alone. v0.6.0 strengthens the protocol by adding a second, deterministic state progression channel:
3. **Bounded mutation complexity**
Mutation program length is capped (`MAX_MUTATION_PROGRAM_BYTES`) and environment cardinality is capped (`MAX_ENV_RECORDS`).
* each heartbeat now includes a mutation step and commitment
* the server authoritatively selects the next mutation program
* the client must preview and commit the same state locally in WASM
* the server rejects any mismatch silently
4. **Strict validation on ingest**
Environment payloads are validated for sortedness, uniqueness, non-zero quantity, and length constraints.
This raises the cost of developing a successful automation attack because the attacker must now maintain both a valid chain and a valid mutation progression state.
5. **Protocol-level mutation handshake**
`InitResponse` and `Heartbeat` payloads now include mutation step/order and commitment fields.
## Core Architecture Changes
6. **DB backend control via `db_type`**
Server CLI/config now supports:
- `sqlite-in-memory` (default)
- `sqlite-in-disk` (active; uses `db_path`)
- `valkey` (active compatibility mode; currently falls back to in-memory)
### Shared Protocol Code
## Implementation Plan
1. Add gene model + deterministic commitment in `shared/gene.rs`.
2. Implement v0.6.0 mutation opcode set in shared VM extensions.
3. Persist mutation state per session (`gene`, `environment`, `pending_mutation`, `pending_mutation_step`).
4. Extend protocol schema for mutation fields in init/heartbeat exchange.
5. Validate mutation step + commitment parity before accepting heartbeat updates.
6. Add WASM preview/commit mutation lifecycle mirroring server behavior.
7. Add `db_type` CLI/config flow and runtime backend initialization strategy.
8. Add migration-safe schema extension (column existence checks + index creation).
`shared/` now contains:
## Testing Strategy (detailed section)
ChronoSeal v0.6.0 test coverage is organized across unit, integration, and randomized/fuzz-style validation.
* gene model and commitment hashing
* mutation opcode semantics
* request/response payload structures
* canonical signing support
* VM execution logic shared by server and WASM
1. **Unit, integration, and property tests**
- Unit tests for gene invariants and encoding/decoding.
- Unit tests for every mutation opcode with stack-effect assertions.
- Integration tests for full session lifecycle and heartbeat acceptance/rejection paths.
- Table-driven randomized tests and fuzz-style random bytecode tests to validate deterministic failure/success symmetry.
Moving mutation semantics into `shared/` eliminates subtle server/client divergence bugs and enables deterministic cross-runtime testing.
2. **Server-client parity testing**
- Shared opcode engine parity tests across seeded mutation sequences.
- Multi-step mutation chain test (`test_mutation_chain`) asserting identical server/client final state.
- 10+ heartbeat deterministic simulation tests in session integration suite.
### Mutation Handshake
3. **Evasion / attack simulation testing**
- Replay attack simulation.
- Mutation step mismatch rejection.
- Mutation commitment tampering rejection.
- Malformed server mutation payload rejection.
- Stack underflow / unknown opcode / truncated program rejection.
v0.6.0 adds the following data to the protocol:
4. **Performance regression testing**
- Bounded execution checks through capped program size and bounded record counts.
- Timing smoke regression test for mutation execution loops.
- End-to-end heartbeat test coverage to detect behavior regressions on hot paths.
* `mutation_step`
* `mutation_order_b64`
* `gene_commitment`
* `next_mutation_step`
* `next_mutation_order_b64`
## Security Analysis
1. **Replay resistance**
Heartbeats are now tied to both chain hash and mutation step progression.
These fields are now part of the session initialization and heartbeat exchange.
2. **Mutation tampering resistance**
Server recomputes candidate gene state from authoritative pending mutation program and rejects commitment mismatch.
### Server Session State
3. **Protocol ambiguity reduction**
Canonical signing payload includes mutation fields, reducing exploitable unsigned state.
The session schema now stores:
4. **Input hardening**
Program size limits, stack underflow checks, and strict environment decoding reduce parser abuse and malformed payload amplification.
* committed gene bytes
* committed environment records
* pending mutation order
* pending mutation step
5. **Deterministic failure semantics**
Invalid mutation instructions fail predictably and symmetrically across server and WASM paths.
The server advances this state only after a heartbeat is accepted.
## Performance Considerations
1. Mutation instructions are lightweight and mostly O(1); only `INSERT`/`DELETE` are O(n) but bounded by max gene size.
2. Environment operations use sorted-vector binary search with tight upper bound (`MAX_ENV_RECORDS`).
3. Commitment hashing is linear in gene size and record count, both bounded.
4. Shared engine avoids duplicate logic and divergence-induced debugging overhead.
### Deterministic WASM Preview
## Migration & Backward Compatibility
1. Schema migration is additive; new columns are created when missing.
2. Existing deployments without mutation fields require updated client+server pair for heartbeat compatibility.
3. `db_type` defaults to in-memory to preserve ephemeral behavior.
4. `sqlite-in-disk` is now directly usable via `db_path`.
5. `valkey` currently runs in compatibility mode (in-memory fallback) to avoid startup failure while preserving CLI contract.
The WASM runtime exposes:
## Risks & Mitigations
1. **Risk: State divergence between server and client**
Mitigation: shared opcode engine + deterministic seeded parity tests + multi-heartbeat integration tests.
* `init_gene_state()`
* `preview_gene_commitment()`
* `commit_gene_preview()`
* `discard_gene_preview()`
* `current_gene_commitment()`
2. **Risk: Mutation opcode abuse via malformed programs**
Mitigation: strict parsing, length caps, explicit underflow/unknown-opcode errors.
This makes the client-side mutation lifecycle explicit and deterministic.
3. **Risk: Performance regressions**
Mitigation: bounded structures, smoke timing tests, and focused hot-path validation.
### Backend Abstraction
4. **Risk: Backend confusion during `db_type` rollout**
Mitigation: explicit CLI command (`chronoseal db-type`), config output visibility, and clear runtime compatibility behavior.
The server runtime now supports a configurable `db_type`.
* `sqlite-in-memory` — default runtime storage with ephemeral session semantics
* `sqlite-disk` — persistent SQLite storage for stateful deployments
* `valkey` — compatibility mode for alternative storage backends
This abstraction makes ChronoSeal easier to operate in both stateless and stateful environments.
### CLI and Service Integration
v0.6.0 improves the CLI surface with operational commands and service introspection.
* `chronoseal run`
* `chronoseal status`
* `chronoseal health`
* `chronoseal config`
* `chronoseal metrics`
* `chronoseal stats`
* `chronoseal db-type`
* `chronoseal completion`
* `chronoseal version`
The runtime now includes PID file handling and graceful termination.
## Testing and Validation
The refactor includes extensive tests for:
* server/WASM parity across mutation sequences
* malformed mutation payload rejection
* replay attack rejection
* mutation step mismatch rejection
* stateful session update semantics
* runtime database mode validation
The codebase now supports deterministic table-driven tests and fuzz-style random program validation.
## Operational Impact
This release makes ChronoSeal suitable for production deployment in Linux environments and for integration into existing web application stacks.
The combination of deterministic mutation parity and shared protocol implementation improves both security and maintainability.
+89 -157
View File
@@ -1,205 +1,137 @@
# ChronoSeal — Threat Model
# ChronoSeal Threat Model
ChronoSeal is a cost-raising cryptographic attestation daemon. It increases the burden on automated clients while preserving privacy, determinism, and operational transparency.
## Purpose
This document defines what ChronoSeal is designed to protect against, what
it explicitly does not protect against, and the reasoning behind each
design decision in security terms.
ChronoSeal protects web resources by making browser automation and replay attacks more expensive and fragile. It is not intended to be a perfect bot blocker.
ChronoSeal is a **cost-raising mechanism**. It does not claim to make
automated access impossible. It makes automated access expensive, complex
to maintain, and operationally fragile at scale.
## Protected Assets
---
## Assets Being Protected
| Asset | Description |
| Asset | Protection focus |
|---|---|
| Web page content | HTML, rendered data, scraped text |
| API responses | JSON endpoints that serve structured data |
| Server compute | CPU and bandwidth consumed by automated clients |
| Rate-limited resources | Endpoints with per-user quotas |
| Behavioral analytics | Metrics polluted by bot traffic |
---
| Page content | Prevent automated scraping and replay of protected content |
| API responses | Reduce scripted access to sensitive endpoints |
| Server compute | Increase attacker resource costs |
| Session continuity | Enforce live session progression |
| Behavioral integrity | Validate plausible browser activity |
## Attacker Profiles
### Level 1 — Script Kiddie / Commodity Scraper
### Level 1 — Commodity Scraper
**Tools:** `curl`, `requests`, `scrapy`, simple HTTP clients.
**Capability:** No browser environment. Cannot execute JavaScript or WASM.
**ChronoSeal response:** Session never initialises. No `session_id` is ever
presented to `/hb`. Content gated behind session validation is never served.
* Tools: `curl`, `requests`, headless HTTP clients
* Capability: no WASM execution, no browser engine
ChronoSeal response:
* cannot initialize a session
* no `session_id` is produced
* content remains protected behind the attestation layer
### Level 2 — Headless Browser Operator
**Tools:** Playwright, Puppeteer, Selenium, undetected-chromedriver.
**Capability:** Full browser environment. Can execute JavaScript and WASM.
Cannot easily synthesise realistic mouse entropy or maintain hash chain state
across concurrent sessions.
**ChronoSeal response:** Mouse entropy validation rejects absent or synthetic
movement. Hash chain requires per-session state synchronisation. Scaling to
hundreds of concurrent sessions requires proportional infrastructure.
* Tools: Playwright, Puppeteer, Selenium
* Capability: browser engine available, but automation is not indistinguishable from a real user
ChronoSeal response:
* mouse entropy and pause checks become active barriers
* hash chain continuity requires per-session state tracking
* synthetic heartbeats become expensive to maintain at scale
### Level 3 — Stealth Automation
**Tools:** Puppeteer Stealth, rebrowser-patches, custom CDP clients with
evasion patches.
**Capability:** Patches `navigator.webdriver`, spoofs browser fingerprints,
can inject synthetic mouse events. May partially pass behavioral checks.
**ChronoSeal response:** Ed25519 signature over the full payload (including
behavioral state and VM execution result) means the attacker must also
correctly execute the WASM program and maintain chain continuity. The private
key is generated fresh per page load and never exposed — it cannot be
extracted from a legitimate session and reused.
* Tools: browser stealth plugins, CDP patching, synthetic event injection
* Capability: can execute JavaScript and WASM, may spoof some browser signals
### Level 4 — Sophisticated Adversary
ChronoSeal response:
**Tools:** Full browser farm with real input devices, WASM reverse engineering,
custom chain maintenance infrastructure.
**Capability:** Can pass all current ChronoSeal checks given sufficient
engineering effort.
**ChronoSeal response:** Significantly increases operational cost. A browser
farm with real input devices costs orders of magnitude more than a commodity
scraper fleet. ChronoSeal is not designed to stop this attacker — no client-
side protection can.
* signature, hash chain, and mutation commitment require correct WASM execution
* private key is generated per page load and never exposes raw key material
* silent rejection hides validation rules from attacker feedback
---
### Level 4 — Sophisticated Operator
* Tools: real browser farms, hardware input devices, custom chain management
* Capability: high engineering investment and real device scale
ChronoSeal response:
* significantly increases operational cost and complexity
* forces a full protocol implementation rather than best-effort scraping
* is not designed to stop such adversaries completely
## Attack Vectors and Mitigations
### Replay Attack
**Attack:** Capture a valid heartbeat payload and retransmit it.
**Mitigation:**
- Timestamp window (±30 seconds): replayed payloads are rejected after 30s.
- Hash chain: each heartbeat must present `H(n-1)` matching the server's
stored state. A replayed heartbeat presents a stale hash that no longer
matches after one successful heartbeat has advanced the chain.
**Attack:** resend a previously observed heartbeat.
**Mitigations:**
* timestamp window enforcement (±30 seconds)
* chained Blake3 hash continuity
* server-issued salt rotation
* mutation step progression
### Signature Forgery
**Attack:** Construct a valid-looking heartbeat payload without the private key.
**Mitigation:** Ed25519 with 128-bit security. The private key is generated
inside WASM `thread_local` memory, never serialised, never passed to
JavaScript, never transmitted. Forgery requires breaking Ed25519 or
extracting the key from WASM memory — neither is practical.
**Attack:** forge a heartbeat without the private key.
### Key Extraction
**Mitigations:**
**Attack:** Inspect WASM linear memory to extract the private signing key.
**Mitigation:** The key is stored in a Rust `thread_local! { RefCell<Option<SigningKey>> }`.
It has no exported symbol and is not referenced by any exported WASM function
that returns raw memory. An attacker with full DevTools access to the WASM
memory can extract it from one session, but it is useless for other sessions
(fresh keypair per page load) and expires with the session.
* Ed25519 signature over the canonical payload
* private key generated and stored inside WASM memory only
* signature verification occurs on every heartbeat
### Hash Chain Forgery
### Mutation Tampering
**Attack:** Compute a valid `H(n)` without the server-side salt.
**Mitigation:** Each chain link incorporates `saltₙ₋₁`, which is a 16-byte
random value known only to the server and returned (once) in the heartbeat
response. An attacker cannot compute `H(n+1)` without first receiving
`saltₙ` from a successful heartbeat response, which requires a valid signature
and all other checks to pass.
**Attack:** send an invalid or stale mutation commitment.
**Mitigations:**
* server recomputes the gene commitment from server-authored mutation orders
* heartbeat request includes `mutation_step` and `gene_commitment`
* mismatched commitment causes silent rejection
### Session Hijacking
**Attack:** Steal a `session_id` and use it from a different client.
**Mitigation:** `session_id` alone is insufficient — the attacker also needs
the private key (to produce valid signatures) and the current chain state
(to present the correct `prev_hash`). All three are required simultaneously.
**Attack:** steal a valid `session_id` and reuse it.
### Enumeration of Validation Rules
**Mitigations:**
**Attack:** Send malformed heartbeats and analyse error responses to map
validation logic.
**Mitigation:** All failure paths return `{"status":"ok"}` with no `next_salt`.
There is no error code, no error message, and no status difference between
a rate limit hit, an invalid signature, a broken chain, and a behavioral
rejection.
* `session_id` alone is insufficient
* attacker also needs current `prev_hash` and private key
* keypair is generated per browser session in WASM
### DoS via Session Flooding
### Fingerprint Enumeration
**Attack:** Open thousands of sessions to exhaust the rate limiter's HashMap
memory.
**Mitigation:** Rate limiter entries are evicted every 60 seconds by the
cleanup task. Each entry is a small `(u32, Instant)` tuple; even at 100,000
concurrent fake sessions, the HashMap occupies roughly 10–15 MB, which is
well within normal server memory budgets. Sessions themselves expire after 30
minutes of inactivity and are purged from SQLite.
**Attack:** probe the API with malformed requests to discover validation logic.
### Clock Manipulation
**Mitigations:**
**Attack:** Manipulate the client's `Date.now()` to bypass the timestamp
window.
**Mitigation:** The timestamp is included in the signed payload. Manipulating
it requires also forging the signature. The server validates against its own
clock — client-side clock manipulation cannot help without the private key.
* all invalid heartbeats return `{"status":"ok"}`
* no explicit error messages are exposed
* silent rejection removes oracle behavior
### Synthetic Mouse Events
## Limitations
**Attack:** Inject programmatic `mousemove` events via `dispatchEvent` or
CDP input simulation.
**Mitigation:** Synthetic events often fail the pause check (no natural dwell
periods), produce unrealistically uniform speed profiles, or fail the minimum
distance threshold. Generating convincingly human mouse traces at scale
requires either real input devices or sophisticated probabilistic models —
both significantly increase operational cost.
ChronoSeal does not protect against:
---
## What ChronoSeal Does Not Protect Against
| Limitation | Explanation |
|---|---|
| Real browsers with real users acting as bots | A human operating a browser manually is indistinguishable from a legitimate visitor. ChronoSeal cannot address this. |
| Server-side vulnerabilities | ChronoSeal is a client attestation layer. It does not protect the server from injection, authentication bypass, or other backend vulnerabilities. |
| Highly resourced nation-state actors | Out of scope for a client-side protection layer. |
| Content visible before session establishment | If the protected content is rendered before the first heartbeat, it can be scraped without a session. Gate content on session validity server-side. |
| Perfect bot prevention | No client-side mechanism can be. WASM can be reverse engineered. ChronoSeal raises cost, not an impenetrable barrier. |
---
* real users intentionally acting as bots
* server-side application vulnerabilities
* full browser farm operators with real input devices
* persistent fingerprinting or identity profiling
* pre-signed session payload reuse after a legitimate success if the attacker also has the current salt and key
## Operational Security Notes
### Log Level
* Do not use `RUST_LOG=debug` in production; it may expose internal identifiers.
* Always serve ChronoSeal traffic over HTTPS.
* Use `sqlite-in-memory` for ephemeral sessions when persistence is not required.
* Use `sqlite-disk` or `valkey` when session state needs to survive restarts.
Do not run with `RUST_LOG=debug` in production. The debug log includes
`session_id` values, which are sensitive identifiers. Use `warn` or `info`.
## Disclosure
### CORS Policy
The default `CorsLayer::permissive()` is suitable for development only.
In production, restrict allowed origins to your own domain:
```rust
CorsLayer::new()
.allow_origin("https://your.domain.com".parse::<HeaderValue>().unwrap())
.allow_methods([Method::POST])
.allow_headers([header::CONTENT_TYPE])
```
### TLS
Serve exclusively over TLS 1.3. The heartbeat payload contains timestamps
and behavioral signals. While each payload is signed and cannot be forged,
plaintext transmission leaks behavioral patterns and timing information that
could assist a sophisticated attacker.
### In-Memory SQLite
All session state is lost on server restart. This is intentional — there is
no persistent state to steal. Clients transparently re-initialise. If your
deployment restarts frequently (e.g. rolling deploys), sessions will be lost
more often; tune `HEARTBEAT_MIN_INTERVAL_MS` and `EXPIRATION_MINUTES`
accordingly so clients recover quickly.
---
## Security Disclosure
See [SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy
and contact details.
See [SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy.
+70 -238
View File
@@ -1,301 +1,133 @@
# ChronoSeal — WASM Build Guide
# ChronoSeal WASM Build Guide
## Overview
ChronoSeal uses a Rust-based WASM runtime to power browser-side attestation logic, signing, hash chaining, VM execution, and mutation commitment preview.
The client-side cryptographic core of ChronoSeal is written in Rust and
compiled to WebAssembly (WASM). The JavaScript frontend (`heartbeat.js`)
imports functions from this WASM module to generate keypairs, sign heartbeat
payloads, compute hash chain links, and execute the stack machine program.
## Why WASM
The import line in `heartbeat.js`:
The WASM runtime provides a deterministic, sandboxed environment for the following tasks:
```js
import init, { generate_keypair, sign_message, compute_next_hash, run_program }
from './pkg/antibot_wasm.js';
```
* generate Ed25519 keypairs in-browser
* sign canonical heartbeat payloads
* execute randomized VM opcode programs
* compute Blake3 hash chain progression
* preview and commit synthetic gene mutations
`./pkg/antibot_wasm.js` is a **generated file**. It does not exist in the
repository and must be produced by building the `wasm/` crate before running
the server.
This enables server/client parity and prevents the private key from leaving the browser runtime.
---
## Build Requirements
## How the WASM Module is Built
The tool that compiles Rust to WASM and generates the JavaScript glue is
[`wasm-pack`](https://rustwasm.github.io/wasm-pack/).
When you run:
```bash
wasm-pack build wasm --target web --release
```
wasm-pack does the following in sequence:
1. Compiles `wasm/src/lib.rs` (and its submodules) to a `.wasm` binary using
the `wasm32-unknown-unknown` target.
2. Runs `wasm-bindgen` to inspect every `#[wasm_bindgen]`-annotated function
and struct and generate a JavaScript wrapper for each one.
3. Optionally runs `wasm-opt` (from Binaryen) to size-optimise the binary.
4. Writes all output to `wasm/pkg/`.
---
## Output: `wasm/pkg/`
After a successful build, `wasm/pkg/` contains:
```
wasm/pkg/
├── antibot_wasm.js ← ES module; the file heartbeat.js imports
├── antibot_wasm_bg.wasm ← compiled WASM binary (~300–800 KB release)
├── antibot_wasm_bg.js ← internal memory bridge (do not import directly)
├── antibot_wasm.d.ts ← TypeScript type declarations
├── antibot_wasm_bg.d.ts ← TypeScript declarations for the bg module
└── package.json
```
### `antibot_wasm.js`
This is the public entry point. It contains:
- An `init()` function that fetches and instantiates the `.wasm` binary.
- One JavaScript wrapper function for each `#[wasm_bindgen]` export in
`wasm/src/`:
| Rust export | JS wrapper | Description |
|---|---|---|
| `generate_keypair()` | `generate_keypair()` | Generate Ed25519 keypair; return hex public key |
| `get_public_key()` | `get_public_key()` | Return hex public key, or `""` if not initialised |
| `sign_message(msg)` | `sign_message(msg)` | Sign string; return hex signature, or `""` if not initialised |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `compute_next_hash(...)` | Compute next Blake3 chain hash |
| `run_program(b64)` | `run_program(b64)` | Execute base64 VM program; return `{ stack, ip }` |
### `antibot_wasm_bg.wasm`
The compiled binary. The `.bg` suffix means "background" — this is the raw
WASM that `antibot_wasm.js` loads internally. You should not reference this
file directly in your HTML.
---
## Step-by-Step Build
### 1. Install the Rust WASM target
Install the Rust WASM target and `wasm-pack`:
```bash
rustup target add wasm32-unknown-unknown
```
This is a one-time step. Without it, the Rust compiler cannot produce WASM
output.
### 2. Install wasm-pack
```bash
cargo install wasm-pack
```
Or via the installer script:
```bash
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
Verify:
```bash
wasm-pack --version
# wasm-pack 0.13.x
```
### 3. Build the WASM module
## Build the WASM Module
From the project root:
From the repository root:
```bash
wasm-pack build wasm --target web --release
```
`--target web` produces an ES module (`import`/`export` syntax) suitable for
use directly in a browser without a bundler. Other targets (`bundler`,
`nodejs`, `no-modules`) produce different output formats and are not
compatible with the ChronoSeal frontend as written.
`--release` enables Rust's release optimisations (inlining, dead code
elimination, size reduction). Omit it during development for faster builds
and better panic messages.
### 4. Move the output to the frontend
```bash
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
The frontend expects the WASM module at `frontend/pkg/antibot_wasm.js`
because `heartbeat.js` imports from `./pkg/antibot_wasm.js` relative to
the `frontend/` directory, which is where the server's static file handler
is rooted.
`--target web` produces an ES module compatible with the existing frontend JavaScript.
---
`--release` enables optimizations for runtime performance and size.
## Using the Build Script
## Output
The convenience script at `scripts/build.sh` performs all steps in order:
After a successful build, `frontend/pkg/` contains:
```bash
bash scripts/build.sh
```
* `antibot_wasm.js`
* `antibot_wasm_bg.wasm`
* `antibot_wasm_bg.js`
* `antibot_wasm.d.ts`
* `antibot_wasm_bg.d.ts`
* `package.json`
This builds the WASM module, moves it to `frontend/pkg/`, and then builds
the server binary. Run this for a clean full build before deployment.
The frontend expects the WASM package under `frontend/pkg/`.
For development iteration where you are only changing Rust WASM code:
## Runtime Exports
```bash
wasm-pack build wasm --target web # (omit --release for speed)
rm -rf frontend/pkg && mv wasm/pkg frontend/pkg
```
The WASM module exports the following functions:
For development where you are only changing server code:
* `generate_keypair()` — generate a new Ed25519 keypair and return public key hex
* `get_public_key()` — return the current public key hex
* `sign_message(msg)` — sign a UTF-8 payload and return the hex signature
* `compute_next_hash(prev, ts, entropy, stack, salt)` — compute the next Blake3 chain hash
* `run_program(b64)` — execute a base64 VM program and return stack state
* `init_gene_state(gene_size)` — initialise the synthetic gene buffer
* `preview_gene_commitment(order_b64)` — preview the next gene commitment from a mutation order
* `commit_gene_preview()` — commit the previewed mutation after successful heartbeat
* `discard_gene_preview()` — discard the previewed mutation after rejection or error
* `current_gene_commitment()` — return the current committed gene commitment
```bash
cargo build -p server
```
## Browser Integration
---
## How `heartbeat.js` Loads the Module
`heartbeat.js` uses a standard ES module dynamic import pattern:
The frontend imports the generated module like this:
```js
import init, { generate_keypair, sign_message, compute_next_hash, run_program }
from './pkg/antibot_wasm.js';
export async function initHeartbeat() {
// 1. Fetch and instantiate the .wasm binary
await init();
// 2. Generate keypair — private key stored in WASM memory only
const pubKeyHex = generate_keypair();
// 3. Send public key to server, receive session_id and chain seed
// ...
}
import init, {
generate_keypair,
sign_message,
compute_next_hash,
run_program,
init_gene_state,
preview_gene_commitment,
commit_gene_preview,
discard_gene_preview,
current_gene_commitment
} from './pkg/antibot_wasm.js';
```
`init()` is the default export from `antibot_wasm.js`. It fetches
`antibot_wasm_bg.wasm` (from the same `pkg/` directory) via `fetch()`,
compiles it in the browser's WASM engine, and links it to the JS glue
layer. After `await init()` returns, all the named exports
(`generate_keypair`, `sign_message`, etc.) are ready to call.
`await init()` must be called before invoking any other exported function.
The `init()` call must complete before any other WASM function is called.
Calling `sign_message()` or `compute_next_hash()` before `await init()`
returns will produce an empty string (the module is not yet instantiated).
## Deployment Note
---
## Serving the WASM Binary
Browsers require WASM files to be served with the correct MIME type:
The `.wasm` binary must be served with the correct MIME type:
```
Content-Type: application/wasm
```
Most web servers set this automatically for `.wasm` files. If you see the
error:
The built-in Axum static file handler already sets the appropriate MIME type for `.wasm` files.
```
WebAssembly.instantiate(): Response has unsupported MIME type
```
## Build Script
Add the MIME type to your server configuration:
**nginx:**
```nginx
types {
application/wasm wasm;
}
```
**Apache `.htaccess`:**
```apache
AddType application/wasm .wasm
```
The Axum `ServeDir` handler used by ChronoSeal's built-in static server
sets the correct MIME type automatically via `tower-http`.
---
## What Is Not in the Repository
| Path | Why excluded |
|---|---|
| `wasm/pkg/` | Generated build output — changes on every build |
| `frontend/pkg/` | Same generated output, moved to serve location |
| `target/` | Standard Rust build artefacts |
Both `wasm/pkg/` and `frontend/pkg/` are listed in `.gitignore`. Committing
them would bloat the repository (the `.wasm` binary alone is 300–800 KB),
create noisy diffs on every rebuild, and give a false impression that the
WASM module is pre-built and ready to use without a build step.
---
## Troubleshooting
### `wasm32-unknown-unknown` target not found
```
error[E0463]: can't find crate for `std`
```
Fix:
Use the convenience script:
```bash
rustup target add wasm32-unknown-unknown
bash scripts/build.sh
```
### `wasm-pack` not found
This builds the WASM package, moves it into `frontend/pkg/`, and builds the server binary.
## Recommended Development Flow
* For WASM-only changes:
```bash
cargo install wasm-pack
wasm-pack build wasm --target web
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
### `wasm-opt` not found (warning, not an error)
wasm-pack prints a warning if `wasm-opt` is not installed. The build still
succeeds; the binary is just not size-optimised.
* For server-only changes:
```bash
# On Debian/Ubuntu/Arch
sudo apt install binaryen # Debian/Ubuntu
sudo pacman -S binaryen # Arch
cargo build -p server
```
### `antibot_wasm_bg.wasm` fetch fails (404)
## Notes
The `.wasm` file is not being served from `frontend/pkg/`. Verify:
```bash
ls /mnt/Programs/ChronoSeal/frontend/pkg/
# Should list: antibot_wasm.js antibot_wasm_bg.wasm ...
```
If the directory is empty or missing, re-run the build steps above.
### MIME type error in browser
See the "Serving the WASM Binary" section above.
### `sign_message` or `generate_keypair` returns empty string
The WASM keypair has not been initialised. Ensure `await init()` and
`generate_keypair()` are called (and awaited) before any other WASM
function. Check the browser console for any errors during `init()`.
Generated files in `wasm/pkg/` and `frontend/pkg/` are not tracked in source control.
They are build artifacts and should be regenerated as part of the release workflow.