From 0ed3cb444de35c6515a1bd980261a1049635277c Mon Sep 17 00:00:00 2001 From: Sunil Thakares Date: Fri, 29 May 2026 21:55:08 +0530 Subject: [PATCH] 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. --- README.md | 814 ++++++++++++++++++++++++++++------- docs/API.md | 327 ++++++++++---- docs/ARCHITECTURE.md | 592 ++++++++++++++++++++----- docs/DEPLOYMENT.md | 376 ++++++++++------ docs/DESIGN-PHILOSOPHY.md | 139 ++++-- docs/PRIVACY POLICY.md | 116 +++-- docs/REFRACTORING-v0.6.0.md | 229 ++++++---- docs/THREAT_MODEL.md | 257 +++++++---- docs/WASM_BUILD.md | 156 ++++--- docs/chronoseal.example.toml | 22 +- server/src/session.rs | 7 +- server/src/storage.rs | 24 +- shared/src/gene.rs | 2 +- shared/src/vm_extensions.rs | 37 +- wasm/src/vm_extensions.rs | 56 ++- 15 files changed, 2357 insertions(+), 797 deletions(-) diff --git a/README.md b/README.md index e1eddbb..4bd65a1 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@

- Privacy-first • Deterministic WASM parity • Silent rejection • Low overhead + Privacy-first | Deterministic WASM parity | Silent rejection | Low overhead

