ARCHITECTURE.md - Full component map with ASCII diagram - Complete session lifecycle (init + heartbeat + failure path) - Cryptographic protocol spec (hash chain formula, canonical JSON) - Stack machine instruction set table with stack effects - Behavioral validation thresholds - SQLite schema, threat model summary, module reference DEPLOYMENT.md - Build instructions (WASM + server + convenience script) - native binary, systemd (with hardened sandbox notes), Docker - nginx, Nginx Proxy Manager, and HAProxy reverse proxy configs - Integration options (sidecar vs proxy-only) - Full configuration table with all constants - Observability (RUST_LOG levels), health check, security checklist API.md - Full /init and /hb request/response schemas with field tables - Canonical signing payload specification - Complete validation rules table (all 13 rejection conditions) - Hash chain byte-level specification - WASM exported function reference THREAT_MODEL.md - Four attacker profiles (script kiddie → sophisticated adversary) - Eight attack vectors with mitigations (replay, forgery, hijack, DoS…) - Explicit out-of-scope limitations - Operational security notes (CORS, TLS, log level, SQLite)
8.5 KiB
ChronoSeal — Threat Model
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 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.
Assets Being Protected
| Asset | Description |
|---|---|
| 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 |
Attacker Profiles
Level 1 — Script Kiddie / 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.
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.
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.
Level 4 — Sophisticated Adversary
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.
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.
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.
Key Extraction
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.
Hash Chain Forgery
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.
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.
Enumeration of Validation Rules
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.
DoS via Session Flooding
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.
Clock Manipulation
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.
Synthetic Mouse Events
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.
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. |
Operational Security Notes
Log Level
Do not run with RUST_LOG=debug in production. The debug log includes
session_id values, which are sensitive identifiers. Use warn or info.
CORS Policy
The default CorsLayer::permissive() is suitable for development only.
In production, restrict allowed origins to your own domain:
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 for the vulnerability disclosure policy and contact details.