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