Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
217dc5f92b | ||
|
|
0b43530651 | ||
|
|
e0f6d7c72c |
No files matched your search
@@ -1,130 +1,332 @@
|
|||||||
# ChronoSeal
|
# ChronoSeal
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="logo/chronoseal.svg" width="220" alt="ChronoSeal Logo">
|
<img src="logo/chronoseal.svg" width="220" alt="ChronoSeal Logo">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Cryptographic anti-automation and browser attestation framework built with Rust, WASM, and behavioral continuity verification.**
|
<p align="center">
|
||||||
|
<strong>Cryptographic attestation daemon and anti-automation framework.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
ChronoSeal makes it computationally expensive and operationally complex for AI scrapers, headless browsers, and automation tools to impersonate real users — while remaining completely invisible and frictionless to legitimate human visitors.
|
<p align="center">
|
||||||
|
Privacy-preserving • Unix-native • Lightweight • WASM-powered
|
||||||
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How It Works
|
ChronoSeal is a lightweight cryptographic attestation daemon designed to raise the operational cost of browser automation, scraping, replay attacks, and synthetic interaction.
|
||||||
|
|
||||||
ChronoSeal establishes a continuous, cryptographically verifiable proof-of-presence for every browser session. It is inspired by the heartbeat model used in IoT firmware (ESP32-class devices): the client must keep emitting signed, chained attestations, or the session is silently invalidated.
|
Instead of relying on:
|
||||||
|
|
||||||
|
* CAPTCHA systems
|
||||||
|
* invasive browser fingerprinting
|
||||||
|
* telemetry-heavy tracking
|
||||||
|
* persistent identifiers
|
||||||
|
|
||||||
|
ChronoSeal establishes a continuous cryptographic proof-of-runtime continuity using:
|
||||||
|
|
||||||
|
* WASM execution
|
||||||
|
* chained cryptographic heartbeats
|
||||||
|
* behavioral entropy validation
|
||||||
|
* ephemeral attestation state
|
||||||
|
|
||||||
|
while remaining completely invisible and frictionless to legitimate human users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
* 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
|
||||||
|
* Silent rejection model
|
||||||
|
* SQLite-backed ephemeral sessions
|
||||||
|
* Connection-pooled runtime architecture
|
||||||
|
* Lightweight deployment footprint
|
||||||
|
* Docker and native deployment support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Start
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Check Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chronoseal status --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Probe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chronoseal health
|
||||||
|
```
|
||||||
|
|
||||||
|
## View Metrics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chronoseal metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
## View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u chronoseal -f
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chronoseal --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ------------ | ------------------------------------------ |
|
||||||
|
| `run` | Run the ChronoSeal daemon |
|
||||||
|
| `status` | Report daemon status |
|
||||||
|
| `health` | Perform daemon health probe |
|
||||||
|
| `config` | Validate and print effective configuration |
|
||||||
|
| `generate` | Generate operational material |
|
||||||
|
| `metrics` | Output Prometheus metrics |
|
||||||
|
| `stats` | Print runtime statistics |
|
||||||
|
| `completion` | Generate shell completions |
|
||||||
|
| `version` | Print version/build information |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chronoseal status --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"running": true,
|
||||||
|
"healthy": true,
|
||||||
|
"bind": "0.0.0.0:3000",
|
||||||
|
"pid_file": "/run/chronoseal.pid",
|
||||||
|
"pid": 79459
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# How It Works
|
||||||
|
|
||||||
|
ChronoSeal establishes a continuous cryptographic proof-of-presence for browser sessions.
|
||||||
|
|
||||||
|
The system is inspired by heartbeat validation models used in embedded and distributed systems.
|
||||||
|
|
||||||
|
## Session Flow
|
||||||
|
|
||||||
|
```text
|
||||||
Browser Server
|
Browser Server
|
||||||
│ │
|
│ │
|
||||||
│ WASM loads, generates Ed25519 keypair │
|
│ WASM loads, generates Ed25519 keypair │
|
||||||
│ Private key never leaves WASM memory │
|
│ Private key never leaves WASM memory │
|
||||||
│ │
|
│ │
|
||||||
├──── POST /init { public_key } ──────────►│ Store session, salt, initial hash
|
├──── POST /init { public_key } ──────────►│
|
||||||
│◄─── { session_id, salt, opcodes, H0 } ────┤
|
│◄─── { session_id, salt, opcodes, H0 } ────┤
|
||||||
│ │
|
│ │
|
||||||
│ Every 12–25s (jittered): │
|
│ Every 12–25s (randomized): │
|
||||||
│ ┌─ Collect mouse entropy │
|
│ ┌─ Collect behavioral entropy │
|
||||||
│ ├─ Execute VM opcodes → stack state │
|
│ ├─ Execute VM opcode program │
|
||||||
│ ├─ Compute H(n) = Blake3(H(n-1) ║ …) │
|
│ ├─ Advance Blake3 hash chain │
|
||||||
│ └─ Sign payload with Ed25519 │
|
│ └─ Sign payload using Ed25519 │
|
||||||
│ │
|
│ │
|
||||||
├──── POST /hb { session_id, sig, … } ────►│ Verify sig → chain → behavior → fingerprint
|
├──── POST /heartbeat { signed_payload } ─►│
|
||||||
│◄─── { status, next_salt } ────────────────┤ Rotate salt, advance chain
|
│◄─── { status, next_salt } ────────────────┤
|
||||||
│ │
|
│ │
|
||||||
│ On failure: server returns {"status":"ok"}│ Silent rejection — indistinguishable
|
│ Invalid sessions silently rejected │
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The server validates:
|
||||||
|
|
||||||
|
* signature authenticity
|
||||||
|
* heartbeat continuity
|
||||||
|
* replay resistance
|
||||||
|
* behavioral entropy
|
||||||
|
* timestamp validity
|
||||||
|
* fingerprint sanity
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Security Model
|
# Security Model
|
||||||
|
|
||||||
### What ChronoSeal protects against
|
## What ChronoSeal Protects Against
|
||||||
|
|
||||||
| Threat | Mechanism |
|
| Threat | Mechanism |
|
||||||
|---|---|
|
| ------------------------ | ------------------------------------- |
|
||||||
| Playwright / Puppeteer Stealth | Mouse entropy validation rejects synthetic or absent movement |
|
| Replay attacks | Blake3 chained heartbeat continuity |
|
||||||
| Replay attacks | Hash chain — each heartbeat references the previous hash; old payloads are invalid |
|
| Signature forgery | Ed25519 keypair generated inside WASM |
|
||||||
| Signature forgery | Ed25519 private key generated inside WASM, never serialised or exposed to JS |
|
| Session cloning | Ephemeral session-bound keypairs |
|
||||||
| Clock manipulation | Server enforces ±30s timestamp window |
|
| Static scraping | Runtime participation requirements |
|
||||||
| Credential sharing | Session is bound to a keypair generated fresh on every page load |
|
| Naive browser automation | Behavioral continuity validation |
|
||||||
| Flooding with fake sessions | Per-session rate limiting (5 req / 10s); stale entries evicted every 60s |
|
| Timestamp replay | Drift-window enforcement |
|
||||||
| Passive analysis of traffic | Server always returns `{"status":"ok"}` — rejections are silent |
|
| Session flooding | Per-session rate limiting |
|
||||||
|
|
||||||
### What ChronoSeal does not claim
|
|
||||||
|
|
||||||
ChronoSeal is a cost-raising mechanism, not an impenetrable barrier. A sufficiently motivated adversary with a real browser, real input devices, and the patience to reverse the WASM can bypass it. The goal is to make scraping expensive and operationally complex enough to be impractical at scale.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## Silent Rejection Model
|
||||||
|
|
||||||
```
|
ChronoSeal intentionally avoids explicit rejection semantics.
|
||||||
chronoseal-rs/
|
|
||||||
├── shared/ Shared types, Blake3 hash-chain logic, constants
|
|
||||||
├── server/ Axum HTTP server
|
|
||||||
│ ├── routes/ /init and /hb handlers
|
|
||||||
│ ├── session.rs Session lifecycle: create, verify, advance chain
|
|
||||||
│ ├── crypto.rs Ed25519 signature verification (BTreeMap canonical JSON)
|
|
||||||
│ ├── trust.rs Behavioral signal validation (mouse speed, pauses)
|
|
||||||
│ ├── fingerprint Browser fingerprint sanity checks
|
|
||||||
│ ├── vm.rs Random opcode program generator
|
|
||||||
│ ├── ratelimit.rs Token-bucket rate limiter with periodic eviction
|
|
||||||
│ └── cleanup.rs Background task: expire sessions + evict rate limiter
|
|
||||||
├── wasm/ Rust → WASM client module
|
|
||||||
│ ├── crypto.rs Ed25519 keypair, signing, hash computation
|
|
||||||
│ └── vm.rs Stack machine executor (PUSH/ADD/SUB/MUL/XOR/AND/OR/ROT/NOT/HASH)
|
|
||||||
└── frontend/ Vanilla JS glue
|
|
||||||
├── heartbeat.js Session init, heartbeat scheduling, chain advancement
|
|
||||||
└── entropy.js Mouse event collection
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stack Machine
|
Invalid sessions may still receive:
|
||||||
|
|
||||||
The server generates a random program (8–16 opcodes) on session init. The client executes it on every heartbeat and includes the resulting stack state in the signed payload. This makes each heartbeat structurally unique without requiring any server round-trip.
|
|
||||||
|
|
||||||
| Opcode | Mnemonic | Effect |
|
|
||||||
|--------|----------|--------|
|
|
||||||
| `0x00` | PUSH u32 | Push 4-byte little-endian literal |
|
|
||||||
| `0x01` | ADD | Pop 2, push `a + b` (wrapping) |
|
|
||||||
| `0x02` | SUB | Pop 2, push `a - b` (wrapping) |
|
|
||||||
| `0x03` | MUL | Pop 2, push `a * b` (wrapping) |
|
|
||||||
| `0x04` | XOR | Pop 2, push `a ^ b` |
|
|
||||||
| `0x05` | AND | Pop 2, push `a & b` |
|
|
||||||
| `0x06` | OR | Pop 2, push `a \| b` |
|
|
||||||
| `0x07` | ROT | Pop 2, push `a.rotate_left(b % 32)` |
|
|
||||||
| `0x08` | NOT | Pop 1, push `!a` (unary) |
|
|
||||||
| `0x09` | HASH | Blake3 of entire stack → single u32 |
|
|
||||||
|
|
||||||
### Hash Chain
|
|
||||||
|
|
||||||
```
|
|
||||||
H(0) = Blake3( session_id ║ pub_key ║ salt₀ )
|
|
||||||
|
|
||||||
H(n) = Blake3( saltₙ₋₁ ║ H(n-1) ║ timestamp ║ Blake3(entropy_json) ║ Blake3(stack_json) )
|
|
||||||
```
|
|
||||||
|
|
||||||
Each heartbeat must present `H(n-1)` matching what the server stored. Forging a valid `H(n)` requires knowing the private key (for the signature), the salt (server-side only), and all prior state.
|
|
||||||
|
|
||||||
### Signature Canonical Form
|
|
||||||
|
|
||||||
The client signs a JSON object with keys sorted alphabetically (matching `JSON.stringify(obj, Object.keys(obj).sort())`):
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "status": "ok" }
|
||||||
"entropyData": { "events": [ { "x": …, "y": …, "t": … } ] },
|
|
||||||
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
|
|
||||||
"prevHash": "hex…",
|
|
||||||
"sessionId": "hex…",
|
|
||||||
"stackState": { "stack": […], "ip": … },
|
|
||||||
"timestamp": 1234567890123
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The server reconstructs this using `BTreeMap` (alphabetical key order) before calling `VerifyingKey::verify_strict`.
|
This prevents:
|
||||||
|
|
||||||
|
* oracle-style probing
|
||||||
|
* protocol learning
|
||||||
|
* easy automation tuning
|
||||||
|
* behavioral enumeration
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SQLite Schema
|
## What ChronoSeal Does Not Claim
|
||||||
|
|
||||||
|
ChronoSeal is a cost-raising mechanism, not an impenetrable barrier.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
chronoseal-rs/
|
||||||
|
├── shared/ Shared types, constants, hash-chain logic
|
||||||
|
├── server/ Axum HTTP daemon
|
||||||
|
│ ├── routes/ API routes
|
||||||
|
│ ├── session.rs Session lifecycle management
|
||||||
|
│ ├── 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
|
||||||
|
├── frontend/ Lightweight JS integration
|
||||||
|
├── scripts/ Build/install/dev scripts
|
||||||
|
└── docs/ Project documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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
|
```sql
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
@@ -139,101 +341,196 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
Sessions are stored in an in-memory SQLite database. All session state is lost on server restart by design — clients re-initialise transparently.
|
ChronoSeal intentionally uses ephemeral session persistence.
|
||||||
|
|
||||||
|
Session continuity is designed to reset transparently.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Build
|
# Runtime Architecture
|
||||||
|
|
||||||
### Prerequisites
|
## Server Runtime
|
||||||
|
|
||||||
- Rust stable (≥ 1.87)
|
* Rust
|
||||||
- [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer/)
|
* Axum
|
||||||
|
* Tokio
|
||||||
|
* SQLite
|
||||||
|
* `r2d2`
|
||||||
|
* `thiserror`
|
||||||
|
|
||||||
### WASM
|
## Browser Runtime
|
||||||
|
|
||||||
```bash
|
* Rust → WASM
|
||||||
wasm-pack build wasm --target web --release
|
* Ed25519 signing
|
||||||
mv wasm/pkg frontend/pkg
|
* Blake3 chaining
|
||||||
```
|
* stack-machine execution
|
||||||
|
|
||||||
### Server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build -p server --release
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dev (all-in-one)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/dev.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The server serves the `frontend/` directory statically at `/` and the API at `/init` and `/hb`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment
|
# Deployment
|
||||||
|
|
||||||
### Native + systemd
|
## Recommended Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build -p server --release
|
sudo bash scripts/install.sh
|
||||||
sudo cp target/release/server /usr/local/bin/chronoseal
|
```
|
||||||
|
|
||||||
|
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 cp chronoseal.service /etc/systemd/system/
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now chronoseal
|
sudo systemctl enable --now chronoseal
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
---
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reverse Proxy
|
|
||||||
|
|
||||||
Place ChronoSeal behind nginx, Nginx Proxy Manager, or HAProxy. Enable:
|
|
||||||
|
|
||||||
- TLS 1.3
|
|
||||||
- HTTP/2
|
|
||||||
- Aggressive upstream timeouts (the heartbeat interval is 12–25s)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Integration
|
# Development
|
||||||
|
|
||||||
Drop two lines into any protected page:
|
## Full Build
|
||||||
|
|
||||||
```html
|
```bash
|
||||||
<script type="module" src="/pkg/antibot_wasm.js"></script>
|
bash scripts/build.sh
|
||||||
<script type="module" src="/main.js"></script>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`main.js` calls `initHeartbeat()` which handles WASM loading, session init, and schedules all subsequent heartbeats automatically. There is no visible UI, no CAPTCHA, no user interaction required.
|
## Development Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Direct Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p server -- run --bind 127.0.0.1:3000
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration
|
# Prerequisites
|
||||||
|
|
||||||
All tunable constants are in `shared/src/constants.rs`:
|
* Rust stable ≥ 1.87
|
||||||
|
* `wasm-pack`
|
||||||
|
|
||||||
| Constant | Default | Description |
|
Install:
|
||||||
|---|---|---|
|
|
||||||
| `SESSION_ID_LEN` | 32 bytes | Session ID entropy |
|
```bash
|
||||||
| `SALT_LEN` | 16 bytes | Per-heartbeat salt size |
|
cargo install wasm-pack
|
||||||
| `HEARTBEAT_MIN_INTERVAL_MS` | 12 000 ms | Minimum heartbeat interval |
|
```
|
||||||
| `HEARTBEAT_MAX_INTERVAL_MS` | 25 000 ms | Maximum heartbeat interval (uniform jitter) |
|
|
||||||
| `EXPIRATION_MINUTES` | 30 min | Session lifetime after last heartbeat |
|
|
||||||
| `RATE_LIMIT_COUNT` | 5 | Max heartbeats per window |
|
|
||||||
| `RATE_LIMIT_WINDOW_SECS` | 10 s | Rate limit window |
|
|
||||||
| `MAX_TIMESTAMP_DRIFT_MS` | 30 000 ms | Anti-replay timestamp window |
|
|
||||||
| `MIN_MOUSE_TOTAL_DIST` | 10.0 px | Minimum cumulative mouse travel |
|
|
||||||
| `MAX_MOUSE_AVG_SPEED` | 2.0 px/ms | Maximum average mouse speed |
|
|
||||||
| `MIN_PAUSE_COUNT` | 1 | Minimum mouse pause events |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
# 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/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
[MIT OR Apache-2.0](LICENSE)
|
[MIT OR Apache-2.0](LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project
|
||||||
|
|
||||||
|
GitHub:
|
||||||
|
https://github.com/thakares/chronoseal-rs
|
||||||
+21
-52
@@ -1,67 +1,36 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=ChronoSeal cryptographic browser attestation service
|
Description=ChronoSeal Cryptographic Attestation Daemon
|
||||||
Documentation=https://chronoseal.rs
|
After=network.target
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|
||||||
User=chronoseal
|
User=chronoseal
|
||||||
Group=chronoseal
|
Group=chronoseal
|
||||||
|
|
||||||
Environment=RUST_LOG=info
|
|
||||||
Environment=CHRONOSEAL_CONFIG=/etc/chronoseal/config.toml
|
|
||||||
Environment=CHRONOSEAL_STATE_DIR=/var/lib/chronoseal
|
|
||||||
Environment=CHRONOSEAL_PID_FILE=/run/chronoseal.pid
|
|
||||||
|
|
||||||
ExecStart=/usr/local/bin/chronoseal run
|
ExecStart=/usr/local/bin/chronoseal run
|
||||||
ExecStartPre=+/usr/bin/touch /run/chronoseal.pid
|
WorkingDirectory=/opt/chronoseal
|
||||||
ExecStartPre=+/usr/bin/chown chronoseal:chronoseal /run/chronoseal.pid
|
Restart=always
|
||||||
ExecReload=/bin/kill -HUP $MAINPID
|
|
||||||
ExecStopPost=+/usr/bin/rm -f /run/chronoseal.pid
|
|
||||||
PIDFile=/run/chronoseal.pid
|
|
||||||
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
TimeoutStopSec=30
|
Environment=RUST_LOG=info
|
||||||
KillSignal=SIGTERM
|
|
||||||
|
|
||||||
RuntimeDirectory=chronoseal
|
# Hardening (production-grade)
|
||||||
RuntimeDirectoryMode=0750
|
|
||||||
StateDirectory=chronoseal
|
|
||||||
StateDirectoryMode=0750
|
|
||||||
LogsDirectory=chronoseal
|
|
||||||
LogsDirectoryMode=0750
|
|
||||||
ConfigurationDirectory=chronoseal
|
|
||||||
ConfigurationDirectoryMode=0750
|
|
||||||
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=read-only
|
ProtectHome=yes
|
||||||
ProtectKernelTunables=true
|
NoNewPrivileges=yes
|
||||||
ProtectKernelModules=true
|
PrivateTmp=yes
|
||||||
ProtectControlGroups=true
|
ProtectKernelTunables=yes
|
||||||
ProtectClock=true
|
ProtectKernelModules=yes
|
||||||
ProtectHostname=true
|
ProtectControlGroups=yes
|
||||||
ProtectProc=invisible
|
MemoryDenyWriteExecute=yes
|
||||||
ProcSubset=pid
|
RestrictRealtime=yes
|
||||||
PrivateDevices=true
|
RestrictSUIDSGID=yes
|
||||||
PrivateIPC=true
|
LockPersonality=yes
|
||||||
|
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
RestrictRealtime=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
RemoveIPC=true
|
|
||||||
|
|
||||||
LockPersonality=true
|
|
||||||
|
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
SystemCallFilter=@system-service
|
ReadWritePaths=/run/chronoseal.pid
|
||||||
SystemCallErrorNumber=EPERM
|
|
||||||
CapabilityBoundingSet=
|
# Logging
|
||||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
+38
-44
@@ -1,52 +1,46 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
set -eu
|
set -euo pipefail
|
||||||
|
|
||||||
CHRONOSEAL_VERSION="${CHRONOSEAL_VERSION:-latest}"
|
echo "🚀 ChronoSeal Installer"
|
||||||
CHRONOSEAL_INSTALL_DIR="${CHRONOSEAL_INSTALL_DIR:-/usr/local/bin}"
|
echo "======================"
|
||||||
CHRONOSEAL_BASE_URL="${CHRONOSEAL_BASE_URL:-https://get.chronoseal.rs/releases}"
|
|
||||||
|
|
||||||
need() {
|
# Create system user
|
||||||
command -v "$1" >/dev/null 2>&1 || {
|
if ! id -u chronoseal &>/dev/null; then
|
||||||
echo "chronoseal installer: missing required command: $1" >&2
|
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chronoseal
|
||||||
exit 1
|
echo "✓ Created chronoseal system user"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
need uname
|
|
||||||
need mktemp
|
|
||||||
need chmod
|
|
||||||
|
|
||||||
arch="$(uname -m)"
|
|
||||||
case "$arch" in
|
|
||||||
x86_64|amd64) target="x86_64-unknown-linux-musl" ;;
|
|
||||||
aarch64|arm64) target="aarch64-unknown-linux-musl" ;;
|
|
||||||
*) echo "chronoseal installer: unsupported architecture: $arch" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
fetch="curl --proto =https --tlsv1.2 -fsSL"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
fetch="wget -qO-"
|
|
||||||
else
|
|
||||||
echo "chronoseal installer: install curl or wget" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tmp="$(mktemp -d)"
|
# Build
|
||||||
trap 'rm -rf "$tmp"' EXIT
|
echo "→ Building ChronoSeal..."
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
bash scripts/build.sh
|
||||||
|
|
||||||
url="$CHRONOSEAL_BASE_URL/$CHRONOSEAL_VERSION/chronoseal-$target.tar.gz"
|
# Install binary
|
||||||
echo "downloading chronoseal $CHRONOSEAL_VERSION for $target"
|
sudo install -Dm755 target/release/chronoseal /usr/local/bin/chronoseal
|
||||||
|
echo "✓ Installed binary to /usr/local/bin/chronoseal"
|
||||||
|
|
||||||
# shellcheck disable=SC2086
|
# Install frontend assets
|
||||||
$fetch "$url" | tar -xz -C "$tmp"
|
sudo mkdir -p /opt/chronoseal
|
||||||
chmod 0755 "$tmp/chronoseal"
|
sudo cp -r frontend /opt/chronoseal/
|
||||||
|
sudo chown -R chronoseal:chronoseal /opt/chronoseal
|
||||||
|
echo "✓ Installed frontend assets"
|
||||||
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
# Install systemd service
|
||||||
install -m 0755 "$tmp/chronoseal" "$CHRONOSEAL_INSTALL_DIR/chronoseal"
|
sudo cp chronoseal.service /etc/systemd/system/chronoseal.service
|
||||||
else
|
sudo systemctl daemon-reload
|
||||||
sudo install -m 0755 "$tmp/chronoseal" "$CHRONOSEAL_INSTALL_DIR/chronoseal"
|
echo "✓ Installed systemd service"
|
||||||
fi
|
|
||||||
|
|
||||||
echo "installed: $CHRONOSEAL_INSTALL_DIR/chronoseal"
|
# Enable and start
|
||||||
echo "try: chronoseal --help"
|
sudo systemctl enable --now chronoseal
|
||||||
|
echo "✓ ChronoSeal service started"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ ChronoSeal installed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Useful commands:"
|
||||||
|
echo " chronoseal status # Check service status"
|
||||||
|
echo " chronoseal health # Health probe"
|
||||||
|
echo " sudo systemctl status chronoseal"
|
||||||
|
echo " sudo journalctl -u chronoseal -f"
|
||||||
|
echo ""
|
||||||
|
echo "To uninstall: sudo systemctl disable --now chronoseal && sudo rm /usr/local/bin/chronoseal"
|
||||||
@@ -20,13 +20,7 @@ pub async fn cleanup_loop(state: Arc<AppState>) {
|
|||||||
|
|
||||||
// Evict stale rate-limiter entries to prevent unbounded HashMap growth.
|
// Evict stale rate-limiter entries to prevent unbounded HashMap growth.
|
||||||
{
|
{
|
||||||
let window_secs = {
|
let window_secs = state.get_config().rate_limit_window_secs;
|
||||||
if let Ok(config) = state.config.read() {
|
|
||||||
config.rate_limit_window_secs
|
|
||||||
} else {
|
|
||||||
10 // fallback default
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut rl = state.rate_limiter.lock().await;
|
let mut rl = state.rate_limiter.lock().await;
|
||||||
rl.evict_stale(window_secs);
|
rl.evict_stale(window_secs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ pub async fn handler(
|
|||||||
// Rate limiting
|
// Rate limiting
|
||||||
{
|
{
|
||||||
let (limit, window_secs) = {
|
let (limit, window_secs) = {
|
||||||
if let Ok(cfg) = state.config.read() {
|
let cfg = state.get_config();
|
||||||
(cfg.rate_limit_count, cfg.rate_limit_window_secs)
|
(cfg.rate_limit_count, cfg.rate_limit_window_secs)
|
||||||
} else {
|
|
||||||
(5, 10)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let mut rl = state.rate_limiter.lock().await;
|
let mut rl = state.rate_limiter.lock().await;
|
||||||
if !rl.check(&payload.session_id, limit, window_secs) {
|
if !rl.check(&payload.session_id, limit, window_secs) {
|
||||||
@@ -29,13 +26,7 @@ pub async fn handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = {
|
let config = state.get_config();
|
||||||
if let Ok(cfg) = state.config.read() {
|
|
||||||
cfg.clone()
|
|
||||||
} else {
|
|
||||||
crate::config::Config::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let conn = match state.db_pool.get() {
|
let conn = match state.db_pool.get() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ pub async fn handler(
|
|||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(payload): Json<InitRequest>,
|
Json(payload): Json<InitRequest>,
|
||||||
) -> Result<Json<InitResponse>, SessionError> {
|
) -> Result<Json<InitResponse>, SessionError> {
|
||||||
let config = {
|
let config = state.get_config();
|
||||||
if let Ok(cfg) = state.config.read() {
|
|
||||||
cfg.clone()
|
|
||||||
} else {
|
|
||||||
crate::config::Config::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let conn = state.db_pool.get()?;
|
let conn = state.db_pool.get()?;
|
||||||
let resp = crate::session::create_session(&conn, &config, &payload.public_key)?;
|
let resp = crate::session::create_session(&conn, &config, &payload.public_key)?;
|
||||||
Ok(Json(resp))
|
Ok(Json(resp))
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ pub struct AppState {
|
|||||||
pub config: std::sync::RwLock<crate::config::Config>,
|
pub config: std::sync::RwLock<crate::config::Config>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn get_config(&self) -> crate::config::Config {
|
||||||
|
if let Ok(cfg) = self.config.read() {
|
||||||
|
cfg.clone()
|
||||||
|
} else {
|
||||||
|
crate::config::Config::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use crate::{crypto, fingerprint, storage, trust, vm};
|
use crate::{crypto, fingerprint, storage, trust, vm};
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use shared::protocol::{HeartbeatRequest, InitResponse};
|
use shared::protocol::{HeartbeatRequest, InitResponse};
|
||||||
|
|||||||
Reference in new issue
Block a user