- License: MIT OR Apache-2.0 - + License: MIT OR Apache-2.0 + - Rust stable ≥ 1.87 + Rust stable >= 1.87 v0.6.0 @@ -27,200 +27,722 @@ --- -ChronoSeal is a mature Unix-native cryptographic attestation daemon for browser session continuity and anti-automation defense. +ChronoSeal is a Linux-native cryptographic attestation service for browser session continuity and anti-automation defense. It runs as a small daemon, serves a browser WASM runtime, and validates signed heartbeat requests through deterministic server/client state progression. -It provides a low-overhead, privacy-respecting proof-of-runtime system built around a deterministic **Synthetic Gene Mutation Engine** and a silent, replay-resistant heartbeat protocol. +The core idea is simple: a real browser session should be able to keep advancing a private cryptographic state, a hash chain, and a deterministic synthetic gene mutation chain. Basic HTTP clients, stale replay attempts, and incomplete automation should fail without receiving a useful failure reason. -v0.6.0 introduces the core innovation: a deterministic synthetic gene mutation chain with server/WASM parity, stronger liveness guarantees, and a domain-separated mutation commitment handshake. +ChronoSeal is a cost-raising attestation layer. It is not a CAPTCHA replacement, identity provider, fingerprinting product, or perfect bot blocker. ---- +## Contents -## What ChronoSeal Provides +- [Features](#features) +- [How It Works](#how-it-works) +- [Repository Layout](#repository-layout) +- [Quick Start](#quick-start) +- [Build From Source](#build-from-source) +- [Run Locally](#run-locally) +- [Install as a Service](#install-as-a-service) +- [Docker](#docker) +- [CLI Reference](#cli-reference) +- [Configuration](#configuration) +- [HTTP API](#http-api) +- [Browser Integration](#browser-integration) +- [Storage Backends](#storage-backends) +- [Operations](#operations) +- [Security Model](#security-model) +- [Development](#development) +- [Troubleshooting](#troubleshooting) +- [Further Reading](#further-reading) +- [License](#license) -* Native Linux daemon with hardened `systemd` integration -* Deterministic mutation engine running in both server Rust and client WASM -* Silent rejection semantics for attacker resilience -* Multi-backend storage: `sqlite-in-memory`, `sqlite-disk`, and `valkey` -* CLI-first operation with rich subcommands -* Structured logging, PID file management, graceful shutdown -* Prometheus-compatible metrics and runtime statistics -* Lightweight browser runtime with WASM-based attestation -* Privacy-first design with ephemeral session state and no persistent tracking +## Features ---- +- Native Unix daemon with `systemd` service support. +- Axum-based HTTP service exposing `/init`, `/hb`, `/health`, `/metrics`, and `/stats`. +- Rust/WASM browser runtime for key generation, signing, VM execution, and mutation preview. +- Deterministic Synthetic Gene Mutation Engine shared by the server and WASM crates. +- Ed25519 signatures over canonical heartbeat payloads. +- Blake3 hash chain continuity across accepted heartbeats. +- Silent rejection semantics: invalid heartbeats still return `{"status":"ok"}`. +- Configurable behavioral trust checks for mouse movement, pauses, timing, and browser signals. +- Runtime storage abstraction with `sqlite-in-memory`, `sqlite-in-disk`, and `valkey` modes. +- CLI-first lifecycle, status, health, metrics, stats, config validation, key generation, and shell completions. +- Privacy-oriented design based on ephemeral session state rather than long-term identity tracking. -## Why ChronoSeal +## How It Works -ChronoSeal raises the operational cost of automation by combining: +ChronoSeal creates a short-lived browser attestation session and advances it through signed heartbeats. -* cryptographic session continuity -* deterministic VM execution -* behavioral entropy validation -* mutation commitment parity -* silent, ambiguous rejection behavior +1. The browser loads the generated WASM package from `frontend/pkg`. +2. The WASM runtime generates an Ed25519 keypair and returns the public key to JavaScript. +3. The browser calls `POST /init` with the public key. +4. The server creates a session and returns: + - `session_id` + - initial salt + - initial hash chain head + - VM opcode program + - gene size + - mutation step + - mutation order + - heartbeat timing bounds +5. For each heartbeat, the browser: + - executes the VM opcode program + - collects entropy and browser signal data + - previews the next synthetic gene commitment in WASM + - signs the canonical heartbeat payload + - posts the request to `POST /hb` +6. The server validates: + - session existence and expiration + - rate limit + - timestamp drift + - Ed25519 signature + - hash chain continuity + - behavioral trust checks + - mutation step + - gene commitment parity +7. If the heartbeat is accepted, the server advances session state and returns the next salt and mutation order. +8. If the heartbeat is rejected, the server returns only `{"status":"ok"}`. -This is not a fingerprinting or surveillance platform. ChronoSeal is designed to make automation expensive, not to collect user identities. +The silent failure model deliberately avoids exposing which validation failed. ---- +## Repository Layout + +```text +. +├── Cargo.toml # Rust workspace +├── server/ # chronoseal daemon and CLI +├── shared/ # shared protocol, gene model, mutation engine +├── wasm/ # WASM runtime crate +├── frontend/ # static browser integration files +├── docs/ # architecture, API, deployment, threat model +├── scripts/ # build, install, release, dev helpers +├── chronoseal.service # systemd unit +├── Dockerfile # container image +└── docker-compose.yml # local container orchestration +``` + +Workspace members: + +- `chronoseal-server`: daemon binary crate. Binary name: `chronoseal`. +- `shared`: common cryptographic and mutation logic used by server and WASM. +- `chronoseal-wasm`: browser runtime compiled with `wasm-pack`. ## Quick Start -### Install +For a full native install: ```bash sudo bash scripts/install.sh ``` -### Verify status +Check the daemon: ```bash chronoseal status --format json -``` - -### Health probe - -```bash chronoseal health -``` - -### View metrics - -```bash chronoseal metrics -``` - -### Follow logs - -```bash sudo journalctl -u chronoseal -f ``` ---- +For local development without installing: -## CLI Overview +```bash +bash scripts/build.sh +cargo run -p chronoseal-server --bin chronoseal -- run \ + --bind 127.0.0.1:3000 \ + --frontend-dir frontend +``` + +Then open: + +```text +http://127.0.0.1:3000 +``` + +## Build From Source + +### Requirements + +| Tool | Minimum | Purpose | +|---|---:|---| +| Rust | 1.87 stable | Build server, shared crate, and tests | +| wasm-pack | 0.13 | Build browser WASM package | +| wasm32 target | current stable | WASM compilation target | +| systemd | 248+ | Optional native service management | +| Docker | 24.x | Optional container workflow | +| Docker Compose | 2.x | Optional local orchestration | + +Install the WASM target and `wasm-pack`: + +```bash +rustup target add wasm32-unknown-unknown +cargo install wasm-pack +``` + +Build everything needed for the browser and server: + +```bash +bash scripts/build.sh +``` + +That script: + +1. Builds `wasm/` with `wasm-pack build --target web --release`. +2. Replaces `frontend/pkg` with the generated WASM package. +3. Builds the `chronoseal` release binary. + +Manual build: + +```bash +wasm-pack build wasm --target web --release +rm -rf frontend/pkg +mv wasm/pkg frontend/pkg + +cargo build -p chronoseal-server --bin chronoseal --release +``` + +The binary is written to: + +```text +target/release/chronoseal +``` + +## Run Locally + +Run the daemon directly from Cargo: + +```bash +cargo run -p chronoseal-server --bin chronoseal -- run \ + --bind 127.0.0.1:3000 \ + --frontend-dir frontend +``` + +Probe it: + +```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 +``` + +Use the CLI wrappers: + +```bash +cargo run -p chronoseal-server --bin chronoseal -- status --bind 127.0.0.1:3000 +cargo run -p chronoseal-server --bin chronoseal -- health --bind 127.0.0.1:3000 +cargo run -p chronoseal-server --bin chronoseal -- stats --bind 127.0.0.1:3000 --format json +``` + +## Install as a Service + +The installer builds the project, installs the binary, copies frontend assets, installs the `systemd` unit, and starts the service. + +```bash +sudo bash scripts/install.sh +``` + +Installed paths: + +| Path | Purpose | +|---|---| +| `/usr/local/bin/chronoseal` | daemon and CLI binary | +| `/opt/chronoseal/frontend` | static frontend assets copied by installer | +| `/etc/systemd/system/chronoseal.service` | service unit | +| `/run/chronoseal.pid` | default PID file | + +Service commands: + +```bash +sudo systemctl status chronoseal +sudo systemctl restart chronoseal +sudo systemctl disable --now chronoseal +sudo journalctl -u chronoseal -f +``` + +## Docker + +Build and run with Compose: + +```bash +docker compose up -d --build +``` + +The provided container exposes port `3000`. + +```bash +curl http://127.0.0.1:3000/health +``` + +Important: the Dockerfile copies `frontend/` from the working tree. Build the WASM package into `frontend/pkg` before building the container if you need the browser runtime inside the image: + +```bash +bash scripts/build.sh +docker compose up -d --build +``` + +## CLI Reference + +Run: ```bash chronoseal --help ``` -### Available commands +Global options: -| Command | Description | -| ------------ | ------------------------------------------ | -| `run` | Run the ChronoSeal daemon | -| `status` | Report daemon status | -| `health` | Perform daemon health probe | -| `config` | Validate and print effective configuration | -| `generate` | Generate operational material | -| `db-type` | List database backend support status | -| `metrics` | Output Prometheus metrics | -| `stats` | Print runtime statistics | -| `completion` | Generate shell completions | -| `version` | Print version/build information | +| Option | Environment | Description | +|---|---|---| +| `--config ` | `CHRONOSEAL_CONFIG` | Explicit TOML config path | +| `--format ` | - | Output format for machine-readable commands | +| `--output ` | - | Alias for `--format` | +| `--log ` | `CHRONOSEAL_LOG` | Tracing filter, for example `info` or `chronoseal=debug` | ---- +Commands: -## Example +| Command | Description | +|---|---| +| `chronoseal run` | Run the daemon | +| `chronoseal status` | Check configured daemon reachability and PID state | +| `chronoseal health` | Perform an HTTP health probe | +| `chronoseal config check` | Validate and print effective configuration | +| `chronoseal generate keypair` | Generate an Ed25519 keypair | +| `chronoseal version` | Print version/build information | +| `chronoseal db-type` | List database backend support status | +| `chronoseal metrics` | Fetch Prometheus metrics from the running daemon | +| `chronoseal stats` | Fetch service statistics from the running daemon | +| `chronoseal completion ` | Generate shell completions | + +Examples: + +```bash +chronoseal run --bind 127.0.0.1:3000 --frontend-dir frontend +chronoseal run --db-type sqlite-in-memory +chronoseal status --format json +chronoseal health --config /etc/chronoseal/config.toml +chronoseal config check --output yaml +chronoseal generate keypair --format json +chronoseal completion bash > chronoseal.bash +``` + +## Configuration + +Configuration precedence: + +1. CLI flags +2. `CHRONOSEAL_*` environment variables +3. TOML config file +4. built-in defaults + +Default config discovery: + +1. path from `CHRONOSEAL_CONFIG`, if it exists +2. `/etc/chronoseal/config.toml` +3. `$XDG_CONFIG_HOME/chronoseal/config.toml` +4. `~/.config/chronoseal/config.toml` + +Example: + +```toml +bind = "0.0.0.0:3000" +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" + +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 +``` + +Common environment variables: + +| Variable | Description | +|---|---| +| `CHRONOSEAL_CONFIG` | Config file path | +| `CHRONOSEAL_BIND` | Bind address, for example `127.0.0.1:3000` | +| `CHRONOSEAL_DB_TYPE` | `sqlite-in-memory`, `sqlite-in-disk`, or `valkey` | +| `CHRONOSEAL_DB_PATH` | SQLite database path | +| `CHRONOSEAL_FRONTEND_DIR` | Static frontend directory served at `/` | +| `CHRONOSEAL_PID_FILE` | PID file path | +| `CHRONOSEAL_LOG` | Tracing filter | +| `CHRONOSEAL_LOG_FILE` | Optional JSON log file | +| `CHRONOSEAL_STATE_DIR` | Base state directory used for default `db_path` | +| `CHRONOSEAL_HEARTBEAT_MIN_INTERVAL_MS` | Minimum accepted heartbeat interval | +| `CHRONOSEAL_HEARTBEAT_MAX_INTERVAL_MS` | Maximum accepted heartbeat interval | +| `CHRONOSEAL_EXPIRATION_MINUTES` | Session lifetime | +| `CHRONOSEAL_RATE_LIMIT_COUNT` | Requests allowed in the rate-limit window | +| `CHRONOSEAL_RATE_LIMIT_WINDOW_SECS` | Rate-limit window length | +| `CHRONOSEAL_MAX_TIMESTAMP_DRIFT_MS` | Accepted client timestamp drift | +| `CHRONOSEAL_MIN_MOUSE_TOTAL_DIST` | Minimum mouse movement distance | +| `CHRONOSEAL_MAX_MOUSE_AVG_SPEED` | Maximum average mouse speed | +| `CHRONOSEAL_MIN_PAUSE_COUNT` | Minimum detected pause count | +| `CHRONOSEAL_REQUIRE_MOUSE_ACTIVITY` | Enable or disable mouse activity requirement | +| `CHRONOSEAL_GENE_SIZE` | Synthetic gene buffer size | +| `CHRONOSEAL_MUTATION_ROUNDS` | Mutation rounds per program | + +Validate configuration: + +```bash +chronoseal config check --format yaml +``` + +## HTTP API + +ChronoSeal exposes a small HTTP surface: + +| Method | Path | Purpose | +|---|---|---| +| `POST` | `/init` | Start a browser attestation session | +| `POST` | `/hb` | Submit a signed heartbeat | +| `GET` | `/health` | Health probe | +| `GET` | `/metrics` | Prometheus metrics | +| `GET` | `/stats` | Runtime statistics | +| `GET` | `/` | Static frontend files from `frontend_dir` | + +### `POST /init` + +Request: + +```json +{ + "public_key": "hex-encoded 32-byte Ed25519 verifying key" +} +``` + +Response: + +```json +{ + "session_id": "64-char hex string", + "salt": "32-char hex string", + "opcodes_b64": "base64-encoded VM program", + "initial_hash": "64-char hex string", + "expires_at": 1234567890123, + "heartbeat_min_interval_ms": 12000, + "heartbeat_max_interval_ms": 25000, + "gene_size": 512, + "mutation_step": 1, + "mutation_order_b64": "base64-encoded mutation program" +} +``` + +### `POST /hb` + +Request: + +```json +{ + "session_id": "64-char hex", + "prev_hash": "64-char hex", + "timestamp": 1234567890123, + "entropy_data": { + "events": [ + { "x": 412.0, "y": 308.5, "t": 1234.567 } + ] + }, + "stack_state": { + "stack": [2971406957, 1234567890], + "ip": 42 + }, + "fingerprint": { + "aspectRatio": "1.7777777778", + "devicePixelRatio": 2, + "hardwareConcurrency": 8 + }, + "mutation_step": 1, + "gene_commitment": "64-char hex", + "signature": "128-char hex" +} +``` + +Accepted response: + +```json +{ + "status": "ok", + "next_salt": "32-char hex string", + "next_mutation_step": 2, + "next_mutation_order_b64": "base64-encoded mutation program" +} +``` + +Rejected response: + +```json +{ + "status": "ok" +} +``` + +Detailed API semantics are documented in [docs/API.md](docs/API.md). + +## Browser Integration + +The frontend imports the generated WASM module from `frontend/pkg`. + +```js +import init, { + generate_keypair, + get_public_key, + sign_message, + compute_next_hash, + run_program, + init_gene_state, + preview_gene_commitment, + commit_gene_preview, + discard_gene_preview, + current_gene_commitment +} from './pkg/chronoseal_wasm.js'; +``` + +Call `await init()` before invoking exported functions. + +WASM exports: + +| Function | Purpose | +|---|---| +| `generate_keypair()` | Generate a browser-local Ed25519 keypair and return public key hex | +| `get_public_key()` | Return current public key hex | +| `sign_message(msg)` | Sign a canonical UTF-8 payload and return signature hex | +| `compute_next_hash(prev, ts, entropy, stack, salt)` | Compute next Blake3 hash-chain value | +| `run_program(b64)` | Execute a base64 VM program and return stack state | +| `init_gene_state(gene_size)` | Initialize the synthetic gene buffer | +| `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` | Preview next gene commitment | +| `commit_gene_preview()` | Commit the previewed gene mutation after accepted heartbeat | +| `discard_gene_preview()` | Discard previewed mutation after rejection or error | +| `current_gene_commitment(session_id, mutation_step)` | Return current committed gene commitment | + +String-returning WASM functions return an empty string on error. Boolean-returning functions indicate success or failure directly. + +## Storage Backends + +Set storage mode with `db_type` or `CHRONOSEAL_DB_TYPE`. + +| Backend | Description | +|---|---| +| `sqlite-in-memory` | Default ephemeral session storage. State is lost on restart. | +| `sqlite-in-disk` | SQLite database persisted at `db_path`. | +| `valkey` | Valkey-compatible backend mode. | + +For Valkey mode, the server reads `CHRONOSEAL_VALKEY_ADDR` and defaults to `127.0.0.1:6666` when it is not set. If the Valkey connection fails, the current implementation falls back to in-memory SQLite and logs a warning. + +## Operations + +### Health + +```bash +chronoseal health +curl http://127.0.0.1:3000/health +``` + +### Status ```bash chronoseal status --format json ``` -```json -{ - "running": true, - "healthy": true, - "bind": "0.0.0.0:3000", - "pid_file": "/run/chronoseal.pid", - "pid": 79459 +### Metrics + +```bash +chronoseal metrics +curl http://127.0.0.1:3000/metrics +``` + +### Stats + +```bash +chronoseal stats --format json +curl http://127.0.0.1:3000/stats +``` + +### Logs + +```bash +sudo journalctl -u chronoseal -f +``` + +Use `RUST_LOG=info` or `CHRONOSEAL_LOG=info` for normal production operation. Avoid debug logging in production because internal identifiers may appear in logs. + +### Reverse Proxy + +ChronoSeal should run behind TLS in production. Terminate HTTPS at a reverse proxy such as nginx, Caddy, HAProxy, or a cloud load balancer, then proxy to the local daemon. + +Example nginx location: + +```nginx +location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } ``` ---- - -## Architecture Summary - -ChronoSeal is composed of three primary runtime components: - -* `shared/` — shared cryptographic primitives, hash chaining, gene model, and mutation engine used by both server and WASM -* `server/` — Axum-based Unix-native daemon, session lifecycle, storage, trust evaluation, and `POST /init` / `POST /hb` routes -* `wasm/` — browser runtime for key generation, signature creation, VM execution, and mutation preview/commit lifecycle - -### Key innovations in v0.6.0 - -* Synthetic Gene Mutation Engine with deterministic, shared opcode semantics -* Server-side gene commitment validation on every heartbeat -* `mutation_step` and `mutation_order_b64` handshake in init and heartbeat responses -* `db_type` runtime backend selection with SQLite and Valkey support - ---- - -## How ChronoSeal Works - -ChronoSeal establishes continuity by chaining signed heartbeats between client and server. - -### Session flow - -1. Client loads the WASM runtime and generates an Ed25519 keypair in WASM memory. -2. Client calls `POST /init` with the public key. -3. Server creates an ephemeral session and returns a `session_id`, initial salt, VM program, and mutation order metadata. -4. Client executes the VM program, collects browser entropy, previews the mutation commitment, signs the heartbeat payload, and sends `POST /hb`. -5. Server verifies signature, hash chain continuity, behavioral sanity, mutation step parity, and gene commitment before returning the next salt and mutation order. - -### Silent failure model - -Invalid heartbeats are returned as `{"status":"ok"}` without mutation fields. This avoids giving attackers explicit feedback. - ---- - -## Storage Backends - -ChronoSeal supports multiple runtime storage backends configured via `db_type`: - -* `sqlite-in-memory` — default ephemeral session storage -* `sqlite-disk` — persisted SQLite storage on disk -* `valkey` — alternative backend compatibility mode for future high-performance storage - ---- - -## Deployment - -ChronoSeal is intended to run as a systemd-managed Unix daemon with strict sandboxing and observable metrics. - -See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for build, installation, and production deployment guidance. - ---- - ## Security Model -ChronoSeal is a cost-raising attestations layer, not a perfect bot blocker. +ChronoSeal is designed to raise the cost of automation and replay attacks. -It protects against: +It helps defend against: -* replay attacks -* session cloning -* invalid signature injection -* broken hash chain continuity -* mutation tampering -* simple synthetic mouse and browser automation +- replayed heartbeats +- stale hash chain state +- forged heartbeat signatures +- mutation commitment tampering +- session cloning using only a stolen `session_id` +- simple scripted clients that do not run the WASM runtime +- basic browser automation with weak interaction simulation -It does not attempt to protect against: +It does not claim to stop: -* real users acting as bots -* server-side application vulnerabilities -* fully resourced adversaries with real browsers and hardware input devices +- real users intentionally acting as bots +- fully resourced browser farms +- attackers with complete control of a real browser and realistic input +- server-side application vulnerabilities +- account abuse outside ChronoSeal's attestation boundary +- long-term identity or fraud decisions by itself -See [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) for the full threat model. +Privacy posture: ---- +- Sessions are short-lived. +- The private key is generated in the browser runtime and is not sent to the server. +- ChronoSeal validates continuity and plausibility rather than creating persistent user identities. +- Invalid heartbeats are rejected silently to reduce oracle feedback. + +Read the full model in [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md). + +## Development + +Run the standard checks: + +```bash +cargo fmt --check +cargo check --workspace +cargo test --workspace +cargo clippy --workspace --all-targets -- -D warnings +``` + +Format: + +```bash +cargo fmt +``` + +Build WASM during frontend/runtime changes: + +```bash +wasm-pack build wasm --target web +rm -rf frontend/pkg +mv wasm/pkg frontend/pkg +``` + +Build release artifacts: + +```bash +bash scripts/build.sh +``` + +Generate shell completion: + +```bash +chronoseal completion bash > chronoseal.bash +chronoseal completion zsh > _chronoseal +``` + +## Troubleshooting + +### Browser fails to load WASM + +Rebuild the WASM package and make sure `frontend/pkg/chronoseal_wasm.js` and the `.wasm` file exist: + +```bash +bash scripts/build.sh +``` + +The `.wasm` file must be served as `application/wasm`. The built-in static file service handles this for normal ChronoSeal deployments. + +### `chronoseal health` fails + +Check that the daemon is running and that the CLI is probing the correct bind address: + +```bash +sudo systemctl status chronoseal +chronoseal health --bind 127.0.0.1:3000 +curl http://127.0.0.1:3000/health +``` + +### Heartbeats return only `{"status":"ok"}` + +That is the expected response for rejected heartbeats. Common causes include: + +- invalid signature +- stale `prev_hash` +- stale `mutation_step` +- mismatched `gene_commitment` +- timestamp drift beyond the configured window +- insufficient mouse movement or pause data +- rate limiting +- expired session + +Use local development logs and tests to debug integration issues. Avoid debug logs in production. + +### SQLite disk mode cannot persist state + +Ensure the daemon user can create and write the configured database path: + +```bash +sudo mkdir -p /var/lib/chronoseal +sudo chown -R chronoseal:chronoseal /var/lib/chronoseal +``` + +Use: + +```toml +db_type = "sqlite-in-disk" +db_path = "/var/lib/chronoseal/chronoseal.sqlite" +``` + +### Config changes do not apply + +Check precedence. CLI flags override environment variables, environment variables override config files, and config files override built-in defaults. + +Print the effective config: + +```bash +chronoseal config check --format yaml +``` ## Further Reading -* [Architecture](docs/ARCHITECTURE.md) -* [API Reference](docs/API.md) -* [Deployment](docs/DEPLOYMENT.md) -* [Threat Model](docs/THREAT_MODEL.md) -* [Design Philosophy](docs/DESIGN-PHILOSOPHY.md) -* [Privacy Policy](docs/PRIVACY%20POLICY.md) -* [WASM Build](docs/WASM_BUILD.md) -* [Refactoring v0.6.0](docs/REFRACTORING-v0.6.0.md) +- [Architecture](docs/ARCHITECTURE.md) +- [API Reference](docs/API.md) +- [Deployment Guide](docs/DEPLOYMENT.md) +- [Threat Model](docs/THREAT_MODEL.md) +- [Design Philosophy](docs/DESIGN-PHILOSOPHY.md) +- [Privacy Policy](docs/PRIVACY%20POLICY.md) +- [WASM Build Guide](docs/WASM_BUILD.md) +- [Refactoring v0.6.0](docs/REFRACTORING-v0.6.0.md) +- [Contributing](CONTRIBUTING.md) +- [Security Policy](SECURITY.md) + +## License + +ChronoSeal is licensed under either of: + +- MIT, see [LICENSE](LICENSE) +- Apache-2.0, see [LICENSE-APACHE](LICENSE-APACHE) + +at your option. diff --git a/docs/API.md b/docs/API.md index 5bdebbb..6339575 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2a03630..5018b30 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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) diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 1984431..b95ec9a 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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. diff --git a/docs/DESIGN-PHILOSOPHY.md b/docs/DESIGN-PHILOSOPHY.md index 1f0fc98..48c9fe3 100644 --- a/docs/DESIGN-PHILOSOPHY.md +++ b/docs/DESIGN-PHILOSOPHY.md @@ -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 diff --git a/docs/PRIVACY POLICY.md b/docs/PRIVACY POLICY.md index 3144f08..467f370 100644 --- a/docs/PRIVACY POLICY.md +++ b/docs/PRIVACY POLICY.md @@ -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. diff --git a/docs/REFRACTORING-v0.6.0.md b/docs/REFRACTORING-v0.6.0.md index dea8b73..b831188 100644 --- a/docs/REFRACTORING-v0.6.0.md +++ b/docs/REFRACTORING-v0.6.0.md @@ -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. diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 5d4feef..2f9767d 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -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. diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md index 82c510f..542ff91 100644 --- a/docs/WASM_BUILD.md +++ b/docs/WASM_BUILD.md @@ -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. diff --git a/docs/chronoseal.example.toml b/docs/chronoseal.example.toml index ac0982c..52df9f0 100644 --- a/docs/chronoseal.example.toml +++ b/docs/chronoseal.example.toml @@ -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 diff --git a/server/src/session.rs b/server/src/session.rs index 2159829..6927809 100644 --- a/server/src/session.rs +++ b/server/src/session.rs @@ -132,11 +132,8 @@ pub fn verify_heartbeat( config.mutation_rounds, ) .map_err(|e| crate::errors::VerificationError::MutationProgram(e.to_string()))?; - let expected_gene_commitment = gene::commitment_hex_with_context( - &candidate_state, - &req.session_id, - req.mutation_step, - ); + let expected_gene_commitment = + gene::commitment_hex_with_context(&candidate_state, &req.session_id, req.mutation_step); if req.gene_commitment != expected_gene_commitment { return Err(crate::errors::VerificationError::MutationCommitmentMismatch); } diff --git a/server/src/storage.rs b/server/src/storage.rs index 9ea0441..96ba516 100644 --- a/server/src/storage.rs +++ b/server/src/storage.rs @@ -52,14 +52,17 @@ impl DbPool { Ok(DbPool::Sqlite(pool)) } crate::config::DbType::Valkey => { - let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR").unwrap_or_else(|_| "127.0.0.1:6666".to_string()); + let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR") + .unwrap_or_else(|_| "127.0.0.1:6666".to_string()); match ValkeyClient::connect(addr) { Ok(client) => Ok(DbPool::Valkey(ValkeyStore { client: Arc::new(Mutex::new(client)), index_key: "sessions:ids".to_string(), })), Err(err) => { - tracing::warn!("valkey connection failed, falling back to sqlite-in-memory: {err}"); + tracing::warn!( + "valkey connection failed, falling back to sqlite-in-memory: {err}" + ); let pool = init_sqlite_pool(Path::new(":memory:"))?; Ok(DbPool::Sqlite(pool)) } @@ -99,7 +102,10 @@ impl DbPool { } } - pub fn load_session(&self, session_id: &str) -> Result, Box> { + pub fn load_session( + &self, + session_id: &str, + ) -> Result, Box> { match self { DbPool::Sqlite(pool) => { let conn = pool.get()?; @@ -191,7 +197,8 @@ impl DbPool { DbPool::Sqlite(pool) => { let conn = pool.get()?; let now = current_time_ms(); - let sessions = conn.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))?; + let sessions = + conn.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))?; let expired_sessions = conn.query_row( "SELECT COUNT(*) FROM sessions WHERE expires_at < ?1", [now], @@ -219,7 +226,9 @@ pub fn init_pool(path: &Path) -> Result> { Ok(DbPool::Sqlite(pool)) } -fn init_sqlite_pool(path: &Path) -> Result, Box> { +fn init_sqlite_pool( + path: &Path, +) -> Result, Box> { let manager = if path == Path::new(":memory:") { r2d2_sqlite::SqliteConnectionManager::memory() } else { @@ -300,7 +309,10 @@ impl ValkeyStore { format!("session:{}", session_id) } - fn load_session(&self, session_id: &str) -> Result, Box> { + fn load_session( + &self, + session_id: &str, + ) -> Result, Box> { let mut client = self.client.lock().unwrap(); if let Some(payload) = client.get(&self.session_key(session_id))? { let record = serde_json::from_str(&payload)?; diff --git a/shared/src/gene.rs b/shared/src/gene.rs index 6a2c433..9da6926 100644 --- a/shared/src/gene.rs +++ b/shared/src/gene.rs @@ -158,7 +158,7 @@ pub fn encode_environment(records: &[EnvironmentRecord]) -> Result, Gene } pub fn decode_environment(blob: &[u8]) -> Result, GeneError> { - if blob.len() % 6 != 0 { + if !blob.len().is_multiple_of(6) { return Err(GeneError::EnvironmentBlobLengthInvalid { len: blob.len() }); } let records_len = blob.len() / 6; diff --git a/shared/src/vm_extensions.rs b/shared/src/vm_extensions.rs index 7816f09..3a0bc5b 100644 --- a/shared/src/vm_extensions.rs +++ b/shared/src/vm_extensions.rs @@ -1,7 +1,7 @@ use crate::{ constants::{ - HASH_OPCODE_INSTRUCTION_COST, MAX_GENE_SIZE, MAX_MUTATION_PROGRAM_BYTES, - MAX_MUTATION_INSTRUCTION_BUDGET, DEFAULT_MUTATION_ROUNDS, SOFT_CAP_DURATION_MS, + DEFAULT_MUTATION_ROUNDS, HASH_OPCODE_INSTRUCTION_COST, MAX_GENE_SIZE, + MAX_MUTATION_INSTRUCTION_BUDGET, MAX_MUTATION_PROGRAM_BYTES, SOFT_CAP_DURATION_MS, }, gene::{ add_env_quantity, get_env_quantity, sub_env_quantity, validate_state, GeneError, GeneState, @@ -153,9 +153,7 @@ pub fn generate_order_with_rng( OP_FINALIZE_GENE_HASH => { program.push(OP_FINALIZE_GENE_HASH); stack_depth += 1; - if hash_ops_needed > 0 { - hash_ops_needed -= 1; - } + hash_ops_needed = hash_ops_needed.saturating_sub(1); } OP_GENE_STORE => { if stack_depth > 0 { @@ -199,17 +197,15 @@ pub fn generate_order_with_rng( push_u16(&mut program, rng.r#gen::()); } } - OP_PRODUCE => { - if stack_depth > 0 { - program.push(OP_PRODUCE); - push_u16(&mut program, rng.r#gen::()); - } + OP_PRODUCE if stack_depth > 0 => { + program.push(OP_PRODUCE); + push_u16(&mut program, rng.r#gen::()); } _ => {} } } - while hash_ops_needed > 0 && program.len() + 1 <= MAX_MUTATION_PROGRAM_BYTES { + while hash_ops_needed > 0 && program.len() < MAX_MUTATION_PROGRAM_BYTES { program.push(OP_FINALIZE_GENE_HASH); hash_ops_needed -= 1; } @@ -269,12 +265,25 @@ pub fn execute_program_with_rounds( } let elapsed = start.elapsed(); - tracing::debug!(rounds = actual_rounds, requested_rounds = rounds, elapsed_ms = elapsed.as_millis(), program_len = program.len(), "mutation execution"); + tracing::debug!( + rounds = actual_rounds, + requested_rounds = rounds, + elapsed_ms = elapsed.as_millis(), + program_len = program.len(), + "mutation execution" + ); if actual_rounds < rounds { - tracing::debug!(requested_rounds = rounds, executed_rounds = actual_rounds, "mutation soft cap reduced mutation rounds to preserve host responsiveness"); + tracing::debug!( + requested_rounds = rounds, + executed_rounds = actual_rounds, + "mutation soft cap reduced mutation rounds to preserve host responsiveness" + ); } if elapsed.as_millis() > SOFT_CAP_DURATION_MS { - tracing::debug!(elapsed_ms = elapsed.as_millis(), "mutation execution exceeded soft cap duration"); + tracing::debug!( + elapsed_ms = elapsed.as_millis(), + "mutation execution exceeded soft cap duration" + ); } Ok(trace.unwrap_or_else(|| ExecutionTrace { diff --git a/wasm/src/vm_extensions.rs b/wasm/src/vm_extensions.rs index b8efde5..efdf12b 100644 --- a/wasm/src/vm_extensions.rs +++ b/wasm/src/vm_extensions.rs @@ -18,7 +18,12 @@ pub fn init_gene_state(gene_size: u32) -> bool { } #[wasm_bindgen] -pub fn preview_gene_commitment(order_b64: &str, session_id: &str, mutation_step: u64, rounds: u8) -> String { +pub fn preview_gene_commitment( + order_b64: &str, + session_id: &str, + mutation_step: u64, + rounds: u8, +) -> String { let order = match shared::vm_extensions::decode_order_b64(mutation_step, order_b64) { Ok(order) => order, Err(_) => return String::new(), @@ -27,10 +32,17 @@ pub fn preview_gene_commitment(order_b64: &str, session_id: &str, mutation_step: let start = Instant::now(); let candidate = GENE_STATE.with(|slot| { let state = slot.borrow(); - let Some(current) = state.as_ref() else { - return None; - }; - shared::vm_extensions::apply_program_clone_with_rounds(current, &order.program, if rounds == 0 { shared::constants::DEFAULT_MUTATION_ROUNDS } else { rounds }).ok() + let current = state.as_ref()?; + shared::vm_extensions::apply_program_clone_with_rounds( + current, + &order.program, + if rounds == 0 { + shared::constants::DEFAULT_MUTATION_ROUNDS + } else { + rounds + }, + ) + .ok() }); let elapsed = start.elapsed(); tracing::debug!(session_id = %session_id, mutation_step = mutation_step, elapsed_ms = elapsed.as_millis(), "wasm mutation preview execution"); @@ -38,7 +50,8 @@ pub fn preview_gene_commitment(order_b64: &str, session_id: &str, mutation_step: let Some(candidate) = candidate else { return String::new(); }; - let commitment = shared::gene::commitment_hex_with_context(&candidate, session_id, mutation_step); + let commitment = + shared::gene::commitment_hex_with_context(&candidate, session_id, mutation_step); PREVIEW_STATE.with(|slot| *slot.borrow_mut() = Some(candidate)); commitment } @@ -63,7 +76,9 @@ pub fn current_gene_commitment(session_id: &str, mutation_step: u64) -> String { GENE_STATE.with(|slot| { slot.borrow() .as_ref() - .map(|state| shared::gene::commitment_hex_with_context(state, session_id, mutation_step)) + .map(|state| { + shared::gene::commitment_hex_with_context(state, session_id, mutation_step) + }) .unwrap_or_default() }) } @@ -95,12 +110,7 @@ mod tests { discard_gene_preview(); GENE_STATE.with(|slot| *slot.borrow_mut() = None); let c = preview_gene_commitment( - &order_b64(vec![ - shared::vm_extensions::OP_MUTATE_POINT, - 0, - 0, - 1, - ]), + &order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 1]), "deadbeef", 1, 0, @@ -173,8 +183,16 @@ mod tests { let preview = preview_gene_commitment(&b64, "deadbeef", 3, 0); let mut expected = shared::gene::new_state(16).unwrap(); - shared::vm_extensions::apply_program_with_rounds(&mut expected, &order.program, shared::constants::DEFAULT_MUTATION_ROUNDS).unwrap(); - assert_eq!(preview, shared::gene::commitment_hex_with_context(&expected, "deadbeef", 3)); + shared::vm_extensions::apply_program_with_rounds( + &mut expected, + &order.program, + shared::constants::DEFAULT_MUTATION_ROUNDS, + ) + .unwrap(); + assert_eq!( + preview, + shared::gene::commitment_hex_with_context(&expected, "deadbeef", 3) + ); } #[test] @@ -194,11 +212,15 @@ mod tests { shared::constants::DEFAULT_MUTATION_ROUNDS, ) .unwrap(); - let expected_commitment = shared::gene::commitment_hex_with_context(&expected, "deadbeef", step + 1); + let expected_commitment = + shared::gene::commitment_hex_with_context(&expected, "deadbeef", step + 1); assert_eq!(preview, expected_commitment); assert!(commit_gene_preview()); - assert_eq!(current_gene_commitment("deadbeef", step + 1), expected_commitment); + assert_eq!( + current_gene_commitment("deadbeef", step + 1), + expected_commitment + ); } } }