Refactor attestation engine and synchronize project documentation

- Refine session and storage lifecycle handling
- Improve VM extension architecture across server, shared, and WASM runtimes
- Enhance synthetic gene mutation engine integration and parity guarantees
- Align deterministic state progression between server and browser execution paths
- Update configuration examples and deployment guidance
- Expand architecture, API, threat model, privacy, and WASM build documentation
- Refresh README with comprehensive project overview, operational workflows,
  browser integration details, storage backend documentation, and security model
- Document v0.6.0 refactoring outcomes and design rationale
- Improve consistency across documentation, configuration, and implementation

This commit consolidates the v0.6.0 architectural refactoring effort,
strengthening deterministic browser/server parity while improving
maintainability, operational clarity, and project documentation.
This commit is contained in:
thakares committed 2026-05-29 21:55:08 +05:30
1 parent 2b8afd54e0
commit 0ed3cb444d
15 files changed
+2357 -797

No files matched your search

+242 -85
View File
@@ -1,16 +1,56 @@
# ChronoSeal API Reference
ChronoSeal defines a small, deterministic API surface for browser attestation and heartbeat verification.
ChronoSeal exposes a small HTTP API for browser attestation, heartbeat verification, health checks, metrics, and runtime statistics.
This document describes the wire format and acceptance semantics. The internal state model is covered in [ARCHITECTURE.md](ARCHITECTURE.md).
## Base URL
All endpoints are relative to the server root. In development: `http://localhost:3000`. In production: the HTTPS origin of the protected site.
All paths are relative to the ChronoSeal server root.
---
- Development default: `http://127.0.0.1:3000`
- Production: the HTTPS origin or reverse-proxy path used by the protected site
## POST /init
Production deployments should use HTTPS. The daemon itself can run behind a local reverse proxy.
Initialise a new browser session.
## Content Type
JSON endpoints expect:
```http
Content-Type: application/json
```
Responses are JSON except `/metrics`, which returns Prometheus text format.
## Endpoint Summary
| Method | Path | Purpose |
|---|---|---|
| `POST` | `/init` | Create a browser attestation session |
| `POST` | `/hb` | Submit and verify a signed heartbeat |
| `GET` | `/health` | Return daemon health |
| `GET` | `/stats` | Return storage/session statistics |
| `GET` | `/metrics` | Return Prometheus-compatible metrics |
| `GET` | `/` | Serve static frontend assets from `frontend_dir` |
## Data Types
Common encodings:
| Value | Encoding |
|---|---|
| Ed25519 public key | 32 raw bytes encoded as 64 hex characters |
| Ed25519 signature | 64 raw bytes encoded as 128 hex characters |
| `session_id` | 32 random bytes encoded as 64 hex characters |
| `salt` | 16 random bytes encoded as 32 hex characters |
| `initial_hash`, `prev_hash`, `gene_commitment` | 32-byte digest encoded as 64 hex characters |
| `opcodes_b64`, `mutation_order_b64` | standard base64 |
| timestamps | Unix time in milliseconds unless otherwise stated |
## `POST /init`
Creates a new attestation session.
### Request
@@ -25,11 +65,18 @@ Content-Type: application/json
}
```
| Field | Type | Description |
|---|---|---|
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 public key generated by the WASM runtime |
| Field | Type | Required | Description |
|---|---|---:|---|
| `public_key` | string | yes | Browser-generated Ed25519 public key as 64 hex characters |
### Response `200 OK`
The private key is generated and retained by the browser WASM runtime. It is not sent to the server.
### Successful Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
@@ -48,26 +95,26 @@ Content-Type: application/json
| Field | Type | Description |
|---|---|---|
| `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 |
| `session_id` | string | Opaque session identifier |
| `salt` | string | Current server salt for the first heartbeat hash computation |
| `opcodes_b64` | string | Randomized VM program executed by the browser runtime |
| `initial_hash` | string | Initial chain head used as `prev_hash` for the first heartbeat |
| `expires_at` | number | Session expiration timestamp in milliseconds |
| `heartbeat_min_interval_ms` | number | Minimum heartbeat delay recommended by the server |
| `heartbeat_max_interval_ms` | number | Maximum heartbeat delay recommended by the server |
| `gene_size` | number | Initial synthetic gene buffer size |
| `mutation_step` | number | Mutation step expected on the first heartbeat |
| `mutation_order_b64` | string | Server-authored mutation order for the first heartbeat |
### Error
### Error Behavior
`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.
`/init` uses normal route-level error handling for invalid payloads or server failures. Invalid public key length, invalid configured gene size, or storage failure can prevent session creation.
---
Unlike `/hb`, initialization failures are not part of the silent heartbeat rejection model.
## POST /hb
## `POST /hb`
Submit a heartbeat to continue the session.
Submits one heartbeat for an existing session.
### Request
@@ -92,7 +139,7 @@ Content-Type: application/json
},
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": 2,
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"mutation_step": 1,
@@ -101,45 +148,61 @@ Content-Type: application/json
}
```
| Field | Type | Description |
|---|---|---|
| `session_id` | `string` | Session ID from `/init` |
| `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 |
| Field | Type | Required | Description |
|---|---|---:|---|
| `session_id` | string | yes | Session ID from `/init` |
| `prev_hash` | string | yes | Current browser view of the accepted hash-chain head |
| `timestamp` | number | yes | Browser wall-clock timestamp in milliseconds |
| `entropy_data.events` | array | yes | Mouse samples since the previous heartbeat |
| `entropy_data.events[].x` | number | yes | Mouse x coordinate |
| `entropy_data.events[].y` | number | yes | Mouse y coordinate |
| `entropy_data.events[].t` | number | yes | Event timestamp in milliseconds relative to the browser sampling window |
| `stack_state.stack` | array | yes | VM stack output as unsigned 32-bit values |
| `stack_state.ip` | number | yes | VM instruction pointer as an unsigned 16-bit value |
| `fingerprint.aspectRatio` | string | yes | Screen aspect ratio; server accepts numeric strings in range `0.5..=3.0` |
| `fingerprint.devicePixelRatio` | string | yes | Device pixel ratio; server accepts numeric strings in range `(0, 5]` |
| `fingerprint.hardwareConcurrency` | number | yes | Positive hardware concurrency value |
| `mutation_step` | number | yes | Mutation step currently expected by the server |
| `gene_commitment` | string | yes | Context-bound commitment produced by the WASM mutation preview |
| `signature` | string | yes | Ed25519 signature over the canonical payload |
### Canonical Signing Payload
The client signs a canonical JSON object with top-level keys sorted alphabetically:
The signature covers a canonical JSON object with sorted top-level keys:
```json
{
"entropyData": { "events": [{ "t": ..., "x": ..., "y": ... }] },
"entropyData": { "events": [{ "t": 1234.567, "x": 412.0, "y": 308.5 }] },
"fingerprint": {
"aspectRatio": "...",
"devicePixelRatio": ...,
"hardwareConcurrency": ...
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"geneCommitment": "...",
"mutationStep": ...,
"prevHash": "...",
"sessionId": "...",
"stackState": { "ip": ..., "stack": [...] },
"timestamp": ...
"geneCommitment": "64-char hex",
"mutationStep": 1,
"prevHash": "64-char hex",
"sessionId": "64-char hex",
"stackState": { "ip": 42, "stack": [2971406957, 1234567890] },
"timestamp": 1234567890123
}
```
Note: the signed payload uses camelCase while the transport request uses snake_case.
Important details:
### Response `200 OK` — Accepted
- The transport payload uses snake_case for several fields.
- The signed payload uses camelCase names.
- Top-level keys must be serialized deterministically in lexical order.
- The `signature` field is not part of the signed payload.
- Nested serialization must match the server's `serde_json` representation.
The server reconstructs the canonical message from the received request before verifying the Ed25519 signature.
### Accepted Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
@@ -152,12 +215,19 @@ Note: the signed payload uses camelCase while the transport request uses snake_c
| 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 |
| `status` | string | Always `ok` |
| `next_salt` | string | Server salt for the next heartbeat |
| `next_mutation_step` | number | Mutation step expected on the next heartbeat |
| `next_mutation_order_b64` | string | Server-authored mutation order for the next heartbeat |
### Response `200 OK` — Rejected
Clients should treat the heartbeat as accepted only when all next-state fields are present.
### Rejected Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
@@ -165,45 +235,132 @@ Note: the signed payload uses camelCase while the transport request uses snake_c
}
```
A rejected heartbeat omits `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
Rejected heartbeats omit:
This silent rejection model avoids giving attackers distinct failure signals.
- `next_salt`
- `next_mutation_step`
- `next_mutation_order_b64`
---
This response shape is intentional. The server does not reveal which validation stage failed.
## Validation Rules
### Heartbeat Validation Order
Heartbeats are rejected silently when any validation step fails:
The server currently validates heartbeats in this order:
* 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
1. Rate-limit check in the route handler.
2. Load session by `session_id`.
3. Check session expiration.
4. Verify Ed25519 signature.
5. Compare `prev_hash` with stored `last_hash`.
6. Compare `mutation_step` with stored `pending_mutation_step`.
7. Apply stored pending mutation to a cloned gene state.
8. Compare expected and submitted `gene_commitment`.
9. Enforce timestamp drift.
10. Validate mouse entropy.
11. Validate fingerprint fields.
12. Compute next hash-chain head.
13. Generate next mutation order and salt.
14. Persist advanced session state.
---
Any failure after route-level JSON decoding returns the silent rejection body.
## `GET /health`
Returns a basic health response.
```http
GET /health
```
```json
{
"status": "healthy"
}
```
## `GET /stats`
Returns storage-derived session statistics.
```http
GET /stats
```
```json
{
"sessions": 1,
"expired_sessions": 0,
"max_chain_length": 4
}
```
| Field | Type | Description |
|---|---|---|
| `sessions` | number | Stored session count |
| `expired_sessions` | number | Expired sessions not yet purged |
| `max_chain_length` | number | Highest stored heartbeat chain length |
## `GET /metrics`
Returns Prometheus-compatible text.
```http
GET /metrics
```
```text
# HELP chronoseal_sessions Active ChronoSeal sessions
# TYPE chronoseal_sessions gauge
chronoseal_sessions 1
# HELP chronoseal_expired_sessions Expired sessions not yet removed
# TYPE chronoseal_expired_sessions gauge
chronoseal_expired_sessions 0
# HELP chronoseal_max_chain_length Maximum heartbeat chain length
# TYPE chronoseal_max_chain_length gauge
chronoseal_max_chain_length 4
```
## Client State Rules
After `/init`, the client stores:
- `session_id`
- `initial_hash` as the first `prev_hash`
- current `salt`
- VM opcode program
- committed gene state
- pending mutation step
- pending mutation order
On accepted `/hb`:
1. Commit the local gene preview.
2. Compute the next local hash using the old salt that was active when the heartbeat was sent.
3. Replace current salt with `next_salt`.
4. Replace pending mutation step and order with server-provided values.
On rejected `/hb`:
1. Discard the local gene preview.
2. Do not advance hash-chain state.
3. Do not advance mutation state.
4. Treat the session as suspect or restart attestation.
## WASM Runtime Exports
The WASM module exports the following functions to JavaScript:
The generated `chronoseal_wasm` package exposes:
| Function | Signature | Description |
|---|---|---|
| `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 |
| `generate_keypair()` | `() -> string` | Generate an Ed25519 keypair and return public key hex |
| `get_public_key()` | `() -> string` | Return current public key hex, or `""` if no keypair exists |
| `sign_message(msg)` | `(string) -> string` | Sign a UTF-8 payload and return hex signature, or `""` on failure |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | Compute next Blake3 chain hash |
| `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 |
| `init_gene_state(gene_size)` | `(u32) -> bool` | Initialize the browser gene state |
| `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` | `(string, string, u64, u8) -> string` | Preview next mutation commitment |
| `commit_gene_preview()` | `() -> bool` | Commit the preview after accepted heartbeat |
| `discard_gene_preview()` | `() -> void` | Discard preview after rejection or error |
| `current_gene_commitment(session_id, mutation_step)` | `(string, u64) -> string` | Return current committed gene commitment |
String-returning functions return `""` on error. Callers must handle empty values and boolean failures gracefully.
String-returning functions use `""` to signal failure. Callers must handle empty strings explicitly.
+490 -102
View File
@@ -1,145 +1,533 @@
# ChronoSeal Architecture
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.
ChronoSeal is a Unix-native browser attestation daemon. It validates browser session continuity by combining signed heartbeats, Blake3 hash-chain progression, deterministic VM execution, behavioral sanity checks, and a shared Synthetic Gene Mutation Engine that runs on both the server and the browser WASM runtime.
## Overview
This document describes the system architecture, state model, validation pipeline, trust boundaries, and operational assumptions. The API wire format is documented separately in [API.md](API.md), and deployment guidance is documented in [DEPLOYMENT.md](DEPLOYMENT.md).
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.
## Architectural Goals
Key characteristics:
ChronoSeal is designed as infrastructure software rather than a consumer-facing widget. The main goals are:
* 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
- Keep the server small, inspectable, and operable as a normal Unix daemon.
- Use deterministic client/server computation so the server can verify browser-side progression without trusting browser claims blindly.
- Make replay, stale state reuse, and incomplete automation expensive.
- Preserve privacy by using short-lived session state instead of persistent identity tracking.
- Avoid attacker feedback oracles by returning indistinguishable success-shaped responses for rejected heartbeats.
- Keep browser integration lightweight: static JavaScript plus a Rust-generated WASM package.
## Core Components
ChronoSeal does not attempt to prove that a human is present. It attempts to prove that a client is maintaining the expected live browser-side cryptographic and mutation state.
## System Context
```text
Protected browser origin
|
| static files and API calls
v
+------------------------------+
| Browser |
| - frontend JavaScript |
| - chronoseal_wasm runtime |
| - Ed25519 session key |
| - VM and gene state |
+---------------+--------------+
|
| POST /init
| POST /hb
v
+------------------------------+
| ChronoSeal daemon |
| - Axum HTTP routes |
| - session verifier |
| - storage abstraction |
| - metrics and health |
+---------------+--------------+
|
| SessionRecord
v
+------------------------------+
| Storage backend |
| - sqlite-in-memory |
| - sqlite-in-disk |
| - valkey |
+------------------------------+
```
ChronoSeal can serve the frontend files itself or sit behind a reverse proxy. TLS termination should happen before traffic reaches the daemon in production.
## Workspace Components
The repository is a Rust workspace with three runtime crates and one static frontend directory.
### `shared/`
Shared protocol and runtime primitives used by both the server and the browser runtime:
`shared/` contains protocol and deterministic runtime code used by both the server and WASM crates.
* Cryptographic primitives: Blake3, Ed25519
* Hash chain logic and session commitment handling
* Synthetic gene model and deterministic mutation engine
* Serialization, encoding, and canonical signing helpers
Responsibilities:
- wire protocol structs for `/init` and `/hb`
- Blake3 hash-chain helpers
- synthetic gene state representation
- environment encoding and validation
- mutation program generation, encoding, decoding, and execution
- deterministic VM extension opcode semantics
Important files:
| File | Responsibility |
|---|---|
| `protocol.rs` | `InitRequest`, `InitResponse`, `HeartbeatRequest`, `HeartbeatResponse`, and supporting payload types |
| `hashing.rs` | initial and next hash-chain computation |
| `gene.rs` | gene state, environment records, validation, and context-bound commitment |
| `vm_extensions.rs` | mutation order generation, opcode interpreter, execution tracing, and tests |
| `constants.rs` | protocol and execution bounds |
`shared/` is the determinism boundary. Any logic that must agree between server and browser belongs here rather than in server-only or frontend-only code.
### `server/`
The server crate implements the runtime daemon:
`server/` builds the `chronoseal` binary. It owns daemon lifecycle, HTTP routing, session verification, storage, metrics, configuration, and CLI behavior.
* `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
Important files:
| File | Responsibility |
|---|---|
| `main.rs` | CLI command dispatch |
| `cli.rs` | command, flag, and environment variable definitions |
| `config.rs` | defaults, TOML loading, environment overrides, validation |
| `runtime.rs` | daemon startup, Axum router, health, metrics, stats, graceful shutdown |
| `routes/init.rs` | `POST /init` handler |
| `routes/heartbeat.rs` | `POST /hb` handler and silent rejection response shape |
| `session.rs` | session creation, heartbeat verification, state advancement |
| `crypto.rs` | canonical signing payload and Ed25519 signature verification |
| `storage.rs` | `DbPool`, SQLite, Valkey compatibility, session persistence, stats |
| `trust.rs` | mouse entropy validation |
| `fingerprint.rs` | browser signal validation |
| `ratelimit.rs` | per-session rate limiting |
| `cleanup.rs` | expired session removal |
The server treats the browser as untrusted. Browser-supplied values are accepted only after signature, continuity, timing, behavioral, and mutation checks pass.
### `wasm/`
The client runtime crate compiles to WebAssembly and powers attestation in the browser.
`wasm/` compiles to the browser runtime package with `wasm-pack --target web`.
* `crypto.rs` — in-WASM signing and hash computation
* `vm.rs` — randomized opcode VM execution
* `vm_extensions.rs` — synthetic gene mutation preview and commit lifecycle
Responsibilities:
- generate and hold the browser-local Ed25519 keypair
- sign canonical heartbeat payloads
- compute hash-chain values used by the browser integration
- execute randomized VM programs
- maintain committed and preview synthetic gene state
- preview mutation commitments before a heartbeat is submitted
- commit or discard preview state after server response
Important files:
| File | Responsibility |
|---|---|
| `crypto.rs` | key generation, public key export, message signing |
| `vm.rs` | base VM program execution |
| `vm_extensions.rs` | gene initialization, mutation preview, commit, discard, current commitment |
The WASM runtime is not a trusted execution environment. It is useful because it forces a browser client to implement the same state transitions as the server and makes simple HTTP automation insufficient.
### `frontend/`
Static browser integration code that loads the WASM module, orchestrates init/heartbeat flow, and collects browser entropy.
`frontend/` contains static JavaScript and browser assets. It loads `frontend/pkg/chronoseal_wasm.js`, calls `/init`, periodically sends `/hb`, and coordinates browser-side state transitions.
## v0.6.0 Innovation
The frontend is intentionally thin. Durable protocol rules live in Rust, not in handwritten JavaScript.
The primary innovation in v0.6.0 is the **Synthetic Gene Mutation Engine**.
## Runtime Topology
This layer adds a deterministic, shared server/WASM mutation handshake to the existing heartbeat continuity model.
The daemon builds a single Axum application with:
Key v0.6.0 behavior:
| Route | Method | Purpose |
|---|---|---|
| `/init` | `POST` | create a new attestation session |
| `/hb` | `POST` | verify and advance a heartbeat |
| `/health` | `GET` | health probe |
| `/metrics` | `GET` | Prometheus-compatible metrics |
| `/stats` | `GET` | storage/session statistics |
| `/` | `GET` | static frontend assets from `frontend_dir` |
* `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
Shared runtime state is held in `AppState`:
This makes replay and tampering attacks significantly more expensive while preserving the existing privacy-first and silent-failure semantics.
- `db_pool`: storage backend handle
- `rate_limiter`: process-local rate limiter
- `config`: runtime configuration snapshot behind an `RwLock`
## Architecture Diagram
Configuration is resolved in this order:
```
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 │
└──────────────────────────────────────────────────────────────┘
1. CLI flags
2. `CHRONOSEAL_*` environment variables
3. TOML configuration file
4. built-in defaults
## Session State Model
The server persists one `SessionRecord` per active session.
| Field | Meaning |
|---|---|
| `session_id` | random 32-byte session identifier encoded as hex |
| `public_key` | browser-generated Ed25519 verifying key |
| `salt` | current server salt for hash-chain progression |
| `last_hash` | current accepted hash-chain head |
| `chain_length` | number of accepted chain states including initialization |
| `created_at` | creation timestamp in milliseconds |
| `last_seen` | timestamp of last accepted heartbeat |
| `expires_at` | session expiration timestamp in milliseconds |
| `gene` | committed synthetic gene byte buffer |
| `environment` | encoded environment records |
| `pending_mutation` | server-issued mutation program for the next heartbeat |
| `pending_mutation_step` | mutation step expected on the next heartbeat |
The committed server state advances only after a heartbeat passes all validation checks. Failed heartbeats do not update `last_hash`, `salt`, `gene`, `environment`, `pending_mutation`, or `pending_mutation_step`.
## Initialization Flow
```text
Browser/WASM Server
------------ ------
generate_keypair()
public key
|
| POST /init { public_key }
v
validate public key length
create GeneState
generate session_id
generate salt
compute initial_hash
generate VM opcodes
generate mutation step 1
persist SessionRecord
^
| InitResponse
|
store session_id, salt,
initial_hash, opcodes,
gene_size, mutation order
```
## Storage Backends
Initialization creates the first server-side commitment state but does not prove liveness. Liveness begins with accepted heartbeats.
ChronoSeal supports pluggable backend modes using the `db_type` configuration option.
The initial response contains:
* `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.
- `session_id`
- `salt`
- `opcodes_b64`
- `initial_hash`
- `expires_at`
- heartbeat interval bounds
- `gene_size`
- `mutation_step`
- `mutation_order_b64`
## Runtime Philosophy
## Heartbeat Flow
ChronoSeal is intentionally designed to behave like traditional Unix infrastructure software:
```text
Browser/WASM Server
------------ ------
execute VM program
collect entropy and fingerprint data
preview pending gene mutation
build canonical signing payload
sign with Ed25519 private key
|
| POST /hb HeartbeatRequest
v
load session
check expiration
verify signature
check hash continuity
check mutation step
apply pending mutation
compare gene commitment
check timestamp drift
validate mouse entropy
validate fingerprint
compute next hash
generate next mutation
generate next salt
persist advanced state
^
| accepted: status + next salt + next mutation
| rejected: { "status": "ok" }
|
commit preview on accepted response
discard or stop on rejected response
```
* 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
Accepted heartbeats return `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
## Integration Points
Rejected heartbeats return only:
* 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
```json
{
"status": "ok"
}
```
## Operating Assumptions
This silent rejection behavior is part of the security model. It prevents the API from acting as an oracle for signature, timing, mutation, or behavior failures.
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.
## Verification Pipeline
It assumes:
Heartbeat verification occurs in `server/src/session.rs`.
* 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
The current validation order is:
1. Load the session by `session_id`.
2. Reject if the session is missing.
3. Reject if `now > expires_at`.
4. Verify the Ed25519 signature over the canonical payload.
5. Decode and compare `prev_hash` with the stored `last_hash`.
6. Compare request `mutation_step` with stored `pending_mutation_step`.
7. Decode the stored gene environment.
8. Apply the stored `pending_mutation` to a cloned server gene state.
9. Compute the expected `gene_commitment` with session and step context.
10. Compare the request `gene_commitment` with the expected commitment.
11. Enforce timestamp drift bounds.
12. Validate mouse entropy.
13. Validate browser fingerprint fields.
14. Compute the next hash-chain value.
15. Generate the next mutation order.
16. Generate the next salt.
17. Persist the advanced session state.
The verifier performs state mutation only after validation succeeds. This preserves replay resistance and avoids desynchronizing the server after invalid requests.
## Canonical Signing Boundary
The heartbeat signature covers a canonical JSON payload built from:
- `entropyData`
- `fingerprint`
- `geneCommitment`
- `mutationStep`
- `prevHash`
- `sessionId`
- `stackState`
- `timestamp`
The server constructs this payload using a `BTreeMap`, which orders top-level keys deterministically before serializing. The transport request uses snake_case field names, while the signed payload uses camelCase names that match the browser-side canonical message.
The signature does not cover the `signature` field itself.
## Hash-Chain Boundary
Each accepted heartbeat advances a Blake3 hash chain.
Inputs include:
- previous hash-chain head
- heartbeat timestamp
- entropy data
- VM stack state
- current server salt
The server stores only the current accepted head as `last_hash`. A replayed heartbeat with an old `prev_hash` fails because the stored `last_hash` has already advanced.
The salt rotates after every accepted heartbeat. The next salt is returned only on acceptance, so rejected clients do not receive the material needed for the next valid chain step.
## Synthetic Gene Mutation Engine
The Synthetic Gene Mutation Engine provides an additional deterministic continuity check.
Core concepts:
- `GeneState`: committed gene byte buffer plus environment records.
- `MutationOrder`: mutation step plus encoded mutation program.
- `pending_mutation`: the server-authored program expected on the next heartbeat.
- `gene_commitment`: context-bound commitment over the candidate gene state, `session_id`, and `mutation_step`.
The server and WASM runtime both execute the same mutation semantics from `shared/vm_extensions.rs`.
Mutation lifecycle:
1. Server stores a pending mutation program and step.
2. Browser previews that mutation against its committed gene state.
3. Browser sends the resulting `gene_commitment`.
4. Server applies the same mutation to a clone of its committed gene state.
5. Server compares the expected commitment with the browser commitment.
6. On success, server commits the candidate state and issues the next mutation.
7. Browser commits its preview only after receiving an accepted response.
This design prevents a client from advancing mutation state independently of the server. The mutation order is server-authored, step-bound, and accepted only once.
## Behavioral Trust Checks
ChronoSeal includes lightweight behavioral checks. These checks are not a complete human verification system; they are an automation cost signal.
Current checks include:
- minimum mouse activity, when enabled
- minimum total mouse movement distance
- maximum average mouse speed
- minimum pause count
- timestamp drift bound
- basic fingerprint field validation
The checks are intentionally bounded and configurable. They should be treated as one layer in the attestation pipeline, not as the primary security primitive.
## Storage Architecture
Storage is abstracted by `DbPool`.
| Backend | `db_type` | Characteristics |
|---|---|---|
| SQLite memory | `sqlite-in-memory` | default, process-local, ephemeral |
| SQLite disk | `sqlite-in-disk` | persisted SQLite file at `db_path` |
| Valkey | `valkey` | Valkey-compatible session store |
The storage layer must support:
- insert session
- load session
- update session
- delete expired sessions
- report statistics
`valkey` mode reads `CHRONOSEAL_VALKEY_ADDR`, defaulting to `127.0.0.1:6666`. If connection setup fails, the current implementation logs a warning and falls back to in-memory SQLite.
## Metrics and Observability
ChronoSeal exposes two operational surfaces:
- CLI commands: `status`, `health`, `metrics`, `stats`, `config check`
- HTTP endpoints: `/health`, `/metrics`, `/stats`
The metrics endpoint reports storage-derived counters including:
- active sessions
- expired sessions
- maximum observed chain length
The daemon uses structured tracing and can log to journald through normal systemd operation. Operators should avoid debug logging in production because internal identifiers may appear in logs.
## Trust Boundaries
### Browser Boundary
The browser is untrusted. It may lie about entropy, fingerprint values, VM output, mutation commitment, timing, and session identifiers.
Mitigation:
- signature verification binds payloads to the browser session key
- hash-chain checks reject stale state
- mutation commitment checks reject incorrect gene progression
- timing and behavioral checks reject implausible requests
### WASM Boundary
WASM code runs in the browser and is therefore not trusted as secure enclave code.
Mitigation:
- the server independently recomputes critical deterministic state
- private key custody raises automation cost but is not treated as hardware-backed secrecy
- failures do not reveal detailed reasons to callers
### Storage Boundary
Storage is trusted for session continuity. If storage is lost, sessions cannot continue. If storage is tampered with, attestation integrity can be affected.
Mitigation:
- use proper filesystem permissions for SQLite disk mode
- deploy Valkey on a trusted network or protected socket
- keep ChronoSeal behind normal host and service hardening
### Network Boundary
ChronoSeal expects production traffic to be protected by TLS. Plaintext deployment weakens confidentiality and makes traffic analysis easier.
Mitigation:
- terminate TLS at a reverse proxy or load balancer
- keep `/init` and `/hb` same-origin with protected content when possible
- avoid exposing internal metrics broadly
## Failure Semantics
ChronoSeal intentionally separates transport success from attestation success.
| Failure class | HTTP behavior | State mutation |
|---|---|---|
| malformed route-level request | normal HTTP error handling | no session advancement |
| invalid heartbeat semantics | `200 OK` with `{"status":"ok"}` | no session advancement |
| rejected heartbeat | `200 OK` with `{"status":"ok"}` | no session advancement |
| accepted heartbeat | `200 OK` with next-state fields | session state advances from the verifier's perspective |
This ambiguity reduces attacker feedback. Application integrations must check for the presence of `next_salt`, `next_mutation_step`, and `next_mutation_order_b64` rather than treating any `status: ok` as an accepted heartbeat.
## Invariants
The architecture relies on these invariants:
- A session has exactly one expected `pending_mutation_step` at a time.
- A pending mutation is consumed only by an accepted heartbeat.
- `last_hash` changes only after a heartbeat passes verification.
- `salt` changes only after a heartbeat passes verification.
- `gene` and `environment` change only after mutation commitment validation succeeds.
- The next mutation order is generated only from an accepted candidate state.
- Rejected heartbeats do not reveal the failed validation stage.
- Browser-side preview state is committed only after an accepted heartbeat response.
Breaking these invariants can introduce replay acceptance, client/server desynchronization, or oracle behavior.
## Concurrency Notes
ChronoSeal currently verifies a heartbeat by loading a session, computing candidate state, and writing the updated record back to storage. The intended operational model is one live heartbeat stream per browser session.
Concurrent heartbeats for the same `session_id` should naturally collapse to at most one accepted progression because both requests present the same `prev_hash` and `mutation_step`; after the first accepted update, the second request becomes stale. Storage backends must preserve update visibility strongly enough for this assumption to hold.
## Deployment Shape
Typical production topology:
```text
Internet
|
v
TLS reverse proxy
|
v
chronoseal daemon on 127.0.0.1:3000
|
v
SQLite disk or Valkey storage
```
Recommended deployment properties:
- run under systemd with a dedicated service user
- bind to localhost behind a reverse proxy unless direct exposure is required
- serve over HTTPS
- keep debug logs disabled
- monitor `/health`, `/metrics`, and `/stats`
- use `sqlite-in-memory` for ephemeral local sessions
- use `sqlite-in-disk` or `valkey` when sessions must survive process restarts
## Limitations
ChronoSeal is not:
- a user authentication system
- a CAPTCHA
- a fraud scoring engine
- a hardware attestation system
- a persistent identity framework
- a complete defense against fully resourced browser farms
It is a protocol layer that makes browser automation and replay more expensive by requiring correct, continuous, stateful execution.
## Related Documents
- [API Reference](API.md)
- [Deployment Guide](DEPLOYMENT.md)
- [Threat Model](THREAT_MODEL.md)
- [WASM Build Guide](WASM_BUILD.md)
- [Design Philosophy](DESIGN-PHILOSOPHY.md)
- [Privacy Policy](PRIVACY%20POLICY.md)
+249 -127
View File
@@ -1,160 +1,244 @@
# 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.
ChronoSeal is intended to run as a small Unix daemon behind TLS, with static browser assets served either by the daemon or by the same protected origin. This guide covers native, service, and container deployment.
## Prerequisites
## Deployment Model
| Tool | Minimum version | Purpose |
|---|---|---|
| Rust | 1.87 stable | Server and WASM compilation |
| wasm-pack | 0.13 | WASM build and packaging |
| Docker | 24.x | Optional container deployment |
| docker-compose | 2.x | Optional local orchestration |
| systemd | 248+ | Service management |
Typical production topology:
Install Rust: [https://rustup.rs](https://rustup.rs)
Install wasm-pack: `cargo install wasm-pack`
```text
Internet
|
v
TLS reverse proxy
|
v
chronoseal daemon on 127.0.0.1:3000
|
v
sqlite-in-disk or valkey storage
```
---
For local evaluation, the daemon can bind directly to `0.0.0.0:3000` or `127.0.0.1:3000`.
## Build Steps
## Requirements
### 1. Build the WASM module
| Tool | Minimum | Purpose |
|---|---:|---|
| Rust | 1.87 stable | Build server and shared crates |
| `wasm32-unknown-unknown` target | current stable | Compile WASM runtime |
| `wasm-pack` | 0.13 | Generate browser WASM package |
| systemd | 248+ | Native service management |
| Docker | 24.x | Optional container image |
| Docker Compose | 2.x | Optional local orchestration |
Install Rust from rustup, then install the WASM tooling:
```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 the browser runtime assets required by the frontend and the server static file handler.
## Build
### 2. Build the server binary
```bash
cargo build -p server --release
```
Binary output: `target/release/server`
### 3. Convenience script
Use the repository build script:
```bash
bash scripts/build.sh
```
This script builds the WASM module, moves the generated package into `frontend/pkg`, and builds the server binary.
The script:
---
1. Builds `wasm/` with `wasm-pack build --target web --release`.
2. Replaces `frontend/pkg` with the generated package.
3. Builds the release daemon binary.
## Deploying as a Native Service
Manual equivalent:
ChronoSeal is intended to run as a proper Unix daemon managed by systemd.
```bash
wasm-pack build wasm --target web --release
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
### Install
cargo build -p chronoseal-server --bin chronoseal --release
```
Release binary:
```text
target/release/chronoseal
```
## Native Install
The installer builds, installs, enables, and starts the service:
```bash
sudo bash scripts/install.sh
```
This installer should perform the following tasks:
Installer actions:
* 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
- create the `chronoseal` system user if missing
- build WASM and server artifacts
- install `target/release/chronoseal` to `/usr/local/bin/chronoseal`
- copy `frontend/` to `/opt/chronoseal/frontend`
- install `chronoseal.service` to `/etc/systemd/system/chronoseal.service`
- reload systemd
- enable and start the service
### Verify the service
Verify:
```bash
sudo systemctl status chronoseal
chronoseal status --format json
chronoseal health
sudo journalctl -u chronoseal -f
```
## Running Without Install
For local development:
```bash
bash scripts/build.sh
cargo run -p chronoseal-server --bin chronoseal -- run \
--bind 127.0.0.1:3000 \
--frontend-dir frontend
```
Probe the daemon:
```bash
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/stats
curl http://127.0.0.1:3000/metrics
```
## Configuration
ChronoSeal resolves configuration in this order:
1. CLI flags
2. `CHRONOSEAL_*` environment variables
3. TOML config file
4. built-in defaults
Default config discovery:
1. `CHRONOSEAL_CONFIG`, if it points to an existing file
2. `/etc/chronoseal/config.toml`
3. `$XDG_CONFIG_HOME/chronoseal/config.toml`
4. `~/.config/chronoseal/config.toml`
Validate effective configuration:
```bash
chronoseal config check --format yaml
```
Example:
```toml
bind = "127.0.0.1:3000"
db_type = "sqlite-in-disk"
pid_file = "/run/chronoseal.pid"
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
frontend_dir = "/usr/share/chronoseal/frontend"
log_file = "/var/log/chronoseal/chronoseal.jsonl"
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 10.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
gene_size = 512
mutation_rounds = 4
```
## Storage Backends
| Backend | `db_type` | Use case |
|---|---|---|
| SQLite memory | `sqlite-in-memory` | ephemeral local or stateless deployment |
| SQLite disk | `sqlite-in-disk` | persisted session continuity across restarts |
| Valkey | `valkey` | external session storage |
For disk persistence:
```bash
sudo mkdir -p /var/lib/chronoseal
sudo chown -R chronoseal:chronoseal /var/lib/chronoseal
```
For Valkey:
```bash
export CHRONOSEAL_DB_TYPE=valkey
export CHRONOSEAL_VALKEY_ADDR=127.0.0.1:6666
```
If Valkey connection setup fails, the current implementation logs a warning and falls back to in-memory SQLite.
## systemd
The supplied service file is intended as the baseline unit. Keep the daemon under a dedicated user and restrict filesystem access to the paths it needs.
Useful commands:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now chronoseal
sudo systemctl restart chronoseal
sudo systemctl status chronoseal
sudo journalctl -u chronoseal -f
```
### Recommended runtime options
Recommended hardening properties include:
Use structured info-level logging in production:
- `NoNewPrivileges=true`
- `PrivateTmp=true`
- `ProtectSystem=strict`
- `ProtectHome=true`
- `ProtectKernelTunables=true`
- `ProtectKernelModules=true`
- `ProtectControlGroups=true`
- `MemoryDenyWriteExecute=true`
- `RestrictRealtime=true`
- `RestrictSUIDSGID=true`
- `SystemCallArchitectures=native`
```bash
export RUST_LOG=info
sudo systemctl restart chronoseal
```
Any hardening must still allow access to:
Avoid `RUST_LOG=debug` in production because debug logs can expose internal session identifiers.
---
## systemd Integration
The supplied `chronoseal.service` is designed for hardened Unix-native operation.
Recommended service options:
* `NoNewPrivileges=true`
* `PrivateTmp=true`
* `ProtectSystem=strict`
* `ProtectHome=true`
* `ProtectKernelTunables=true`
* `ProtectKernelModules=true`
* `ProtectControlGroups=true`
* `MemoryDenyWriteExecute=true`
* `RestrictRealtime=true`
* `RestrictSUIDSGID=true`
* `SystemCallArchitectures=native`
These options reduce the host attack surface and keep the daemon constrained to its required runtime privileges.
---
## Configuration
ChronoSeal reads configuration from a TOML file, environment variables, and CLI overrides. Use `chronoseal config` to validate the effective configuration.
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.
---
- the binary
- frontend assets
- PID file directory
- optional log file directory
- SQLite database directory, if using `sqlite-in-disk`
## 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.
ChronoSeal should be served over HTTPS in production. Terminate TLS at a reverse proxy or load balancer and proxy to the local daemon.
### nginx example
Minimal nginx example:
```nginx
server {
listen 443 ssl http2;
server_name your.domain.com;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
ssl_protocols TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
proxy_read_timeout 35s;
proxy_send_timeout 10s;
proxy_read_timeout 35s;
proxy_send_timeout 10s;
location / {
proxy_pass http://127.0.0.1:3000;
@@ -168,41 +252,79 @@ server {
server {
listen 80;
server_name your.domain.com;
server_name example.com;
return 301 https://$host$request_uri;
}
```
### Docker deployment
Keep `/init`, `/hb`, and frontend assets on the same origin when possible. If you split origins, configure CORS and cookie/application policy deliberately.
## Docker
Build and run:
```bash
bash scripts/build.sh
docker compose up -d --build
```
The supplied `docker-compose.yml` is intended for local evaluation and development. It mounts `frontend/` and exposes port `3000`.
The Compose file exposes port `3000`.
Note: build the WASM package before container startup, or mount a pre-built `frontend/pkg/` volume.
```bash
curl http://127.0.0.1:3000/health
```
---
The Dockerfile copies `frontend/` from the working tree. Build `frontend/pkg` before building the image when the browser WASM runtime is required inside the container.
## Production Best Practices
## Observability
* 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
CLI:
---
```bash
chronoseal status --format json
chronoseal health
chronoseal stats --format json
chronoseal metrics
```
## Health and Metrics
HTTP:
ChronoSeal exposes runtime endpoints for health and metrics.
```bash
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/stats
curl http://127.0.0.1:3000/metrics
```
* `chronoseal health` — health probe
* `chronoseal metrics` — Prometheus metrics output
* `chronoseal status` — runtime status report
* `chronoseal stats` — runtime statistics
Prometheus metrics:
These endpoints are accessible locally from the daemon and may be proxied or scraped by monitoring infrastructure.
- `chronoseal_sessions`
- `chronoseal_expired_sessions`
- `chronoseal_max_chain_length`
## Logging
Use info-level logs for production:
```bash
CHRONOSEAL_LOG=info chronoseal run
```
or with systemd:
```bash
sudo systemctl edit chronoseal
```
Avoid debug logging in production because internal identifiers may be written to logs.
## Production Checklist
- Build `frontend/pkg` before packaging.
- Serve ChronoSeal traffic over HTTPS.
- Bind the daemon to localhost behind a reverse proxy unless direct exposure is required.
- Use a dedicated service user.
- Keep debug logs disabled.
- Choose storage intentionally: `sqlite-in-memory`, `sqlite-in-disk`, or `valkey`.
- Protect SQLite and log directories with correct ownership.
- Monitor `/health`, `/stats`, and `/metrics`.
- Verify `chronoseal config check` after environment or config changes.
+103 -36
View File
@@ -1,58 +1,125 @@
# ChronoSeal Design Philosophy
ChronoSeal is built for operators who value clarity, stability, and Unix-native infrastructure.
ChronoSeal is designed for operators who want a local, inspectable, Unix-native browser attestation layer rather than a hosted anti-bot black box.
## Core Philosophy
## Core Position
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 infrastructure software. It should feel closer to `nginx`, `redis-server`, or a small system daemon than to a third-party analytics platform.
### Design priorities
Design priorities:
* **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.
- CLI-first operation
- explicit configuration
- deterministic protocol behavior
- small runtime surface
- privacy-preserving state
- observable health and metrics
- no hidden telemetry
- no persistent user profiling
## Execution Model
## What ChronoSeal Optimizes For
ChronoSeal emphasizes deterministic, stateless request validation with a lightweight server-side session store.
### Operator Control
* 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.
Operators should be able to build, run, inspect, configure, monitor, and stop the service with ordinary Unix tools.
This is why ChronoSeal provides:
- `chronoseal run`
- `chronoseal status`
- `chronoseal health`
- `chronoseal config check`
- `chronoseal metrics`
- `chronoseal stats`
- shell completions
- systemd integration
### Determinism
The protocol depends on deterministic agreement between server Rust and browser WASM.
Shared logic belongs in `shared/` when divergence would create security or correctness risk. This includes:
- protocol structs
- hash-chain semantics
- synthetic gene model
- mutation opcode behavior
- mutation order encoding
### Cost Escalation
ChronoSeal does not claim impossible security. It raises the cost of automation by making clients maintain:
- a browser-local signing key
- a signed canonical heartbeat payload
- a Blake3 hash chain
- VM execution output
- server-issued mutation progression
- plausible timing and interaction signals
The objective is to make cheap automation brittle and expensive automation more complex.
### Silent Rejection
Heartbeat rejection is intentionally ambiguous. Invalid heartbeats receive the same `status` value as accepted heartbeats, but accepted responses include next-state fields.
This avoids turning the API into a validation oracle. Integrators must check for `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
### Privacy
ChronoSeal should not become a surveillance system.
It avoids:
- long-term user identifiers
- browser history
- cross-site identity graphs
- fingerprint databases
- behavioral profiling as a product feature
It stores only the session state required for continuity.
## Non-Goals
ChronoSeal does not aim to be:
ChronoSeal is not:
* a tracking platform
* a browser fingerprinting database
* a long-term behavioral analytics engine
* a platform for user profiling
* a SaaS or cloud-first service
Instead, ChronoSeal aims to be an infrastructure layer that raises attacker cost while leaving legitimate users unobstructed.
- a CAPTCHA
- a fraud scoring engine
- an authentication provider
- a hosted SaaS product
- a persistent fingerprinting system
- a replacement for authorization checks
- a complete defense against real browser farms
## 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
- Linux or a Unix-like host
- systemd for production service management
- TLS in production
- browser clients can execute WASM
- operators can manage config files and service users
- application owners decide how attestation status gates protected resources
## Privacy and Trust
## Engineering Biases
The project is designed so that the verification mechanism is:
When the project faces tradeoffs, prefer:
* ephemeral
* difficult to reverse-engineer at scale
* not based on personal identifiers
* not dependent on long-term user history
- explicit configuration over implicit magic
- server-side recomputation over browser trust
- bounded deterministic execution over unbounded heuristics
- clear CLI output over hidden dashboards
- local deployment over mandatory cloud dependencies
- privacy by data minimization over privacy by policy alone
These choices reflect the belief that the best anti-automation system is one that can be operated without becoming a surveillance platform.
## Success Criteria
ChronoSeal is succeeding when:
- legitimate browser sessions advance without user friction
- simple scrapers cannot pass the protocol
- automation requires a full stateful implementation
- operators can debug deployments with normal Unix tools
- stored data remains minimal and short-lived
- documentation reflects the implementation precisely
+78 -38
View File
@@ -1,66 +1,106 @@
# ChronoSeal Privacy Policy
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 privacy-oriented browser attestation system. It is designed to validate short-lived session continuity without creating persistent user profiles.
## What ChronoSeal Collects
This document describes what ChronoSeal itself collects and stores. Applications that integrate ChronoSeal may collect additional data under their own policies.
ChronoSeal only collects the minimum ephemeral data required to validate a live browser session:
## Data ChronoSeal Processes
* `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
ChronoSeal processes the minimum protocol data needed to validate a live browser session.
## What ChronoSeal Does Not Store
| Data | Purpose |
|---|---|
| `session_id` | Opaque session lookup key |
| public key | Verify signed heartbeats for the session |
| `salt` | Hash-chain progression |
| `initial_hash` / `prev_hash` / `last_hash` | Replay-resistant continuity |
| `timestamp` | Drift and liveness validation |
| mouse event samples | Behavioral plausibility checks |
| VM stack state | Input to hash-chain progression |
| basic fingerprint fields | Sanity validation |
| gene bytes and environment records | Mutation continuity |
| pending mutation program and step | Next heartbeat verification |
| expiration and last-seen timestamps | Session lifecycle and cleanup |
ChronoSeal does not store or persist:
Basic fingerprint fields currently include:
* IP addresses as a core artifact
* browser history
* user identifiers
* personal data
* device fingerprint databases
* long-term behavioral profiles
* cross-session tracking records
- aspect ratio
- device pixel ratio
- hardware concurrency
If you need browser telemetry or user profiling, ChronoSeal is not the right tool.
## Data ChronoSeal Does Not Intentionally Collect
## Session Ephemerality
ChronoSeal does not intentionally collect or build:
By default, ChronoSeal uses `sqlite-in-memory` storage. Sessions are ephemeral and are expected to be recreated after process restarts.
- browser history
- page content history
- account identity
- email addresses
- names
- payment data
- location history
- cross-site tracking identifiers
- persistent fingerprint databases
- long-term behavioral profiles
Persistent state is only stored when the operator explicitly configures `sqlite-disk` or `valkey`.
ChronoSeal is not intended for analytics, advertising, or identity graph construction.
## Session Lifetime
Sessions are short-lived and expire according to `expiration_minutes`, which defaults to 30 minutes.
Expired sessions are removed by cleanup behavior. In-memory storage is lost when the process exits.
## Storage Modes and Persistence
| Mode | Persistence |
|---|---|
| `sqlite-in-memory` | process lifetime only |
| `sqlite-in-disk` | persisted to the configured SQLite file |
| `valkey` | persisted according to the Valkey deployment configuration |
Persistent state is operator-selected. The default backend is `sqlite-in-memory`.
## Client-Side Key Handling
The Ed25519 signing keypair is generated inside the WASM runtime and is never serialized or transmitted in full.
The browser WASM runtime generates an Ed25519 keypair for the session.
* Private key: stays inside WASM linear memory
* Public key: transmitted once during session initialization
- The public key is sent to `/init`.
- The private key is not sent to the server.
- Heartbeat payloads are signed in the browser runtime.
This design minimizes the amount of sensitive material exposed outside the browser runtime.
This is a continuity mechanism, not a long-term identity mechanism.
## Intentional Silent Rejection
## Silent Rejection
ChronoSeal intentionally returns a uniform `{"status":"ok"}` response for invalid heartbeats.
ChronoSeal returns the same basic heartbeat status for accepted and rejected heartbeat requests:
This is a privacy-preserving decision: it avoids emitting detailed rejection reasons that could be used to fingerprint or probe clients.
```json
{
"status": "ok"
}
```
## Data Retention
Accepted responses additionally include next-state fields. Rejected responses omit them.
Session state is retained only as long as it is needed for heartbeat continuity.
This reduces attacker feedback and avoids returning detailed failure classifications to clients.
Expired sessions are purged automatically by cleanup tasks. Ephemeral backend modes do not write state to disk beyond the current process lifetime.
## Logs
## Transparency
Operators control logging through `CHRONOSEAL_LOG`, `RUST_LOG`, and optional log-file configuration.
The source code is open and the verification model is documented. Operators can inspect exactly what ChronoSeal stores and validates.
Production deployments should avoid debug logging because internal session identifiers or validation context may appear in logs.
## Operator Responsibilities
Operators should:
- serve traffic over HTTPS
- protect SQLite, Valkey, and log storage
- restrict access to metrics and stats endpoints
- choose persistence mode deliberately
- disclose any application-level data collection separately
## Summary
ChronoSeal is designed to provide anti-automation defense without becoming a tracking or surveillance platform.
It is a privacy-aware, ephemeral attestation layer with strong operational guardrails.
ChronoSeal validates live session continuity using short-lived cryptographic and deterministic state. It is designed to raise automation cost without becoming a persistent tracking or profiling system.
+151 -78
View File
@@ -1,118 +1,191 @@
# ChronoSeal v0.6.0 — Refactoring and System Upgrade
# ChronoSeal v0.6.0 Refactoring and System Upgrade
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.
ChronoSeal v0.6.0 changed the project from a lightweight heartbeat prototype into a Unix-native attestation daemon with shared server/WASM protocol logic, deterministic mutation parity, operational CLI commands, and pluggable storage modes.
## Summary of Changes
This document summarizes the architectural changes introduced in the v0.6.0 line.
* 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.
## Summary
## Why This Refactor?
Major changes:
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:
- introduced the Synthetic Gene Mutation Engine
- added server-side validation of `mutation_step` and `gene_commitment`
- moved protocol and deterministic mutation logic into `shared/`
- added `chronoseal-wasm` browser runtime support for mutation preview and commit
- expanded persisted session state with gene and pending mutation fields
- added storage modes: `sqlite-in-memory`, `sqlite-in-disk`, and `valkey`
- added health, metrics, stats, config, status, completion, and version CLI surfaces
- added PID file handling, structured logging, and graceful shutdown behavior
- preserved silent heartbeat rejection semantics
* 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
## Motivation
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.
The earlier model relied mainly on:
## Core Architecture Changes
- heartbeat timing
- behavioral entropy
- hash-chain continuity
- signature verification
### Shared Protocol Code
v0.6.0 added a second deterministic state channel: a server-authored synthetic gene mutation sequence. This makes successful automation maintain both:
`shared/` now contains:
- the cryptographic hash/signature chain
- the synthetic mutation state expected by the server
* gene model and commitment hashing
* mutation opcode semantics
* request/response payload structures
* canonical signing support
* VM execution logic shared by server and WASM
## Shared Crate Refactor
Moving mutation semantics into `shared/` eliminates subtle server/client divergence bugs and enables deterministic cross-runtime testing.
`shared/` now owns the parts of the protocol that must remain identical across server and browser runtime:
### Mutation Handshake
- request and response structs
- hashing helpers
- synthetic gene state
- mutation environment encoding
- mutation order generation and encoding
- opcode execution semantics
- protocol constants
v0.6.0 adds the following data to the protocol:
This reduces the risk of server/WASM drift.
* `mutation_step`
* `mutation_order_b64`
* `gene_commitment`
* `next_mutation_step`
* `next_mutation_order_b64`
## Mutation Handshake
These fields are now part of the session initialization and heartbeat exchange.
New protocol fields:
### Server Session State
- `gene_size`
- `mutation_step`
- `mutation_order_b64`
- `gene_commitment`
- `next_mutation_step`
- `next_mutation_order_b64`
The session schema now stores:
Lifecycle:
* committed gene bytes
* committed environment records
* pending mutation order
* pending mutation step
1. `/init` returns mutation step 1 and a server-authored mutation order.
2. The browser previews the mutation in WASM.
3. The browser signs and submits the resulting `gene_commitment`.
4. The server applies the same pending mutation to its committed state.
5. The server compares commitments.
6. On success, server commits the candidate state and issues the next mutation.
7. The browser commits its preview only after receiving the accepted response.
The server advances this state only after a heartbeat is accepted.
## Session Schema Changes
### Deterministic WASM Preview
The persisted session record now includes:
The WASM runtime exposes:
- committed gene bytes
- encoded environment records
- pending mutation program
- pending mutation step
* `init_gene_state()`
* `preview_gene_commitment()`
* `commit_gene_preview()`
* `discard_gene_preview()`
* `current_gene_commitment()`
State advances only after a heartbeat is accepted. Rejected heartbeats do not rotate salt, update hash state, commit gene state, or consume the pending mutation.
This makes the client-side mutation lifecycle explicit and deterministic.
## WASM Runtime Changes
### Backend Abstraction
The WASM crate now supports:
The server runtime now supports a configurable `db_type`.
- `generate_keypair()`
- `get_public_key()`
- `sign_message()`
- `compute_next_hash()`
- `run_program()`
- `init_gene_state()`
- `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)`
- `commit_gene_preview()`
- `discard_gene_preview()`
- `current_gene_commitment(session_id, mutation_step)`
* `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
The generated package uses the `chronoseal_wasm` prefix.
This abstraction makes ChronoSeal easier to operate in both stateless and stateful environments.
## Storage Refactor
### CLI and Service Integration
The storage layer is abstracted behind `DbPool`.
v0.6.0 improves the CLI surface with operational commands and service introspection.
Supported modes:
* `chronoseal run`
* `chronoseal status`
* `chronoseal health`
* `chronoseal config`
* `chronoseal metrics`
* `chronoseal stats`
* `chronoseal db-type`
* `chronoseal completion`
* `chronoseal version`
| Mode | Behavior |
|---|---|
| `sqlite-in-memory` | default ephemeral in-process SQLite |
| `sqlite-in-disk` | persisted SQLite database at `db_path` |
| `valkey` | Valkey-compatible external store |
The runtime now includes PID file handling and graceful termination.
The storage interface supports insert, load, update, delete expired sessions, and stats.
## Testing and Validation
## CLI and Runtime Changes
The refactor includes extensive tests for:
The `chronoseal` binary now provides:
* 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
- `run`
- `status`
- `health`
- `config check`
- `generate keypair`
- `version`
- `db-type`
- `metrics`
- `stats`
- `completion`
The codebase now supports deterministic table-driven tests and fuzz-style random program validation.
The daemon exposes:
- `POST /init`
- `POST /hb`
- `GET /health`
- `GET /metrics`
- `GET /stats`
- static frontend serving at `/`
## Validation Improvements
The heartbeat verifier now checks:
- session presence
- expiration
- signature
- hash-chain continuity
- mutation step
- mutation commitment parity
- timestamp drift
- behavioral mouse checks
- fingerprint ranges
- rate limiting at the route layer
Accepted heartbeats return next-state fields. Rejected heartbeats return only `{"status":"ok"}`.
## Testing Impact
The refactor added or strengthened tests for:
- gene environment encoding and validation
- mutation opcode behavior
- mutation order round-trips
- deterministic mutation generation with seeded RNG
- server/client mutation parity
- random program divergence resistance
- replay rejection
- mutation step mismatch rejection
- mutation commitment tamper rejection
- storage backend stats
- route-level silent rejection behavior
## Operational Impact
This release makes ChronoSeal suitable for production deployment in Linux environments and for integration into existing web application stacks.
v0.6.0 makes ChronoSeal more suitable for deployment as a real service:
The combination of deterministic mutation parity and shared protocol implementation improves both security and maintainability.
- explicit daemon lifecycle
- CLI-first operations
- systemd-oriented install path
- health and metrics endpoints
- configurable persistence
- shared protocol implementation
- clearer docs and threat model
## Compatibility Notes
Important names in the current implementation:
- binary: `chronoseal`
- server crate: `chronoseal-server`
- WASM crate: `chronoseal-wasm`
- generated WASM module prefix: `chronoseal_wasm`
- persistent SQLite mode: `sqlite-in-disk`
Older docs or integrations may refer to `sqlite-disk`, `server`, or `antibot_wasm`; those names are stale for the current codebase.
+180 -77
View File
@@ -1,137 +1,240 @@
# 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.
ChronoSeal is a cost-raising browser attestation layer. It makes replay, stale state reuse, and incomplete automation more expensive by requiring signed, continuous, deterministic browser-side state progression.
## Purpose
It is not a perfect bot blocker, CAPTCHA replacement, hardware attestation system, fraud engine, or identity provider.
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.
## Security Objectives
ChronoSeal aims to:
- reject stale or replayed heartbeat payloads
- reject heartbeats that do not maintain the server-issued mutation sequence
- bind heartbeat payloads to a browser-local Ed25519 session key
- make basic HTTP clients insufficient
- make browser automation maintain multiple synchronized state channels
- avoid detailed rejection feedback
- preserve privacy by avoiding persistent user identity state
## Protected Assets
| Asset | Protection focus |
|---|---|
| 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 |
| Protected page/API access | Require live attestation before allowing continued access |
| Session continuity | Ensure each accepted heartbeat advances from the last accepted state |
| Server compute | Rate-limit and reject invalid clients without expensive application work |
| Protocol state | Protect hash-chain, salt, and mutation progression |
| User privacy | Avoid long-term tracking and detailed failure disclosure |
## Attacker Profiles
## Trust Assumptions
### Level 1 — Commodity Scraper
ChronoSeal assumes:
* Tools: `curl`, `requests`, headless HTTP clients
* Capability: no WASM execution, no browser engine
- the server host and daemon process are trusted
- storage is trusted for session continuity
- TLS protects traffic in production
- browser clients can run JavaScript and WASM
- operators configure reverse proxy, filesystem permissions, and logs appropriately
ChronoSeal response:
ChronoSeal does not assume:
* cannot initialize a session
* no `session_id` is produced
* content remains protected behind the attestation layer
- the browser is honest
- WASM is a secure enclave
- mouse data proves human presence
- fingerprint values are unforgeable
- attackers cannot run a full browser
### Level 2 — Headless Browser Operator
## Attacker Levels
* Tools: Playwright, Puppeteer, Selenium
* Capability: browser engine available, but automation is not indistinguishable from a real user
### Level 1: Commodity HTTP Client
ChronoSeal response:
Examples:
* mouse entropy and pause checks become active barriers
* hash chain continuity requires per-session state tracking
* synthetic heartbeats become expensive to maintain at scale
- `curl`
- `requests`
- scraper scripts without browser or WASM execution
### Level 3 — Stealth Automation
Expected result:
* Tools: browser stealth plugins, CDP patching, synthetic event injection
* Capability: can execute JavaScript and WASM, may spoof some browser signals
- cannot produce valid signatures
- cannot maintain hash-chain state
- cannot execute mutation preview
- cannot produce accepted heartbeats
ChronoSeal response:
### Level 2: Basic Headless Browser
* 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
Examples:
### Level 4 — Sophisticated Operator
- Playwright
- Puppeteer
- Selenium
* Tools: real browser farms, hardware input devices, custom chain management
* Capability: high engineering investment and real device scale
Expected result:
ChronoSeal response:
- can load JavaScript and WASM
- must preserve keypair, hash chain, salt, VM, and mutation state
- must generate plausible timing and mouse event windows
- silent rejection complicates debugging and scaling
* significantly increases operational cost and complexity
* forces a full protocol implementation rather than best-effort scraping
* is not designed to stop such adversaries completely
### Level 3: Stealth Automation
Examples:
- patched browser runtime
- synthetic event generation
- custom protocol client with WASM or Rust reimplementation
Expected result:
- can attempt full protocol implementation
- must still match canonical signing, hash progression, mutation parity, and timing
- must handle changing server-issued mutation programs
- receives limited failure feedback
### Level 4: Resourced Browser Farm
Examples:
- real browsers
- realistic input devices
- human-assisted workflows
- distributed session management
Expected result:
- ChronoSeal raises cost and complexity
- ChronoSeal does not claim complete prevention
- additional application-level controls are required
## Attack Vectors and Mitigations
### Replay Attack
### Replay
**Attack:** resend a previously observed heartbeat.
Attack: resend a previously accepted heartbeat.
**Mitigations:**
Mitigations:
* timestamp window enforcement (±30 seconds)
* chained Blake3 hash continuity
* server-issued salt rotation
* mutation step progression
- stored `last_hash` must match request `prev_hash`
- accepted heartbeats rotate salt
- mutation step advances after acceptance
- timestamp drift is bounded
### Signature Forgery
**Attack:** forge a heartbeat without the private key.
Attack: submit a heartbeat without the browser session private key.
**Mitigations:**
Mitigations:
* Ed25519 signature over the canonical payload
* private key generated and stored inside WASM memory only
* signature verification occurs on every heartbeat
- Ed25519 signature over canonical payload
- public key registered during `/init`
- signature verified on every heartbeat
- signature covers mutation step and gene commitment
### Hash-Chain Desynchronization
Attack: submit a heartbeat from stale client state.
Mitigations:
- server compares request `prev_hash` to stored `last_hash`
- server computes the next hash only after all validation passes
- rejected heartbeats do not advance server state
### Mutation Tampering
**Attack:** send an invalid or stale mutation commitment.
Attack: forge or skip synthetic gene mutations.
**Mitigations:**
Mitigations:
* server recomputes the gene commitment from server-authored mutation orders
* heartbeat request includes `mutation_step` and `gene_commitment`
* mismatched commitment causes silent rejection
- server stores the pending mutation program
- request must include the expected `mutation_step`
- server applies the mutation independently
- commitment includes candidate gene state, `session_id`, and step
- mismatch causes silent rejection
### Session Hijacking
### Session Identifier Theft
**Attack:** steal a valid `session_id` and reuse it.
Attack: reuse a stolen `session_id`.
**Mitigations:**
Mitigations:
* `session_id` alone is insufficient
* attacker also needs current `prev_hash` and private key
* keypair is generated per browser session in WASM
- `session_id` alone is insufficient
- attacker also needs current private key, hash state, salt, mutation step, and mutation state
- stale attempts fail after the real session advances
### Fingerprint Enumeration
### Failure Oracle Probing
**Attack:** probe the API with malformed requests to discover validation logic.
Attack: send malformed requests and inspect responses to infer validation rules.
**Mitigations:**
Mitigations:
* all invalid heartbeats return `{"status":"ok"}`
* no explicit error messages are exposed
* silent rejection removes oracle behavior
- heartbeat semantic failures return `200 OK` with `{"status":"ok"}`
- accepted heartbeats are distinguished only by next-state fields
- detailed validation errors are not returned to the client
### Storage Tampering
Attack: alter persisted session state.
Mitigations:
- run the daemon under a dedicated user
- restrict SQLite database permissions
- protect Valkey behind trusted network boundaries
- use normal host hardening and backups where persistence matters
Storage is trusted. If an attacker can modify storage, they can affect session continuity.
## Behavioral Checks
ChronoSeal validates:
- minimum event count
- minimum movement distance
- maximum average speed
- pause count
- timestamp drift
- basic fingerprint field ranges
These checks are cost signals. They are not proof of humanity and should not be the only security layer for high-risk actions.
## Privacy Constraints
ChronoSeal intentionally avoids:
- persistent user identifiers
- browser history collection
- device fingerprint databases
- cross-session identity graphs
- long-term behavioral profiles
Session data is short-lived by default. Persistent storage is operator-selected through `sqlite-in-disk` or `valkey`.
## Limitations
ChronoSeal does not protect against:
* 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
- real users intentionally automating or abusing access
- complete browser farms with realistic input
- compromised server hosts
- tampered storage
- server-side application vulnerabilities
- credential theft outside ChronoSeal
- policy decisions that require identity, risk scoring, or business context
## Operational Security Notes
## Operational Security
* 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.
Recommended:
- serve all traffic over HTTPS
- keep `/init` and `/hb` same-origin with protected content when possible
- run behind a reverse proxy
- keep debug logs disabled in production
- protect storage and log directories
- monitor health and metrics
- use `sqlite-in-memory` for ephemeral sessions
- use `sqlite-in-disk` or `valkey` only when persistence is required
## Disclosure
See [SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy.
See [../SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy.
+93 -63
View File
@@ -1,22 +1,22 @@
# ChronoSeal WASM Build Guide
ChronoSeal uses a Rust-based WASM runtime to power browser-side attestation logic, signing, hash chaining, VM execution, and mutation commitment preview.
ChronoSeal uses a Rust-generated WASM package for browser-side attestation. The package is built from `wasm/` and copied into `frontend/pkg`.
## Why WASM
## Responsibilities
The WASM runtime provides a deterministic, sandboxed environment for the following tasks:
The WASM runtime:
* 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
- generates a browser-local Ed25519 keypair
- signs canonical heartbeat payloads
- computes Blake3 hash-chain progression
- executes server-issued VM opcode programs
- initializes synthetic gene state
- previews gene mutation commitments
- commits or discards preview state after heartbeat response
This enables server/client parity and prevents the private key from leaving the browser runtime.
The WASM runtime is not treated as a secure enclave. The server independently recomputes deterministic state.
## Build Requirements
Install the Rust WASM target and `wasm-pack`:
## Requirements
```bash
rustup target add wasm32-unknown-unknown
@@ -29,7 +29,7 @@ Verify:
wasm-pack --version
```
## Build the WASM Module
## Build
From the repository root:
@@ -39,45 +39,41 @@ rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
`--target web` produces an ES module compatible with the existing frontend JavaScript.
`--target web` emits native ES modules compatible with the static frontend.
`--release` enables optimizations for runtime performance and size.
Development build:
## Output
```bash
wasm-pack build wasm --target web
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
After a successful build, `frontend/pkg/` contains:
Full project build:
* `antibot_wasm.js`
* `antibot_wasm_bg.wasm`
* `antibot_wasm_bg.js`
* `antibot_wasm.d.ts`
* `antibot_wasm_bg.d.ts`
* `package.json`
```bash
bash scripts/build.sh
```
The frontend expects the WASM package under `frontend/pkg/`.
## Output Files
## Runtime Exports
The package name comes from the crate name `chronoseal-wasm`, so generated files use the `chronoseal_wasm` prefix.
The WASM module exports the following functions:
Expected `frontend/pkg/` contents include:
* `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
- `chronoseal_wasm.js`
- `chronoseal_wasm_bg.wasm`
- `chronoseal_wasm.d.ts`
- `package.json`
## Browser Integration
Generated files in `wasm/pkg/` and `frontend/pkg/` are build artifacts and should be regenerated during release.
The frontend imports the generated module like this:
## Browser Import
```js
import init, {
generate_keypair,
get_public_key,
sign_message,
compute_next_hash,
run_program,
@@ -86,48 +82,82 @@ import init, {
commit_gene_preview,
discard_gene_preview,
current_gene_commitment
} from './pkg/antibot_wasm.js';
} from './pkg/chronoseal_wasm.js';
```
`await init()` must be called before invoking any other exported function.
Call `await init()` before using any exported function.
## Deployment Note
## Exported Functions
The `.wasm` binary must be served with the correct MIME type:
| Function | Signature | Failure value |
|---|---|---|
| `generate_keypair()` | `() -> string` | `""` only on unexpected failure |
| `get_public_key()` | `() -> string` | `""` if no keypair exists |
| `sign_message(msg)` | `(string) -> string` | `""` if no keypair exists or signing fails |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | panic/error path should be avoided by valid inputs |
| `run_program(b64)` | `(string) -> JsValue` | returns empty/default stack state on invalid execution path |
| `init_gene_state(gene_size)` | `(u32) -> bool` | `false` |
| `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` | `(string, string, u64, u8) -> string` | `""` |
| `commit_gene_preview()` | `() -> bool` | `false` |
| `discard_gene_preview()` | `() -> void` | none |
| `current_gene_commitment(session_id, mutation_step)` | `(string, u64) -> string` | `""` if no committed state exists |
`rounds = 0` in `preview_gene_commitment` selects the shared default mutation round count.
## Mutation State Lifecycle
The browser must keep two gene states:
- committed state: the last accepted state
- preview state: candidate state for the heartbeat currently being sent
Expected sequence:
1. Call `init_gene_state(gene_size)` after `/init`.
2. Call `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` before signing `/hb`.
3. Include the returned commitment and mutation step in the signed heartbeat.
4. If the response contains next-state fields, call `commit_gene_preview()`.
5. If the heartbeat is rejected or errors, call `discard_gene_preview()`.
Never commit preview state before the server accepts the heartbeat.
## Hash-Chain Ordering
After an accepted heartbeat, compute the next local hash with the salt that was active when the heartbeat was sent. Then replace the local salt with `next_salt`.
Correct order:
```js
const sentSalt = currentSalt;
currentSalt = resp.next_salt;
prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt);
```
This mirrors the server, which computes and stores the new hash before rotating to the next salt.
## Serving WASM
The `.wasm` file must be served with:
```text
Content-Type: application/wasm
```
The built-in Axum static file handler already sets the appropriate MIME type for `.wasm` files.
ChronoSeal's built-in static file service handles this for normal deployments.
## Build Script
## Validation
Use the convenience script:
Recommended checks after WASM changes:
```bash
bash scripts/build.sh
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
wasm-pack build wasm --target web
```
This builds the WASM package, moves it into `frontend/pkg/`, and builds the server binary.
## Recommended Development Flow
* For WASM-only changes:
Then refresh `frontend/pkg`:
```bash
wasm-pack build wasm --target web
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
* For server-only changes:
```bash
cargo build -p server
```
## Notes
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.
+20 -2
View File
@@ -1,9 +1,27 @@
bind = "0.0.0.0:3000"
# sqlite-in-memory (default), sqlite-in-disk, valkey (v0.6.0 compatibility mode)
# Storage backend: sqlite-in-memory (default), sqlite-in-disk, or valkey.
db_type = "sqlite-in-memory"
pid_file = "/run/chronoseal.pid"
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
frontend_dir = "/usr/share/chronoseal/frontend"
log_file = "/var/log/chronoseal/chronoseal.jsonl"
# synthetic gene size (1..=65536), default 512
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 10.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
# Synthetic gene size. Current valid range: 1..=4096. Default: 512.
gene_size = 512
# Current valid range: 1..=10. Default: 4.
mutation_rounds = 4