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

This commit consolidates the v0.6.0 architectural refactoring effort,
strengthening deterministic browser/server parity while improving
maintainability, operational clarity, and project documentation.
2026-05-29 21:55:08 +05:30

534 lines
20 KiB
Markdown

# ChronoSeal Architecture
ChronoSeal is a Unix-native browser attestation daemon. It validates browser session continuity by combining signed heartbeats, Blake3 hash-chain progression, deterministic VM execution, behavioral sanity checks, and a shared Synthetic Gene Mutation Engine that runs on both the server and the browser WASM runtime.
This document describes the system architecture, state model, validation pipeline, trust boundaries, and operational assumptions. The API wire format is documented separately in [API.md](API.md), and deployment guidance is documented in [DEPLOYMENT.md](DEPLOYMENT.md).
## Architectural Goals
ChronoSeal is designed as infrastructure software rather than a consumer-facing widget. The main goals are:
- Keep the server small, inspectable, and operable as a normal Unix daemon.
- Use deterministic client/server computation so the server can verify browser-side progression without trusting browser claims blindly.
- Make replay, stale state reuse, and incomplete automation expensive.
- Preserve privacy by using short-lived session state instead of persistent identity tracking.
- Avoid attacker feedback oracles by returning indistinguishable success-shaped responses for rejected heartbeats.
- Keep browser integration lightweight: static JavaScript plus a Rust-generated WASM package.
ChronoSeal does not attempt to prove that a human is present. It attempts to prove that a client is maintaining the expected live browser-side cryptographic and mutation state.
## System Context
```text
Protected browser origin
|
| static files and API calls
v
+------------------------------+
| Browser |
| - frontend JavaScript |
| - chronoseal_wasm runtime |
| - Ed25519 session key |
| - VM and gene state |
+---------------+--------------+
|
| POST /init
| POST /hb
v
+------------------------------+
| ChronoSeal daemon |
| - Axum HTTP routes |
| - session verifier |
| - storage abstraction |
| - metrics and health |
+---------------+--------------+
|
| SessionRecord
v
+------------------------------+
| Storage backend |
| - sqlite-in-memory |
| - sqlite-in-disk |
| - valkey |
+------------------------------+
```
ChronoSeal can serve the frontend files itself or sit behind a reverse proxy. TLS termination should happen before traffic reaches the daemon in production.
## Workspace Components
The repository is a Rust workspace with three runtime crates and one static frontend directory.
### `shared/`
`shared/` contains protocol and deterministic runtime code used by both the server and WASM crates.
Responsibilities:
- wire protocol structs for `/init` and `/hb`
- Blake3 hash-chain helpers
- synthetic gene state representation
- environment encoding and validation
- mutation program generation, encoding, decoding, and execution
- deterministic VM extension opcode semantics
Important files:
| File | Responsibility |
|---|---|
| `protocol.rs` | `InitRequest`, `InitResponse`, `HeartbeatRequest`, `HeartbeatResponse`, and supporting payload types |
| `hashing.rs` | initial and next hash-chain computation |
| `gene.rs` | gene state, environment records, validation, and context-bound commitment |
| `vm_extensions.rs` | mutation order generation, opcode interpreter, execution tracing, and tests |
| `constants.rs` | protocol and execution bounds |
`shared/` is the determinism boundary. Any logic that must agree between server and browser belongs here rather than in server-only or frontend-only code.
### `server/`
`server/` builds the `chronoseal` binary. It owns daemon lifecycle, HTTP routing, session verification, storage, metrics, configuration, and CLI behavior.
Important files:
| File | Responsibility |
|---|---|
| `main.rs` | CLI command dispatch |
| `cli.rs` | command, flag, and environment variable definitions |
| `config.rs` | defaults, TOML loading, environment overrides, validation |
| `runtime.rs` | daemon startup, Axum router, health, metrics, stats, graceful shutdown |
| `routes/init.rs` | `POST /init` handler |
| `routes/heartbeat.rs` | `POST /hb` handler and silent rejection response shape |
| `session.rs` | session creation, heartbeat verification, state advancement |
| `crypto.rs` | canonical signing payload and Ed25519 signature verification |
| `storage.rs` | `DbPool`, SQLite, Valkey compatibility, session persistence, stats |
| `trust.rs` | mouse entropy validation |
| `fingerprint.rs` | browser signal validation |
| `ratelimit.rs` | per-session rate limiting |
| `cleanup.rs` | expired session removal |
The server treats the browser as untrusted. Browser-supplied values are accepted only after signature, continuity, timing, behavioral, and mutation checks pass.
### `wasm/`
`wasm/` compiles to the browser runtime package with `wasm-pack --target web`.
Responsibilities:
- generate and hold the browser-local Ed25519 keypair
- sign canonical heartbeat payloads
- compute hash-chain values used by the browser integration
- execute randomized VM programs
- maintain committed and preview synthetic gene state
- preview mutation commitments before a heartbeat is submitted
- commit or discard preview state after server response
Important files:
| File | Responsibility |
|---|---|
| `crypto.rs` | key generation, public key export, message signing |
| `vm.rs` | base VM program execution |
| `vm_extensions.rs` | gene initialization, mutation preview, commit, discard, current commitment |
The WASM runtime is not a trusted execution environment. It is useful because it forces a browser client to implement the same state transitions as the server and makes simple HTTP automation insufficient.
### `frontend/`
`frontend/` contains static JavaScript and browser assets. It loads `frontend/pkg/chronoseal_wasm.js`, calls `/init`, periodically sends `/hb`, and coordinates browser-side state transitions.
The frontend is intentionally thin. Durable protocol rules live in Rust, not in handwritten JavaScript.
## Runtime Topology
The daemon builds a single Axum application with:
| Route | Method | Purpose |
|---|---|---|
| `/init` | `POST` | create a new attestation session |
| `/hb` | `POST` | verify and advance a heartbeat |
| `/health` | `GET` | health probe |
| `/metrics` | `GET` | Prometheus-compatible metrics |
| `/stats` | `GET` | storage/session statistics |
| `/` | `GET` | static frontend assets from `frontend_dir` |
Shared runtime state is held in `AppState`:
- `db_pool`: storage backend handle
- `rate_limiter`: process-local rate limiter
- `config`: runtime configuration snapshot behind an `RwLock`
Configuration is resolved in this order:
1. CLI flags
2. `CHRONOSEAL_*` environment variables
3. TOML configuration file
4. built-in defaults
## Session State Model
The server persists one `SessionRecord` per active session.
| Field | Meaning |
|---|---|
| `session_id` | random 32-byte session identifier encoded as hex |
| `public_key` | browser-generated Ed25519 verifying key |
| `salt` | current server salt for hash-chain progression |
| `last_hash` | current accepted hash-chain head |
| `chain_length` | number of accepted chain states including initialization |
| `created_at` | creation timestamp in milliseconds |
| `last_seen` | timestamp of last accepted heartbeat |
| `expires_at` | session expiration timestamp in milliseconds |
| `gene` | committed synthetic gene byte buffer |
| `environment` | encoded environment records |
| `pending_mutation` | server-issued mutation program for the next heartbeat |
| `pending_mutation_step` | mutation step expected on the next heartbeat |
The committed server state advances only after a heartbeat passes all validation checks. Failed heartbeats do not update `last_hash`, `salt`, `gene`, `environment`, `pending_mutation`, or `pending_mutation_step`.
## Initialization Flow
```text
Browser/WASM Server
------------ ------
generate_keypair()
public key
|
| POST /init { public_key }
v
validate public key length
create GeneState
generate session_id
generate salt
compute initial_hash
generate VM opcodes
generate mutation step 1
persist SessionRecord
^
| InitResponse
|
store session_id, salt,
initial_hash, opcodes,
gene_size, mutation order
```
Initialization creates the first server-side commitment state but does not prove liveness. Liveness begins with accepted heartbeats.
The initial response contains:
- `session_id`
- `salt`
- `opcodes_b64`
- `initial_hash`
- `expires_at`
- heartbeat interval bounds
- `gene_size`
- `mutation_step`
- `mutation_order_b64`
## Heartbeat Flow
```text
Browser/WASM Server
------------ ------
execute VM program
collect entropy and fingerprint data
preview pending gene mutation
build canonical signing payload
sign with Ed25519 private key
|
| POST /hb HeartbeatRequest
v
load session
check expiration
verify signature
check hash continuity
check mutation step
apply pending mutation
compare gene commitment
check timestamp drift
validate mouse entropy
validate fingerprint
compute next hash
generate next mutation
generate next salt
persist advanced state
^
| accepted: status + next salt + next mutation
| rejected: { "status": "ok" }
|
commit preview on accepted response
discard or stop on rejected response
```
Accepted heartbeats return `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
Rejected heartbeats return only:
```json
{
"status": "ok"
}
```
This silent rejection behavior is part of the security model. It prevents the API from acting as an oracle for signature, timing, mutation, or behavior failures.
## Verification Pipeline
Heartbeat verification occurs in `server/src/session.rs`.
The current validation order is:
1. Load the session by `session_id`.
2. Reject if the session is missing.
3. Reject if `now > expires_at`.
4. Verify the Ed25519 signature over the canonical payload.
5. Decode and compare `prev_hash` with the stored `last_hash`.
6. Compare request `mutation_step` with stored `pending_mutation_step`.
7. Decode the stored gene environment.
8. Apply the stored `pending_mutation` to a cloned server gene state.
9. Compute the expected `gene_commitment` with session and step context.
10. Compare the request `gene_commitment` with the expected commitment.
11. Enforce timestamp drift bounds.
12. Validate mouse entropy.
13. Validate browser fingerprint fields.
14. Compute the next hash-chain value.
15. Generate the next mutation order.
16. Generate the next salt.
17. Persist the advanced session state.
The verifier performs state mutation only after validation succeeds. This preserves replay resistance and avoids desynchronizing the server after invalid requests.
## Canonical Signing Boundary
The heartbeat signature covers a canonical JSON payload built from:
- `entropyData`
- `fingerprint`
- `geneCommitment`
- `mutationStep`
- `prevHash`
- `sessionId`
- `stackState`
- `timestamp`
The server constructs this payload using a `BTreeMap`, which orders top-level keys deterministically before serializing. The transport request uses snake_case field names, while the signed payload uses camelCase names that match the browser-side canonical message.
The signature does not cover the `signature` field itself.
## Hash-Chain Boundary
Each accepted heartbeat advances a Blake3 hash chain.
Inputs include:
- previous hash-chain head
- heartbeat timestamp
- entropy data
- VM stack state
- current server salt
The server stores only the current accepted head as `last_hash`. A replayed heartbeat with an old `prev_hash` fails because the stored `last_hash` has already advanced.
The salt rotates after every accepted heartbeat. The next salt is returned only on acceptance, so rejected clients do not receive the material needed for the next valid chain step.
## Synthetic Gene Mutation Engine
The Synthetic Gene Mutation Engine provides an additional deterministic continuity check.
Core concepts:
- `GeneState`: committed gene byte buffer plus environment records.
- `MutationOrder`: mutation step plus encoded mutation program.
- `pending_mutation`: the server-authored program expected on the next heartbeat.
- `gene_commitment`: context-bound commitment over the candidate gene state, `session_id`, and `mutation_step`.
The server and WASM runtime both execute the same mutation semantics from `shared/vm_extensions.rs`.
Mutation lifecycle:
1. Server stores a pending mutation program and step.
2. Browser previews that mutation against its committed gene state.
3. Browser sends the resulting `gene_commitment`.
4. Server applies the same mutation to a clone of its committed gene state.
5. Server compares the expected commitment with the browser commitment.
6. On success, server commits the candidate state and issues the next mutation.
7. Browser commits its preview only after receiving an accepted response.
This design prevents a client from advancing mutation state independently of the server. The mutation order is server-authored, step-bound, and accepted only once.
## Behavioral Trust Checks
ChronoSeal includes lightweight behavioral checks. These checks are not a complete human verification system; they are an automation cost signal.
Current checks include:
- minimum mouse activity, when enabled
- minimum total mouse movement distance
- maximum average mouse speed
- minimum pause count
- timestamp drift bound
- basic fingerprint field validation
The checks are intentionally bounded and configurable. They should be treated as one layer in the attestation pipeline, not as the primary security primitive.
## Storage Architecture
Storage is abstracted by `DbPool`.
| Backend | `db_type` | Characteristics |
|---|---|---|
| SQLite memory | `sqlite-in-memory` | default, process-local, ephemeral |
| SQLite disk | `sqlite-in-disk` | persisted SQLite file at `db_path` |
| Valkey | `valkey` | Valkey-compatible session store |
The storage layer must support:
- insert session
- load session
- update session
- delete expired sessions
- report statistics
`valkey` mode reads `CHRONOSEAL_VALKEY_ADDR`, defaulting to `127.0.0.1:6666`. If connection setup fails, the current implementation logs a warning and falls back to in-memory SQLite.
## Metrics and Observability
ChronoSeal exposes two operational surfaces:
- CLI commands: `status`, `health`, `metrics`, `stats`, `config check`
- HTTP endpoints: `/health`, `/metrics`, `/stats`
The metrics endpoint reports storage-derived counters including:
- active sessions
- expired sessions
- maximum observed chain length
The daemon uses structured tracing and can log to journald through normal systemd operation. Operators should avoid debug logging in production because internal identifiers may appear in logs.
## Trust Boundaries
### Browser Boundary
The browser is untrusted. It may lie about entropy, fingerprint values, VM output, mutation commitment, timing, and session identifiers.
Mitigation:
- signature verification binds payloads to the browser session key
- hash-chain checks reject stale state
- mutation commitment checks reject incorrect gene progression
- timing and behavioral checks reject implausible requests
### WASM Boundary
WASM code runs in the browser and is therefore not trusted as secure enclave code.
Mitigation:
- the server independently recomputes critical deterministic state
- private key custody raises automation cost but is not treated as hardware-backed secrecy
- failures do not reveal detailed reasons to callers
### Storage Boundary
Storage is trusted for session continuity. If storage is lost, sessions cannot continue. If storage is tampered with, attestation integrity can be affected.
Mitigation:
- use proper filesystem permissions for SQLite disk mode
- deploy Valkey on a trusted network or protected socket
- keep ChronoSeal behind normal host and service hardening
### Network Boundary
ChronoSeal expects production traffic to be protected by TLS. Plaintext deployment weakens confidentiality and makes traffic analysis easier.
Mitigation:
- terminate TLS at a reverse proxy or load balancer
- keep `/init` and `/hb` same-origin with protected content when possible
- avoid exposing internal metrics broadly
## Failure Semantics
ChronoSeal intentionally separates transport success from attestation success.
| Failure class | HTTP behavior | State mutation |
|---|---|---|
| malformed route-level request | normal HTTP error handling | no session advancement |
| invalid heartbeat semantics | `200 OK` with `{"status":"ok"}` | no session advancement |
| rejected heartbeat | `200 OK` with `{"status":"ok"}` | no session advancement |
| accepted heartbeat | `200 OK` with next-state fields | session state advances from the verifier's perspective |
This ambiguity reduces attacker feedback. Application integrations must check for the presence of `next_salt`, `next_mutation_step`, and `next_mutation_order_b64` rather than treating any `status: ok` as an accepted heartbeat.
## Invariants
The architecture relies on these invariants:
- A session has exactly one expected `pending_mutation_step` at a time.
- A pending mutation is consumed only by an accepted heartbeat.
- `last_hash` changes only after a heartbeat passes verification.
- `salt` changes only after a heartbeat passes verification.
- `gene` and `environment` change only after mutation commitment validation succeeds.
- The next mutation order is generated only from an accepted candidate state.
- Rejected heartbeats do not reveal the failed validation stage.
- Browser-side preview state is committed only after an accepted heartbeat response.
Breaking these invariants can introduce replay acceptance, client/server desynchronization, or oracle behavior.
## Concurrency Notes
ChronoSeal currently verifies a heartbeat by loading a session, computing candidate state, and writing the updated record back to storage. The intended operational model is one live heartbeat stream per browser session.
Concurrent heartbeats for the same `session_id` should naturally collapse to at most one accepted progression because both requests present the same `prev_hash` and `mutation_step`; after the first accepted update, the second request becomes stale. Storage backends must preserve update visibility strongly enough for this assumption to hold.
## Deployment Shape
Typical production topology:
```text
Internet
|
v
TLS reverse proxy
|
v
chronoseal daemon on 127.0.0.1:3000
|
v
SQLite disk or Valkey storage
```
Recommended deployment properties:
- run under systemd with a dedicated service user
- bind to localhost behind a reverse proxy unless direct exposure is required
- serve over HTTPS
- keep debug logs disabled
- monitor `/health`, `/metrics`, and `/stats`
- use `sqlite-in-memory` for ephemeral local sessions
- use `sqlite-in-disk` or `valkey` when sessions must survive process restarts
## Limitations
ChronoSeal is not:
- a user authentication system
- a CAPTCHA
- a fraud scoring engine
- a hardware attestation system
- a persistent identity framework
- a complete defense against fully resourced browser farms
It is a protocol layer that makes browser automation and replay more expensive by requiring correct, continuous, stateful execution.
## Related Documents
- [API Reference](API.md)
- [Deployment Guide](DEPLOYMENT.md)
- [Threat Model](THREAT_MODEL.md)
- [WASM Build Guide](WASM_BUILD.md)
- [Design Philosophy](DESIGN-PHILOSOPHY.md)
- [Privacy Policy](PRIVACY%20POLICY.md)