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

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

No files matched your search

+2
View File
@@ -5,3 +5,5 @@ dist/
*.log
.env
.idea/
.antigravitycli/
Generated
+12
View File
@@ -269,6 +269,7 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"valkey",
]
[[package]]
@@ -285,6 +286,7 @@ dependencies = [
"serde-wasm-bindgen",
"serde_json",
"shared",
"tracing",
"wasm-bindgen",
]
@@ -1303,6 +1305,7 @@ dependencies = [
"rand 0.8.6",
"serde",
"serde_json",
"tracing",
]
[[package]]
@@ -1749,6 +1752,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valkey"
version = "0.0.0-alpha5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "591043068c3f8db7fc1dcf34852eb03994175d39045cf01ad036c099f3c4f888"
dependencies = [
"tracing",
]
[[package]]
name = "valuable"
version = "0.1.1"
+97 -656
View File
@@ -5,11 +5,11 @@
</p>
<p align="center">
<strong>Cryptographic attestation daemon and anti-automation framework.</strong>
<strong>Unix-native cryptographic attestation daemon for browser session continuity.</strong>
</p>
<p align="center">
Privacy-preserving • Unix-native • Lightweight • WASM-powered
Privacy-first • Deterministic WASM parity • Silent rejection • Low overhead
</p>
<p align="center">
@@ -27,82 +27,69 @@
---
ChronoSeal is a lightweight cryptographic attestation daemon designed to raise the operational cost of browser automation, scraping, replay attacks, and synthetic interaction.
ChronoSeal is a mature Unix-native cryptographic attestation daemon for browser session continuity and anti-automation defense.
Instead of relying on:
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.
* CAPTCHA systems
* invasive browser fingerprinting
* telemetry-heavy tracking
* persistent identifiers
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 establishes a continuous cryptographic proof-of-runtime continuity using:
---
* WASM execution
* chained cryptographic heartbeats
## What ChronoSeal Provides
* 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
---
## Why ChronoSeal
ChronoSeal raises the operational cost of automation by combining:
* cryptographic session continuity
* deterministic VM execution
* behavioral entropy validation
* ephemeral attestation state
* mutation commitment parity
* silent, ambiguous rejection behavior
while remaining completely invisible and frictionless to legitimate human users.
v0.6.0 adds a deterministic synthetic gene mutation chain (hybrid `Vec<u8>` gene + bounded environment records) to strengthen anti-replay continuity with server/WASM parity.
See [docs/REFRACTORING-v0.6.0.md](docs/REFRACTORING-v0.6.0.md) for the full refactoring details.
This is not a fingerprinting or surveillance platform. ChronoSeal is designed to make automation expensive, not to collect user identities.
---
# Features
## Quick Start
* CLI-first Unix-native architecture
* Rich operational subcommands
* Machine-readable JSON/YAML outputs
* Hardened systemd integration
* Graceful shutdown and signal handling
* One-line installation workflow
* Prometheus-compatible metrics
* WASM-based client runtime
* Ed25519 + Blake3 cryptographic chaining
* Behavioral entropy validation
* Randomized stack-machine verification
* Deterministic synthetic gene mutation chain
* Server/WASM mutation parity checks
* Silent rejection model
* SQLite-backed ephemeral sessions
* Configurable runtime DB backend selection (`db_type`)
* Connection-pooled runtime architecture
* Lightweight deployment footprint
* Docker and native deployment support
* Adaptive trust scoring
* GPLv3 licensed
---
# Quick Start
## Install
### Install
```bash
sudo bash scripts/install.sh
```
## Check Status
### Verify status
```bash
chronoseal status --format json
```
## Health Probe
### Health probe
```bash
chronoseal health
```
## View Metrics
### View metrics
```bash
chronoseal metrics
```
## View Logs
### Follow logs
```bash
sudo journalctl -u chronoseal -f
@@ -110,13 +97,13 @@ sudo journalctl -u chronoseal -f
---
# CLI
## CLI Overview
```bash
chronoseal --help
```
## Available Commands
### Available commands
| Command | Description |
| ------------ | ------------------------------------------ |
@@ -151,635 +138,89 @@ chronoseal status --format json
---
# How It Works
## Architecture Summary
ChronoSeal establishes a continuous cryptographic proof-of-presence for browser sessions.
ChronoSeal is composed of three primary runtime components:
The system is inspired by heartbeat validation models used in embedded and distributed systems.
* `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
## Session Flow
### Key innovations in v0.6.0
```text
Browser Server
│ │
│ WASM loads, generates Ed25519 keypair │
│ Private key never leaves WASM memory │
│ │
├──── POST /init { public_key } ──────────►│
│◄─── { session_id, salt, opcodes_b64, H0, │
│ mutation_step, mutation_order_b64 } ──┤
│ │
│ Every 12–25s (randomized): │
│ ┌─ Collect behavioral entropy │
│ ├─ Execute verification VM opcodes │
│ ├─ Preview mutation commitment │
│ ├─ Attach mutation_step + commitment │
│ ├─ Advance Blake3 hash chain │
│ └─ Sign payload using Ed25519 │
│ │
├──── POST /hb { signed_payload } ────────►│
│◄─── { status, next_salt, │
│ next_mutation_step, │
│ next_mutation_order_b64 } ────────────┤
│ │
│ Invalid sessions silently rejected │
│ (`status=ok` without next_* fields) │
```
The server validates:
* signature authenticity
* heartbeat continuity
* replay resistance
* mutation step parity
* mutation commitment parity
* behavioral entropy
* timestamp validity
* fingerprint sanity
* 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
---
# Security Model
## How ChronoSeal Works
## What ChronoSeal Protects Against
ChronoSeal establishes continuity by chaining signed heartbeats between client and server.
| Threat | Mechanism |
| ------------------------ | ------------------------------------- |
| Replay attacks | Blake3 chained heartbeat continuity |
| Signature forgery | Ed25519 keypair generated inside WASM |
| Session cloning | Ephemeral session-bound keypairs |
| Static scraping | Runtime participation requirements |
| Naive browser automation | Behavioral continuity validation |
| Timestamp replay | Drift-window enforcement |
| Session flooding | Per-session rate limiting |
| Mutation tampering | Server-side commitment parity checks |
### 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.
---
## Silent Rejection Model
## Storage Backends
ChronoSeal intentionally avoids explicit rejection semantics.
ChronoSeal supports multiple runtime storage backends configured via `db_type`:
Invalid sessions may still receive:
```json
{ "status": "ok" }
```
This prevents:
* oracle-style probing
* protocol learning
* easy automation tuning
* behavioral enumeration
* `sqlite-in-memory` — default ephemeral session storage
* `sqlite-disk` — persisted SQLite storage on disk
* `valkey` — alternative backend compatibility mode for future high-performance storage
---
## What ChronoSeal Does Not Claim
## Deployment
ChronoSeal is a cost-raising mechanism, not an impenetrable barrier.
ChronoSeal is intended to run as a systemd-managed Unix daemon with strict sandboxing and observable metrics.
A sufficiently motivated adversary with:
* real browsers
* genuine input devices
* enough reverse engineering effort
can eventually bypass the system.
The goal is to make automation:
* expensive
* operationally complex
* difficult to scale
* harder to replay deterministically
See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for build, installation, and production deployment guidance.
---
# Architecture
## Security Model
```text
chronoseal-rs/
├── shared/ Shared types, hash chain, gene + mutation engine
├── server/ Axum HTTP daemon
│ ├── routes/ API routes
│ ├── session.rs Session lifecycle + mutation parity checks
│ ├── crypto.rs Ed25519 verification
│ ├── trust.rs Behavioral validation
│ ├── fingerprint/ Browser sanity validation
│ ├── vm.rs Random opcode generator
│ ├── ratelimit.rs Token bucket limiter
│ ├── cleanup.rs Session expiration lifecycle
│ └── metrics.rs Prometheus metrics
├── wasm/ Rust → WASM runtime
│ ├── crypto.rs Signing + hash chaining
│ ├── vm.rs Stack-machine executor
│ └── vm_extensions.rs Gene mutation preview/commit
├── frontend/ Lightweight JS integration
├── scripts/ Build/install/dev scripts
└── docs/ Project documentation
```
ChronoSeal is a cost-raising attestations layer, not a perfect bot blocker.
It protects against:
* replay attacks
* session cloning
* invalid signature injection
* broken hash chain continuity
* mutation tampering
* simple synthetic mouse and browser automation
It does not attempt to protect against:
* real users acting as bots
* server-side application vulnerabilities
* fully resourced adversaries with real browsers and hardware input devices
See [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) for the full threat model.
---
# Stack Machine
ChronoSeal includes a lightweight randomized stack-machine execution engine.
The server generates a randomized opcode program during session initialization.
The client executes this program on every heartbeat and includes the resulting stack state in the signed payload.
This makes heartbeat payloads structurally dynamic.
## Supported Opcodes
| Opcode | Mnemonic | Effect |
| ------ | -------- | ----------------------- |
| `0x00` | PUSH | Push literal |
| `0x01` | ADD | Wrapping addition |
| `0x02` | SUB | Wrapping subtraction |
| `0x03` | MUL | Wrapping multiplication |
| `0x04` | XOR | Bitwise XOR |
| `0x05` | AND | Bitwise AND |
| `0x06` | OR | Bitwise OR |
| `0x07` | ROT | Rotate left |
| `0x08` | NOT | Unary inversion |
| `0x09` | HASH | Blake3 stack hash |
## Mutation Opcodes (v0.6.0)
| Opcode | Mnemonic | Effect |
| ------ | -------------------- | ------ |
| `0x23` | GENE_LOAD | Push `gene[idx]` |
| `0x24` | GENE_STORE | Pop and store at `gene[idx]` |
| `0x25` | MUTATE_POINT | Apply wrapping byte delta at index |
| `0x26` | INSERT | Insert popped byte at index |
| `0x27` | DELETE | Delete byte at index and push removed value |
| `0x28` | TRANSCRIBE | Push deterministic transcription hash |
| `0x29` | APPLY_MUTAGEN | Mix environment symbol quantity into gene byte |
| `0x2A` | FINALIZE_GENE_HASH | Push commitment-derived `u32` |
| `0x2B` | CONSUME | Pop amount, subtract environment quantity |
| `0x2C` | PRODUCE | Pop amount, add environment quantity |
---
# Hash Chain
ChronoSeal uses Blake3 chained continuity validation.
## Initial Hash
```text
H(0) = Blake3( session_id ║ public_key ║ salt₀ )
```
## Heartbeat Progression
```text
H(n) = Blake3(
saltₙ₋₁ ║
H(n-1) ║
timestamp ║
Blake3(entropy_json) ║
Blake3(stack_json)
)
```
Each heartbeat depends on:
* prior continuity
* prior server-issued salt
* behavioral entropy
* VM execution result
* timestamp progression
---
# Signature Canonicalization
Heartbeat payloads are serialized into canonical key order before signing.
The server reconstructs payloads identically before:
* Ed25519 verification
* hash progression validation
This prevents:
* serialization inconsistencies
* ambiguous signing layouts
* malformed payload tricks
---
# SQLite Schema
```sql
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
public_key BLOB NOT NULL,
salt BLOB NOT NULL,
last_hash BLOB NOT NULL,
chain_length INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
last_seen INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
gene BLOB NOT NULL DEFAULT X'',
environment BLOB NOT NULL DEFAULT X'',
pending_mutation BLOB NOT NULL DEFAULT X'',
pending_mutation_step INTEGER NOT NULL DEFAULT 0
);
```
ChronoSeal intentionally uses ephemeral session persistence.
Session continuity is designed to reset transparently.
---
# v0.6.0 — Synthetic Gene Mutation System
## Overview & Motivation
ChronoSeal v0.6.0 introduces a synthetic mutation chain model to strengthen attestation liveness and anti-replay guarantees while preserving privacy-first behavior. The core model combines:
* a primary byte-oriented gene buffer (`Vec<u8>`), and
* a bounded secondary environment map (`Vec<(u16 symbol, u32 quantity)>`).
Each heartbeat now carries deterministic mutation progression evidence (`mutation_step`, `gene_commitment`) that is validated server-side against the exact server-issued mutation order. This design increases attacker workload by coupling cryptographic chain continuity with stateful deterministic mutation parity.
## Architectural Goals
1. Keep runtime behavior deterministic across server and WASM execution.
2. Preserve ephemerality and low operational complexity.
3. Minimize additional latency on the heartbeat path.
4. Improve protocol resistance against replay and mutation tampering.
5. Maintain a maintainable codebase with explicit invariants and focused modules.
## Design Decisions
**Shared mutation engine** — Mutation opcode semantics live in `shared/src/vm_extensions.rs` to guarantee server/client parity from one implementation.
**Deterministic gene commitment** — A domain-separated BLAKE3 commitment (`chronoseal/gene/v1`) binds both gene bytes and sorted environment records.
**Bounded mutation complexity** — Mutation program length is capped (`MAX_MUTATION_PROGRAM_BYTES`) and environment cardinality is capped (`MAX_ENV_RECORDS`).
**Strict validation on ingest** — Environment payloads are validated for sortedness, uniqueness, non-zero quantity, and length constraints.
**Protocol-level mutation handshake** — `InitResponse` and `Heartbeat` payloads now include mutation step/order and commitment fields.
**DB backend control via `db_type`** — Server CLI/config now supports:
* `sqlite-in-memory` (default)
* `sqlite-in-disk` (active; uses `db_path`)
* `valkey` (active compatibility mode; currently falls back to in-memory)
## Implementation
1. Gene model + deterministic commitment in `shared/gene.rs`.
2. v0.6.0 mutation opcode set in shared VM extensions.
3. Mutation state persisted per session (`gene`, `environment`, `pending_mutation`, `pending_mutation_step`).
4. Protocol schema extended for mutation fields in init/heartbeat exchange.
5. Mutation step + commitment parity validated before accepting heartbeat updates.
6. WASM preview/commit mutation lifecycle mirrors server behavior.
7. `db_type` CLI/config flow and runtime backend initialization strategy.
8. Migration-safe schema extension (column existence checks + index creation).
## Testing Strategy
ChronoSeal v0.6.0 test coverage is organized across unit, integration, and randomized/fuzz-style validation.
**Unit, integration, and property tests:**
* Unit tests for gene invariants and encoding/decoding.
* Unit tests for every mutation opcode with stack-effect assertions.
* Integration tests for full session lifecycle and heartbeat acceptance/rejection paths.
* Table-driven randomized tests and fuzz-style random bytecode tests to validate deterministic failure/success symmetry.
**Server-client parity testing:**
* Shared opcode engine parity tests across seeded mutation sequences.
* Multi-step mutation chain test (`test_mutation_chain`) asserting identical server/client final state.
* 10+ heartbeat deterministic simulation tests in session integration suite.
**Evasion / attack simulation testing:**
* Replay attack simulation.
* Mutation step mismatch rejection.
* Mutation commitment tampering rejection.
* Malformed server mutation payload rejection.
* Stack underflow / unknown opcode / truncated program rejection.
**Performance regression testing:**
* Bounded execution checks through capped program size and bounded record counts.
* Timing smoke regression test for mutation execution loops.
* End-to-end heartbeat test coverage to detect behavior regressions on hot paths.
## Security Analysis
**Replay resistance** — Heartbeats are now tied to both chain hash and mutation step progression.
**Mutation tampering resistance** — Server recomputes candidate gene state from authoritative pending mutation program and rejects commitment mismatch.
**Protocol ambiguity reduction** — Canonical signing payload includes mutation fields, reducing exploitable unsigned state.
**Input hardening** — Program size limits, stack underflow checks, and strict environment decoding reduce parser abuse and malformed payload amplification.
**Deterministic failure semantics** — Invalid mutation instructions fail predictably and symmetrically across server and WASM paths.
## Performance Considerations
* Mutation instructions are lightweight and mostly O(1); only `INSERT`/`DELETE` are O(n) but bounded by max gene size.
* Environment operations use sorted-vector binary search with tight upper bound (`MAX_ENV_RECORDS`).
* Commitment hashing is linear in gene size and record count, both bounded.
* Shared engine avoids duplicate logic and divergence-induced debugging overhead.
## Migration & Backward Compatibility
* Schema migration is additive; new columns are created when missing.
* Existing deployments without mutation fields require updated client+server pair for heartbeat compatibility.
* `db_type` defaults to in-memory to preserve ephemeral behavior.
* `sqlite-in-disk` is now directly usable via `db_path`.
* `valkey` currently runs in compatibility mode (in-memory fallback) to avoid startup failure while preserving CLI contract.
## Risks & Mitigations
| Risk | Mitigation |
| ---- | ---------- |
| State divergence between server and client | Shared opcode engine + deterministic seeded parity tests + multi-heartbeat integration tests |
| Mutation opcode abuse via malformed programs | Strict parsing, length caps, explicit underflow/unknown-opcode errors |
| Performance regressions | Bounded structures, smoke timing tests, focused hot-path validation |
| Backend confusion during `db_type` rollout | Explicit CLI command (`chronoseal db-type`), config output visibility, and clear runtime compatibility behavior |
---
# Runtime Architecture
## Server Runtime
* Rust
* Axum
* Tokio
* SQLite (`sqlite-in-memory` / `sqlite-in-disk`)
* `db_type=valkey` compatibility mode (falls back to in-memory in v0.6.0)
* `r2d2`
* `thiserror`
## Browser Runtime
* Rust → WASM
* Ed25519 signing
* Blake3 chaining
* Stack-machine execution
---
# Deployment
## Recommended Installation
```bash
sudo bash scripts/install.sh
```
The installer:
* creates `chronoseal` service user
* builds release artifacts
* installs frontend assets
* deploys hardened systemd service
* enables and starts daemon
---
## Manual Installation
```bash
bash scripts/build.sh
sudo cp target/release/chronoseal /usr/local/bin/
sudo cp chronoseal.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now chronoseal
```
---
## Docker
```bash
docker compose up -d --build
```
---
# Development
## Full Build
```bash
bash scripts/build.sh
```
## Development Mode
```bash
bash scripts/dev.sh
```
## Direct Execution
```bash
cargo run -p server -- run --bind 127.0.0.1:3000
```
---
# Prerequisites
* Rust stable ≥ 1.87
* `wasm-pack`
* NodeJS (optional frontend tooling)
Install `wasm-pack`:
```bash
cargo install wasm-pack
```
Build backend:
```bash
cargo run -p server --release
```
Build WASM:
```bash
wasm-pack build wasm --target web --release
```
---
# Configuration
## Precedence
```text
CLI flags > CHRONOSEAL_* environment variables > config file > defaults
```
## Default Config Locations
```text
/etc/chronoseal/config.toml
$XDG_CONFIG_HOME/chronoseal/config.toml
~/.config/chronoseal/config.toml
```
## Runtime State
```text
~/.local/state/chronoseal/
```
## Database Backend Selection (v0.6.0)
Choose backend with config, env var, or CLI flag:
* Config: `db_type = "sqlite-in-memory" | "sqlite-in-disk" | "valkey"`
* Env: `CHRONOSEAL_DB_TYPE=...`
* CLI: `chronoseal run --db-type sqlite-in-disk --db-path /var/lib/chronoseal/chronoseal.sqlite`
Inspect backend status:
```bash
chronoseal db-type --format text
```
---
# Observability
ChronoSeal exposes:
* health probes
* runtime statistics
* Prometheus metrics
## Metrics Example
```bash
chronoseal metrics
```
```text
# HELP chronoseal_sessions Active ChronoSeal sessions
# TYPE chronoseal_sessions gauge
chronoseal_sessions 1
```
---
# Lightweight Runtime
Current release artifacts:
```text
chronoseal ~8.5 MB
chronoseal_wasm.wasm ~719 KB
```
ChronoSeal intentionally avoids:
* heavyweight frontend frameworks
* Electron-style packaging
* telemetry-heavy dependencies
* oversized runtime models
---
# Philosophy
ChronoSeal is intentionally not:
* a surveillance framework
* invasive browser fingerprinting
* a CAPTCHA replacement
* a telemetry ecosystem
ChronoSeal is:
* a cryptographic attestation runtime
* a behavioral continuity engine
* a proof-of-runtime framework
* a lightweight Unix-native daemon
---
# Contributing
## Requirements
* Rust stable
* wasm-pack
* NodeJS (optional frontend tooling)
## Development Workflow
```bash
cargo fmt
cargo clippy
cargo test
```
## Guidelines
* Keep security-sensitive logic inside Rust/WASM
* Avoid placing trust logic in JavaScript
* Preserve silent-failure behavior
* Maintain deterministic protocol serialization
---
# Security Policy
## Reporting Vulnerabilities
Please do not disclose security vulnerabilities publicly before responsible disclosure.
Contact maintainers privately with:
* reproduction steps
* affected versions
* impact assessment
* proof-of-concept if applicable
## Scope
ChronoSeal intentionally operates as:
* anti-automation middleware
* behavioral attestation layer
* cryptographic continuity verifier
Security hardening evolves continuously.
---
# Language Breakdown
| Language | Share |
| ---------- | ------ |
| Rust | 82.4% |
| JavaScript | 13.7% |
| Shell | 1.6% |
| Dockerfile | 1.4% |
| HTML | 0.9% |
---
Topics: `rust` · `cryptography` · `wasm` · `antibot` · `browser-security` · `behavioral-analysis` · `anti-scraping` · `headless-detection`
## 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)
+100 -137
View File
@@ -1,20 +1,18 @@
# ChronoSeal — API Reference
# ChronoSeal API Reference
ChronoSeal defines a small, deterministic API surface for browser attestation and heartbeat verification.
## Base URL
All endpoints are relative to the server root. In development: `http://localhost:3000`.
In production: your HTTPS domain via reverse proxy.
All endpoints are relative to the server root. In development: `http://localhost:3000`. In production: the HTTPS origin of the protected site.
---
## Endpoints
## POST /init
### `POST /init`
Initialise a new browser session.
Initialise a new session. Called once per page load, immediately after the
WASM module generates an Ed25519 keypair.
#### Request
### Request
```http
POST /init
@@ -29,17 +27,17 @@ Content-Type: application/json
| Field | Type | Description |
|---|---|---|
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 verifying key generated by the WASM module |
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 public key generated by the WASM runtime |
#### Response `200 OK`
### Response `200 OK`
```json
{
"session_id": "64-char hex string (32 bytes)",
"salt": "32-char hex string (16 bytes)",
"opcodes_b64": "base64-encoded VM program (8–16 opcodes)",
"initial_hash": "64-char hex string (32 bytes Blake3)",
"expires_at": 1234567890123,
"session_id": "64-char hex string",
"salt": "32-char hex string",
"opcodes_b64": "base64-encoded VM program",
"initial_hash": "64-char hex string",
"expires_at": 1234567890123,
"heartbeat_min_interval_ms": 12000,
"heartbeat_max_interval_ms": 25000,
"gene_size": 512,
@@ -50,29 +48,28 @@ Content-Type: application/json
| Field | Type | Description |
|---|---|---|
| `session_id` | `string` | Opaque session identifier; include in every heartbeat |
| `salt` | `string` | Initial salt; used to compute `H(0)` and first `H(1)` |
| `opcodes_b64` | `string` | Base64 VM program; execute with `run_program()` on every heartbeat |
| `initial_hash` | `string` | `H(0) = Blake3(session_id ║ pub_key ║ salt)`; the first `prev_hash` |
| `expires_at` | `number` | Unix timestamp in milliseconds; session expires after 30 minutes of inactivity |
| `heartbeat_min_interval_ms` | `number` | Lower bound for randomized heartbeat scheduling |
| `heartbeat_max_interval_ms` | `number` | Upper bound for randomized heartbeat scheduling |
| `gene_size` | `number` | Initial synthetic gene size used by server and WASM (default 512) |
| `mutation_step` | `number` | Server-issued mutation order step expected on next heartbeat |
| `mutation_order_b64` | `string` | Base64-encoded mutation opcode program for the current step |
| `session_id` | `string` | Opaque session identifier for the current browser session |
| `salt` | `string` | Random 16-byte salt used to seed the hash chain |
| `opcodes_b64` | `string` | Base64-encoded randomized VM program executed on every heartbeat |
| `initial_hash` | `string` | Initial chain hash `H(0)` used as `prev_hash` for the first heartbeat |
| `expires_at` | `number` | Unix timestamp in milliseconds after which the session expires |
| `heartbeat_min_interval_ms` | `number` | Minimum heartbeat interval in milliseconds |
| `heartbeat_max_interval_ms` | `number` | Maximum heartbeat interval in milliseconds |
| `gene_size` | `number` | Size of the initial synthetic gene buffer |
| `mutation_step` | `number` | Initial mutation step expected on the first heartbeat |
| `mutation_order_b64` | `string` | Base64-encoded mutation order for gene commitment preview |
#### Error
### Error
Returns `500 Internal Server Error` only on server-side failures (DB errors,
invalid public key length). No meaningful error body is returned.
`POST /init` returns `500 Internal Server Error` only for server-side failures such as invalid public key length or persistence errors. No detailed error information is exposed to callers.
---
### `POST /hb`
## POST /hb
Submit a heartbeat. Called every 12–25 seconds with uniform random jitter.
Submit a heartbeat to continue the session.
#### Request
### Request
```http
POST /hb
@@ -81,13 +78,12 @@ Content-Type: application/json
```json
{
"session_id": "64-char hex",
"prev_hash": "64-char hex",
"timestamp": 1234567890123,
"session_id": "64-char hex",
"prev_hash": "64-char hex",
"timestamp": 1234567890123,
"entropy_data": {
"events": [
{ "x": 412.0, "y": 308.5, "t": 1234.567 },
{ "x": 415.2, "y": 310.1, "t": 1285.123 }
{ "x": 412.0, "y": 308.5, "t": 1234.567 }
]
},
"stack_state": {
@@ -95,74 +91,73 @@ Content-Type: application/json
"ip": 42
},
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"aspectRatio": "1.7777777778",
"devicePixelRatio": 2,
"hardwareConcurrency": 8
},
"mutation_step": 1,
"gene_commitment": "64-char hex Blake3 commitment",
"signature": "128-char hex Ed25519 signature"
"gene_commitment": "64-char hex",
"signature": "128-char hex"
}
```
| Field | Type | Description |
|---|---|---|
| `session_id` | `string` | Session ID from `/init` |
| `prev_hash` | `string` | Hash chain head from previous heartbeat (or `initial_hash` for the first) |
| `timestamp` | `number` | `Date.now()` in milliseconds; must be within ±30s of server time |
| `entropy_data.events` | `array` | Mouse events since previous heartbeat; each has `x`, `y` (px), `t` (performance.now ms) |
| `stack_state.stack` | `array` | `u32[]` result of executing the VM program |
| `stack_state.ip` | `number` | Instruction pointer after execution |
| `fingerprint.aspectRatio` | `string` | `(screen.width / screen.height).toFixed(10)` |
| `fingerprint.devicePixelRatio` | `string` | `String(window.devicePixelRatio)` |
| `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency \|\| 1` |
| `mutation_step` | `number` | Must match server-side pending mutation step |
| `gene_commitment` | `string` | Commitment of the locally previewed candidate gene after applying `mutation_order_b64` |
| `prev_hash` | `string` | Previous hash chain head (`initial_hash` on first heartbeat) |
| `timestamp` | `number` | `Date.now()` in milliseconds |
| `entropy_data.events` | `array` | Mouse event list since the previous heartbeat |
| `stack_state.stack` | `array` | VM stack contents after program execution |
| `stack_state.ip` | `number` | VM instruction pointer after execution |
| `fingerprint.aspectRatio` | `string` | `screen.width / screen.height` to 10 decimal places |
| `fingerprint.devicePixelRatio` | `number` | `window.devicePixelRatio` |
| `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency || 1` |
| `mutation_step` | `number` | Current mutation step sent by the client |
| `gene_commitment` | `string` | Gene commitment produced by the WASM preview mutation engine |
| `signature` | `string` | Hex-encoded 64-byte Ed25519 signature over the canonical payload |
#### Canonical Signing Payload
### Canonical Signing Payload
The client signs the following JSON object. Top-level keys must be sorted
alphabetically. Nested object keys follow their natural serialisation order.
The client signs a canonical JSON object with top-level keys sorted alphabetically:
```json
{
"entropyData": { "events": [{ "t": …, "x": …, "y": … }] },
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
"geneCommitment":"…",
"mutationStep": …,
"prevHash": "…",
"sessionId": "…",
"stackState": { "ip": …, "stack": […] },
"timestamp": …
"entropyData": { "events": [{ "t": ..., "x": ..., "y": ... }] },
"fingerprint": {
"aspectRatio": "...",
"devicePixelRatio": ...,
"hardwareConcurrency": ...
},
"geneCommitment": "...",
"mutationStep": ...,
"prevHash": "...",
"sessionId": "...",
"stackState": { "ip": ..., "stack": [...] },
"timestamp": ...
}
```
Note: field names in the signing payload use camelCase (`sessionId`,
`prevHash`, `entropyData`, `stackState`, `mutationStep`, `geneCommitment`)
while the request body uses snake_case (`session_id`, `prev_hash`,
`entropy_data`, `stack_state`, `mutation_step`, `gene_commitment`).
Note: the signed payload uses camelCase while the transport request uses snake_case.
#### Response `200 OK` — Accepted
### Response `200 OK` — Accepted
```json
{
"status": "ok",
"next_salt": "32-char hex string (16 bytes)",
"status": "ok",
"next_salt": "32-char hex string",
"next_mutation_step": 2,
"next_mutation_order_b64": "base64-encoded mutation program"
}
```
The client must:
1. Preview commitment locally from `mutation_order_b64` and send it in the heartbeat.
2. Capture `sentSalt = currentSalt` before updating.
3. Set `currentSalt = next_salt`.
4. Compute `prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt)`.
5. Commit the previewed gene state.
6. Replace pending mutation values with `next_mutation_step` and `next_mutation_order_b64`.
| Field | Type | Description |
|---|---|---|
| `status` | `string` | Always `ok` |
| `next_salt` | `string` | Next server salt for the following heartbeat |
| `next_mutation_step` | `number` | Next mutation step to apply after acceptance |
| `next_mutation_order_b64` | `string` | Base64-encoded next mutation program |
#### Response `200 OK` — Rejected
### Response `200 OK` — Rejected
```json
{
@@ -170,77 +165,45 @@ The client must:
}
```
`next_salt`, `next_mutation_step`, and `next_mutation_order_b64` are absent.
The response body is intentionally identical in
structure. Rejections are silent — the caller cannot distinguish a validation
failure from a rate limit hit or an expired session.
A rejected heartbeat omits `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
The client should log a warning and continue scheduling heartbeats (they will
continue to fail until the page is reloaded and a new session is established).
This silent rejection model avoids giving attackers distinct failure signals.
---
## Validation Rules (Server-Side)
## Validation Rules
Heartbeats are rejected (silently) if any of the following checks fail:
Heartbeats are rejected silently when any validation step fails:
| Check | Condition for rejection |
|---|---|
| Rate limit | > 5 requests per 10-second window for this `session_id` |
| Session not found | `session_id` not in SQLite |
| Session expired | `current_time_ms > expires_at` |
| Signature invalid | Ed25519 verification fails against stored public key |
| Hash chain broken | `hex(prev_hash) ≠ stored last_hash` |
| Mutation step mismatch | `mutation_step ≠ pending_mutation_step` |
| Mutation commitment mismatch | `gene_commitment` does not match server-computed candidate commitment |
| Timestamp drift | `\|server_now_ms - timestamp\| > 30 000` |
| Insufficient mouse events | `events.len() < 3` |
| Insufficient mouse distance | `total_dist < 10.0 px` |
| Mouse speed too high | `total_dist / total_time_ms > 2.0 px/ms` |
| No mouse pauses | `pause_count < 1` |
| Invalid aspect ratio | `ar < 0.5` or `ar > 3.0` |
| Invalid devicePixelRatio | `dpr ≤ 0.0` or `dpr > 5.0` |
| Zero hardwareConcurrency | `hardware_concurrency == 0` |
* session missing or expired
* signature invalid
* hash chain mismatch
* mutation step mismatch
* gene commitment mismatch
* timestamp outside ±30 seconds
* insufficient mouse events
* insufficient mouse movement
* unrealistic speed profile
* missing pause intervals
* invalid fingerprint values
---
## Hash Chain Specification
## WASM Runtime Exports
```
H(0) = Blake3( session_id_bytes ║ pub_key_bytes ║ salt₀_bytes )
H(n) = Blake3(
saltₙ₋₁_bytes
║ H(n-1)_bytes
║ timestamp_u64_le_bytes
║ Blake3( UTF-8( JSON(entropy_data) ) )
║ Blake3( UTF-8( JSON(stack_state) ) )
)
```
All inputs are concatenated in the order shown. `timestamp` is encoded as a
64-bit unsigned integer in little-endian byte order. JSON serialisation of
`entropy_data` and `stack_state` uses the field order defined by the shared
Rust types (serde derive, no custom ordering).
---
## WASM API
The WASM module (`chronoseal_wasm`) exports the following functions to JavaScript:
The WASM module exports the following functions to JavaScript:
| Function | Signature | Description |
|---|---|---|
| `generate_keypair()` | `() → string` | Generate Ed25519 keypair; return hex public key. Private key stored in WASM memory. |
| `get_public_key()` | `() → string` | Return hex public key, or `""` if not initialised. |
| `sign_message(msg)` | `(string) → string` | Sign UTF-8 string; return hex signature, or `""` if not initialised. |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) → string` | Compute next Blake3 chain hash; all inputs/output hex or JSON strings. |
| `run_program(b64)` | `(string) → JsValue` | Execute base64 VM program; return `{ stack: u32[], ip: number }`. |
| `init_gene_state(gene_size)` | `(u32) → bool` | Initialise synthetic gene state in WASM memory. |
| `preview_gene_commitment(order_b64)` | `(string) → string` | Apply mutation order on preview state and return commitment hex. |
| `commit_gene_preview()` | `() → bool` | Commit previewed mutation state after accepted heartbeat. |
| `discard_gene_preview()` | `() → void` | Discard previewed mutation state after rejection/error. |
| `current_gene_commitment()` | `() → string` | Return current committed gene commitment hex. |
| `generate_keypair()` | `() -> string` | Generate a new Ed25519 keypair and return the public key hex |
| `get_public_key()` | `() -> string` | Return the current public key hex |
| `sign_message(msg)` | `(string) -> string` | Sign a UTF-8 string payload and return the hex signature |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | Compute the next Blake3 hash chain value |
| `run_program(b64)` | `(string) -> JsValue` | Execute a base64 VM program and return stack state |
| `init_gene_state(gene_size)` | `(u32) -> bool` | Initialise the synthetic gene buffer in WASM memory |
| `preview_gene_commitment(order_b64)` | `(string) -> string` | Preview the next gene commitment from a mutation order |
| `commit_gene_preview()` | `() -> bool` | Commit the previewed mutation after an accepted heartbeat |
| `discard_gene_preview()` | `() -> void` | Discard the previewed mutation after rejection or error |
| `current_gene_commitment()` | `() -> string` | Return the current committed gene commitment |
String-returning functions return `""` on error rather than panicking. Callers
must check for empty strings and boolean return values before use.
String-returning functions return `""` on error. Callers must handle empty values and boolean failures gracefully.
+108 -489
View File
@@ -1,526 +1,145 @@
# ChronoSeal — Architecture
# ChronoSeal Architecture
> Note (v0.6.0): Synthetic Gene Mutation flow and mutation handshake updates are documented in [REFRACTORING-v0.6.0.md](https://github.com/thakares/chronoseal-rs/blob/main/docs/REFRACTORING-v0.6.0.md) and [API.md](https://github.com/thakares/chronoseal-rs/blob/main/docs/API.md).
ChronoSeal is a Unix-native cryptographic attestation daemon that validates browser session continuity through deterministic VM execution, chained cryptographic state, and a shared Synthetic Gene Mutation Engine.
## Overview
ChronoSeal is a stateless, cryptographic browser attestation framework. Its
purpose is to make automated clients (headless browsers, AI scrapers, API
harvesters) computationally expensive and operationally complex to operate,
while remaining completely invisible to real human users.
ChronoSeal is designed as a production-grade infrastructure component, not as a consumer-facing widget. It is a lightweight daemon that can be operated, monitored, and integrated like any other native Linux service.
The design is inspired by the heartbeat model used in embedded IoT firmware:
a device that stops sending signed, chained attestations is assumed to be
offline or compromised. ChronoSeal applies the same principle to browser
sessions.
Key characteristics:
---
* Unix-native daemon with systemd-compatible lifecycle
* CLI-first control plane and configuration
* Shared Rust/WASM runtime for server/client parity
* Modular storage backend abstraction (`sqlite-in-memory`, `sqlite-disk`, `valkey`)
* Silent rejection semantics for attacker resilience
* Privacy-preserving ephemeral session state
## Design Principles
## Core Components
**Stateless per request.** The server carries no per-request state beyond what
is stored in SQLite keyed on `session_id`. Every HTTP request is independently
verifiable.
### `shared/`
**Silent failure.** Validation failures never return an error status or an
error body. The server always responds `{"status":"ok"}` and simply omits `next_salt`. The client degrades gracefully. Attackers cannot enumerate
validation rules by probing error responses.
Shared protocol and runtime primitives used by both the server and the browser runtime:
**Private key isolation.** The Ed25519 signing key is generated inside the
WASM module and never serialised, never exposed to the JavaScript environment,
and never transmitted. It exists only in WASM linear memory for the lifetime
of the page.
* Cryptographic primitives: Blake3, Ed25519
* Hash chain logic and session commitment handling
* Synthetic gene model and deterministic mutation engine
* Serialization, encoding, and canonical signing helpers
**Layered validation.** A heartbeat must pass five independent checks: session
existence, expiry, signature, hash chain, and behavioral signals. Bypassing
one layer is not sufficient.
### `server/`
**Cost asymmetry.** Each heartbeat requires a real browser environment, mouse
activity, correct WASM execution, chain state synchronisation, and a valid
Ed25519 signature over a time-windowed payload. For an automated client, the
synchronisation burden alone makes scaled operation expensive.
The server crate implements the runtime daemon:
**Deterministic mutation parity (v0.6.0).** Each heartbeat additionally carries
a `mutation_step` and `gene_commitment` derived from a server-issued mutation
program. Server and WASM execute the same shared opcode engine
(`shared/src/vm_extensions.rs`), and the server rejects any heartbeat where
the recomputed commitment does not match the client-supplied value.
* `routes/init.rs` — session initialization API
* `routes/heartbeat.rs` — heartbeat verification API
* `session.rs` — session lifecycle, mutation parity, and heartbeat validation
* `storage.rs` — backend abstraction and persistence
* `crypto.rs` — signature verification and key handling
* `trust.rs` — behavioral entropy and sanity validation
* `ratelimit.rs` — per-session request throttling
* `cleanup.rs` — expiration and eviction tasks
* `runtime.rs` — daemon bootstrap, metrics, and state management
### High-Level Design
### `wasm/`
- **Core**: Rust + Axum (async web framework)
- **Storage**: `db_type` selectable (`sqlite-in-memory`, `sqlite-in-disk`, `valkey` compatibility mode)
- **Client**: WASM + Rust (runs in browser for proof generation)
- **Security Model**: Behavioral analysis + hash chaining + entropy scoring + deterministic gene mutation parity
- **Deployment**: Static musl binary, systemd service, optional Docker
The client runtime crate compiles to WebAssembly and powers attestation in the browser.
### Key Components
* `crypto.rs` — in-WASM signing and hash computation
* `vm.rs` — randomized opcode VM execution
* `vm_extensions.rs` — synthetic gene mutation preview and commit lifecycle
- `shared/` — Types, constants, crypto primitives, gene model, and mutation engine used by server and WASM
- `server/` — Axum routes, session management, trust engine, rate limiting, cleanup tasks
- `wasm/` — Client-side proof generation, mutation preview/commit lifecycle
- `frontend/` — Static assets served by the application
### `frontend/`
### Unix-Native Design Decisions
Static browser integration code that loads the WASM module, orchestrates init/heartbeat flow, and collects browser entropy.
- Runs as a proper systemd service with strict sandboxing
- All state is either in-memory or in standard locations (`/run/`, `/var/log/`, `/etc/`)
- Graceful shutdown and reload support via signals
- Logging designed for `journalctl` and structured parsing
- Configuration is fully runtime (no recompile needed)
## v0.6.0 Innovation
### Design Goal
The primary innovation in v0.6.0 is the **Synthetic Gene Mutation Engine**.
## ChronoSeal should feel as natural to use as `nginx` or `redis-server` on a Linux system.
This layer adds a deterministic, shared server/WASM mutation handshake to the existing heartbeat continuity model.
## Component Map
Key v0.6.0 behavior:
* `mutation_order_b64` is issued at session initialization and after every accepted heartbeat
* `mutation_step` is tracked on both client and server
* `gene_commitment` is computed locally in WASM and validated by the server
* mutation state is persisted per session and advanced only on accepted heartbeats
* scalar mutation programs are deterministic and bounded in cost
This makes replay and tampering attacks significantly more expensive while preserving the existing privacy-first and silent-failure semantics.
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ entropy.js │ │ heartbeat.js │ │ transport.js│ │
│ │ │ │ │ │ │ │
│ │ mousemove │──►│ orchestrates │──►│ fetch POST │ │
│ │ event ring │ │ init + HB │ │ /init /hb │ │
│ └─────────────┘ └──────┬───────┘ └─────────────┘ │
│ │ │
│ ┌──────▼───────────────────────┐ │
│ │ WASM Module (antibot_wasm) │ │
│ │ │ │
│ │ crypto.rs vm.rs │ │
│ │ ├ generate_keypair() │ │
│ │ ├ sign_message() │ │
│ │ ├ compute_next_hash() │ │
│ │ ├ run_program() │ │
│ │ vm_extensions.rs │ │
│ │ ├ preview_mutation() │ │
│ │ └ commit_mutation() │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ HTTPS
┌─────────────────────────▼───────────────────────────────┐
│ Server (Axum) │
│ │
│ routes/init.rs routes/heartbeat.rs │
│ │ │ │
│ └──────────┬───────────────┘ │
│ ▼ │
│ session.rs │
│ ├ create_session() │
│ └ verify_heartbeat() │
│ └ validate_mutation_parity() [v0.6.0] │
│ │ │
│ ┌──────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ crypto.rs trust.rs fingerprint.rs │
│ (sig verify) (mouse (aspect ratio, │
│ speed) DPR, HW conc.) │
│ │ │
│ ▼ │
│ shared::hashing (Blake3 hash chain) │
│ shared::gene (gene model + commitment) [v0.6.0] │
│ shared::vm_extensions (mutation opcodes) [v0.6.0] │
│ │ │
│ ▼ │
│ storage.rs (SQLite: in-memory / in-disk / valkey) │
│ │
│ ratelimit.rs cleanup.rs vm.rs middleware.rs │
└─────────────────────────────────────────────────────────┘
Browser Server
┌──────────────────────────────────────────────────────────────┐
│ frontend/ + WASM runtime │
│ - generate_keypair() │
│ - sign_message() │
│ - compute_next_hash() │
│ - run_program() │
│ - preview_gene_commitment() │
│ - commit_gene_preview() │
│ │
│ POST /init -> │
│ POST /hb -> │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ server/ │
│ - signature validation │
│ - hash chain continuity │
│ - rate limiting │
│ - behavioral trust checks │
│ - mutation step validation │
│ - gene commitment verification │
│ - session persistence │
│ - metrics and health │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ storage backends │
│ - sqlite-in-memory │
│ - sqlite-disk │
│ - valkey compatibility mode │
└──────────────────────────────────────────────────────────────┘
```
---
## Storage Backends
## Session Lifecycle
ChronoSeal supports pluggable backend modes using the `db_type` configuration option.
### 1. Initialisation — `POST /init`
* `sqlite-in-memory` — default ephemeral session storage. No persistence across restarts.
* `sqlite-disk` — persisted SQLite database on disk through `db_path`.
* `valkey` — compatibility mode for alternative storage backends, currently supported alongside SQLite compatibility semantics.
```
Client Server
│ │
│ generate Ed25519 keypair (in WASM) │
│ pub_key = verifying_key.to_bytes() │
│ │
├─── { public_key: hex(pub_key) } ──────►│
│ │ session_id = rand::random::<[u8;32]>()
│ │ salt₀ = rand::random::<[u8;16]>()
│ │ H(0) = Blake3(session_id║pub_key║salt₀)
│ │ opcodes = generate_random_program(8..=16)
│ │ gene = initial gene buffer [v0.6.0]
│ │ mutation_order = generate_mutation_program() [v0.6.0]
│ │ INSERT INTO sessions …
│ │
│◄── { session_id, salt, opcodes_b64, │
│ initial_hash, expires_at, │
│ mutation_step, │ [v0.6.0]
│ mutation_order_b64 } ─────────────┤ [v0.6.0]
│ │
│ prevHash = initial_hash │
│ currentSalt = salt │
│ opcodesB64 = opcodes_b64 │
│ mutationStep = mutation_step │ [v0.6.0]
│ mutationOrderB64 = mutation_order_b64 │ [v0.6.0]
```
## Runtime Philosophy
### 2. Heartbeat — `POST /hb`
ChronoSeal is intentionally designed to behave like traditional Unix infrastructure software:
Fired every 12–25 seconds with uniform random jitter.
* explicit CLI operations (`run`, `status`, `health`, `config`, `metrics`, `stats`, `db-type`)
* structured logging for `journalctl`
* PID file management and graceful shutdown
* systemd sandbox support
* runtime configuration via TOML and CLI overrides
* clear separation of protocol, persistence, and runtime concerns
```
Client Server
│ │
│ stackState = run_program(opcodesB64) │
│ commitment = preview_mutation( │ [v0.6.0]
│ mutationOrderB64, mutationStep) │
│ events = collectEntropy(lastTime)│
│ ts = Date.now() │
│ │
│ signable = { │
│ entropyData, fingerprint, │ ← keys sorted alphabetically
│ prevHash, sessionId, │
│ stackState, timestamp, │
│ mutation_step, gene_commitment │ [v0.6.0]
│ } │
│ sig = sign_message( │
│ JSON.stringify(signable, keys.sort))│
│ │
├─── { session_id, prev_hash, timestamp, │
│ entropy_data, stack_state, │
│ fingerprint, signature, │
│ mutation_step, gene_commitment }─►│ [v0.6.0]
│ │ 1. Rate limit check
│ │ 2. Lookup session, check expiry
│ │ 3. Verify Ed25519 signature
│ │ 4. Verify hash chain continuity
│ │ 5. Validate timestamp window ±30s
│ │ 6. Validate mouse behavior
│ │ 7. Validate fingerprint signals
│ │ 8. Validate mutation step parity [v0.6.0]
│ │ 9. Validate gene commitment [v0.6.0]
│ │ 10. Compute H(n), rotate salt
│ │ 11. Advance gene state [v0.6.0]
│ │ 12. UPDATE sessions …
│ │
│◄── { status: "ok", next_salt, │
│ next_mutation_step, │ [v0.6.0]
│ next_mutation_order_b64 } ────────┤ [v0.6.0]
│ │
│ sentSalt = currentSalt ◄── captured BEFORE rotation
│ currentSalt = next_salt │
│ mutationStep = next_mutation_step │ [v0.6.0]
│ mutationOrderB64 = next_mutation_order_b64 [v0.6.0]
│ prevHash = compute_next_hash( │
│ prevHash, ts, entropy, │
│ stackState, sentSalt) │
```
## Integration Points
### 3. Failure Path
* Browser clients consume the WASM module and call `/init` and `/hb`
* Existing sites can proxy these API routes through their own web server
* Frontend assets can be served by ChronoSeal directly or mounted in a sidecar deployment
* TLS termination should be handled by a reverse proxy in production
On any validation failure the server returns `{"status":"ok"}` with no `next_salt`. The client logs a warning and continues scheduling heartbeats.
The chain is broken — subsequent heartbeats will also fail silently.
No error is surfaced to the page or its visitors.
## Operating Assumptions
---
ChronoSeal is not a general-purpose authentication service. It is a cryptographic attestation and anti-automation layer intended to be integrated with existing site logic.
## Cryptographic Protocol
It assumes:
### Key Generation
```
Ed25519 keypair generated via ed25519-dalek + rand::thread_rng (OS-seeded)
Private key: stored in WASM thread_local, never leaves WASM memory
Public key: 32 bytes, hex-encoded, sent to server at init
```
### Hash Chain
```
H(0) = Blake3( session_id ║ pub_key ║ salt₀ )
H(n) = Blake3(
saltₙ₋₁ ← server-side only, rotated each heartbeat
║ H(n-1) ← must match stored last_hash
║ timestamp_u64_le
║ Blake3( JSON(entropy_data) )
║ Blake3( JSON(stack_state) )
)
```
Salt rotation means an attacker who intercepts a heartbeat cannot compute
future chain links without also intercepting every subsequent server response.
### Gene Commitment (v0.6.0)
A domain-separated BLAKE3 commitment binds both the gene buffer and the sorted
environment records into a single 32-byte value that is included in the signed
heartbeat payload and validated server-side:
```
gene_commitment = BLAKE3(
"chronoseal/gene/v1" ← domain separator
║ gene_bytes ← Vec<u8> gene buffer
║ for each (symbol, qty) sorted by symbol:
symbol_u16_le ║ qty_u32_le
)
```
The server recomputes the candidate gene state from its authoritative
`pending_mutation` program and rejects any heartbeat where
`recomputed_commitment != client_gene_commitment`.
### Canonical Signing Payload
The signed message is a JSON object with top-level keys sorted alphabetically,
serialised with no extra whitespace:
```
{
"entropyData": { "events": [{"t":…,"x":…,"y":…}] },
"fingerprint": { "aspectRatio":"…","devicePixelRatio":"…","hardwareConcurrency":… },
"gene_commitment": "hex…",
"mutation_step": N,
"prevHash": "hex…",
"sessionId": "hex…",
"stackState": { "ip":…,"stack":[…] },
"timestamp": 1234567890123
}
```
The server reconstructs this using `std::collections::BTreeMap` (alphabetical
key order) before calling `VerifyingKey::verify_strict`. Any field mismatch,
key order difference, or whitespace difference causes a signature failure.
### Hashing Algorithm
Blake3 is used throughout: hash chain links, entropy data digest, stack state
digest, gene commitment, and the VM HASH opcode. Blake3 is chosen for speed
in WASM, resistance to length-extension attacks, and a clean Rust API.
---
## Stack Machine
The server generates a random program on session init. The client executes it
on every heartbeat and includes the resulting `StackState { stack, ip }` in
the signed payload. This ensures each heartbeat carries unique, verifiable
computation without additional round-trips.
### Core Instruction Set
| Opcode | Mnemonic | Operand | Stack effect | Description |
| ------ | -------- | ----------- | ------------ | -------------------------------------- |
| `0x00` | PUSH | u32 (4B LE) | +1 | Push literal |
| `0x01` | ADD | — | −1 | `a + b` wrapping |
| `0x02` | SUB | — | −1 | `a - b` wrapping |
| `0x03` | MUL | — | −1 | `a * b` wrapping |
| `0x04` | XOR | — | −1 | `a ^ b` |
| `0x05` | AND | — | −1 | `a & b` |
| `0x06` | OR | — | −1 | `a \| b` |
| `0x07` | ROT | — | −1 | `a.rotate_left(b % 32)` |
| `0x08` | NOT | — | 0 | `!a` (unary) |
| `0x09` | HASH | — | -(depth-1) | Blake3 of all stack items → single u32 |
The generator ensures ≥ 2 items on the stack before any binary opcode.
NOT (0x08) does not change depth. HASH resets depth to 1.
### Mutation Opcodes (v0.6.0)
The gene mutation extension operates on a separate `Vec<u8>` gene buffer and a
bounded environment map `Vec<(u16 symbol, u32 quantity)>`. These opcodes are
defined in `shared/src/vm_extensions.rs` and executed identically by both
server and WASM to guarantee deterministic parity.
| Opcode | Mnemonic | Effect |
| ------ | ------------------ | ------------------------------------------------------------- |
| `0x23` | GENE_LOAD | Push `gene[idx]` onto the stack |
| `0x24` | GENE_STORE | Pop stack top and store at `gene[idx]` |
| `0x25` | MUTATE_POINT | Apply wrapping byte delta at index |
| `0x26` | INSERT | Insert popped byte at index |
| `0x27` | DELETE | Delete byte at index and push the removed value |
| `0x28` | TRANSCRIBE | Push deterministic transcription hash of current gene state |
| `0x29` | APPLY_MUTAGEN | Mix environment symbol quantity into gene byte at index |
| `0x2A` | FINALIZE_GENE_HASH | Push commitment-derived `u32` onto the stack |
| `0x2B` | CONSUME | Pop amount, subtract from environment symbol quantity |
| `0x2C` | PRODUCE | Pop amount, add to environment symbol quantity |
**Constraints enforced at runtime:**
- Mutation program length capped at `MAX_MUTATION_PROGRAM_BYTES`
- Environment record count capped at `MAX_ENV_RECORDS`
- Environment records validated for sortedness, uniqueness, non-zero quantity
- Stack underflow and unknown opcodes cause deterministic, symmetric failures on both server and WASM paths
---
## Behavioral Validation
### Mouse Entropy
Every heartbeat includes the mouse events collected since the previous
heartbeat. Server checks:
| Check | Threshold |
| --------------------------- | ------------------------------------ |
| Minimum event count | ≥ 3 |
| Minimum cumulative distance | ≥ 10 px |
| Maximum average speed | ≤ 2.0 px/ms (distance / elapsed ms) |
| Minimum pause count | ≥ 1 (movement < 0.2 px over > 50 ms) |
### Browser Fingerprint
| Signal | Valid range |
| ------------------------------ | ------------- |
| `aspectRatio` (width / height) | 0.5 – 3.0 |
| `devicePixelRatio` | 0 < dpr ≤ 5.0 |
| `hardwareConcurrency` | ≥ 1 |
---
## Rate Limiting
Token bucket per `session_id`: 5 requests / 10-second window.
Stale entries evicted every 60 seconds by the cleanup task.
Rate-limited responses are indistinguishable from validation failures.
---
## SQLite Schema
The schema is extended in v0.6.0 with four new columns to persist per-session
gene mutation state. Migration is additive — columns are created when missing,
preserving compatibility with existing deployments.
```sql
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
public_key BLOB NOT NULL, -- 32-byte Ed25519 verifying key
salt BLOB NOT NULL, -- 16-byte current salt
last_hash BLOB NOT NULL, -- 32-byte Blake3 chain head
chain_length INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL, -- Unix ms
last_seen INTEGER NOT NULL, -- Unix ms
expires_at INTEGER NOT NULL, -- Unix ms
-- v0.6.0: gene mutation state
gene BLOB NOT NULL DEFAULT X'', -- Vec<u8> gene buffer
environment BLOB NOT NULL DEFAULT X'', -- Vec<(u16, u32)> env records
pending_mutation BLOB NOT NULL DEFAULT X'', -- server-issued mutation program
pending_mutation_step INTEGER NOT NULL DEFAULT 0 -- current mutation step counter
);
```
In-memory SQLite (`sqlite-in-memory`) — all sessions lost on server restart by
design. Clients re-initialise transparently on the next page load. Use
`sqlite-in-disk` for persistent sessions across restarts.
---
## Storage Backend (v0.6.0)
ChronoSeal v0.6.0 introduces selectable database backends via the `db_type`
configuration option. The default remains in-memory to preserve ephemeral
session behavior.
### Backend Options
| `db_type` | Behavior |
| ------------------ | -------------------------------------------------------------------- |
| `sqlite-in-memory` | Default. All sessions ephemeral; lost on restart. Zero disk I/O. |
| `sqlite-in-disk` | Persistent sessions. Requires `db_path`. Survives restarts. |
| `valkey` | Compatibility mode. Currently falls back to in-memory. CLI contract preserved. |
### Configuration
**Config file** (`/etc/chronoseal/config.toml`):
```toml
db_type = "sqlite-in-disk"
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
```
**Environment variable**:
```bash
CHRONOSEAL_DB_TYPE=sqlite-in-disk
CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
```
**CLI flag**:
```bash
chronoseal run --db-type sqlite-in-disk --db-path /var/lib/chronoseal/chronoseal.sqlite
```
**Inspect active backend**:
```bash
chronoseal db-type --format text
```
### Precedence
```
CLI flags > CHRONOSEAL_* environment variables > config file > defaults
```
### Migration Notes
- Schema migration is additive; new columns are created when missing on startup.
- Switching from `sqlite-in-memory` to `sqlite-in-disk` requires no code changes — only config.
- `valkey` is available in the CLI contract today; full backend support is tracked for a future release.
- Existing deployments without the v0.6.0 mutation columns will have those columns added automatically on first start.
---
## Threat Model
### In Scope
| Threat | Mitigation |
| ------------------------------------------ | --------------------------------------------------------------- |
| Playwright / Puppeteer / Selenium | Mouse entropy + behavioral validation |
| Puppeteer Stealth, undetected-chromedriver | Signature over VM execution state |
| Heartbeat replay | Hash chain + ±30s timestamp window |
| Mutation replay | mutation_step + gene_commitment parity check (v0.6.0) |
| Mutation tampering | Server recomputes candidate gene from authoritative program (v0.6.0) |
| Signature forgery | Private key isolated in WASM memory |
| Parallel session sharing | Each session bound to a unique keypair |
| Brute-forced session IDs | 256-bit random entropy |
| Flooding with fake session IDs | Rate limiter + periodic HashMap eviction |
| Traffic analysis | Uniform `{"status":"ok"}` on all failure paths |
| Malformed mutation programs | Strict parsing, length caps, underflow/unknown-opcode errors |
### Out of Scope
| Threat | Reason |
| ---------------------------------- | ---------------------------------------- |
| Real browser with real human input | Indistinguishable from a legitimate user |
| WASM reverse engineering | Obfuscation is not a security primitive |
| Server-side compromise | Outside the scope of client attestation |
ChronoSeal raises cost and complexity of automated access. It is not a
cryptographic proof of humanity and does not claim to be.
---
## Module Reference
| Path | Purpose |
| ------------------------------------- | -------------------------------------------------------------------- |
| `shared/src/protocol.rs` | Shared types: `InitRequest`, `HeartbeatRequest`, `StackState`, … |
| `shared/src/hashing.rs` | `initial_hash`, `next_chain_hash`, `hash_stack` |
| `shared/src/constants.rs` | All tunable parameters |
| `shared/src/gene.rs` | Gene model, deterministic commitment (`chronoseal/gene/v1`) [v0.6.0] |
| `shared/src/vm_extensions.rs` | Mutation opcode set, shared engine for server/WASM parity [v0.6.0] |
| `server/src/routes/init.rs` | `POST /init` handler |
| `server/src/routes/heartbeat.rs` | `POST /hb` handler |
| `server/src/session.rs` | `create_session`, `verify_heartbeat`, `validate_mutation_parity` |
| `server/src/crypto.rs` | `verify_signature` — BTreeMap canonical JSON |
| `server/src/trust.rs` | `validate_mouse` — speed, distance, pauses |
| `server/src/fingerprint.rs` | `validate` — aspect ratio, DPR, HW concurrency |
| `server/src/vm.rs` | `generate_random_program` |
| `server/src/ratelimit.rs` | `RateLimiter::check`, `evict_stale` |
| `server/src/cleanup.rs` | Background loop: expire sessions + evict rate limiter |
| `server/src/storage.rs` | SQLite init, backend selection, `current_time_ms` |
| `wasm/src/crypto.rs` | `generate_keypair`, `sign_message`, `compute_next_hash` |
| `wasm/src/vm.rs` | `run_program` — stack machine executor |
| `wasm/src/vm_extensions.rs` | `preview_mutation`, `commit_mutation` [v0.6.0] |
| `frontend/heartbeat.js` | Session init, heartbeat loop, chain advancement |
| `frontend/entropy.js` | Mouse event ring buffer, `collectEntropy` |
| `frontend/transport.js` | `sendRequest` fetch wrapper |
* browser clients can execute WASM
* heartbeats will arrive every 12–25 seconds
* session state can be safely persisted in SQLite or Valkey
* service operators want Unix-native systemd deployment and observability
+107 -245
View File
@@ -1,32 +1,37 @@
# ChronoSeal — Deployment Guide
# ChronoSeal Deployment Guide
ChronoSeal v0.6.0 is designed for production deployment as a Unix-native daemon with hardened systemd support, lightweight WASM client runtime, and flexible backend storage.
## Prerequisites
| Tool | Minimum version | Purpose |
|---|---|---|
| Rust | 1.87 stable | Server + WASM compilation |
| Rust | 1.87 stable | Server and WASM compilation |
| wasm-pack | 0.13 | WASM build and packaging |
| Docker + Compose | 24 / 2.x | Container deployment |
| nginx / NPM / HAProxy | any | TLS termination, reverse proxy |
| Docker | 24.x | Optional container deployment |
| docker-compose | 2.x | Optional local orchestration |
| systemd | 248+ | Service management |
Install Rust: https://rustup.rs
Install Rust: [https://rustup.rs](https://rustup.rs)
Install wasm-pack: `cargo install wasm-pack`
---
## Build
## Build Steps
### 1. Build the WASM module
```bash
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
wasm-pack build wasm --target web --release
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
This produces `frontend/pkg/antibot_wasm.js` and `frontend/pkg/antibot_wasm_bg.wasm`,
which are loaded by `frontend/main.js` at runtime.
This produces the browser runtime assets required by the frontend and the server static file handler.
### 2. Build the server
### 2. Build the server binary
```bash
cargo build -p server --release
@@ -34,158 +39,109 @@ cargo build -p server --release
Binary output: `target/release/server`
### 3. Build both (convenience script)
### 3. Convenience script
```bash
bash scripts/build.sh
```
---
## Running
### Development
```bash
bash scripts/dev.sh
```
Runs the server with `cargo run --release`. The server serves the `frontend/`
directory statically at `/` via tower-http `ServeDir`.
Open `http://localhost:3000` in a browser. Open DevTools console — heartbeats
should appear every 12–25 seconds. No visible UI is rendered; the protection
is entirely silent.
### Production (native binary)
```bash
cargo build -p server --release
sudo cp target/release/server /usr/local/bin/chronoseal
```
Set environment variables before running:
```bash
export RUST_LOG=info # or warn for quieter output
chronoseal
```
The server binds to `0.0.0.0:3000` by default. Place behind a reverse proxy
for TLS — do not expose port 3000 directly.
This script builds the WASM module, moves the generated package into `frontend/pkg`, and builds the server binary.
---
## systemd
## Deploying as a Native Service
### Service file
The provided `chronoseal.service` includes hardened systemd sandboxing:
```
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
SystemCallArchitectures=native
```
ChronoSeal is intended to run as a proper Unix daemon managed by systemd.
### Install
```bash
# Create a dedicated system user
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chronoseal
# Install binary and frontend
sudo cp target/release/server /usr/local/bin/chronoseal
sudo mkdir -p /opt/chronoseal/frontend
sudo cp -r frontend/ /opt/chronoseal/frontend/
sudo chown -R chronoseal:chronoseal /opt/chronoseal
# Install and enable service
sudo cp chronoseal.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now chronoseal
sudo bash scripts/install.sh
```
### Verify
This installer should perform the following tasks:
* create a system user for `chronoseal`
* install the server binary into `/usr/local/bin/chronoseal`
* install static frontend assets into `/opt/chronoseal/frontend`
* install `chronoseal.service` into `/etc/systemd/system/`
* enable and start the service
### Verify the service
```bash
sudo systemctl status chronoseal
journalctl -u chronoseal -f
sudo journalctl -u chronoseal -f
```
### Recommended runtime options
Use structured info-level logging in production:
```bash
export RUST_LOG=info
sudo systemctl restart chronoseal
```
Avoid `RUST_LOG=debug` in production because debug logs can expose internal session identifiers.
---
## Docker
## systemd Integration
### Build and run
The supplied `chronoseal.service` is designed for hardened Unix-native operation.
```bash
docker compose up -d --build
```
Recommended service options:
### docker-compose.yml overview
* `NoNewPrivileges=true`
* `PrivateTmp=true`
* `ProtectSystem=strict`
* `ProtectHome=true`
* `ProtectKernelTunables=true`
* `ProtectKernelModules=true`
* `ProtectControlGroups=true`
* `MemoryDenyWriteExecute=true`
* `RestrictRealtime=true`
* `RestrictSUIDSGID=true`
* `SystemCallArchitectures=native`
```yaml
services:
chronoseal:
build: .
restart: unless-stopped
ports:
- "3000:3000"
environment:
RUST_LOG: info
tmpfs:
- /tmp
```
The `tmpfs` mount ensures the in-memory SQLite database is never written to
disk, even if Docker's storage driver were to flush the container filesystem.
### Dockerfile stages
The Dockerfile uses a two-stage build:
1. `rust:1.87-bookworm` — compiles the server binary
2. `debian:bookworm-slim` — minimal runtime image with only `ca-certificates`
The WASM module and frontend must be built separately (wasm-pack requires a
browser toolchain not present in the server image) and mounted or copied into
the container at `/opt/chronoseal/frontend/`.
```bash
# Build WASM first
wasm-pack build wasm --target web --release
mv wasm/pkg frontend/pkg
# Then build and run the container
docker compose up -d --build
```
Or mount the pre-built frontend as a volume:
```yaml
volumes:
- ./frontend:/opt/chronoseal/frontend:ro
```
These options reduce the host attack surface and keep the daemon constrained to its required runtime privileges.
---
## Reverse Proxy
## Configuration
ChronoSeal must be served over HTTPS. The heartbeat payload contains a
timestamp; if traffic is observable in plaintext, timing attacks become
easier. TLS 1.3 is strongly recommended.
ChronoSeal reads configuration from a TOML file, environment variables, and CLI overrides. Use `chronoseal config` to validate the effective configuration.
### nginx
Example runtime configuration options:
```toml
bind = "0.0.0.0:3000"
pid_file = "/run/chronoseal.pid"
log_level = "info"
db_type = "sqlite-in-memory"
db_path = "/var/lib/chronoseal/chronoseal.db"
```
### Supported `db_type`
* `sqlite-in-memory`
* `sqlite-disk`
* `valkey`
`sqlite-in-memory` is the default and preserves ephemeral session semantics.
`sqlite-disk` will persist session state to a file specified by `db_path`.
`valkey` selects the Valkey-compatible backend mode and may be useful for future deployment scenarios.
---
## Reverse Proxy and TLS
ChronoSeal should be served over HTTPS in production. The heartbeat protocol includes timestamps and entropy data; serving that traffic in plaintext weakens security and allows easier traffic analysis.
### nginx example
```nginx
server {
@@ -197,7 +153,6 @@ server {
ssl_protocols TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Tight timeouts — heartbeat interval is 12–25s
proxy_read_timeout 35s;
proxy_send_timeout 10s;
@@ -218,129 +173,36 @@ server {
}
```
### Nginx Proxy Manager
1. Add a new Proxy Host pointing to `http://chronoseal:3000`
2. Enable SSL, Request Let's Encrypt certificate
3. Enable HTTP/2, Force SSL
4. Under Advanced, add:
```
proxy_read_timeout 35s;
proxy_send_timeout 10s;
```
### HAProxy
```haproxy
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/your.domain.pem alpn h2,http/1.1
default_backend chronoseal_back
backend chronoseal_back
server chronoseal 127.0.0.1:3000 check
timeout connect 5s
timeout server 35s
```
---
## Integration into an Existing Site
ChronoSeal is designed to run as a sidecar — its `/init` and `/hb` endpoints
can be proxied from any existing web server. The frontend assets (`pkg/`) need
to be served from the same origin as the protected page (or CORS must be
configured).
### Option A — Serve everything from ChronoSeal
ChronoSeal serves `frontend/` statically. Put your protected HTML inside
`frontend/` and let ChronoSeal serve it directly.
### Option B — Proxy only the API endpoints
Keep your existing server. Proxy `/init` and `/hb` to ChronoSeal, and serve
the WASM and JS assets from your CDN or existing static file server.
```nginx
# On your existing server:
location ~ ^/(init|hb)$ {
proxy_pass http://127.0.0.1:3000;
}
```
Add to your protected pages:
```html
<script type="module" src="/pkg/antibot_wasm.js"></script>
<script type="module" src="/main.js"></script>
```
---
## Configuration
All parameters are in `shared/src/constants.rs`. Recompile after changes.
| Constant | Default | Notes |
|---|---|---|
| `SESSION_ID_LEN` | 32 bytes | 256-bit entropy — do not reduce |
| `SALT_LEN` | 16 bytes | Per-heartbeat salt |
| `HEARTBEAT_MIN_INTERVAL_MS` | 12 000 ms | Increase to reduce server load |
| `HEARTBEAT_MAX_INTERVAL_MS` | 25 000 ms | Jitter upper bound |
| `EXPIRATION_MINUTES` | 30 min | Session TTL after last heartbeat |
| `RATE_LIMIT_COUNT` | 5 | Max heartbeats per window per session |
| `RATE_LIMIT_WINDOW_SECS` | 10 s | Rate limit window |
| `MAX_TIMESTAMP_DRIFT_MS` | 30 000 ms | Anti-replay window; account for NTP skew |
| `MIN_MOUSE_TOTAL_DIST` | 10.0 px | Lower for low-activity pages |
| `MAX_MOUSE_AVG_SPEED` | 2.0 px/ms | Raise if legitimate users are rejected |
| `MIN_PAUSE_COUNT` | 1 | Minimum natural pause events |
---
## Observability
ChronoSeal uses `tracing` with `tracing-subscriber`. Log levels:
| Level | Events |
|---|---|
| `INFO` | Server start, request method + path + status |
| `WARN` | Heartbeat validation failures (with session ID and reason) |
| `DEBUG` | Rate limit hits |
### Docker deployment
```bash
RUST_LOG=info chronoseal # production
RUST_LOG=debug chronoseal # development
RUST_LOG=warn chronoseal # minimal output
docker compose up -d --build
```
Log format is plain text to stdout. Pipe to `journald`, `fluentd`, or any
log aggregator via stdout capture.
The supplied `docker-compose.yml` is intended for local evaluation and development. It mounts `frontend/` and exposes port `3000`.
Note: build the WASM package before container startup, or mount a pre-built `frontend/pkg/` volume.
---
## Health Check
## Production Best Practices
The server has no dedicated `/health` endpoint. Use a TCP check on port 3000,
or a lightweight HTTP check on `GET /` (which serves `index.html`).
```bash
# Docker health check (add to docker-compose.yml if needed)
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3000/"]
interval: 30s
timeout: 5s
retries: 3
```
* Use TLS termination at the perimeter
* Run ChronoSeal behind a reverse proxy or firewall
* Keep `RUST_LOG` at `info` or `warn`
* Use `systemctl` for lifecycle management
* Monitor `chronoseal` metrics with Prometheus
* Place the frontend under the same origin as the protected pages or configure CORS carefully
---
## Security Checklist
## Health and Metrics
- [ ] TLS 1.3 enabled, TLS 1.0/1.1 disabled
- [ ] HTTP/2 enabled
- [ ] Port 3000 not exposed to the public internet (only via reverse proxy)
- [ ] `RUST_LOG=warn` or `info` in production (not `debug` — session IDs appear in logs)
- [ ] systemd service running as `chronoseal` user with hardened sandbox
- [ ] `MemoryDenyWriteExecute=true` in service file (prevents JIT in process)
- [ ] CORS `CorsLayer::permissive()` replaced with origin-restricted policy for production
- [ ] Frontend assets served over the same HTTPS origin as protected pages
ChronoSeal exposes runtime endpoints for health and metrics.
* `chronoseal health` — health probe
* `chronoseal metrics` — Prometheus metrics output
* `chronoseal status` — runtime status report
* `chronoseal stats` — runtime statistics
These endpoints are accessible locally from the daemon and may be proxied or scraped by monitoring infrastructure.
+44 -27
View File
@@ -1,41 +1,58 @@
# ChronoSeal Design Philosophy
**"Everything is a File" — Unix-Native Software Design**
ChronoSeal is built for operators who value clarity, stability, and Unix-native infrastructure.
ChronoSeal is intentionally built as a **first-class citizen of Linux**. The entire application is designed to behave like a well-engineered native file within the Unix filesystem.
## Core Philosophy
### Why This Philosophy Matters
ChronoSeal is a Unix-first, CLI-first cryptographic attestation daemon. It is intentionally designed to feel like infrastructure software such as `nginx`, `redis-server`, or `systemd` itself.
ChronoSeal is designed so that administrators can operate, monitor, configure, and integrate it using the same reliable, transparent, and trusted tools and patterns they already use on Linux systems — without fighting the operating environment.
### Design priorities
### Core Principles
* **Unix-native operation** — systemd integration, PID files, structured logs, and predictable lifecycle semantics.
* **CLI as source of truth** — all runtime operations available through the command line.
* **Minimal opacity** — no hidden telemetry, no opaque fingerprinting database.
* **Privacy-first** — ephemeral session state and no persistent user profiling.
* **Deterministic runtime behavior** — shared Rust/WASM implementation for the mutation engine and heartbeat protocol.
* **Incremental cost escalation** — make automation painful to scale without claiming impossible security.
* **Operational transparency** — expose health, metrics, status, and config as first-class artifacts.
- **Everything is a File**: The application must be controllable, inspectable, and composable through standard Unix interfaces (CLI, files, signals, pipes, and environment).
- **CLI as Source of Truth**: All operations — starting, stopping, configuring, monitoring, and debugging — must be possible from the command line with excellent discoverability.
- **Behave Like a Native File**: Predictable lifecycle management through commands, signals (`SIGHUP`, `SIGTERM`, `SIGUSR1`), logs, configuration files, and standard process semantics.
- **Composability**: Must work naturally with pipes, redirection, scripts, systemd, Ansible, Docker, and orchestration tools.
- **Observability by Default**: All important state and metrics should be accessible as text or structured data.
- **Minimal Friction, Maximum Durability**: One-line installer, world-class `--help`, proper man pages, and decades-long maintainability are non-negotiable.
- **Respect for the OS**: Follows Linux Filesystem Hierarchy Standard (FHS), XDG Base Directory specification, and hardened systemd practices.
## Execution Model
### Non-Goals
ChronoSeal emphasizes deterministic, stateless request validation with a lightweight server-side session store.
ChronoSeal is **not** designed to be:
- Cloud-first or vendor-specific
- Browser-first or JavaScript-heavy
- Dependency-heavy or framework-driven
- GUI-centric (any graphical interface must be a thin wrapper)
- Telemetry-oriented or privacy-invasive
- Optimized for rapid prototyping at the cost of long-term reliability
* The server persists only the small session state required for continuity.
* The client executes a deterministic WASM runtime for every heartbeat.
* The protocol is intentionally ambiguous on rejection to avoid leaking validation rules.
These non-goals help keep the project focused on stability, simplicity, security, and deep Unix integration.
## Non-Goals
### Development Mindset
ChronoSeal does not aim to be:
- Production robustness, security, and long-term sustainability take clear precedence over development speed.
- Every design decision is evaluated against one question:
**“Does this make ChronoSeal feel like it naturally belongs in `/usr/bin/`?”**
* a tracking platform
* a browser fingerprinting database
* a long-term behavioral analytics engine
* a platform for user profiling
* a SaaS or cloud-first service
This philosophy guided the complete refactoring of ChronoSeal and continues to drive all future development.
Instead, ChronoSeal aims to be an infrastructure layer that raises attacker cost while leaving legitimate users unobstructed.
**Status**: Core architecture and systemd integration completed. Rich CLI, runtime configuration system, and one-line installer are in active development.
## Operational Assumptions
ChronoSeal assumes:
* the host environment is Linux
* systemd is available for service management
* TLS is used in production
* browser clients can execute WASM
* operators can manage native binaries and configuration files
## Privacy and Trust
The project is designed so that the verification mechanism is:
* ephemeral
* difficult to reverse-engineer at scale
* not based on personal identifiers
* not dependent on long-term user history
These choices reflect the belief that the best anti-automation system is one that can be operated without becoming a surveillance platform.
+40 -252
View File
@@ -1,278 +1,66 @@
# ChronoSeal Privacy & Design Principles
# ChronoSeal Privacy Policy
## Privacy-First Browser Attestation Framework
ChronoSeal is a privacy-first cryptographic attestation system. It is intentionally designed to avoid long-term profiling, tracking, and persistent identity storage.
ChronoSeal is a lightweight, privacy-first browser attestation framework designed to resist:
## What ChronoSeal Collects
- automated bots
- AI-driven browser automation
- scripted abuse
- browser surveillance ecosystems
ChronoSeal only collects the minimum ephemeral data required to validate a live browser session:
Unlike conventional anti-bot systems, ChronoSeal is intentionally designed to operate **without collecting or storing client identity data**.
* `session_id` — ephemeral session identifier
* `prev_hash` / `initial_hash` — cryptographic chain state
* `timestamp` — heartbeat timing information
* `entropy_data` — recent mouse event samples for behavioral plausibility
* `stack_state` — VM execution result for heartbeat uniqueness
* `fingerprint` signals — basic browser sanity values such as aspect ratio, DPR, and hardware concurrency
* `mutation_step` / `gene_commitment` — synthetic mutation parity values for protocol continuity
---
## What ChronoSeal Does Not Store
# Core Philosophy
ChronoSeal does not store or persist:
ChronoSeal verifies:
* IP addresses as a core artifact
* browser history
* user identifiers
* personal data
* device fingerprint databases
* long-term behavioral profiles
* cross-session tracking records
- session continuity
- runtime coherence
- cryptographic synchronization
If you need browser telemetry or user profiling, ChronoSeal is not the right tool.
It does **not** verify:
## Session Ephemerality
- personal identity
- browsing history
- behavioral profiles
- long-term reputation
By default, ChronoSeal uses `sqlite-in-memory` storage. Sessions are ephemeral and are expected to be recreated after process restarts.
The framework is built around one principle:
Persistent state is only stored when the operator explicitly configures `sqlite-disk` or `valkey`.
> Verify live browser participation without turning users into telemetry.
## Client-Side Key Handling
---
The Ed25519 signing keypair is generated inside the WASM runtime and is never serialized or transmitted in full.
# Privacy-First By Architecture
* Private key: stays inside WASM linear memory
* Public key: transmitted once during session initialization
ChronoSeal is intentionally engineered to avoid becoming:
This design minimizes the amount of sensitive material exposed outside the browser runtime.
- a tracking platform
- a fingerprinting database
- a telemetry pipeline
- a surveillance system
## Intentional Silent Rejection
## ChronoSeal Does NOT Store
ChronoSeal intentionally returns a uniform `{"status":"ok"}` response for invalid heartbeats.
- IP addresses
- Browser history
- Persistent fingerprints
- User profiles
- Behavioral telemetry
- Tracking identifiers
- Device databases
- Long-term session history
- Cross-site correlation data
This is a privacy-preserving decision: it avoids emitting detailed rejection reasons that could be used to fingerprint or probe clients.
No client-side personal information is persisted.
## Data Retention
---
Session state is retained only as long as it is needed for heartbeat continuity.
# Stateless Trust Model
Expired sessions are purged automatically by cleanup tasks. Ephemeral backend modes do not write state to disk beyond the current process lifetime.
ChronoSeal focuses on:
## Transparency
- ephemeral runtime verification
- cryptographic continuity
- synchronized challenge progression
- live execution integrity
The source code is open and the verification model is documented. Operators can inspect exactly what ChronoSeal stores and validates.
The server only validates:
## Summary
- whether the current browser session behaves like a coherent participant *right now*
ChronoSeal is designed to provide anti-automation defense without becoming a tracking or surveillance platform.
ChronoSeal does not maintain:
- user identity databases
- reputation systems
- persistent surveillance records
---
# Anti-Bot Without Surveillance
Most modern anti-bot systems rely heavily on:
- fingerprinting
- behavioral tracking
- telemetry aggregation
- centralized analytics
ChronoSeal deliberately rejects this model.
Instead, ChronoSeal uses:
- synchronized cryptographic chains
- WASM-isolated signing
- protocol continuity
- transient verification state
This provides bot resistance while preserving user privacy.
---
# Lightweight By Design
ChronoSeal is intentionally engineered to remain:
- compact
- dependency-light
- operationally simple
- Unix-native
## Current Footprint
### Server Binary
Compiled x86_64 Linux server binary:
- ~8.4 MB
### WASM Runtime
`chronoseal_wasm_bg.wasm`
- ~218 KB
### Full WASM Package
Entire generated WASM package:
- ~720 KB
Includes:
- WASM runtime
- JavaScript glue code
- Type definitions
---
# No Frontend Framework Dependency
ChronoSeal does not depend on:
- React
- Angular
- Vue
- Electron
- Node.js runtime
- Browser bundler ecosystems
The browser runtime uses:
- native ES modules
- direct WebAssembly loading
- lightweight JavaScript glue
This minimizes:
- dependency complexity
- supply-chain risk
- build fragility
- browser overhead
---
# Clean Repository Philosophy
ChronoSeal keeps generated artefacts out of version control.
## What Is NOT Stored In The Repository
| Path | Reason |
|---|---|
| `wasm/pkg/` | Generated build output |
| `frontend/pkg/` | Generated serve-time artefacts |
| `target/` | Standard Rust build artefacts |
Generated binaries change frequently and are reproducible from source.
The repository intentionally stores:
- source code
- architecture
- reproducible build logic only
---
# Unix-Native Operational Model
ChronoSeal is designed as:
- infrastructure software
- not browser-centric SaaS
Core operational principles:
- CLI-first operation
- systemd-native deployment
- structured logs
- explicit configuration
- inspectable runtime behavior
- minimal hidden state
ChronoSeal should feel natural on Linux systems:
- simple to deploy
- easy to audit
- understandable years later
---
# Security Through Operational Asymmetry
ChronoSeal increases attacker cost through:
- synchronization burden
- runtime continuity requirements
- WASM-isolated cryptographic execution
- chained session progression
It does not attempt:
- invasive tracking
- permanent identification
- surveillance-driven scoring
---
# Design Goals
ChronoSeal prioritizes:
- Privacy
- Simplicity
- Transparency
- Operational clarity
- Long-term maintainability
- Minimalism
- Unix-native behavior
- Low deployment friction
---
# Non-Goals
ChronoSeal is intentionally NOT:
- A surveillance platform
- A telemetry collection system
- A browser fingerprinting database
- An analytics engine
- A cloud lock-in service
- A JavaScript-heavy frontend platform
- An advertising or tracking framework
---
# Summary
ChronoSeal is designed to prove:
> “A live browser session is coherently participating right now.”
without storing:
- who the user is
- where they came from
- what they previously did
It is a lightweight, privacy-preserving, Unix-native browser attestation framework focused on:
- anti-bot resistance
- anti-automation
- operational simplicity
without compromising user privacy.
It is a privacy-aware, ephemeral attestation layer with strong operational guardrails.
+92 -89
View File
@@ -1,115 +1,118 @@
# ChronoSeal v0.6.0 — Synthetic Gene Mutation System
# ChronoSeal v0.6.0 — Refactoring and System Upgrade
## Overview & Motivation
ChronoSeal v0.6.0 introduces a synthetic mutation chain model to strengthen attestation liveness and anti-replay guarantees while preserving privacy-first behavior. The core model combines:
- a primary byte-oriented gene buffer (`Vec<u8>`), and
- a bounded secondary environment map (`Vec<(u16 symbol, u32 quantity)>`).
ChronoSeal v0.6.0 is a major architecture and protocol update that transforms the project from a lightweight heartbeat service into a mature Unix-native attestation daemon with deterministic mutation parity and pluggable storage backends.
Each heartbeat now carries deterministic mutation progression evidence (`mutation_step`, `gene_commitment`) that is validated server-side against the exact server-issued mutation order. This design increases attacker workload by coupling cryptographic chain continuity with stateful deterministic mutation parity.
## Summary of Changes
## Architectural Goals
1. Keep runtime behavior deterministic across server and WASM execution.
2. Preserve ephemerality and low operational complexity.
3. Minimize additional latency on the heartbeat path.
4. Improve protocol resistance against replay and mutation tampering.
5. Maintain a maintainable codebase with explicit invariants and focused modules.
* Introduced the **Synthetic Gene Mutation Engine** for deterministic mutation parity across server and WASM.
* Added server-side validation of `mutation_step` and `gene_commitment`.
* Centralized shared protocol logic in `shared/` for server/WASM parity.
* Added support for multiple storage backend modes: `sqlite-in-memory`, `sqlite-disk`, and `valkey` compatibility.
* Hardened runtime architecture with `systemd` readiness, graceful shutdown, PID file support, and structured logging.
* Expanded CLI with rich subcommands and effective runtime configuration.
* Preserved silent rejection semantics while improving anti-replay and liveness guarantees.
## Design Decisions
1. **Shared mutation engine**
Mutation opcode semantics live in `shared/src/vm_extensions.rs` to guarantee server/client parity from one implementation.
## Why This Refactor?
2. **Deterministic gene commitment**
A domain-separated BLAKE3 commitment (`chronoseal/gene/v1`) binds both gene bytes and sorted environment records.
The previous model relied on heartbeat continuity and behavioral entropy alone. v0.6.0 strengthens the protocol by adding a second, deterministic state progression channel:
3. **Bounded mutation complexity**
Mutation program length is capped (`MAX_MUTATION_PROGRAM_BYTES`) and environment cardinality is capped (`MAX_ENV_RECORDS`).
* each heartbeat now includes a mutation step and commitment
* the server authoritatively selects the next mutation program
* the client must preview and commit the same state locally in WASM
* the server rejects any mismatch silently
4. **Strict validation on ingest**
Environment payloads are validated for sortedness, uniqueness, non-zero quantity, and length constraints.
This raises the cost of developing a successful automation attack because the attacker must now maintain both a valid chain and a valid mutation progression state.
5. **Protocol-level mutation handshake**
`InitResponse` and `Heartbeat` payloads now include mutation step/order and commitment fields.
## Core Architecture Changes
6. **DB backend control via `db_type`**
Server CLI/config now supports:
- `sqlite-in-memory` (default)
- `sqlite-in-disk` (active; uses `db_path`)
- `valkey` (active compatibility mode; currently falls back to in-memory)
### Shared Protocol Code
## Implementation Plan
1. Add gene model + deterministic commitment in `shared/gene.rs`.
2. Implement v0.6.0 mutation opcode set in shared VM extensions.
3. Persist mutation state per session (`gene`, `environment`, `pending_mutation`, `pending_mutation_step`).
4. Extend protocol schema for mutation fields in init/heartbeat exchange.
5. Validate mutation step + commitment parity before accepting heartbeat updates.
6. Add WASM preview/commit mutation lifecycle mirroring server behavior.
7. Add `db_type` CLI/config flow and runtime backend initialization strategy.
8. Add migration-safe schema extension (column existence checks + index creation).
`shared/` now contains:
## Testing Strategy (detailed section)
ChronoSeal v0.6.0 test coverage is organized across unit, integration, and randomized/fuzz-style validation.
* gene model and commitment hashing
* mutation opcode semantics
* request/response payload structures
* canonical signing support
* VM execution logic shared by server and WASM
1. **Unit, integration, and property tests**
- Unit tests for gene invariants and encoding/decoding.
- Unit tests for every mutation opcode with stack-effect assertions.
- Integration tests for full session lifecycle and heartbeat acceptance/rejection paths.
- Table-driven randomized tests and fuzz-style random bytecode tests to validate deterministic failure/success symmetry.
Moving mutation semantics into `shared/` eliminates subtle server/client divergence bugs and enables deterministic cross-runtime testing.
2. **Server-client parity testing**
- Shared opcode engine parity tests across seeded mutation sequences.
- Multi-step mutation chain test (`test_mutation_chain`) asserting identical server/client final state.
- 10+ heartbeat deterministic simulation tests in session integration suite.
### Mutation Handshake
3. **Evasion / attack simulation testing**
- Replay attack simulation.
- Mutation step mismatch rejection.
- Mutation commitment tampering rejection.
- Malformed server mutation payload rejection.
- Stack underflow / unknown opcode / truncated program rejection.
v0.6.0 adds the following data to the protocol:
4. **Performance regression testing**
- Bounded execution checks through capped program size and bounded record counts.
- Timing smoke regression test for mutation execution loops.
- End-to-end heartbeat test coverage to detect behavior regressions on hot paths.
* `mutation_step`
* `mutation_order_b64`
* `gene_commitment`
* `next_mutation_step`
* `next_mutation_order_b64`
## Security Analysis
1. **Replay resistance**
Heartbeats are now tied to both chain hash and mutation step progression.
These fields are now part of the session initialization and heartbeat exchange.
2. **Mutation tampering resistance**
Server recomputes candidate gene state from authoritative pending mutation program and rejects commitment mismatch.
### Server Session State
3. **Protocol ambiguity reduction**
Canonical signing payload includes mutation fields, reducing exploitable unsigned state.
The session schema now stores:
4. **Input hardening**
Program size limits, stack underflow checks, and strict environment decoding reduce parser abuse and malformed payload amplification.
* committed gene bytes
* committed environment records
* pending mutation order
* pending mutation step
5. **Deterministic failure semantics**
Invalid mutation instructions fail predictably and symmetrically across server and WASM paths.
The server advances this state only after a heartbeat is accepted.
## Performance Considerations
1. Mutation instructions are lightweight and mostly O(1); only `INSERT`/`DELETE` are O(n) but bounded by max gene size.
2. Environment operations use sorted-vector binary search with tight upper bound (`MAX_ENV_RECORDS`).
3. Commitment hashing is linear in gene size and record count, both bounded.
4. Shared engine avoids duplicate logic and divergence-induced debugging overhead.
### Deterministic WASM Preview
## Migration & Backward Compatibility
1. Schema migration is additive; new columns are created when missing.
2. Existing deployments without mutation fields require updated client+server pair for heartbeat compatibility.
3. `db_type` defaults to in-memory to preserve ephemeral behavior.
4. `sqlite-in-disk` is now directly usable via `db_path`.
5. `valkey` currently runs in compatibility mode (in-memory fallback) to avoid startup failure while preserving CLI contract.
The WASM runtime exposes:
## Risks & Mitigations
1. **Risk: State divergence between server and client**
Mitigation: shared opcode engine + deterministic seeded parity tests + multi-heartbeat integration tests.
* `init_gene_state()`
* `preview_gene_commitment()`
* `commit_gene_preview()`
* `discard_gene_preview()`
* `current_gene_commitment()`
2. **Risk: Mutation opcode abuse via malformed programs**
Mitigation: strict parsing, length caps, explicit underflow/unknown-opcode errors.
This makes the client-side mutation lifecycle explicit and deterministic.
3. **Risk: Performance regressions**
Mitigation: bounded structures, smoke timing tests, and focused hot-path validation.
### Backend Abstraction
4. **Risk: Backend confusion during `db_type` rollout**
Mitigation: explicit CLI command (`chronoseal db-type`), config output visibility, and clear runtime compatibility behavior.
The server runtime now supports a configurable `db_type`.
* `sqlite-in-memory` — default runtime storage with ephemeral session semantics
* `sqlite-disk` — persistent SQLite storage for stateful deployments
* `valkey` — compatibility mode for alternative storage backends
This abstraction makes ChronoSeal easier to operate in both stateless and stateful environments.
### CLI and Service Integration
v0.6.0 improves the CLI surface with operational commands and service introspection.
* `chronoseal run`
* `chronoseal status`
* `chronoseal health`
* `chronoseal config`
* `chronoseal metrics`
* `chronoseal stats`
* `chronoseal db-type`
* `chronoseal completion`
* `chronoseal version`
The runtime now includes PID file handling and graceful termination.
## Testing and Validation
The refactor includes extensive tests for:
* server/WASM parity across mutation sequences
* malformed mutation payload rejection
* replay attack rejection
* mutation step mismatch rejection
* stateful session update semantics
* runtime database mode validation
The codebase now supports deterministic table-driven tests and fuzz-style random program validation.
## Operational Impact
This release makes ChronoSeal suitable for production deployment in Linux environments and for integration into existing web application stacks.
The combination of deterministic mutation parity and shared protocol implementation improves both security and maintainability.
+89 -157
View File
@@ -1,205 +1,137 @@
# ChronoSeal — Threat Model
# ChronoSeal Threat Model
ChronoSeal is a cost-raising cryptographic attestation daemon. It increases the burden on automated clients while preserving privacy, determinism, and operational transparency.
## Purpose
This document defines what ChronoSeal is designed to protect against, what
it explicitly does not protect against, and the reasoning behind each
design decision in security terms.
ChronoSeal protects web resources by making browser automation and replay attacks more expensive and fragile. It is not intended to be a perfect bot blocker.
ChronoSeal is a **cost-raising mechanism**. It does not claim to make
automated access impossible. It makes automated access expensive, complex
to maintain, and operationally fragile at scale.
## Protected Assets
---
## Assets Being Protected
| Asset | Description |
| Asset | Protection focus |
|---|---|
| Web page content | HTML, rendered data, scraped text |
| API responses | JSON endpoints that serve structured data |
| Server compute | CPU and bandwidth consumed by automated clients |
| Rate-limited resources | Endpoints with per-user quotas |
| Behavioral analytics | Metrics polluted by bot traffic |
---
| Page content | Prevent automated scraping and replay of protected content |
| API responses | Reduce scripted access to sensitive endpoints |
| Server compute | Increase attacker resource costs |
| Session continuity | Enforce live session progression |
| Behavioral integrity | Validate plausible browser activity |
## Attacker Profiles
### Level 1 — Script Kiddie / Commodity Scraper
### Level 1 — Commodity Scraper
**Tools:** `curl`, `requests`, `scrapy`, simple HTTP clients.
**Capability:** No browser environment. Cannot execute JavaScript or WASM.
**ChronoSeal response:** Session never initialises. No `session_id` is ever
presented to `/hb`. Content gated behind session validation is never served.
* Tools: `curl`, `requests`, headless HTTP clients
* Capability: no WASM execution, no browser engine
ChronoSeal response:
* cannot initialize a session
* no `session_id` is produced
* content remains protected behind the attestation layer
### Level 2 — Headless Browser Operator
**Tools:** Playwright, Puppeteer, Selenium, undetected-chromedriver.
**Capability:** Full browser environment. Can execute JavaScript and WASM.
Cannot easily synthesise realistic mouse entropy or maintain hash chain state
across concurrent sessions.
**ChronoSeal response:** Mouse entropy validation rejects absent or synthetic
movement. Hash chain requires per-session state synchronisation. Scaling to
hundreds of concurrent sessions requires proportional infrastructure.
* Tools: Playwright, Puppeteer, Selenium
* Capability: browser engine available, but automation is not indistinguishable from a real user
ChronoSeal response:
* mouse entropy and pause checks become active barriers
* hash chain continuity requires per-session state tracking
* synthetic heartbeats become expensive to maintain at scale
### Level 3 — Stealth Automation
**Tools:** Puppeteer Stealth, rebrowser-patches, custom CDP clients with
evasion patches.
**Capability:** Patches `navigator.webdriver`, spoofs browser fingerprints,
can inject synthetic mouse events. May partially pass behavioral checks.
**ChronoSeal response:** Ed25519 signature over the full payload (including
behavioral state and VM execution result) means the attacker must also
correctly execute the WASM program and maintain chain continuity. The private
key is generated fresh per page load and never exposed — it cannot be
extracted from a legitimate session and reused.
* Tools: browser stealth plugins, CDP patching, synthetic event injection
* Capability: can execute JavaScript and WASM, may spoof some browser signals
### Level 4 — Sophisticated Adversary
ChronoSeal response:
**Tools:** Full browser farm with real input devices, WASM reverse engineering,
custom chain maintenance infrastructure.
**Capability:** Can pass all current ChronoSeal checks given sufficient
engineering effort.
**ChronoSeal response:** Significantly increases operational cost. A browser
farm with real input devices costs orders of magnitude more than a commodity
scraper fleet. ChronoSeal is not designed to stop this attacker — no client-
side protection can.
* signature, hash chain, and mutation commitment require correct WASM execution
* private key is generated per page load and never exposes raw key material
* silent rejection hides validation rules from attacker feedback
---
### Level 4 — Sophisticated Operator
* Tools: real browser farms, hardware input devices, custom chain management
* Capability: high engineering investment and real device scale
ChronoSeal response:
* significantly increases operational cost and complexity
* forces a full protocol implementation rather than best-effort scraping
* is not designed to stop such adversaries completely
## Attack Vectors and Mitigations
### Replay Attack
**Attack:** Capture a valid heartbeat payload and retransmit it.
**Mitigation:**
- Timestamp window (±30 seconds): replayed payloads are rejected after 30s.
- Hash chain: each heartbeat must present `H(n-1)` matching the server's
stored state. A replayed heartbeat presents a stale hash that no longer
matches after one successful heartbeat has advanced the chain.
**Attack:** resend a previously observed heartbeat.
**Mitigations:**
* timestamp window enforcement (±30 seconds)
* chained Blake3 hash continuity
* server-issued salt rotation
* mutation step progression
### Signature Forgery
**Attack:** Construct a valid-looking heartbeat payload without the private key.
**Mitigation:** Ed25519 with 128-bit security. The private key is generated
inside WASM `thread_local` memory, never serialised, never passed to
JavaScript, never transmitted. Forgery requires breaking Ed25519 or
extracting the key from WASM memory — neither is practical.
**Attack:** forge a heartbeat without the private key.
### Key Extraction
**Mitigations:**
**Attack:** Inspect WASM linear memory to extract the private signing key.
**Mitigation:** The key is stored in a Rust `thread_local! { RefCell<Option<SigningKey>> }`.
It has no exported symbol and is not referenced by any exported WASM function
that returns raw memory. An attacker with full DevTools access to the WASM
memory can extract it from one session, but it is useless for other sessions
(fresh keypair per page load) and expires with the session.
* Ed25519 signature over the canonical payload
* private key generated and stored inside WASM memory only
* signature verification occurs on every heartbeat
### Hash Chain Forgery
### Mutation Tampering
**Attack:** Compute a valid `H(n)` without the server-side salt.
**Mitigation:** Each chain link incorporates `saltₙ₋₁`, which is a 16-byte
random value known only to the server and returned (once) in the heartbeat
response. An attacker cannot compute `H(n+1)` without first receiving
`saltₙ` from a successful heartbeat response, which requires a valid signature
and all other checks to pass.
**Attack:** send an invalid or stale mutation commitment.
**Mitigations:**
* server recomputes the gene commitment from server-authored mutation orders
* heartbeat request includes `mutation_step` and `gene_commitment`
* mismatched commitment causes silent rejection
### Session Hijacking
**Attack:** Steal a `session_id` and use it from a different client.
**Mitigation:** `session_id` alone is insufficient — the attacker also needs
the private key (to produce valid signatures) and the current chain state
(to present the correct `prev_hash`). All three are required simultaneously.
**Attack:** steal a valid `session_id` and reuse it.
### Enumeration of Validation Rules
**Mitigations:**
**Attack:** Send malformed heartbeats and analyse error responses to map
validation logic.
**Mitigation:** All failure paths return `{"status":"ok"}` with no `next_salt`.
There is no error code, no error message, and no status difference between
a rate limit hit, an invalid signature, a broken chain, and a behavioral
rejection.
* `session_id` alone is insufficient
* attacker also needs current `prev_hash` and private key
* keypair is generated per browser session in WASM
### DoS via Session Flooding
### Fingerprint Enumeration
**Attack:** Open thousands of sessions to exhaust the rate limiter's HashMap
memory.
**Mitigation:** Rate limiter entries are evicted every 60 seconds by the
cleanup task. Each entry is a small `(u32, Instant)` tuple; even at 100,000
concurrent fake sessions, the HashMap occupies roughly 10–15 MB, which is
well within normal server memory budgets. Sessions themselves expire after 30
minutes of inactivity and are purged from SQLite.
**Attack:** probe the API with malformed requests to discover validation logic.
### Clock Manipulation
**Mitigations:**
**Attack:** Manipulate the client's `Date.now()` to bypass the timestamp
window.
**Mitigation:** The timestamp is included in the signed payload. Manipulating
it requires also forging the signature. The server validates against its own
clock — client-side clock manipulation cannot help without the private key.
* all invalid heartbeats return `{"status":"ok"}`
* no explicit error messages are exposed
* silent rejection removes oracle behavior
### Synthetic Mouse Events
## Limitations
**Attack:** Inject programmatic `mousemove` events via `dispatchEvent` or
CDP input simulation.
**Mitigation:** Synthetic events often fail the pause check (no natural dwell
periods), produce unrealistically uniform speed profiles, or fail the minimum
distance threshold. Generating convincingly human mouse traces at scale
requires either real input devices or sophisticated probabilistic models —
both significantly increase operational cost.
ChronoSeal does not protect against:
---
## What ChronoSeal Does Not Protect Against
| Limitation | Explanation |
|---|---|
| Real browsers with real users acting as bots | A human operating a browser manually is indistinguishable from a legitimate visitor. ChronoSeal cannot address this. |
| Server-side vulnerabilities | ChronoSeal is a client attestation layer. It does not protect the server from injection, authentication bypass, or other backend vulnerabilities. |
| Highly resourced nation-state actors | Out of scope for a client-side protection layer. |
| Content visible before session establishment | If the protected content is rendered before the first heartbeat, it can be scraped without a session. Gate content on session validity server-side. |
| Perfect bot prevention | No client-side mechanism can be. WASM can be reverse engineered. ChronoSeal raises cost, not an impenetrable barrier. |
---
* real users intentionally acting as bots
* server-side application vulnerabilities
* full browser farm operators with real input devices
* persistent fingerprinting or identity profiling
* pre-signed session payload reuse after a legitimate success if the attacker also has the current salt and key
## Operational Security Notes
### Log Level
* Do not use `RUST_LOG=debug` in production; it may expose internal identifiers.
* Always serve ChronoSeal traffic over HTTPS.
* Use `sqlite-in-memory` for ephemeral sessions when persistence is not required.
* Use `sqlite-disk` or `valkey` when session state needs to survive restarts.
Do not run with `RUST_LOG=debug` in production. The debug log includes
`session_id` values, which are sensitive identifiers. Use `warn` or `info`.
## Disclosure
### CORS Policy
The default `CorsLayer::permissive()` is suitable for development only.
In production, restrict allowed origins to your own domain:
```rust
CorsLayer::new()
.allow_origin("https://your.domain.com".parse::<HeaderValue>().unwrap())
.allow_methods([Method::POST])
.allow_headers([header::CONTENT_TYPE])
```
### TLS
Serve exclusively over TLS 1.3. The heartbeat payload contains timestamps
and behavioral signals. While each payload is signed and cannot be forged,
plaintext transmission leaks behavioral patterns and timing information that
could assist a sophisticated attacker.
### In-Memory SQLite
All session state is lost on server restart. This is intentional — there is
no persistent state to steal. Clients transparently re-initialise. If your
deployment restarts frequently (e.g. rolling deploys), sessions will be lost
more often; tune `HEARTBEAT_MIN_INTERVAL_MS` and `EXPIRATION_MINUTES`
accordingly so clients recover quickly.
---
## Security Disclosure
See [SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy
and contact details.
See [SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy.
+70 -238
View File
@@ -1,301 +1,133 @@
# ChronoSeal — WASM Build Guide
# ChronoSeal WASM Build Guide
## Overview
ChronoSeal uses a Rust-based WASM runtime to power browser-side attestation logic, signing, hash chaining, VM execution, and mutation commitment preview.
The client-side cryptographic core of ChronoSeal is written in Rust and
compiled to WebAssembly (WASM). The JavaScript frontend (`heartbeat.js`)
imports functions from this WASM module to generate keypairs, sign heartbeat
payloads, compute hash chain links, and execute the stack machine program.
## Why WASM
The import line in `heartbeat.js`:
The WASM runtime provides a deterministic, sandboxed environment for the following tasks:
```js
import init, { generate_keypair, sign_message, compute_next_hash, run_program }
from './pkg/antibot_wasm.js';
```
* generate Ed25519 keypairs in-browser
* sign canonical heartbeat payloads
* execute randomized VM opcode programs
* compute Blake3 hash chain progression
* preview and commit synthetic gene mutations
`./pkg/antibot_wasm.js` is a **generated file**. It does not exist in the
repository and must be produced by building the `wasm/` crate before running
the server.
This enables server/client parity and prevents the private key from leaving the browser runtime.
---
## Build Requirements
## How the WASM Module is Built
The tool that compiles Rust to WASM and generates the JavaScript glue is
[`wasm-pack`](https://rustwasm.github.io/wasm-pack/).
When you run:
```bash
wasm-pack build wasm --target web --release
```
wasm-pack does the following in sequence:
1. Compiles `wasm/src/lib.rs` (and its submodules) to a `.wasm` binary using
the `wasm32-unknown-unknown` target.
2. Runs `wasm-bindgen` to inspect every `#[wasm_bindgen]`-annotated function
and struct and generate a JavaScript wrapper for each one.
3. Optionally runs `wasm-opt` (from Binaryen) to size-optimise the binary.
4. Writes all output to `wasm/pkg/`.
---
## Output: `wasm/pkg/`
After a successful build, `wasm/pkg/` contains:
```
wasm/pkg/
├── antibot_wasm.js ← ES module; the file heartbeat.js imports
├── antibot_wasm_bg.wasm ← compiled WASM binary (~300–800 KB release)
├── antibot_wasm_bg.js ← internal memory bridge (do not import directly)
├── antibot_wasm.d.ts ← TypeScript type declarations
├── antibot_wasm_bg.d.ts ← TypeScript declarations for the bg module
└── package.json
```
### `antibot_wasm.js`
This is the public entry point. It contains:
- An `init()` function that fetches and instantiates the `.wasm` binary.
- One JavaScript wrapper function for each `#[wasm_bindgen]` export in
`wasm/src/`:
| Rust export | JS wrapper | Description |
|---|---|---|
| `generate_keypair()` | `generate_keypair()` | Generate Ed25519 keypair; return hex public key |
| `get_public_key()` | `get_public_key()` | Return hex public key, or `""` if not initialised |
| `sign_message(msg)` | `sign_message(msg)` | Sign string; return hex signature, or `""` if not initialised |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `compute_next_hash(...)` | Compute next Blake3 chain hash |
| `run_program(b64)` | `run_program(b64)` | Execute base64 VM program; return `{ stack, ip }` |
### `antibot_wasm_bg.wasm`
The compiled binary. The `.bg` suffix means "background" — this is the raw
WASM that `antibot_wasm.js` loads internally. You should not reference this
file directly in your HTML.
---
## Step-by-Step Build
### 1. Install the Rust WASM target
Install the Rust WASM target and `wasm-pack`:
```bash
rustup target add wasm32-unknown-unknown
```
This is a one-time step. Without it, the Rust compiler cannot produce WASM
output.
### 2. Install wasm-pack
```bash
cargo install wasm-pack
```
Or via the installer script:
```bash
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
Verify:
```bash
wasm-pack --version
# wasm-pack 0.13.x
```
### 3. Build the WASM module
## Build the WASM Module
From the project root:
From the repository root:
```bash
wasm-pack build wasm --target web --release
```
`--target web` produces an ES module (`import`/`export` syntax) suitable for
use directly in a browser without a bundler. Other targets (`bundler`,
`nodejs`, `no-modules`) produce different output formats and are not
compatible with the ChronoSeal frontend as written.
`--release` enables Rust's release optimisations (inlining, dead code
elimination, size reduction). Omit it during development for faster builds
and better panic messages.
### 4. Move the output to the frontend
```bash
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
The frontend expects the WASM module at `frontend/pkg/antibot_wasm.js`
because `heartbeat.js` imports from `./pkg/antibot_wasm.js` relative to
the `frontend/` directory, which is where the server's static file handler
is rooted.
`--target web` produces an ES module compatible with the existing frontend JavaScript.
---
`--release` enables optimizations for runtime performance and size.
## Using the Build Script
## Output
The convenience script at `scripts/build.sh` performs all steps in order:
After a successful build, `frontend/pkg/` contains:
```bash
bash scripts/build.sh
```
* `antibot_wasm.js`
* `antibot_wasm_bg.wasm`
* `antibot_wasm_bg.js`
* `antibot_wasm.d.ts`
* `antibot_wasm_bg.d.ts`
* `package.json`
This builds the WASM module, moves it to `frontend/pkg/`, and then builds
the server binary. Run this for a clean full build before deployment.
The frontend expects the WASM package under `frontend/pkg/`.
For development iteration where you are only changing Rust WASM code:
## Runtime Exports
```bash
wasm-pack build wasm --target web # (omit --release for speed)
rm -rf frontend/pkg && mv wasm/pkg frontend/pkg
```
The WASM module exports the following functions:
For development where you are only changing server code:
* `generate_keypair()` — generate a new Ed25519 keypair and return public key hex
* `get_public_key()` — return the current public key hex
* `sign_message(msg)` — sign a UTF-8 payload and return the hex signature
* `compute_next_hash(prev, ts, entropy, stack, salt)` — compute the next Blake3 chain hash
* `run_program(b64)` — execute a base64 VM program and return stack state
* `init_gene_state(gene_size)` — initialise the synthetic gene buffer
* `preview_gene_commitment(order_b64)` — preview the next gene commitment from a mutation order
* `commit_gene_preview()` — commit the previewed mutation after successful heartbeat
* `discard_gene_preview()` — discard the previewed mutation after rejection or error
* `current_gene_commitment()` — return the current committed gene commitment
```bash
cargo build -p server
```
## Browser Integration
---
## How `heartbeat.js` Loads the Module
`heartbeat.js` uses a standard ES module dynamic import pattern:
The frontend imports the generated module like this:
```js
import init, { generate_keypair, sign_message, compute_next_hash, run_program }
from './pkg/antibot_wasm.js';
export async function initHeartbeat() {
// 1. Fetch and instantiate the .wasm binary
await init();
// 2. Generate keypair — private key stored in WASM memory only
const pubKeyHex = generate_keypair();
// 3. Send public key to server, receive session_id and chain seed
// ...
}
import init, {
generate_keypair,
sign_message,
compute_next_hash,
run_program,
init_gene_state,
preview_gene_commitment,
commit_gene_preview,
discard_gene_preview,
current_gene_commitment
} from './pkg/antibot_wasm.js';
```
`init()` is the default export from `antibot_wasm.js`. It fetches
`antibot_wasm_bg.wasm` (from the same `pkg/` directory) via `fetch()`,
compiles it in the browser's WASM engine, and links it to the JS glue
layer. After `await init()` returns, all the named exports
(`generate_keypair`, `sign_message`, etc.) are ready to call.
`await init()` must be called before invoking any other exported function.
The `init()` call must complete before any other WASM function is called.
Calling `sign_message()` or `compute_next_hash()` before `await init()`
returns will produce an empty string (the module is not yet instantiated).
## Deployment Note
---
## Serving the WASM Binary
Browsers require WASM files to be served with the correct MIME type:
The `.wasm` binary must be served with the correct MIME type:
```
Content-Type: application/wasm
```
Most web servers set this automatically for `.wasm` files. If you see the
error:
The built-in Axum static file handler already sets the appropriate MIME type for `.wasm` files.
```
WebAssembly.instantiate(): Response has unsupported MIME type
```
## Build Script
Add the MIME type to your server configuration:
**nginx:**
```nginx
types {
application/wasm wasm;
}
```
**Apache `.htaccess`:**
```apache
AddType application/wasm .wasm
```
The Axum `ServeDir` handler used by ChronoSeal's built-in static server
sets the correct MIME type automatically via `tower-http`.
---
## What Is Not in the Repository
| Path | Why excluded |
|---|---|
| `wasm/pkg/` | Generated build output — changes on every build |
| `frontend/pkg/` | Same generated output, moved to serve location |
| `target/` | Standard Rust build artefacts |
Both `wasm/pkg/` and `frontend/pkg/` are listed in `.gitignore`. Committing
them would bloat the repository (the `.wasm` binary alone is 300–800 KB),
create noisy diffs on every rebuild, and give a false impression that the
WASM module is pre-built and ready to use without a build step.
---
## Troubleshooting
### `wasm32-unknown-unknown` target not found
```
error[E0463]: can't find crate for `std`
```
Fix:
Use the convenience script:
```bash
rustup target add wasm32-unknown-unknown
bash scripts/build.sh
```
### `wasm-pack` not found
This builds the WASM package, moves it into `frontend/pkg/`, and builds the server binary.
## Recommended Development Flow
* For WASM-only changes:
```bash
cargo install wasm-pack
wasm-pack build wasm --target web
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
### `wasm-opt` not found (warning, not an error)
wasm-pack prints a warning if `wasm-opt` is not installed. The build still
succeeds; the binary is just not size-optimised.
* For server-only changes:
```bash
# On Debian/Ubuntu/Arch
sudo apt install binaryen # Debian/Ubuntu
sudo pacman -S binaryen # Arch
cargo build -p server
```
### `antibot_wasm_bg.wasm` fetch fails (404)
## Notes
The `.wasm` file is not being served from `frontend/pkg/`. Verify:
```bash
ls /mnt/Programs/ChronoSeal/frontend/pkg/
# Should list: antibot_wasm.js antibot_wasm_bg.wasm ...
```
If the directory is empty or missing, re-run the build steps above.
### MIME type error in browser
See the "Serving the WASM Binary" section above.
### `sign_message` or `generate_keypair` returns empty string
The WASM keypair has not been initialised. Ensure `await init()` and
`generate_keypair()` are called (and awaited) before any other WASM
function. Check the browser console for any errors during `init()`.
Generated files in `wasm/pkg/` and `frontend/pkg/` are not tracked in source control.
They are build artifacts and should be regenerated as part of the release workflow.
+1
View File
@@ -30,3 +30,4 @@ hex = "0.4"
base64 = "0.22"
rand = "0.8"
ed25519-dalek = "2"
valkey = "0.0.0-alpha5"
+3 -9
View File
@@ -5,16 +5,10 @@ pub async fn cleanup_loop(state: Arc<AppState>) {
loop {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
// Evict expired sessions from SQLite.
// Evict expired sessions from the configured storage backend.
{
if let Ok(conn) = state.db_pool.get() {
let now = crate::storage::current_time_ms();
let _ = conn.execute(
"DELETE FROM sessions WHERE expires_at < ?1",
rusqlite::params![now],
);
} else {
tracing::error!("Failed to get database connection from pool for cleanup");
if let Err(err) = state.db_pool.delete_expired_sessions() {
tracing::error!("Failed to evict expired sessions: {}", err);
}
}
+4
View File
@@ -123,6 +123,10 @@ pub struct RunArgs {
/// Optional structured JSON log file.
#[arg(long, env = "CHRONOSEAL_LOG_FILE")]
pub log_file: Option<PathBuf>,
/// Number of mutation rounds to execute per program.
#[arg(long, env = "CHRONOSEAL_MUTATION_ROUNDS")]
pub mutation_rounds: Option<u8>,
}
#[derive(Debug, Clone, Args)]
+26
View File
@@ -46,6 +46,7 @@ pub struct Config {
pub min_pause_count: u32,
pub require_mouse_activity: bool,
pub gene_size: usize,
pub mutation_rounds: u8,
}
impl Default for Config {
@@ -68,6 +69,7 @@ impl Default for Config {
min_pause_count: 1,
require_mouse_activity: true,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
mutation_rounds: shared::constants::DEFAULT_MUTATION_ROUNDS,
}
}
}
@@ -118,6 +120,9 @@ impl Config {
if let Some(log_file) = &args.log_file {
self.log_file = Some(log_file.clone());
}
if let Some(mutation_rounds) = args.mutation_rounds {
self.mutation_rounds = mutation_rounds;
}
}
pub fn validate(&self) -> Result<(), ConfigError> {
@@ -132,6 +137,11 @@ impl Config {
size: self.gene_size,
});
}
if !(1..=shared::constants::MAX_MUTATION_ROUNDS).contains(&self.mutation_rounds) {
return Err(ConfigError::InvalidMutationRounds {
rounds: self.mutation_rounds,
});
}
Ok(())
}
@@ -214,6 +224,11 @@ impl Config {
self.gene_size = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_MUTATION_ROUNDS") {
if let Ok(val) = value.parse() {
self.mutation_rounds = val;
}
}
}
}
@@ -234,6 +249,9 @@ pub enum ConfigError {
InvalidGeneSize {
size: usize,
},
InvalidMutationRounds {
rounds: u8,
},
}
impl std::fmt::Display for ConfigError {
@@ -253,6 +271,13 @@ impl std::fmt::Display for ConfigError {
shared::constants::MAX_GENE_SIZE
)
}
Self::InvalidMutationRounds { rounds } => {
write!(
f,
"invalid mutation rounds {rounds}; expected 1..={}",
shared::constants::MAX_MUTATION_ROUNDS
)
}
}
}
}
@@ -316,6 +341,7 @@ mod tests {
db_path: None,
frontend_dir: None,
log_file: None,
mutation_rounds: None,
};
cfg.apply_run_args(&args);
assert_eq!(cfg.db_type, DbType::SqliteInDisk);
+6
View File
@@ -14,6 +14,9 @@ pub enum SessionError {
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Storage error: {0}")]
Storage(String),
#[error("R2D2 pool error: {0}")]
Pool(#[from] r2d2::Error),
@@ -48,6 +51,9 @@ pub enum VerificationError {
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Storage error: {0}")]
Storage(String),
#[error("Hex decoding error: {0}")]
Hex(#[from] hex::FromHexError),
+8 -20
View File
@@ -29,22 +29,7 @@ pub async fn handler(
}
let config = state.get_config();
let conn = match state.db_pool.get() {
Ok(c) => c,
Err(e) => {
tracing::error!("Db pool error: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(HeartbeatResponse {
status: "error".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
);
}
};
match crate::session::verify_heartbeat(&conn, &config, &payload) {
match crate::session::verify_heartbeat(&state.db_pool, &config, &payload) {
Ok(result) => (
StatusCode::OK,
Json(HeartbeatResponse {
@@ -145,7 +130,11 @@ mod tests {
hardware_concurrency: 8,
},
mutation_step,
gene_commitment: shared::gene::commitment_hex(&candidate),
gene_commitment: shared::gene::commitment_hex_with_context(
&candidate,
&init.session_id,
mutation_step,
),
signature: String::new(),
};
sign_request(sk, &mut req);
@@ -157,7 +146,7 @@ mod tests {
) -> (Arc<AppState>, InitResponse, SigningKey) {
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
let state = Arc::new(AppState {
db_pool: pool,
db_pool: pool.clone(),
rate_limiter: tokio::sync::Mutex::new(crate::ratelimit::RateLimiter::new()),
config: std::sync::RwLock::new(config.clone()),
});
@@ -165,8 +154,7 @@ mod tests {
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let conn = state.db_pool.get().unwrap();
let init = crate::session::create_session(&conn, &config, &pk_hex).unwrap();
let init = crate::session::create_session(&pool, &config, &pk_hex).unwrap();
(state, init, sk)
}
+1 -2
View File
@@ -9,7 +9,6 @@ pub async fn handler(
Json(payload): Json<InitRequest>,
) -> Result<Json<InitResponse>, SessionError> {
let config = state.get_config();
let conn = state.db_pool.get()?;
let resp = crate::session::create_session(&conn, &config, &payload.public_key)?;
let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key)?;
Ok(Json(resp))
}
+18 -31
View File
@@ -204,14 +204,7 @@ pub fn db_type_report() -> DbTypeReport {
}
fn init_db_pool(config: &Config) -> Result<storage::DbPool, Box<dyn std::error::Error>> {
match config.db_type {
crate::config::DbType::SqliteInMemory => storage::init_pool(Path::new(":memory:")),
crate::config::DbType::SqliteInDisk => storage::init_pool(&config.db_path),
crate::config::DbType::Valkey => {
warn!("db_type=valkey selected; using sqlite-in-memory compatibility mode in v0.6.0");
storage::init_pool(Path::new(":memory:"))
}
}
storage::DbPool::init(config)
}
pub fn probe_health(config: &Config) -> HealthReport {
@@ -278,11 +271,9 @@ async fn health_handler() -> impl IntoResponse {
async fn stats_handler(
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<Json<StoreStats>, (StatusCode, String)> {
let db = state
state
.db_pool
.get()
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
storage::stats(&db)
.stats()
.map(Json)
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
}
@@ -290,11 +281,9 @@ async fn stats_handler(
async fn metrics_handler(
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<String, (StatusCode, String)> {
let db = state
state
.db_pool
.get()
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
storage::stats(&db)
.stats()
.map(|stats| {
format!(
"# HELP chronoseal_sessions Active ChronoSeal sessions\n# TYPE chronoseal_sessions gauge\nchronoseal_sessions {}\n# HELP chronoseal_expired_sessions Expired sessions not yet removed\n# TYPE chronoseal_expired_sessions gauge\nchronoseal_expired_sessions {}\n# HELP chronoseal_max_chain_length Maximum heartbeat chain length\n# TYPE chronoseal_max_chain_length gauge\nchronoseal_max_chain_length {}\n",
@@ -430,6 +419,7 @@ mod tests {
min_pause_count: 0,
require_mouse_activity: false,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
mutation_rounds: shared::constants::DEFAULT_MUTATION_ROUNDS,
}
}
@@ -445,11 +435,10 @@ mod tests {
fn test_init_db_pool_sqlite_in_memory() {
let config = base_config();
let pool = init_db_pool(&config).unwrap();
let conn = pool.get().unwrap();
let count: u64 = conn
.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 0);
assert_eq!(stats.expired_sessions, 0);
assert_eq!(stats.max_chain_length, 0);
}
#[test]
@@ -459,11 +448,10 @@ mod tests {
config.db_path = std::path::PathBuf::from("/tmp/chronoseal-db-type-disk.sqlite");
let _ = std::fs::remove_file(&config.db_path);
let pool = init_db_pool(&config).unwrap();
let conn = pool.get().unwrap();
let count: u64 = conn
.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 0);
assert_eq!(stats.expired_sessions, 0);
assert_eq!(stats.max_chain_length, 0);
}
#[test]
@@ -471,10 +459,9 @@ mod tests {
let mut config = base_config();
config.db_type = crate::config::DbType::Valkey;
let pool = init_db_pool(&config).unwrap();
let conn = pool.get().unwrap();
let count: u64 = conn
.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 0);
assert_eq!(stats.expired_sessions, 0);
assert_eq!(stats.max_chain_length, 0);
}
}
+114 -147
View File
@@ -15,7 +15,6 @@ impl AppState {
}
use crate::{crypto, fingerprint, storage, trust, vm};
use rusqlite::params;
use shared::{
gene::{self, GeneState},
protocol::{HeartbeatRequest, InitResponse},
@@ -30,7 +29,7 @@ pub struct HeartbeatVerificationResult {
}
pub fn create_session(
conn: &rusqlite::Connection,
db: &storage::DbPool,
config: &crate::config::Config,
pub_key_hex: &str,
) -> Result<InitResponse, crate::errors::SessionError> {
@@ -56,25 +55,23 @@ pub fn create_session(
let initial_mutation = vm_extensions::generate_order(1, config.gene_size);
let initial_mutation_b64 = vm_extensions::encode_order_b64(&initial_mutation);
conn.execute(
"INSERT INTO sessions (
session_id, public_key, salt, last_hash, chain_length, created_at, last_seen, expires_at,
gene, environment, pending_mutation, pending_mutation_step
) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
session_id,
pub_key,
salt.to_vec(),
initial_hash,
now,
now,
expires_at,
gene_state.gene,
environment_blob,
initial_mutation.program,
initial_mutation.step,
],
)?;
let record = storage::SessionRecord {
session_id: session_id.clone(),
public_key: pub_key,
salt: salt.to_vec(),
last_hash: initial_hash.clone(),
chain_length: 1,
created_at: now,
last_seen: now,
expires_at,
gene: gene_state.gene,
environment: environment_blob,
pending_mutation: initial_mutation.program,
pending_mutation_step: initial_mutation.step,
};
db.insert_session(&record)
.map_err(|err| crate::errors::SessionError::Storage(err.to_string()))?;
Ok(InitResponse {
session_id,
@@ -91,85 +88,55 @@ pub fn create_session(
}
pub fn verify_heartbeat(
conn: &rusqlite::Connection,
db: &storage::DbPool,
config: &crate::config::Config,
req: &HeartbeatRequest,
) -> Result<HeartbeatVerificationResult, crate::errors::VerificationError> {
let mut stmt = conn.prepare(
"SELECT public_key, salt, last_hash, expires_at, gene, environment, pending_mutation, pending_mutation_step
FROM sessions WHERE session_id = ?1",
)?;
let (
pub_key,
salt,
stored_last_hash,
expires_at,
gene_blob,
environment_blob,
pending_mutation,
pending_step,
): (
Vec<u8>,
Vec<u8>,
Vec<u8>,
u64,
Vec<u8>,
Vec<u8>,
Vec<u8>,
u64,
) = stmt
.query_row(params![req.session_id], |row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
row.get(6)?,
row.get(7)?,
))
})
.map_err(|e| {
if matches!(e, rusqlite::Error::QueryReturnedNoRows) {
crate::errors::VerificationError::SessionNotFound
} else {
crate::errors::VerificationError::Database(e)
}
})?;
let session = db
.load_session(&req.session_id)
.map_err(|e| crate::errors::VerificationError::Storage(e.to_string()))?;
let session = session.ok_or(crate::errors::VerificationError::SessionNotFound)?;
let now = storage::current_time_ms();
if now > expires_at {
if now > session.expires_at {
return Err(crate::errors::VerificationError::Expired);
}
// 1. Verify signature
crypto::verify_signature(&pub_key, req)
crypto::verify_signature(&session.public_key, req)
.map_err(|e| crate::errors::VerificationError::Signature(e.to_string()))?;
// 2. Check chain continuity
let prev_hash_bytes = hex::decode(&req.prev_hash)?;
if stored_last_hash != prev_hash_bytes {
if session.last_hash != prev_hash_bytes {
return Err(crate::errors::VerificationError::ChainBroken);
}
// 3. Mutation step and deterministic mutation parity
if req.mutation_step != pending_step {
if req.mutation_step != session.pending_mutation_step {
return Err(crate::errors::VerificationError::MutationStepMismatch {
expected: pending_step,
expected: session.pending_mutation_step,
got: req.mutation_step,
});
}
let environment = gene::decode_environment(&environment_blob)
let environment = gene::decode_environment(&session.environment)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
let server_state = GeneState {
gene: gene_blob,
gene: session.gene.clone(),
environment,
};
let candidate_state = vm_extensions::apply_program_clone(&server_state, &pending_mutation)
.map_err(|e| crate::errors::VerificationError::MutationProgram(e.to_string()))?;
let expected_gene_commitment = gene::commitment_hex(&candidate_state);
let candidate_state = vm_extensions::apply_program_clone_with_rounds(
&server_state,
&session.pending_mutation,
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,
);
if req.gene_commitment != expected_gene_commitment {
return Err(crate::errors::VerificationError::MutationCommitmentMismatch);
}
@@ -192,11 +159,11 @@ pub fn verify_heartbeat(
req.timestamp,
&req.entropy_data,
&req.stack_state,
&salt,
&session.salt,
);
// 7. Prepare next mutation order and salt
let next_step = pending_step + 1;
let next_step = session.pending_mutation_step + 1;
let next_mutation = vm_extensions::generate_order(next_step, candidate_state.gene.len());
let next_mutation_b64 = vm_extensions::encode_order_b64(&next_mutation);
@@ -205,28 +172,22 @@ pub fn verify_heartbeat(
let next_environment_blob = gene::encode_environment(&candidate_state.environment)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
conn.execute(
"UPDATE sessions SET
last_hash=?1,
salt=?2,
chain_length=chain_length+1,
last_seen=?3,
gene=?4,
environment=?5,
pending_mutation=?6,
pending_mutation_step=?7
WHERE session_id=?8",
params![
new_hash,
next_salt.to_vec(),
now,
candidate_state.gene,
next_environment_blob,
next_mutation.program,
next_step,
req.session_id
],
)?;
let update_record = storage::SessionRecord {
session_id: req.session_id.clone(),
public_key: session.public_key,
salt: next_salt.to_vec(),
last_hash: new_hash.clone(),
chain_length: session.chain_length + 1,
created_at: session.created_at,
last_seen: now,
expires_at: session.expires_at,
gene: candidate_state.gene,
environment: next_environment_blob,
pending_mutation: next_mutation.program,
pending_mutation_step: next_step,
};
db.update_session(&update_record)
.map_err(|e| crate::errors::VerificationError::Storage(e.to_string()))?;
Ok(HeartbeatVerificationResult {
next_salt_hex,
@@ -239,6 +200,7 @@ pub fn verify_heartbeat(
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
use rusqlite::params;
use shared::protocol::{EntropyData, Fingerprint, HeartbeatRequest, MouseEvent, StackState};
use std::path::Path;
@@ -310,13 +272,13 @@ mod tests {
}
fn create_test_session(
conn: &rusqlite::Connection,
db: &storage::DbPool,
config: &crate::config::Config,
) -> (InitResponse, SigningKey) {
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init = create_session(conn, config, &pk_hex).unwrap();
let init = create_session(db, config, &pk_hex).unwrap();
(init, sk)
}
@@ -355,7 +317,11 @@ mod tests {
stack_state: stack.clone(),
fingerprint: test_fingerprint(),
mutation_step: client.pending_mutation_step,
gene_commitment: gene::commitment_hex(&candidate_state),
gene_commitment: gene::commitment_hex_with_context(
&candidate_state,
&client.session_id,
client.pending_mutation_step,
),
signature: String::new(),
};
sign_request(&client.signing_key, &mut req);
@@ -382,28 +348,22 @@ mod tests {
client.committed_gene_state = candidate_state;
}
fn load_server_gene_state(conn: &rusqlite::Connection, session_id: &str) -> GeneState {
let (gene_blob, env_blob): (Vec<u8>, Vec<u8>) = conn
.query_row(
"SELECT gene, environment FROM sessions WHERE session_id=?1",
[session_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
fn load_server_gene_state(db: &storage::DbPool, session_id: &str) -> GeneState {
let session = db.load_session(session_id).unwrap().unwrap();
GeneState {
gene: gene_blob,
environment: gene::decode_environment(&env_blob).unwrap(),
gene: session.gene,
environment: gene::decode_environment(&session.environment).unwrap(),
}
}
fn run_successful_heartbeat(
conn: &rusqlite::Connection,
db: &storage::DbPool,
config: &crate::config::Config,
client: &mut SimulatedClient,
) -> HeartbeatRequest {
let timestamp = storage::current_time_ms();
let (req, candidate_state, entropy, stack) = build_request(client, timestamp);
let result = verify_heartbeat(conn, config, &req).unwrap();
let result = verify_heartbeat(db, config, &req).unwrap();
apply_successful_response(client, &req, candidate_state, &entropy, &stack, &result);
req
}
@@ -411,20 +371,19 @@ mod tests {
#[test]
fn test_session_lifecycle_and_verification() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
assert_eq!(init.gene_size, config.gene_size as u32);
assert!(!init.mutation_order_b64.is_empty());
assert_eq!(init.mutation_step, 1);
let mut client = client_from_init(&init, signing_key);
for _ in 0..5 {
run_successful_heartbeat(&conn, &config, &mut client);
run_successful_heartbeat(&pool, &config, &mut client);
}
let stats = storage::stats(&conn).unwrap();
let stats = pool.stats().unwrap();
assert_eq!(stats.sessions, 1);
assert_eq!(stats.max_chain_length, 6);
}
@@ -432,14 +391,13 @@ mod tests {
#[test]
fn test_deterministic_server_client_parity_across_many_heartbeats() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
for _ in 0..12 {
run_successful_heartbeat(&conn, &config, &mut client);
let server_state = load_server_gene_state(&conn, &client.session_id);
run_successful_heartbeat(&pool, &config, &mut client);
let server_state = load_server_gene_state(&pool, &client.session_id);
assert_eq!(server_state, client.committed_gene_state);
}
}
@@ -447,14 +405,13 @@ mod tests {
#[test]
fn test_replay_attack_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (req, candidate_state, entropy, stack) = build_request(&client, timestamp);
let result = verify_heartbeat(&conn, &config, &req).unwrap();
let result = verify_heartbeat(&pool, &config, &req).unwrap();
apply_successful_response(
&mut client,
&req,
@@ -464,7 +421,7 @@ mod tests {
&result,
);
let replay = verify_heartbeat(&conn, &config, &req);
let replay = verify_heartbeat(&pool, &config, &req);
assert!(matches!(
replay.unwrap_err(),
crate::errors::VerificationError::ChainBroken
@@ -474,9 +431,8 @@ mod tests {
#[test]
fn test_mutation_step_mismatch_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
@@ -484,7 +440,7 @@ mod tests {
req.mutation_step += 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
@@ -494,9 +450,8 @@ mod tests {
#[test]
fn test_mutation_commitment_tamper_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
@@ -504,7 +459,7 @@ mod tests {
req.gene_commitment = "00".repeat(32);
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationCommitmentMismatch
@@ -514,9 +469,12 @@ mod tests {
#[test]
fn test_malformed_server_mutation_program_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let conn = match &pool {
storage::DbPool::Sqlite(pool) => pool.get().unwrap(),
_ => panic!("expected sqlite pool for test"),
};
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
@@ -525,9 +483,18 @@ mod tests {
)
.unwrap();
let updated: Vec<u8> = conn
.query_row(
"SELECT pending_mutation FROM sessions WHERE session_id=?1",
params![client.session_id.clone()],
|row| row.get(0),
)
.unwrap();
assert_eq!(updated, vec![0xFFu8]);
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationProgram(_)
@@ -537,9 +504,12 @@ mod tests {
#[test]
fn test_expired_session_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let conn = match &pool {
storage::DbPool::Sqlite(pool) => pool.get().unwrap(),
_ => panic!("expected sqlite pool for test"),
};
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
@@ -550,16 +520,15 @@ mod tests {
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(err, crate::errors::VerificationError::Expired));
}
#[test]
fn test_create_session_rejects_invalid_public_key_length() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let err = create_session(&conn, &config, "00ff").unwrap_err();
let err = create_session(&pool, &config, "00ff").unwrap_err();
assert!(matches!(
err,
crate::errors::SessionError::InvalidPublicKeyLength
@@ -569,19 +538,18 @@ mod tests {
#[test]
fn test_stale_mutation_step_after_success_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
run_successful_heartbeat(&conn, &config, &mut client);
run_successful_heartbeat(&pool, &config, &mut client);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.mutation_step -= 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&conn, &config, &req).unwrap_err();
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
@@ -591,15 +559,14 @@ mod tests {
#[test]
fn test_repeated_simulation_keeps_server_and_client_commitments_equal() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap();
let mut config = test_config();
config.gene_size = 128;
let (init, signing_key) = create_test_session(&conn, &config);
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
for _ in 0..10 {
run_successful_heartbeat(&conn, &config, &mut client);
let server_state = load_server_gene_state(&conn, &client.session_id);
run_successful_heartbeat(&pool, &config, &mut client);
let server_state = load_server_gene_state(&pool, &client.session_id);
assert_eq!(
gene::commitment(&server_state),
gene::commitment(&client.committed_gene_state)
+282 -21
View File
@@ -1,7 +1,9 @@
use rusqlite::Connection;
use crate::config::Config;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use valkey::Client as ValkeyClient;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreStats {
@@ -10,9 +12,214 @@ pub struct StoreStats {
pub max_chain_length: u64,
}
pub type DbPool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
#[derive(Debug, Clone)]
pub enum DbPool {
Sqlite(r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>),
Valkey(ValkeyStore),
}
#[derive(Debug, Clone)]
pub struct ValkeyStore {
client: Arc<Mutex<ValkeyClient>>,
index_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRecord {
pub session_id: String,
pub public_key: Vec<u8>,
pub salt: Vec<u8>,
pub last_hash: Vec<u8>,
pub chain_length: u64,
pub created_at: u64,
pub last_seen: u64,
pub expires_at: u64,
pub gene: Vec<u8>,
pub environment: Vec<u8>,
pub pending_mutation: Vec<u8>,
pub pending_mutation_step: u64,
}
impl DbPool {
pub fn init(config: &Config) -> Result<Self, Box<dyn std::error::Error>> {
match config.db_type {
crate::config::DbType::SqliteInMemory => {
let pool = init_sqlite_pool(Path::new(":memory:"))?;
Ok(DbPool::Sqlite(pool))
}
crate::config::DbType::SqliteInDisk => {
let pool = init_sqlite_pool(&config.db_path)?;
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());
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}");
let pool = init_sqlite_pool(Path::new(":memory:"))?;
Ok(DbPool::Sqlite(pool))
}
}
}
}
}
pub fn insert_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"INSERT INTO sessions (
session_id, public_key, salt, last_hash, chain_length,
created_at, last_seen, expires_at, gene, environment,
pending_mutation, pending_mutation_step
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
)?;
stmt.execute(rusqlite::params![
record.session_id,
&record.public_key,
&record.salt,
&record.last_hash,
record.chain_length,
record.created_at,
record.last_seen,
record.expires_at,
&record.gene,
&record.environment,
&record.pending_mutation,
record.pending_mutation_step,
])?;
Ok(())
}
DbPool::Valkey(store) => store.insert_session(record),
}
}
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()?;
let mut stmt = conn.prepare(
"SELECT session_id, public_key, salt, last_hash, chain_length, created_at, last_seen, expires_at, gene, environment, pending_mutation, pending_mutation_step
FROM sessions WHERE session_id = ?1",
)?;
let row = stmt.query_row([session_id], |row| {
Ok(SessionRecord {
session_id: row.get(0)?,
public_key: row.get(1)?,
salt: row.get(2)?,
last_hash: row.get(3)?,
chain_length: row.get(4)?,
created_at: row.get(5)?,
last_seen: row.get(6)?,
expires_at: row.get(7)?,
gene: row.get(8)?,
environment: row.get(9)?,
pending_mutation: row.get(10)?,
pending_mutation_step: row.get(11)?,
})
});
match row {
Ok(rec) => Ok(Some(rec)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(Box::new(err)),
}
}
DbPool::Valkey(store) => store.load_session(session_id),
}
}
pub fn update_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
conn.execute(
"UPDATE sessions SET
public_key=?1,
salt=?2,
last_hash=?3,
chain_length=?4,
created_at=?5,
last_seen=?6,
expires_at=?7,
gene=?8,
environment=?9,
pending_mutation=?10,
pending_mutation_step=?11
WHERE session_id=?12",
rusqlite::params![
&record.public_key,
&record.salt,
&record.last_hash,
record.chain_length,
record.created_at,
record.last_seen,
record.expires_at,
&record.gene,
&record.environment,
&record.pending_mutation,
record.pending_mutation_step,
&record.session_id,
],
)?;
Ok(())
}
DbPool::Valkey(store) => store.insert_session(record),
}
}
pub fn delete_expired_sessions(&self) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
conn.execute(
"DELETE FROM sessions WHERE expires_at < ?1",
rusqlite::params![current_time_ms()],
)?;
Ok(())
}
DbPool::Valkey(store) => store.purge_expired_sessions(),
}
}
pub fn stats(&self) -> Result<StoreStats, Box<dyn std::error::Error>> {
match self {
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 expired_sessions = conn.query_row(
"SELECT COUNT(*) FROM sessions WHERE expires_at < ?1",
[now],
|row| row.get(0),
)?;
let max_chain_length = conn.query_row(
"SELECT COALESCE(MAX(chain_length), 0) FROM sessions",
[],
|row| row.get(0),
)?;
Ok(StoreStats {
sessions,
expired_sessions,
max_chain_length,
})
}
DbPool::Valkey(store) => store.stats(),
}
}
}
#[cfg(test)]
pub fn init_pool(path: &Path) -> Result<DbPool, Box<dyn std::error::Error>> {
let pool = init_sqlite_pool(path)?;
Ok(DbPool::Sqlite(pool))
}
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 {
@@ -21,7 +228,6 @@ pub fn init_pool(path: &Path) -> Result<DbPool, Box<dyn std::error::Error>> {
}
r2d2_sqlite::SqliteConnectionManager::file(path)
};
let pool = r2d2::Pool::new(manager)?;
let conn = pool.get()?;
init_schema(&conn)?;
@@ -89,24 +295,79 @@ fn ensure_column(
Ok(())
}
pub fn stats(conn: &Connection) -> Result<StoreStats, rusqlite::Error> {
let now = current_time_ms();
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],
|row| row.get(0),
)?;
let max_chain_length = conn.query_row(
"SELECT COALESCE(MAX(chain_length), 0) FROM sessions",
[],
|row| row.get(0),
)?;
Ok(StoreStats {
sessions,
expired_sessions,
max_chain_length,
})
impl ValkeyStore {
fn session_key(&self, session_id: &str) -> String {
format!("session:{}", session_id)
}
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)?;
Ok(Some(record))
} else {
Ok(None)
}
}
fn insert_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.client.lock().unwrap();
let value = serde_json::to_string(record)?;
client.set(&self.session_key(&record.session_id), &value)?;
let existing = client.get(&self.index_key)?;
let mut ids = existing.unwrap_or_default();
if !ids.split('\n').any(|id| id == record.session_id) {
if !ids.is_empty() {
ids.push('\n');
}
ids.push_str(&record.session_id);
client.set(&self.index_key, &ids)?;
}
Ok(())
}
fn purge_expired_sessions(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.client.lock().unwrap();
let ids = client.get(&self.index_key)?.unwrap_or_default();
let now = current_time_ms();
let mut remaining: Vec<String> = Vec::new();
for id in ids.split('\n').filter(|id| !id.is_empty()) {
if let Some(payload) = client.get(&self.session_key(id))? {
if let Ok(record) = serde_json::from_str::<SessionRecord>(&payload) {
if record.expires_at > now {
remaining.push(id.to_string());
}
}
}
}
client.set(&self.index_key, &remaining.join("\n"))?;
Ok(())
}
fn stats(&self) -> Result<StoreStats, Box<dyn std::error::Error>> {
let mut client = self.client.lock().unwrap();
let ids = client.get(&self.index_key)?.unwrap_or_default();
let now = current_time_ms();
let mut sessions = 0;
let mut expired_sessions = 0;
let mut max_chain_length = 0;
for id in ids.split('\n').filter(|id| !id.is_empty()) {
if let Some(payload) = client.get(&self.session_key(id))? {
if let Ok(record) = serde_json::from_str::<SessionRecord>(&payload) {
sessions += 1;
if record.expires_at < now {
expired_sessions += 1;
}
max_chain_length = max_chain_length.max(record.chain_length);
}
}
}
Ok(StoreStats {
sessions,
expired_sessions,
max_chain_length,
})
}
}
pub fn current_time_ms() -> u64 {
+1
View File
@@ -11,3 +11,4 @@ hex = "0.4"
base64 = "0.22"
rand = "0.8"
ed25519-dalek = { version = "2", features = ["rand_core"] }
tracing = "0.1"
+6
View File
@@ -4,3 +4,9 @@ pub const DEFAULT_GENE_SIZE: usize = 512;
pub const MAX_GENE_SIZE: usize = 4096;
pub const MAX_ENV_RECORDS: usize = 48;
pub const MAX_MUTATION_PROGRAM_BYTES: usize = 256;
pub const DEFAULT_MUTATION_ROUNDS: u8 = 4;
pub const MIN_MUTATION_ROUNDS: u8 = 3;
pub const MAX_MUTATION_ROUNDS: u8 = 10;
pub const MAX_MUTATION_INSTRUCTION_BUDGET: usize = 2048;
pub const HASH_OPCODE_INSTRUCTION_COST: usize = 16;
pub const SOFT_CAP_DURATION_MS: u128 = 50;
+13
View File
@@ -197,6 +197,19 @@ pub fn commitment_hex(state: &GeneState) -> String {
hex::encode(commitment(state))
}
pub fn commitment_with_context(state: &GeneState, session_id: &str, step: u64) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"chronoseal/gene/v1");
h.update(session_id.as_bytes());
h.update(&step.to_le_bytes());
h.update(&commitment(state));
*h.finalize().as_bytes()
}
pub fn commitment_hex_with_context(state: &GeneState, session_id: &str, step: u64) -> String {
hex::encode(commitment_with_context(state, session_id, step))
}
fn validate_environment(records: &[EnvironmentRecord]) -> Result<(), GeneError> {
if records.len() > MAX_ENV_RECORDS {
return Err(GeneError::TooManyEnvironmentRecords { len: records.len() });
+128 -21
View File
@@ -1,11 +1,15 @@
use crate::{
constants::{MAX_GENE_SIZE, MAX_MUTATION_PROGRAM_BYTES},
constants::{
HASH_OPCODE_INSTRUCTION_COST, MAX_GENE_SIZE, MAX_MUTATION_PROGRAM_BYTES,
MAX_MUTATION_INSTRUCTION_BUDGET, DEFAULT_MUTATION_ROUNDS, SOFT_CAP_DURATION_MS,
},
gene::{
add_env_quantity, get_env_quantity, sub_env_quantity, validate_state, GeneError, GeneState,
},
};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::time::Instant;
// Stack-machine mutation opcodes (v0.6.0).
//
@@ -107,48 +111,65 @@ pub fn generate_order_with_rng<R: Rng + ?Sized>(
step: u64,
gene_size: usize,
) -> MutationOrder {
let mut program = Vec::with_capacity(96);
let mut program = Vec::with_capacity(128);
let mut stack_depth: i32 = 0;
let mut estimated_gene_len = gene_size.clamp(1, MAX_GENE_SIZE);
let ops = rng.gen_range(8usize..=18usize);
let ops = rng.gen_range(20usize..=36usize);
let mut hash_ops_needed = rng.gen_range(2..=3);
for _ in 0..ops {
let op = if stack_depth <= 0 {
for idx in 0..ops {
let remaining = ops - idx;
let op = if hash_ops_needed > 0 && remaining <= hash_ops_needed {
OP_FINALIZE_GENE_HASH
} else if stack_depth <= 0 {
rng.gen_range(0u8..3u8)
} else {
rng.gen_range(0u8..10u8)
match rng.gen_range(0u8..12u8) {
0..=1 => OP_GENE_LOAD,
2..=3 => OP_TRANSCRIBE,
4 => OP_FINALIZE_GENE_HASH,
5 => OP_GENE_STORE,
6 => OP_MUTATE_POINT,
7 => OP_INSERT,
8 => OP_DELETE,
9 => OP_APPLY_MUTAGEN,
10 => OP_CONSUME,
_ => OP_PRODUCE,
}
};
match op {
// Pushers
0 => {
OP_GENE_LOAD => {
program.push(OP_GENE_LOAD);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth += 1;
}
1 => {
OP_TRANSCRIBE => {
program.push(OP_TRANSCRIBE);
push_u16(&mut program, rng.r#gen::<u16>());
program.push(rng.gen_range(1u8..=16u8));
stack_depth += 1;
}
2 => {
OP_FINALIZE_GENE_HASH => {
program.push(OP_FINALIZE_GENE_HASH);
stack_depth += 1;
if hash_ops_needed > 0 {
hash_ops_needed -= 1;
}
}
// Consumers
3 => {
OP_GENE_STORE => {
if stack_depth > 0 {
program.push(OP_GENE_STORE);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth -= 1;
}
}
4 => {
OP_MUTATE_POINT => {
program.push(OP_MUTATE_POINT);
push_u16(&mut program, rng.r#gen::<u16>());
program.push(rng.r#gen::<u8>());
}
5 => {
OP_INSERT => {
if stack_depth > 0 && estimated_gene_len < MAX_GENE_SIZE {
program.push(OP_INSERT);
push_u16(&mut program, rng.r#gen::<u16>());
@@ -156,7 +177,7 @@ pub fn generate_order_with_rng<R: Rng + ?Sized>(
estimated_gene_len += 1;
}
}
6 => {
OP_DELETE => {
program.push(OP_DELETE);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth += 1;
@@ -164,7 +185,7 @@ pub fn generate_order_with_rng<R: Rng + ?Sized>(
estimated_gene_len -= 1;
}
}
7 => {
OP_APPLY_MUTAGEN => {
if stack_depth > 0 {
program.push(OP_APPLY_MUTAGEN);
push_u16(&mut program, rng.r#gen::<u16>());
@@ -172,35 +193,121 @@ pub fn generate_order_with_rng<R: Rng + ?Sized>(
stack_depth -= 1;
}
}
8 => {
OP_CONSUME => {
if stack_depth > 0 {
program.push(OP_CONSUME);
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 {
program.push(OP_FINALIZE_GENE_HASH);
hash_ops_needed -= 1;
}
MutationOrder { step, program }
}
pub fn apply_program_clone(state: &GeneState, program: &[u8]) -> Result<GeneState, MutationError> {
apply_program_clone_with_rounds(state, program, DEFAULT_MUTATION_ROUNDS)
}
pub fn apply_program_clone_with_rounds(
state: &GeneState,
program: &[u8],
rounds: u8,
) -> Result<GeneState, MutationError> {
let mut next = state.clone();
apply_program(&mut next, program)?;
execute_program_with_rounds(&mut next, program, rounds)?;
Ok(next)
}
pub fn apply_program(state: &mut GeneState, program: &[u8]) -> Result<(), MutationError> {
let _ = execute_program(state, program)?;
pub fn apply_program_with_rounds(
state: &mut GeneState,
program: &[u8],
rounds: u8,
) -> Result<(), MutationError> {
let _ = execute_program_with_rounds(state, program, rounds)?;
Ok(())
}
pub fn apply_program(state: &mut GeneState, program: &[u8]) -> Result<(), MutationError> {
let _ = execute_program_with_rounds(state, program, DEFAULT_MUTATION_ROUNDS)?;
Ok(())
}
pub fn execute_program_with_rounds(
state: &mut GeneState,
program: &[u8],
rounds: u8,
) -> Result<ExecutionTrace, MutationError> {
if state.gene.is_empty() {
return Err(MutationError::EmptyGene);
}
validate_state(state)?;
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
return Err(MutationError::ProgramTooLong { len: program.len() });
}
let program_cost = estimate_program_cost(program);
let max_rounds = std::cmp::max(1, MAX_MUTATION_INSTRUCTION_BUDGET / program_cost);
let actual_rounds = std::cmp::min(rounds as usize, max_rounds) as u8;
let start = Instant::now();
let mut trace = None;
for _round in 0..actual_rounds {
trace = Some(execute_program(state, program)?);
}
let elapsed = start.elapsed();
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");
}
if elapsed.as_millis() > SOFT_CAP_DURATION_MS {
tracing::debug!(elapsed_ms = elapsed.as_millis(), "mutation execution exceeded soft cap duration");
}
Ok(trace.unwrap_or_else(|| ExecutionTrace {
final_ip: 0,
final_stack: Vec::new(),
final_gene_commitment_hex: crate::gene::commitment_hex(state),
}))
}
fn estimate_program_cost(program: &[u8]) -> usize {
let mut ip = 0;
let mut cost = 0;
while ip < program.len() {
let opcode = program[ip];
ip += 1;
cost += if opcode == OP_FINALIZE_GENE_HASH {
HASH_OPCODE_INSTRUCTION_COST
} else {
1
};
ip += match opcode {
OP_GENE_LOAD | OP_GENE_STORE | OP_INSERT | OP_DELETE | OP_CONSUME | OP_PRODUCE => 2,
OP_MUTATE_POINT | OP_TRANSCRIBE => 3,
OP_APPLY_MUTAGEN => 4,
OP_FINALIZE_GENE_HASH => 0,
_ => 0,
}
}
cost.max(1)
}
pub fn execute_program(
state: &mut GeneState,
program: &[u8],
+1
View File
@@ -18,3 +18,4 @@ getrandom = { version = "0.2", features = ["js"] }
hex = "0.4"
base64 = "0.22"
serde-wasm-bindgen = "0.6"
tracing = "0.1"
+40 -26
View File
@@ -1,4 +1,5 @@
use std::cell::RefCell;
use std::time::Instant;
use wasm_bindgen::prelude::*;
thread_local! {
@@ -17,24 +18,27 @@ pub fn init_gene_state(gene_size: u32) -> bool {
}
#[wasm_bindgen]
pub fn preview_gene_commitment(order_b64: &str) -> String {
let order = match shared::vm_extensions::decode_order_b64(0, order_b64) {
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(),
};
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(current, &order.program).ok()
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");
let Some(candidate) = candidate else {
return String::new();
};
let commitment = shared::gene::commitment_hex(&candidate);
let commitment = shared::gene::commitment_hex_with_context(&candidate, session_id, mutation_step);
PREVIEW_STATE.with(|slot| *slot.borrow_mut() = Some(candidate));
commitment
}
@@ -55,11 +59,11 @@ pub fn discard_gene_preview() {
}
#[wasm_bindgen]
pub fn current_gene_commitment() -> String {
pub fn current_gene_commitment(session_id: &str, mutation_step: u64) -> String {
GENE_STATE.with(|slot| {
slot.borrow()
.as_ref()
.map(shared::gene::commitment_hex)
.map(|state| shared::gene::commitment_hex_with_context(state, session_id, mutation_step))
.unwrap_or_default()
})
}
@@ -77,7 +81,7 @@ mod tests {
#[test]
fn test_init_gene_state_success() {
assert!(init_gene_state(64));
let commitment = current_gene_commitment();
let commitment = current_gene_commitment("deadbeef", 1);
assert_eq!(commitment.len(), 64);
}
@@ -90,43 +94,48 @@ mod tests {
fn test_preview_requires_initialized_state() {
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,
let c = preview_gene_commitment(
&order_b64(vec![
shared::vm_extensions::OP_MUTATE_POINT,
0,
0,
1,
]),
"deadbeef",
1,
]));
0,
);
assert!(c.is_empty());
}
#[test]
fn test_preview_rejects_invalid_order() {
init_gene_state(16);
let c = preview_gene_commitment("***bad-base64***");
let c = preview_gene_commitment("***bad-base64***", "deadbeef", 1, 0);
assert!(c.is_empty());
}
#[test]
fn test_commit_applies_preview() {
init_gene_state(16);
let before = current_gene_commitment();
let before = current_gene_commitment("deadbeef", 1);
let order = order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 1]);
let preview = preview_gene_commitment(&order);
let preview = preview_gene_commitment(&order, "deadbeef", 1, 0);
assert_ne!(preview, before);
assert!(commit_gene_preview());
let after = current_gene_commitment();
let after = current_gene_commitment("deadbeef", 1);
assert_eq!(preview, after);
}
#[test]
fn test_discard_preview_keeps_committed_state() {
init_gene_state(16);
let before = current_gene_commitment();
let before = current_gene_commitment("deadbeef", 1);
let order = order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 0xFF]);
let preview = preview_gene_commitment(&order);
let preview = preview_gene_commitment(&order, "deadbeef", 1, 0);
assert_ne!(preview, before);
discard_gene_preview();
let after = current_gene_commitment();
let after = current_gene_commitment("deadbeef", 1);
assert_eq!(before, after);
}
@@ -161,11 +170,11 @@ mod tests {
};
let b64 = shared::vm_extensions::encode_order_b64(&order);
let preview = preview_gene_commitment(&b64);
let preview = preview_gene_commitment(&b64, "deadbeef", 3, 0);
let mut expected = shared::gene::new_state(16).unwrap();
shared::vm_extensions::apply_program(&mut expected, &order.program).unwrap();
assert_eq!(preview, shared::gene::commitment_hex(&expected));
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]
@@ -178,13 +187,18 @@ mod tests {
let order = shared::vm_extensions::generate_order_with_rng(&mut rng, step + 1, 64);
let b64 = shared::vm_extensions::encode_order_b64(&order);
let preview = preview_gene_commitment(&b64);
shared::vm_extensions::apply_program(&mut expected, &order.program).unwrap();
let expected_commitment = shared::gene::commitment_hex(&expected);
let preview = preview_gene_commitment(&b64, "deadbeef", step + 1, 0);
shared::vm_extensions::apply_program_with_rounds(
&mut expected,
&order.program,
shared::constants::DEFAULT_MUTATION_ROUNDS,
)
.unwrap();
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(), expected_commitment);
assert_eq!(current_gene_commitment("deadbeef", step + 1), expected_commitment);
}
}
}