docs: comprehensive ARCHITECTURE, DEPLOYMENT, API, and THREAT_MODEL
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)
This commit is contained in:
1 parent
851d3b4876
commit
4b27a342d1
4 files changed
+1017
-256
No files matched your search
@@ -0,0 +1,205 @@
|
||||
# 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:
|
||||
|
||||
```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.
|
||||
Reference in new issue
Block a user