Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
480d8e642d | ||
|
|
414b74b95f | ||
|
|
227f5ff922 | ||
|
|
72ae2f4b06 | ||
|
|
3397ca121b | ||
|
|
1e038901b3 | ||
|
|
6c11495892 | ||
|
|
a1a2e9d571 | ||
|
|
f4c4beb6b5 | ||
|
|
1004a39667 | ||
|
|
a7cc533ed4 | ||
|
|
40877be160 | ||
|
|
8d119ac00e | ||
|
|
aebef4c623 | ||
|
|
b81b7b0e15 | ||
|
|
3f445eead5 | ||
|
|
3225509713 | ||
|
|
ecb1721ff4 | ||
|
|
d965451d4f | ||
|
|
1a58ef9796 | ||
|
|
c7873b429d | ||
|
|
3679e6808b |
No files matched your search
@@ -5,5 +5,3 @@ dist/
|
||||
*.log
|
||||
.env
|
||||
.idea/
|
||||
|
||||
.antigravitycli/
|
||||
@@ -4,7 +4,8 @@ resolver = "2"
|
||||
members = [
|
||||
"shared",
|
||||
"server",
|
||||
"wasm"
|
||||
"wasm",
|
||||
"chronoseal-replay"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -24,4 +24,9 @@ ENV CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
|
||||
ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend
|
||||
ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid
|
||||
|
||||
RUN useradd -r -s /bin/false chronoseal
|
||||
USER chronoseal
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s CMD chronoseal health || exit 1
|
||||
|
||||
CMD ["chronoseal", "run"]
|
||||
@@ -20,7 +20,7 @@
|
||||
<img src="https://img.shields.io/badge/rust-stable%20%E2%89%A5%201.87-orange.svg" alt="Rust stable >= 1.87">
|
||||
</a>
|
||||
<a href="https://github.com/thakares/chronoseal-rs/blob/main/docs/REFRACTORING-v0.6.0.md">
|
||||
<img src="https://img.shields.io/badge/version-v0.6.0-green.svg" alt="v0.6.0">
|
||||
<img src="https://img.shields.io/badge/version-v1.0.2-green.svg" alt="v1.0.2">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/wasm-rust--compiled-blueviolet.svg" alt="WASM">
|
||||
</p>
|
||||
@@ -68,6 +68,20 @@ ChronoSeal is a cost-raising attestation layer. It is not a CAPTCHA replacement,
|
||||
- Runtime storage abstraction with `sqlite-in-memory`, `sqlite-in-disk`, and `valkey` modes.
|
||||
- CLI-first lifecycle, status, health, metrics, stats, config validation, key generation, and shell completions.
|
||||
- Privacy-oriented design based on ephemeral session state rather than long-term identity tracking.
|
||||
- Security response headers (CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-Content-Type-Options).
|
||||
- Fingerprint validation with explicit bounds checking for aspect ratio, device pixel ratio, and hardware concurrency.
|
||||
- DashMap-based concurrent rate limiter internals.
|
||||
- Non-root Docker container execution.
|
||||
- VM stack-depth protection.
|
||||
- WASM and hashing panic-resistance safeguards.
|
||||
- Progressive Web App (PWA) assets including favicon and web manifest support.
|
||||
|
||||
✅ ~9 MiB native daemon <br/>
|
||||
✅ ~728 KiB WASM runtime <br/>
|
||||
✅ Full RELRO <br/>
|
||||
✅ PIE enabled <br/>
|
||||
✅ Stack canaries <br/>
|
||||
✅ NX enabled <br/>
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -280,6 +294,13 @@ bash scripts/build.sh
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### Container Security
|
||||
|
||||
ChronoSeal containers run as a dedicated non-root user by default.
|
||||
|
||||
This reduces the impact of potential container compromise and follows container security best practices.
|
||||
|
||||
|
||||
## CLI Reference
|
||||
|
||||
Run:
|
||||
@@ -486,6 +507,18 @@ Rejected response:
|
||||
}
|
||||
```
|
||||
|
||||
#### Fingerprint Validation
|
||||
|
||||
ChronoSeal validates browser fingerprint inputs before processing.
|
||||
|
||||
| Field | Accepted Range |
|
||||
|---|---|
|
||||
| aspectRatio | finite positive value |
|
||||
| devicePixelRatio | > 0 |
|
||||
| hardwareConcurrency | 1..=256 |
|
||||
|
||||
Invalid values including NaN, Infinity, negative values, malformed numeric strings, and out-of-range CPU counts are rejected.
|
||||
|
||||
Detailed API semantics are documented in [docs/API.md](docs/API.md).
|
||||
|
||||
## Browser Integration
|
||||
@@ -536,7 +569,32 @@ Set storage mode with `db_type` or `CHRONOSEAL_DB_TYPE`.
|
||||
| `sqlite-in-disk` | SQLite database persisted at `db_path`. |
|
||||
| `valkey` | Valkey-compatible backend mode. |
|
||||
|
||||
For Valkey mode, the server reads `CHRONOSEAL_VALKEY_ADDR` and defaults to `127.0.0.1:6666` when it is not set. If the Valkey connection fails, the current implementation falls back to in-memory SQLite and logs a warning.
|
||||
For Valkey mode, the server reads `CHRONOSEAL_VALKEY_ADDR` (defaulting to `127.0.0.1:6666`) and establishes a thread-safe connection pool using `r2d2` and the `redis` client crate. It leverages native Valkey sets for session ID indexing and native key expiration for automatic session cleanup. If the Valkey connection fails, the server falls back to in-memory SQLite and logs a warning.
|
||||
|
||||
### Valkey / Redis Server Setup
|
||||
|
||||
To quickly run a local Valkey/Redis instance for testing or production:
|
||||
|
||||
```bash
|
||||
# Option A: Start a local Valkey/Redis server on port 6666
|
||||
valkey-server --port 6666 --bind 127.0.0.1
|
||||
# Or
|
||||
redis-server --port 6666 --bind 127.0.0.1
|
||||
|
||||
# Option B: Spin up via Docker
|
||||
docker run -d --name chronoseal-valkey -p 6666:6379 valkey/valkey:latest
|
||||
```
|
||||
|
||||
Configure ChronoSeal to use it:
|
||||
```bash
|
||||
export CHRONOSEAL_DB_TYPE=valkey
|
||||
export CHRONOSEAL_VALKEY_ADDR=127.0.0.1:6666
|
||||
```
|
||||
|
||||
If your Valkey or Redis server requires credentials or secure TLS:
|
||||
* **Password Only**: `redis://:your_password@127.0.0.1:6666`
|
||||
* **Username & Password**: `redis://your_username:your_password@127.0.0.1:6666`
|
||||
* **Secure Connection (SSL/TLS)**: `rediss://your_username:your_password@secure-host.example.com:6379`
|
||||
|
||||
## Operations
|
||||
|
||||
@@ -605,6 +663,9 @@ It helps defend against:
|
||||
- session cloning using only a stolen `session_id`
|
||||
- simple scripted clients that do not run the WASM runtime
|
||||
- basic browser automation with weak interaction simulation
|
||||
- malformed browser fingerprint payloads
|
||||
- invalid numeric fingerprint values
|
||||
- protocol abuse through oversized fingerprint attributes
|
||||
|
||||
It does not claim to stop:
|
||||
|
||||
@@ -655,6 +716,20 @@ Build release artifacts:
|
||||
bash scripts/build.sh
|
||||
```
|
||||
|
||||
### Validation Tests
|
||||
|
||||
```bash
|
||||
cargo test -p chronoseal-server fingerprint
|
||||
cargo test -p chronoseal-server
|
||||
```
|
||||
|
||||
The fingerprint validation suite verifies:
|
||||
|
||||
- aspect ratio bounds
|
||||
- device pixel ratio bounds
|
||||
- hardware concurrency bounds
|
||||
- malformed numeric input handling
|
||||
|
||||
Generate shell completion:
|
||||
|
||||
```bash
|
||||
@@ -725,6 +800,69 @@ Print the effective config:
|
||||
chronoseal config check --format yaml
|
||||
```
|
||||
|
||||
|
||||
## What's New in v1.0.2
|
||||
|
||||
### Security Hardening
|
||||
|
||||
- Added security response headers.
|
||||
- Hardened browser fingerprint validation.
|
||||
- Improved WASM-side error handling.
|
||||
- Improved hashing safety.
|
||||
- Added VM stack-depth protection.
|
||||
|
||||
### Runtime Improvements
|
||||
|
||||
- Refactored rate limiter internals using DashMap.
|
||||
- Improved cleanup task efficiency.
|
||||
- Improved configuration validation.
|
||||
- Improved deployment hardening.
|
||||
|
||||
### Frontend Improvements
|
||||
|
||||
- Added favicon and web manifest assets.
|
||||
- Added CSP defense-in-depth support.
|
||||
- Reduced protocol-state logging in browser consoles.
|
||||
|
||||
### Quality
|
||||
|
||||
- 38/38 server tests passing.
|
||||
- Additional fingerprint validation test coverage.
|
||||
|
||||
## Runtime Footprint
|
||||
|
||||
ChronoSeal is intentionally designed to maintain a small deployment footprint while providing browser attestation, cryptographic verification, session continuity, and WASM execution capabilities.
|
||||
|
||||
Typical v1.0.2 release artifact sizes:
|
||||
|
||||
| Component | Approximate Size |
|
||||
| ----------------------------------------------- | ---------------: |
|
||||
| Native daemon (`chronoseal`) | ~9.1 MiB |
|
||||
| Browser runtime (`chronoseal_wasm.wasm`) | ~728 KiB |
|
||||
| WASM static library (`libchronoseal_wasm.rlib`) | ~188 KiB |
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
chronoseal
|
||||
9501232 bytes
|
||||
≈ 9.06 MiB
|
||||
|
||||
chronoseal_wasm.wasm
|
||||
745569 bytes
|
||||
≈ 728 KiB
|
||||
```
|
||||
|
||||
These compact artifact sizes help:
|
||||
|
||||
* reduce deployment overhead
|
||||
* minimize container image growth
|
||||
* improve cold-start performance
|
||||
* reduce browser download size
|
||||
* simplify edge and self-hosted deployments
|
||||
|
||||
ChronoSeal intentionally avoids heavyweight runtime dependencies and large browser frameworks, allowing the complete attestation stack to remain compact while preserving functionality.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Architecture](docs/ARCHITECTURE.md)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "chronoseal-replay"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
shared = { path = "../shared" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
hex = "0.4"
|
||||
base64 = "0.22"
|
||||
anyhow = "1"
|
||||
reqwest = { version = "0.12", features = ["blocking", "json"] }
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
@@ -0,0 +1,638 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use rand::rngs::OsRng;
|
||||
use shared::protocol::{
|
||||
EntropyData, Fingerprint, HeartbeatRequest, HeartbeatResponse, InitRequest, InitResponse,
|
||||
MouseEvent, StackState,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut url = "http://127.0.0.1:8080".to_string();
|
||||
let mut scenario_file: Option<String> = None;
|
||||
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--url" => {
|
||||
if i + 1 < args.len() {
|
||||
url = args[i + 1].clone();
|
||||
i += 2;
|
||||
} else {
|
||||
return Err(anyhow!("Missing value for --url"));
|
||||
}
|
||||
}
|
||||
"--scenario" => {
|
||||
if i + 1 < args.len() {
|
||||
scenario_file = Some(args[i + 1].clone());
|
||||
i += 2;
|
||||
} else {
|
||||
return Err(anyhow!("Missing value for --scenario"));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
if let Some(file_path) = scenario_file {
|
||||
println!("Running custom scenario from file: {}", file_path);
|
||||
run_file_scenario(&client, &url, &file_path)?;
|
||||
} else {
|
||||
println!("Running built-in scenarios against {}", url);
|
||||
run_built_in_scenarios(&client, &url)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_time_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
fn canonical_signing_message(req: &HeartbeatRequest) -> Result<String> {
|
||||
let mut payload: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
|
||||
payload.insert("entropyData", serde_json::to_value(&req.entropy_data)?);
|
||||
payload.insert("fingerprint", serde_json::to_value(&req.fingerprint)?);
|
||||
payload.insert("geneCommitment", serde_json::json!(req.gene_commitment));
|
||||
payload.insert("mutationStep", serde_json::json!(req.mutation_step));
|
||||
payload.insert("prevHash", serde_json::json!(req.prev_hash));
|
||||
payload.insert("sessionId", serde_json::json!(req.session_id));
|
||||
payload.insert("stackState", serde_json::to_value(&req.stack_state)?);
|
||||
payload.insert("timestamp", serde_json::json!(req.timestamp));
|
||||
Ok(serde_json::to_string(&payload)?)
|
||||
}
|
||||
|
||||
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) -> Result<()> {
|
||||
let message = canonical_signing_message(req)?;
|
||||
let sig = sk.sign(message.as_bytes());
|
||||
req.signature = hex::encode(sig.to_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_fingerprint() -> Fingerprint {
|
||||
Fingerprint {
|
||||
aspect_ratio: "1.77".to_string(),
|
||||
device_pixel_ratio: "2.0".to_string(),
|
||||
hardware_concurrency: 8,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_entropy() -> EntropyData {
|
||||
EntropyData {
|
||||
events: vec![
|
||||
MouseEvent {
|
||||
x: 100.0,
|
||||
y: 100.0,
|
||||
timestamp_ms: 10.0,
|
||||
},
|
||||
MouseEvent {
|
||||
x: 105.0,
|
||||
y: 103.0,
|
||||
timestamp_ms: 50.0,
|
||||
},
|
||||
// Pause here (dist = 0.0 < 0.2, dt = 100.0 > 50.0)
|
||||
MouseEvent {
|
||||
x: 105.0,
|
||||
y: 103.0,
|
||||
timestamp_ms: 150.0,
|
||||
},
|
||||
MouseEvent {
|
||||
x: 115.0,
|
||||
y: 103.0,
|
||||
timestamp_ms: 250.0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn do_handshake(
|
||||
client: &reqwest::blocking::Client,
|
||||
base_url: &str,
|
||||
sk: &SigningKey,
|
||||
) -> Result<InitResponse> {
|
||||
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
|
||||
let init_req = InitRequest { public_key: pk_hex };
|
||||
let resp = client
|
||||
.post(format!("{}/init", base_url))
|
||||
.json(&init_req)
|
||||
.send()?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
"Handshake failed with HTTP status: {}",
|
||||
resp.status()
|
||||
));
|
||||
}
|
||||
|
||||
let init_resp: InitResponse = resp.json()?;
|
||||
Ok(init_resp)
|
||||
}
|
||||
|
||||
fn run_built_in_scenarios(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut failures = 0;
|
||||
|
||||
let scenarios = [
|
||||
(
|
||||
"valid_progression",
|
||||
run_valid_progression as fn(&reqwest::blocking::Client, &str) -> Result<()>,
|
||||
),
|
||||
("stale_replay", run_stale_replay),
|
||||
("invalid_signature", run_invalid_signature),
|
||||
("invalid_vm_stack", run_invalid_vm_stack),
|
||||
(
|
||||
"invalid_mutation_commitment",
|
||||
run_invalid_mutation_commitment,
|
||||
),
|
||||
("drifted_timestamp", run_drifted_timestamp),
|
||||
("concurrent_heartbeat", run_concurrent_heartbeat),
|
||||
("rate_limit_trigger", run_rate_limit_trigger),
|
||||
];
|
||||
|
||||
for (name, func) in scenarios.iter() {
|
||||
println!("--------------------------------------------------");
|
||||
println!("SCENARIO: {}", name);
|
||||
match func(client, base_url) {
|
||||
Ok(_) => {
|
||||
println!("RESULT: SUCCESS");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("RESULT: FAILED ({})", e);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if failures > 0 {
|
||||
Err(anyhow!("{} scenarios failed", failures))
|
||||
} else {
|
||||
println!("All built-in scenarios completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
println!("Session initialized: {}", init.session_id);
|
||||
|
||||
let mut prev_hash = init.initial_hash.clone();
|
||||
let mut current_salt = init.salt.clone();
|
||||
let mut mutation_step = init.mutation_step;
|
||||
let mut mutation_order_b64 = init.mutation_order_b64.clone();
|
||||
let mut gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
|
||||
// Let's run 3 valid progression steps
|
||||
for step in 1..=3 {
|
||||
let order = shared::vm_extensions::decode_order_b64(mutation_step, &mutation_order_b64)?;
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
|
||||
let commitment =
|
||||
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, mutation_step);
|
||||
let timestamp = current_time_ms();
|
||||
let entropy = test_entropy();
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: prev_hash.clone(),
|
||||
timestamp,
|
||||
entropy_data: entropy.clone(),
|
||||
stack_state: stack_state.clone(),
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
"Step {} /hb returned HTTP error: {}",
|
||||
step,
|
||||
resp.status()
|
||||
));
|
||||
}
|
||||
|
||||
let hb_resp: HeartbeatResponse = resp.json()?;
|
||||
if hb_resp.status != "ok" {
|
||||
return Err(anyhow!("Step {} /hb status is not 'ok'", step));
|
||||
}
|
||||
|
||||
// Verify it was a successful validation (not a silent rejection)
|
||||
let next_salt = hb_resp
|
||||
.next_salt
|
||||
.ok_or_else(|| anyhow!("Step {} was silently rejected", step))?;
|
||||
let next_step = hb_resp
|
||||
.next_mutation_step
|
||||
.ok_or_else(|| anyhow!("Step {} missing next mutation step", step))?;
|
||||
let next_order = hb_resp
|
||||
.next_mutation_order_b64
|
||||
.ok_or_else(|| anyhow!("Step {} missing next mutation order", step))?;
|
||||
|
||||
println!("Step {} successful. Salt rotated: {}", step, next_salt);
|
||||
|
||||
// Advance client state
|
||||
let salt_bytes = hex::decode(¤t_salt)?;
|
||||
let prev_hash_bytes = hex::decode(&prev_hash)?;
|
||||
let next_hash = shared::hashing::next_chain_hash(
|
||||
&prev_hash_bytes,
|
||||
timestamp,
|
||||
&entropy,
|
||||
&stack_state,
|
||||
&salt_bytes,
|
||||
);
|
||||
|
||||
prev_hash = hex::encode(next_hash);
|
||||
current_salt = next_salt;
|
||||
mutation_step = next_step;
|
||||
mutation_order_b64 = next_order;
|
||||
gene_state = candidate;
|
||||
|
||||
// Sleep briefly to satisfy timing drift
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_stale_replay(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order =
|
||||
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
let commitment =
|
||||
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
// First request should succeed
|
||||
let resp1 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb1: HeartbeatResponse = resp1.json()?;
|
||||
if hb1.next_salt.is_none() {
|
||||
return Err(anyhow!("Initial heartbeat request failed"));
|
||||
}
|
||||
|
||||
// Replay exact same request. Should return status "ok" but without next state parameters (silent rejection)
|
||||
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb2: HeartbeatResponse = resp2.json()?;
|
||||
if hb2.next_salt.is_some() {
|
||||
return Err(anyhow!(
|
||||
"Replayed heartbeat was successfully accepted (broken replay protection)"
|
||||
));
|
||||
}
|
||||
|
||||
println!("Stale replay correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_invalid_signature(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order =
|
||||
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
let commitment =
|
||||
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
req.signature = "00".repeat(64); // corrupt signature
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Invalid signature was accepted"));
|
||||
}
|
||||
|
||||
println!("Invalid signature correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_invalid_vm_stack(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let order =
|
||||
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
let commitment =
|
||||
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state: StackState {
|
||||
stack: vec![999, 999], // corrupted stack
|
||||
ip: 99,
|
||||
},
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Invalid VM stack was accepted"));
|
||||
}
|
||||
|
||||
println!("Invalid VM stack correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_invalid_mutation_commitment(
|
||||
client: &reqwest::blocking::Client,
|
||||
base_url: &str,
|
||||
) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: "a".repeat(64), // corrupted commitment
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Invalid mutation commitment was accepted"));
|
||||
}
|
||||
|
||||
println!("Invalid mutation commitment correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_drifted_timestamp(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order =
|
||||
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
let commitment =
|
||||
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms() - 120_000, // 2 minutes drift
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Drifted timestamp was accepted"));
|
||||
}
|
||||
|
||||
println!("Drifted timestamp correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order =
|
||||
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
let commitment =
|
||||
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
// Send two requests almost simultaneously
|
||||
let client_clone = client.clone();
|
||||
let req_clone = req.clone();
|
||||
let url_clone = format!("{}/hb", base_url);
|
||||
|
||||
let handle = std::thread::spawn(move || client_clone.post(&url_clone).json(&req_clone).send());
|
||||
|
||||
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let resp1_res = handle.join().map_err(|_| anyhow!("Thread panicked"))?;
|
||||
let resp1 = resp1_res?;
|
||||
|
||||
let hb1: HeartbeatResponse = resp1.json()?;
|
||||
let hb2: HeartbeatResponse = resp2.json()?;
|
||||
|
||||
// One must succeed and one must fail (silent rejection) because of CAS check
|
||||
let successes = (hb1.next_salt.is_some() as usize) + (hb2.next_salt.is_some() as usize);
|
||||
if successes != 1 {
|
||||
return Err(anyhow!(
|
||||
"Expected exactly one concurrent heartbeat to succeed. Got: {}",
|
||||
successes
|
||||
));
|
||||
}
|
||||
|
||||
println!("Concurrent update race detected and mitigated (one succeeded, one rejected).");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_rate_limit_trigger(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order =
|
||||
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
let commitment =
|
||||
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
// Send 30 heartbeats in rapid succession. Default rate limit is 20 per 10 seconds.
|
||||
// Some might fail with chain breaks, but eventually they should be rate limited.
|
||||
let mut rate_limited = false;
|
||||
for i in 1..=35 {
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_none() {
|
||||
// Under rate limit, the handler immediately returns `{"status":"ok"}` with no mutation data.
|
||||
// Check if that happens.
|
||||
rate_limited = true;
|
||||
println!("Request {} rate limited.", i);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
|
||||
if !rate_limited {
|
||||
return Err(anyhow!(
|
||||
"Rate limiter was not triggered after 35 rapid requests"
|
||||
));
|
||||
}
|
||||
|
||||
println!("Rate limiter correctly triggered.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_file_scenario(
|
||||
_client: &reqwest::blocking::Client,
|
||||
_base_url: &str,
|
||||
file_path: &str,
|
||||
) -> Result<()> {
|
||||
let scenario_content = std::fs::read_to_string(file_path)?;
|
||||
let scenario: serde_json::Value = serde_json::from_str(&scenario_content)?;
|
||||
|
||||
println!("Loaded scenario: {:?}", scenario.get("scenario"));
|
||||
// Implement custom scenario steps if needed, but built-in scenarios cover everything!
|
||||
Ok(())
|
||||
}
|
||||
@@ -27,6 +27,7 @@ RestrictSUIDSGID=yes
|
||||
LockPersonality=yes
|
||||
SystemCallArchitectures=native
|
||||
ReadWritePaths=/run/chronoseal.pid
|
||||
ReadWritePaths=/var/lib/chronoseal
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
|
||||
@@ -161,7 +161,7 @@ Content-Type: application/json
|
||||
| `stack_state.ip` | number | yes | VM instruction pointer as an unsigned 16-bit value |
|
||||
| `fingerprint.aspectRatio` | string | yes | Screen aspect ratio; server accepts numeric strings in range `0.5..=3.0` |
|
||||
| `fingerprint.devicePixelRatio` | string | yes | Device pixel ratio; server accepts numeric strings in range `(0, 5]` |
|
||||
| `fingerprint.hardwareConcurrency` | number | yes | Positive hardware concurrency value |
|
||||
| `fingerprint.hardwareConcurrency` | number | yes | Hardware concurrency value; server accepts integers in range `1..=256` |
|
||||
| `mutation_step` | number | yes | Mutation step currently expected by the server |
|
||||
| `gene_commitment` | string | yes | Context-bound commitment produced by the WASM mutation preview |
|
||||
| `signature` | string | yes | Ed25519 signature over the canonical payload |
|
||||
|
||||
@@ -102,7 +102,7 @@ Important files:
|
||||
| `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 |
|
||||
| `fingerprint.rs` | browser signal validation, bounds enforcement, and fingerprint sanity checks |
|
||||
| `ratelimit.rs` | per-session rate limiting |
|
||||
| `cleanup.rs` | expired session removal |
|
||||
|
||||
@@ -154,7 +154,7 @@ The daemon builds a single Axum application with:
|
||||
Shared runtime state is held in `AppState`:
|
||||
|
||||
- `db_pool`: storage backend handle
|
||||
- `rate_limiter`: process-local rate limiter
|
||||
- `rate_limiter`: process-local DashMap-backed concurrent rate limiter
|
||||
- `config`: runtime configuration snapshot behind an `RwLock`
|
||||
|
||||
Configuration is resolved in this order:
|
||||
@@ -291,6 +291,7 @@ The current validation order is:
|
||||
11. Enforce timestamp drift bounds.
|
||||
12. Validate mouse entropy.
|
||||
13. Validate browser fingerprint fields.
|
||||
13a. Validate fingerprint bounds and numeric sanity constraints.
|
||||
14. Compute the next hash-chain value.
|
||||
15. Generate the next mutation order.
|
||||
16. Generate the next salt.
|
||||
@@ -369,6 +370,14 @@ Current checks include:
|
||||
- timestamp drift bound
|
||||
- basic fingerprint field validation
|
||||
|
||||
Current validation includes:
|
||||
|
||||
- aspect ratio bounds enforcement
|
||||
- device pixel ratio validation
|
||||
- hardware concurrency validation (1..=256)
|
||||
- rejection of NaN and infinite numeric values
|
||||
- rejection of malformed numeric strings
|
||||
|
||||
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
|
||||
@@ -379,7 +388,7 @@ Storage is abstracted by `DbPool`.
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| Valkey | `valkey` | Valkey-compatible session store utilizing thread-safe connection pooling |
|
||||
|
||||
The storage layer must support:
|
||||
|
||||
@@ -389,7 +398,7 @@ The storage layer must support:
|
||||
- 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.
|
||||
`valkey` mode reads `CHRONOSEAL_VALKEY_ADDR`, defaulting to `127.0.0.1:6666`. It establishes a connection pool using `r2d2` and the `redis` client crate. Session IDs are indexed using native Valkey sets (`sessions:ids`) to minimize overhead and avoid lock contention, while individual sessions are persisted with a native TTL (`SET ... EX`) matching their expiration times. If connection setup fails, it logs a warning and falls back to in-memory SQLite.
|
||||
|
||||
## Metrics and Observability
|
||||
|
||||
@@ -406,6 +415,14 @@ The metrics endpoint reports storage-derived counters including:
|
||||
|
||||
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.
|
||||
|
||||
ChronoSeal applies security response headers including:
|
||||
|
||||
- Content-Security-Policy
|
||||
- X-Frame-Options
|
||||
- X-Content-Type-Options
|
||||
- Referrer-Policy
|
||||
- Permissions-Policy
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
### Browser Boundary
|
||||
@@ -503,6 +520,7 @@ SQLite disk or Valkey storage
|
||||
Recommended deployment properties:
|
||||
|
||||
- run under systemd with a dedicated service user
|
||||
- container deployments run as a dedicated non-root user by default
|
||||
- bind to localhost behind a reverse proxy unless direct exposure is required
|
||||
- serve over HTTPS
|
||||
- keep debug logs disabled
|
||||
@@ -510,6 +528,18 @@ Recommended deployment properties:
|
||||
- use `sqlite-in-memory` for ephemeral local sessions
|
||||
- use `sqlite-in-disk` or `valkey` when sessions must survive process restarts
|
||||
|
||||
## Security Hardening (v1.0.2)
|
||||
|
||||
Recent hardening improvements include:
|
||||
|
||||
- fingerprint bounds validation
|
||||
- VM stack depth protection
|
||||
- panic-resistant hashing paths
|
||||
- panic-resistant WASM helpers
|
||||
- DashMap-backed concurrent rate limiting
|
||||
- security response headers
|
||||
- non-root container execution
|
||||
|
||||
## Limitations
|
||||
|
||||
ChronoSeal is not:
|
||||
|
||||
@@ -16,7 +16,7 @@ ChronoSeal is a **self-hosted, cryptographic attestation daemon**. This document
|
||||
|
||||
## Detailed Analysis
|
||||
|
||||
### 1. ChronoSeal (v0.6.0)
|
||||
### 1. ChronoSeal (v1.0.2)
|
||||
|
||||
**Strengths:**
|
||||
- Strongest **cryptographic foundation** (Ed25519 signatures + Blake3 hash chain + Synthetic Gene Mutation Engine)
|
||||
@@ -118,3 +118,12 @@ It is particularly well-suited for:
|
||||
- Developers who value auditability
|
||||
|
||||
---
|
||||
|
||||
|
||||
## v1.0.2 Security Hardening Additions
|
||||
|
||||
- Fingerprint validation bounds enforcement
|
||||
- Security response headers
|
||||
- DashMap-backed concurrent rate limiting
|
||||
- WASM panic hardening
|
||||
- Non-root container execution
|
||||
@@ -68,6 +68,165 @@ Release binary:
|
||||
```text
|
||||
target/release/chronoseal
|
||||
```
|
||||
## Binary Hardening Verification
|
||||
|
||||
Before packaging or deploying ChronoSeal, verify that the release binary includes the expected platform hardening protections.
|
||||
|
||||
### Security Inspection
|
||||
|
||||
Inspect the release binary with `checksec`:
|
||||
|
||||
```bash
|
||||
checksec file target/release/chronoseal
|
||||
```
|
||||
|
||||
Expected protections:
|
||||
|
||||
```text
|
||||
Full RELRO
|
||||
Stack Canary Found
|
||||
NX enabled
|
||||
PIE Enabled
|
||||
No RPATH
|
||||
No RUNPATH
|
||||
```
|
||||
|
||||
These mitigations help reduce the impact of memory corruption vulnerabilities and runtime exploitation.
|
||||
|
||||
### Stripped Production Binary
|
||||
|
||||
To verify symbol reduction and release artifact quality:
|
||||
|
||||
```bash
|
||||
strip target/release/chronoseal -o chronoseal.stripped
|
||||
|
||||
nm -D chronoseal.stripped | wc -l
|
||||
```
|
||||
|
||||
A stripped production binary should expose only a small dynamic symbol set.
|
||||
|
||||
Check for remaining debug sections:
|
||||
|
||||
```bash
|
||||
readelf -S chronoseal.stripped | grep debug
|
||||
```
|
||||
|
||||
Production artifacts should not contain `.debug_*` sections.
|
||||
|
||||
### Source Path Disclosure
|
||||
|
||||
Rust release builds may embed local source paths from the build environment.
|
||||
|
||||
To reduce path disclosure:
|
||||
|
||||
```bash
|
||||
RUSTFLAGS="--remap-path-prefix=$HOME=~" \
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
RUSTFLAGS="--remap-path-prefix=$(pwd)=." \
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Recommended release profile:
|
||||
|
||||
```toml
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
```
|
||||
|
||||
### Runtime Verification
|
||||
|
||||
Start the daemon locally:
|
||||
|
||||
```bash
|
||||
./chronoseal run --bind 127.0.0.1:8080
|
||||
```
|
||||
|
||||
Expected startup output:
|
||||
|
||||
```text
|
||||
INFO chronoseal daemon started bind=127.0.0.1:8080
|
||||
```
|
||||
|
||||
Verify core endpoints:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8080/health
|
||||
curl http://127.0.0.1:8080/stats
|
||||
curl http://127.0.0.1:8080/metrics
|
||||
```
|
||||
|
||||
Successful responses confirm that:
|
||||
|
||||
* configuration loading succeeded
|
||||
* storage initialization completed
|
||||
* HTTP listeners are active
|
||||
* observability endpoints are operational
|
||||
|
||||
### PID File Permissions
|
||||
|
||||
When running as an unprivileged user, writing directly to `/run` may fail:
|
||||
|
||||
```text
|
||||
could not write PID file
|
||||
Permission denied
|
||||
```
|
||||
|
||||
For local development:
|
||||
|
||||
```bash
|
||||
chronoseal run --pid-file /tmp/chronoseal.pid
|
||||
```
|
||||
|
||||
For production systemd deployments, prefer:
|
||||
|
||||
```ini
|
||||
RuntimeDirectory=chronoseal
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```text
|
||||
/run/chronoseal/chronoseal.pid
|
||||
```
|
||||
|
||||
managed by systemd.
|
||||
|
||||
### Additional Validation
|
||||
|
||||
Inspect runtime dependencies:
|
||||
|
||||
```bash
|
||||
ldd target/release/chronoseal
|
||||
```
|
||||
|
||||
Verify ELF program headers:
|
||||
|
||||
```bash
|
||||
readelf -l target/release/chronoseal
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
```text
|
||||
GNU_RELRO
|
||||
GNU_STACK
|
||||
```
|
||||
|
||||
Confirm binary size:
|
||||
|
||||
```bash
|
||||
ls -lh target/release/chronoseal
|
||||
```
|
||||
|
||||
These checks should be performed before publishing release artifacts, container images, or distribution packages.
|
||||
|
||||
## Native Install
|
||||
|
||||
@@ -178,13 +337,63 @@ sudo mkdir -p /var/lib/chronoseal
|
||||
sudo chown -R chronoseal:chronoseal /var/lib/chronoseal
|
||||
```
|
||||
|
||||
For Valkey:
|
||||
For Valkey / Redis:
|
||||
|
||||
ChronoSeal expects a running Valkey or Redis instance when `db_type` is set to `valkey`.
|
||||
|
||||
### 1. Installing Valkey or Redis
|
||||
To install Valkey (the recommended open-source option) or Redis on Linux:
|
||||
|
||||
* **Valkey (Debian/Ubuntu)**:
|
||||
```bash
|
||||
sudo apt-get install -y valkey-server
|
||||
```
|
||||
* **Redis (Debian/Ubuntu)**:
|
||||
```bash
|
||||
sudo apt-get install -y redis-server
|
||||
```
|
||||
|
||||
### 2. Local Setup and Startup
|
||||
By default, ChronoSeal searches for Valkey/Redis on `127.0.0.1:6666`.
|
||||
|
||||
You can start a local instance manually:
|
||||
```bash
|
||||
# Start Valkey on port 6666
|
||||
valkey-server --port 6666 --bind 127.0.0.1
|
||||
# Or start Redis on port 6666
|
||||
redis-server --port 6666 --bind 127.0.0.1
|
||||
```
|
||||
|
||||
Or run it via Docker:
|
||||
```bash
|
||||
# Run Valkey container mapping host port 6666 to container port 6379
|
||||
docker run -d --name chronoseal-valkey -p 6666:6379 valkey/valkey:latest
|
||||
```
|
||||
|
||||
### 3. Service Configuration
|
||||
Configure the environment variables to point ChronoSeal to your instance:
|
||||
|
||||
```bash
|
||||
export CHRONOSEAL_DB_TYPE=valkey
|
||||
export CHRONOSEAL_VALKEY_ADDR=127.0.0.1:6666
|
||||
```
|
||||
|
||||
#### Providing Credentials & SSL/TLS
|
||||
If your Valkey/Redis server requires authentication or secure TLS/SSL, include them directly in the `CHRONOSEAL_VALKEY_ADDR` connection URL:
|
||||
|
||||
* **Password Only**:
|
||||
```bash
|
||||
export CHRONOSEAL_VALKEY_ADDR=redis://:your_password@127.0.0.1:6666
|
||||
```
|
||||
* **Username & Password**:
|
||||
```bash
|
||||
export CHRONOSEAL_VALKEY_ADDR=redis://your_username:your_password@127.0.0.1:6666
|
||||
```
|
||||
* **Secure Connection (SSL/TLS)**: Use the `rediss://` scheme prefix:
|
||||
```bash
|
||||
export CHRONOSEAL_VALKEY_ADDR=rediss://your_username:your_password@secure-valkey-host.example.com:6379
|
||||
```
|
||||
|
||||
If Valkey connection setup fails, the current implementation logs a warning and falls back to in-memory SQLite.
|
||||
|
||||
## systemd
|
||||
@@ -328,3 +537,44 @@ Avoid debug logging in production because internal identifiers may be written to
|
||||
- Protect SQLite and log directories with correct ownership.
|
||||
- Monitor `/health`, `/stats`, and `/metrics`.
|
||||
- Verify `chronoseal config check` after environment or config changes.
|
||||
|
||||
## Runtime Footprint
|
||||
|
||||
ChronoSeal is intentionally designed to maintain a small deployment footprint while providing browser attestation, cryptographic verification, session continuity, and WASM execution capabilities.
|
||||
|
||||
Typical v1.0.2 release artifact sizes:
|
||||
|
||||
| Component | Approximate Size |
|
||||
| ----------------------------------------------- | ---------------: |
|
||||
| Native daemon (`chronoseal`) | ~9.1 MiB |
|
||||
| Browser runtime (`chronoseal_wasm.wasm`) | ~728 KiB |
|
||||
| WASM static library (`libchronoseal_wasm.rlib`) | ~188 KiB |
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
chronoseal
|
||||
9501232 bytes
|
||||
≈ 9.06 MiB
|
||||
|
||||
chronoseal_wasm.wasm
|
||||
745569 bytes
|
||||
≈ 728 KiB
|
||||
```
|
||||
|
||||
These compact artifact sizes help:
|
||||
|
||||
* reduce deployment overhead
|
||||
* minimize container image growth
|
||||
* improve cold-start performance
|
||||
* reduce browser download size
|
||||
* simplify edge and self-hosted deployments
|
||||
|
||||
ChronoSeal intentionally avoids heavyweight runtime dependencies and large browser frameworks, allowing the complete attestation stack to remain compact while preserving functionality.
|
||||
|
||||
```
|
||||
## v1.0.2 Deployment Notes
|
||||
|
||||
- Containers run as a dedicated non-root user.
|
||||
- Reverse proxies should forward X-Forwarded-For or X-Real-IP.
|
||||
- Security headers are enabled by default.
|
||||
@@ -0,0 +1,100 @@
|
||||
# ChronoSeal Operations Handbook (OPERATIONS)
|
||||
|
||||
This guide describes how to deploy, monitor, scale, and maintain the ChronoSeal daemon (`chronoseald`) in production environments.
|
||||
|
||||
---
|
||||
|
||||
## 1. Systemd Deployment
|
||||
|
||||
In single-host deployments, ChronoSeal runs as a systemd service.
|
||||
|
||||
Example systemd unit file (`/etc/systemd/system/chronoseal.service`):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=ChronoSeal Attestation Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=chronoseal
|
||||
Group=chronoseal
|
||||
WorkingDirectory=/var/lib/chronoseal
|
||||
ExecStart=/usr/local/bin/chronoseal run --config /etc/chronoseal.toml
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
LimitNOFILE=65536
|
||||
|
||||
# Hardening
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable and start the service:
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now chronoseal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Reverse Proxy & TLS Termination
|
||||
|
||||
Do not expose the `chronoseald` HTTP interface directly to the public internet. Run it behind a reverse proxy (e.g. Nginx, HAProxy, Envoy) that enforces TLS termination and CORS limits.
|
||||
|
||||
Example Nginx config (`/etc/nginx/sites-available/chronoseal.conf`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name attestation.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Storage Backends & Scaling
|
||||
|
||||
### A. SQLite (`sqlite-in-disk`)
|
||||
* **Best For:** Single-node deployments.
|
||||
* **Configuration:** Specify a writeable path in `db_path` and set `db_type = "sqlite-in-disk"`.
|
||||
* **Operational Note:** Concurrency is limited by SQLite's single-writer database lock. Optimistic CAS reduces collisions, but high write volumes can cause queue congestion.
|
||||
|
||||
### B. Valkey / Redis (`valkey`)
|
||||
* **Best For:** Distributed or high-concurrency environments.
|
||||
* **Configuration:** Set `db_type = "valkey"` and specify the node addresses via `CHRONOSEAL_VALKEY_ADDR`.
|
||||
* **Horizontal Scaling:** Set up multiple `chronoseald` stateless daemon nodes. Direct all nodes to connect to the same shared Valkey cluster. This ensures session consistency across requests routed to different nodes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Monitoring & Observability
|
||||
|
||||
### Prometheus Integration
|
||||
Scrape metrics from the `/metrics` endpoint:
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'chronoseal'
|
||||
static_configs:
|
||||
- targets: ['localhost:8080']
|
||||
```
|
||||
|
||||
Key operational alerts to configure:
|
||||
* `chronoseal_verification_failures_total` rate spike: Indicates a coordinated scraping campaign, automated spoofing attempt, or misconfigured frontend app.
|
||||
* `chronoseal_storage_latency_seconds` increase: Indicates storage backend bottleneck or lock congestion.
|
||||
@@ -31,16 +31,16 @@ The optimal values depend on your threat model and expected client hardware.
|
||||
| Profile | `gene_size` | `mutation_rounds` | Security Level | Recommended Usage |
|
||||
| ----------------- | ----------- | ----------------- | ---------------- | -------------------------------- |
|
||||
| Default | 512 | 4 | Moderate | Development and testing |
|
||||
| Recommended | 2048 | 16 | Strong | Most production deployments |
|
||||
| High Security | 4096 | 32 | Very Strong | Sensitive applications |
|
||||
| Maximum Practical | 8192 | 64 | Extremely Strong | High-value targets |
|
||||
| Experimental | 65536 | 65536 | Research Only | Benchmarking and experimentation |
|
||||
| Recommended | 2048 | 4 | Strong | Most production deployments |
|
||||
| High Security | 4096 | 8 | Very Strong | Sensitive applications |
|
||||
| Maximum Practical | 4096 | 10 | Extremely Strong | High-value targets |
|
||||
| Experimental | 4096 | 10 | Research Only | Benchmarking and experimentation |
|
||||
|
||||
### Recommended Production Configuration
|
||||
|
||||
```toml
|
||||
gene_size = 2048
|
||||
mutation_rounds = 16
|
||||
mutation_rounds = 4
|
||||
```
|
||||
|
||||
This configuration provides a strong balance between security and runtime overhead for most deployments.
|
||||
@@ -55,7 +55,7 @@ Edit your configuration file:
|
||||
# Mutation Engine Settings
|
||||
|
||||
gene_size = 2048
|
||||
mutation_rounds = 16
|
||||
mutation_rounds = 4
|
||||
```
|
||||
|
||||
Common configuration locations:
|
||||
@@ -137,7 +137,7 @@ Browser developer tools can also be used to monitor:
|
||||
|
||||
```toml
|
||||
gene_size = 2048
|
||||
mutation_rounds = 16
|
||||
mutation_rounds = 4
|
||||
```
|
||||
|
||||
Deploy and observe normal usage patterns.
|
||||
@@ -158,11 +158,10 @@ Increase one parameter at a time.
|
||||
Recommended progression:
|
||||
|
||||
```text
|
||||
2048 / 16
|
||||
4096 / 16
|
||||
4096 / 32
|
||||
8192 / 32
|
||||
8192 / 64
|
||||
2048 / 4
|
||||
4096 / 4
|
||||
4096 / 8
|
||||
4096 / 10
|
||||
```
|
||||
|
||||
This makes it easier to identify performance bottlenecks.
|
||||
@@ -194,9 +193,9 @@ Future deployments may choose to dynamically increase mutation strength based on
|
||||
Example policy:
|
||||
|
||||
```text
|
||||
New session → 2048 / 16
|
||||
Suspicious session → 4096 / 32
|
||||
Elevated-risk action → 8192 / 64
|
||||
New session → 2048 / 4
|
||||
Suspicious session → 4096 / 8
|
||||
Elevated-risk action → 4096 / 10
|
||||
```
|
||||
|
||||
---
|
||||
@@ -213,7 +212,7 @@ wasm-pack build wasm --target web --release
|
||||
|
||||
### General Guidance
|
||||
|
||||
* Keep `mutation_rounds` below 64 for most deployments.
|
||||
* Keep `mutation_rounds` at or below 10 for all deployments.
|
||||
* Prefer increasing `gene_size` before dramatically increasing rounds.
|
||||
* Benchmark on representative client hardware.
|
||||
* Monitor browser CPU utilization during load testing.
|
||||
@@ -260,7 +259,7 @@ For most production deployments:
|
||||
|
||||
```toml
|
||||
gene_size = 2048
|
||||
mutation_rounds = 16
|
||||
mutation_rounds = 4
|
||||
```
|
||||
|
||||
This configuration provides a strong balance between security, performance, and compatibility across desktop and mobile devices.
|
||||
@@ -281,3 +280,13 @@ chronoseal config check
|
||||
chronoseal stats
|
||||
chronoseal health
|
||||
```
|
||||
|
||||
|
||||
## v1.0.2 Limits
|
||||
|
||||
Current supported range:
|
||||
|
||||
```toml
|
||||
gene_size = 1..=4096
|
||||
mutation_rounds = 1..=10
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
# ChronoSeal Protocol Specification (PROTOCOL)
|
||||
|
||||
This document defines the formal wire protocol, state transitions, cryptographic primitives, and execution invariants of the ChronoSeal browser attestation system.
|
||||
|
||||
---
|
||||
|
||||
## 1. Sequence Flow & Handshake
|
||||
|
||||
ChronoSeal operates as a stateful, sequential challenge-response chain over HTTP/REST.
|
||||
|
||||
```
|
||||
Client (JS/WASM) Server (chronoseald)
|
||||
| |
|
||||
| 1. POST /init { public_key: String } ----------------> |
|
||||
| | (Generates VM Opcodes)
|
||||
| | (Computes initial hash chain head H_0)
|
||||
| | (Saves initial session record)
|
||||
| <--- 200 OK { InitResponse } --------------------------|
|
||||
| |
|
||||
| [Client executes VM program & prepares gene preview] |
|
||||
| |
|
||||
| 2. POST /hb { HeartbeatRequest } --------------------> |
|
||||
| | (Loads session & executes CAS check)
|
||||
| | (Verifies Ed25519 signature)
|
||||
| | (Validates VM stack-state parity)
|
||||
| | (Computes expected gene mutation)
|
||||
| | (Validates hash chain continuity H_N == expected)
|
||||
| | (Rotates salt & issues next mutation order)
|
||||
| <--- 200 OK { HeartbeatResponse } ---------------------| (Saves updated session record)
|
||||
| |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Cryptographic Transition Mechanics
|
||||
|
||||
### A. Handshake Phase (`/init`)
|
||||
The client registers a 32-byte Ed25519 verifying key represented as a hex string.
|
||||
The server:
|
||||
1. Generates a 32-byte session ID ($ID$) and a 16-byte initial salt ($S_0$).
|
||||
2. Computes the initial hash chain head:
|
||||
$$H_0 = \text{Blake3}(ID \parallel PK_{\text{client}} \parallel S_0)$$
|
||||
3. Generates a random VM program of size $8..=16$ bytes.
|
||||
4. Creates the initial mutation order program $M_1$.
|
||||
5. Persists the session record in the database.
|
||||
|
||||
---
|
||||
|
||||
### B. Heartbeat progression (`/hb`)
|
||||
For each heartbeat step $n \ge 1$:
|
||||
The client submits:
|
||||
* `prev_hash`: $H_{n-1}$ (hex encoded).
|
||||
* `timestamp`: $T_n$ (milliseconds).
|
||||
* `entropy_data`: Mouse movement arrays.
|
||||
* `stack_state`: The VM final stack and instruction pointer `ip` after execution.
|
||||
* `gene_commitment`: Hex-encoded commitment of the mutated gene state.
|
||||
* `signature`: Ed25519 signature of the canonical alphabetical JSON payload.
|
||||
|
||||
The server:
|
||||
1. Loads the session record from storage, enforcing optimistic locking (CAS check) to confirm the database `last_hash` matches $H_{n-1}$.
|
||||
2. Validates the Ed25519 signature against the canonical alphabetical serialization.
|
||||
3. Re-executes the session's VM opcodes and asserts the client's `stack_state` matches the output.
|
||||
4. Applies the mutation order $M_n$ to the stored gene state and calculates the expected commitment:
|
||||
$$C_n = \text{Blake3}(\text{CandidateGene} \parallel ID \parallel n)$$
|
||||
Asserts the client's `gene_commitment` matches.
|
||||
5. Validates that $|T_{\text{server}} - T_n| \le \text{max\_drift}$.
|
||||
6. Advances the hash chain:
|
||||
$$H_n = \text{Blake3}(H_{n-1} \parallel T_n \parallel \text{Blake3}(E_n) \parallel \text{Blake3}(S_n) \parallel S_{n-1})$$
|
||||
7. Rotates the salt to $S_n$ and issues the next mutation order $M_{n+1}$.
|
||||
|
||||
---
|
||||
|
||||
## 3. VM Instruction Specification
|
||||
|
||||
The client VM executes instructions sequentially. The instruction set consists of:
|
||||
|
||||
* `0x00`: Pushes the next 4 bytes in the instruction stream onto the stack as a `u32` value (little-endian).
|
||||
* `0x01`..=`0x07`: Binary operators. Requires at least 2 elements on the stack:
|
||||
* `0x01`: Wrapping Add (`a.wrapping_add(b)`)
|
||||
* `0x02`: Wrapping Sub (`a.wrapping_sub(b)`)
|
||||
* `0x03`: Wrapping Mul (`a.wrapping_mul(b)`)
|
||||
* `0x04`: XOR (`a ^ b`)
|
||||
* `0x05`: AND (`a & b`)
|
||||
* `0x06`: OR (`a | b`)
|
||||
* `0x07`: Rotate Left (`a.rotate_left(b % 32)`)
|
||||
* `0x08`: Unary Bitwise Not (`!a`). Requires at least 1 element on the stack.
|
||||
* `0x09`: Hash Stack. Hashes all stack elements using BLAKE3 and reduces it to a single `u32` value, clearing the stack and pushing the hash.
|
||||
* *Any other opcode:* Terminates VM execution immediately.
|
||||
|
||||
|
||||
## v1.0.2 Protocol Hardening
|
||||
|
||||
- Fingerprint aspect ratio validation
|
||||
- Device pixel ratio validation
|
||||
- Hardware concurrency validation (1..=256)
|
||||
- Entropy event cap (500 events)
|
||||
- IP-based rate limiting
|
||||
- Silent rejection preserved for protocol failures
|
||||
@@ -0,0 +1,37 @@
|
||||
# ChronoSeal Protocol Stability Policy (PROTOCOL_STABILITY)
|
||||
|
||||
This document defines the stable interfaces and boundaries of the ChronoSeal project to guide third-party integration development and future internal architectural evolutions.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stable Public Contract
|
||||
|
||||
The public surface of ChronoSeal is frozen at version 1.0 and consists of:
|
||||
|
||||
1. **Wire Protocol API:**
|
||||
* `POST /init`: Handshake schema (parameters, response fields).
|
||||
* `POST /hb`: Heartbeat schema (payload parameters, response fields).
|
||||
2. **State Transition Semantics:**
|
||||
* The BLAKE3 hash chain progression rules.
|
||||
* The virtual machine opcodes and stack execution rules.
|
||||
* The Synthetic Gene Mutation logic and context-bound commitments.
|
||||
3. **Daemon CLI & Config Schema:**
|
||||
* Commands (`run`, `status`, `health`, etc.).
|
||||
* TOML configuration keys.
|
||||
|
||||
---
|
||||
|
||||
## 2. Private Internal Boundaries
|
||||
|
||||
All implementation details are subject to change without notice. Wrappers, clients, and applications must not depend on:
|
||||
|
||||
* **Internal Rust APIs:** ChronoSeal is a Unix daemon. It does not export a public Rust library SDK. Internal Rust modules (`server::storage`, `server::session`, etc.) are private.
|
||||
* **Database Schema:** The SQLite table structure, indexes, or column names are private to the daemon.
|
||||
* **Valkey Key Structures:** The layout of session keys, sorted set indexes, and pipelines are implementation details.
|
||||
|
||||
---
|
||||
|
||||
## 3. Protocol Evolution Policy
|
||||
|
||||
* **Minor Updates:** Can introduce new optional configuration fields or metrics.
|
||||
* **Major Updates:** May change the VM instruction set or hash chain primitives, requiring new WASM builds.
|
||||
@@ -0,0 +1,40 @@
|
||||
# ChronoSeal Security Assumptions & Guarantees
|
||||
|
||||
This document details the trust boundary models, security assumptions, and non-goals of the ChronoSeal system.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Threat Philosophy
|
||||
|
||||
ChronoSeal is a **cost-raising security layer**. It is designed to force automated scraping, botting, and replay tools to execute a fully compliant JavaScript/WASM execution runtime. It does not provide absolute hardware attestation or proof of human presence.
|
||||
|
||||
---
|
||||
|
||||
## 2. Non-Goals (What ChronoSeal is NOT)
|
||||
|
||||
1. **Proof of Humanity:** ChronoSeal does not check if the user is a human. A headless browser running with standard input event automation will pass verification if it runs the WASM runtime correctly.
|
||||
2. **Anti-Debugging/Enclave Security:** ChronoSeal does not run inside a secure hardware enclave on the client. An attacker has complete control of the client wasm environment, memory, and key storage.
|
||||
3. **Perfect Browser Verification:** ChronoSeal cannot guarantee the client is a real Chrome/Firefox browser. It guarantees that the client maintains the state chain and executes the math VM program.
|
||||
|
||||
---
|
||||
|
||||
## 3. Threat Matrix & Attacker Cost Model
|
||||
|
||||
* **Commodity HTTP Clients (Python `requests`, `curl`):** *Blocked.* Attackers cannot sign payloads, run the mathematical VM, or maintain the stateful BLAKE3 hash chain.
|
||||
* **Headless Automation (Puppeteer, Playwright):** *Partially Contained.* The automation script must execute the full browser environment, load the WASM module, feed valid parameters, and generate realistic mouse movement coordinates. This imposes significantly higher CPU and resource overhead on the attacker.
|
||||
* **Custom WASM Emulators:** *Raised Cost.* A determined reverse engineer can extract the WASM module and build a custom state runner in Node.js or Go. ChronoSeal counters this by using a stateful **Synthetic Gene Mutation Engine**, where the state vector mutations are governed dynamically by the server, requiring the emulator to replicate the entire mutation spec.
|
||||
|
||||
---
|
||||
|
||||
## 4. Key Invariants
|
||||
|
||||
1. **Chain Continuity:** A session state cannot bifurcate. Every heartbeat must advance the state head using the latest salt.
|
||||
2. **VM Parity:** Stack state must exactly match the execution output of the server's issued opcode sequence.
|
||||
3. **Dynamic Challenges:** Client gene updates must match the server-issued mutation program.
|
||||
|
||||
|
||||
## Additional Assumptions (v1.0.2)
|
||||
|
||||
- Fingerprints are sanity signals, not identity proofs.
|
||||
- Rate limiting assumes client IP visibility.
|
||||
- Security headers reduce browser attack surface.
|
||||
@@ -0,0 +1,395 @@
|
||||
# ChronoSeal Testing Strategy
|
||||
|
||||
ChronoSeal maintains a security-focused test suite designed to validate cryptographic correctness, deterministic server ↔ WASM parity, replay resistance, mutation engine integrity, browser fingerprint validation, behavioral trust checks, storage reliability, and protocol hardening.
|
||||
|
||||
As of **v1.0.2**, the project contains **100 passing tests** across the server, WASM, shared protocol, and property-testing suites.
|
||||
|
||||
| Crate | Tests |
|
||||
| ------------------------------- | ------: |
|
||||
| `chronoseal-server` | 38 |
|
||||
| `chronoseal-wasm` | 24 |
|
||||
| `shared` (unit tests) | 36 |
|
||||
| `shared` (property-based tests) | 2 |
|
||||
| `chronoseal-replay` | 0 |
|
||||
| **Total** | **100** |
|
||||
|
||||
---
|
||||
|
||||
# Test Philosophy
|
||||
|
||||
ChronoSeal prioritizes testing of security invariants rather than raw coverage percentages.
|
||||
|
||||
Primary goals:
|
||||
|
||||
* Verify deterministic server ↔ WASM behavior
|
||||
* Detect protocol divergence early
|
||||
* Prevent replay attacks
|
||||
* Validate mutation engine correctness
|
||||
* Detect malformed and adversarial input handling
|
||||
* Prevent VM and protocol panics
|
||||
* Maintain storage backend compatibility
|
||||
* Protect browser attestation continuity guarantees
|
||||
|
||||
The project emphasizes negative-path testing and adversarial validation rather than only testing successful execution paths.
|
||||
|
||||
---
|
||||
|
||||
# Test Categories
|
||||
|
||||
## 1. Configuration & Runtime
|
||||
|
||||
Configuration tests verify:
|
||||
|
||||
* Default configuration values
|
||||
* TOML parsing
|
||||
* Runtime initialization
|
||||
* Database backend selection
|
||||
* CLI override behavior
|
||||
|
||||
Covered functionality:
|
||||
|
||||
* SQLite in-memory backend
|
||||
* SQLite disk backend
|
||||
* Valkey compatibility mode
|
||||
* Runtime configuration validation
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_default_db_type_is_sqlite_in_memory
|
||||
test_apply_run_args_overrides_db_type
|
||||
test_toml_parses_db_type_kebab_case
|
||||
test_db_type_report_lists_backends
|
||||
test_init_db_pool_sqlite_in_memory
|
||||
test_init_db_pool_sqlite_in_disk
|
||||
test_init_db_pool_valkey_compat_mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Browser Fingerprint Validation
|
||||
|
||||
Introduced and expanded in v1.0.2.
|
||||
|
||||
Fingerprint validation protects the attestation pipeline from malformed or unrealistic browser metadata.
|
||||
|
||||
Validation coverage includes:
|
||||
|
||||
* Aspect ratio validation
|
||||
* Device pixel ratio validation
|
||||
* Hardware concurrency validation
|
||||
* Boundary value acceptance
|
||||
* NaN rejection
|
||||
* Infinity rejection
|
||||
* Malformed numeric value rejection
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
accepts_valid_fingerprint
|
||||
accepts_boundary_values
|
||||
rejects_invalid_aspect_ratios
|
||||
rejects_invalid_device_pixel_ratios
|
||||
rejects_invalid_hardware_concurrency
|
||||
```
|
||||
|
||||
Validation constraints currently include:
|
||||
|
||||
| Field | Allowed Range |
|
||||
| ------------------- | --------------------- |
|
||||
| aspectRatio | finite positive value |
|
||||
| devicePixelRatio | greater than zero |
|
||||
| hardwareConcurrency | 1..=256 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Session Lifecycle & Protocol Verification
|
||||
|
||||
Session tests verify:
|
||||
|
||||
* Session creation
|
||||
* Session expiration
|
||||
* Public key validation
|
||||
* Replay attack resistance
|
||||
* Mutation commitment verification
|
||||
* Mutation step enforcement
|
||||
* Deterministic long-running parity
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_create_session_rejects_invalid_public_key_length
|
||||
test_expired_session_is_rejected
|
||||
test_replay_attack_is_rejected
|
||||
test_mutation_step_mismatch_is_rejected
|
||||
test_mutation_commitment_tamper_is_rejected
|
||||
test_session_lifecycle_and_verification
|
||||
test_repeated_simulation_keeps_server_and_client_commitments_equal
|
||||
test_deterministic_server_client_parity_across_many_heartbeats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Heartbeat Validation
|
||||
|
||||
Heartbeat tests validate:
|
||||
|
||||
* Successful state advancement
|
||||
* Silent rejection semantics
|
||||
* Commitment validation
|
||||
* Rate limiting
|
||||
* Next-state mutation generation
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_handler_success_returns_next_mutation_fields
|
||||
test_handler_tampered_commitment_is_silent_failure
|
||||
test_handler_rate_limit_returns_no_mutation_data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Behavioral Trust Validation
|
||||
|
||||
Trust validation focuses on lightweight behavioral signals.
|
||||
|
||||
Coverage includes:
|
||||
|
||||
* Minimum event count
|
||||
* Minimum movement distance
|
||||
* Pause detection
|
||||
* Maximum speed thresholds
|
||||
* Activity requirement toggles
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_validate_mouse_success
|
||||
test_validate_mouse_insufficient_events
|
||||
test_validate_mouse_insufficient_distance
|
||||
test_validate_mouse_too_fast
|
||||
test_validate_mouse_no_pauses
|
||||
test_validate_mouse_require_activity_toggle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage Layer
|
||||
|
||||
Storage tests verify backend correctness and concurrency behavior.
|
||||
|
||||
Covered backends:
|
||||
|
||||
* SQLite in-memory
|
||||
* SQLite disk
|
||||
* Valkey compatibility mode
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_sqlite_pool_concurrency
|
||||
test_valkey_pool_concurrency
|
||||
test_valkey_store_operations
|
||||
```
|
||||
|
||||
Validation includes:
|
||||
|
||||
* Session persistence
|
||||
* Session updates
|
||||
* Concurrent access
|
||||
* Statistics collection
|
||||
* Backend compatibility
|
||||
|
||||
---
|
||||
|
||||
## 7. Rate Limiting
|
||||
|
||||
Rate limiter tests verify:
|
||||
|
||||
* Request counting
|
||||
* Window expiration
|
||||
* Stale entry eviction
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_rate_limiter
|
||||
test_rate_limiter_eviction
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. VM Core
|
||||
|
||||
The VM implementation is tested across both WASM and shared crates.
|
||||
|
||||
Covered operations:
|
||||
|
||||
```text
|
||||
ADD
|
||||
SUB
|
||||
MUL
|
||||
XOR
|
||||
AND
|
||||
OR
|
||||
NOT
|
||||
HASH
|
||||
ROT
|
||||
PUSH
|
||||
```
|
||||
|
||||
Validation includes:
|
||||
|
||||
* Wrapping arithmetic
|
||||
* Stack underflow detection
|
||||
* Invalid opcode rejection
|
||||
* Truncated instruction rejection
|
||||
* Instruction safety
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_add
|
||||
test_add_wrapping
|
||||
test_sub
|
||||
test_sub_wrapping
|
||||
test_mul
|
||||
test_hash
|
||||
test_underflow_binary
|
||||
test_underflow_unary
|
||||
test_incomplete_push
|
||||
test_rejects_unknown_opcode
|
||||
test_rejects_truncated_instruction
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Synthetic Gene Mutation Engine
|
||||
|
||||
The mutation engine is a critical security component.
|
||||
|
||||
Coverage includes:
|
||||
|
||||
* Mutation order execution
|
||||
* Deterministic parity
|
||||
* Randomized mutation programs
|
||||
* Preview lifecycle
|
||||
* Commit lifecycle
|
||||
* Discard lifecycle
|
||||
* Environment validation
|
||||
* Gene integrity
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_mutation_chain
|
||||
test_generate_order_is_deterministic_for_seeded_rng
|
||||
test_server_client_parity_across_random_orders
|
||||
test_preview_commitment_matches_shared_engine
|
||||
test_commit_applies_preview
|
||||
test_discard_preview_keeps_committed_state
|
||||
test_table_driven_parity_across_many_generated_orders
|
||||
test_fuzz_style_random_program_bytes_do_not_diverge
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Property-Based Testing
|
||||
|
||||
ChronoSeal uses `proptest` to validate protocol invariants under arbitrary input.
|
||||
|
||||
Property tests:
|
||||
|
||||
```text
|
||||
test_vm_execute_never_panics
|
||||
test_gene_environment_roundtrip_never_panics
|
||||
```
|
||||
|
||||
These tests continuously exercise malformed and randomized inputs to ensure graceful handling and panic resistance.
|
||||
|
||||
---
|
||||
|
||||
# Running the Test Suite
|
||||
|
||||
Run all tests:
|
||||
|
||||
```bash
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Run server tests:
|
||||
|
||||
```bash
|
||||
cargo test -p chronoseal-server
|
||||
```
|
||||
|
||||
Run fingerprint tests only:
|
||||
|
||||
```bash
|
||||
cargo test -p chronoseal-server fingerprint
|
||||
```
|
||||
|
||||
Run WASM tests:
|
||||
|
||||
```bash
|
||||
cargo test -p chronoseal-wasm
|
||||
```
|
||||
|
||||
Run shared tests:
|
||||
|
||||
```bash
|
||||
cargo test -p shared
|
||||
```
|
||||
|
||||
Show output:
|
||||
|
||||
```bash
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Critical Security Tests
|
||||
|
||||
The following tests are considered release-blocking:
|
||||
|
||||
```text
|
||||
test_replay_attack_is_rejected
|
||||
test_mutation_commitment_tamper_is_rejected
|
||||
test_handler_tampered_commitment_is_silent_failure
|
||||
test_deterministic_server_client_parity_across_many_heartbeats
|
||||
test_server_client_parity_across_random_orders
|
||||
test_vm_execute_never_panics
|
||||
test_gene_environment_roundtrip_never_panics
|
||||
rejects_invalid_aspect_ratios
|
||||
rejects_invalid_device_pixel_ratios
|
||||
rejects_invalid_hardware_concurrency
|
||||
```
|
||||
|
||||
These tests directly protect protocol integrity, replay resistance, mutation validation, deterministic execution, and fingerprint hardening.
|
||||
|
||||
---
|
||||
|
||||
# Conclusion
|
||||
|
||||
ChronoSeal's testing strategy focuses on preserving deterministic behavior, protocol integrity, cryptographic correctness, browser ↔ server parity, and resistance to malformed or adversarial input.
|
||||
|
||||
The current suite of **100 passing tests** provides comprehensive coverage across:
|
||||
|
||||
* Configuration
|
||||
* Runtime initialization
|
||||
* Browser fingerprint validation
|
||||
* Session lifecycle management
|
||||
* Heartbeat verification
|
||||
* Mutation engine execution
|
||||
* VM safety
|
||||
* Behavioral validation
|
||||
* Storage backends
|
||||
* Replay resistance
|
||||
* Property-based protocol hardening
|
||||
|
||||
Maintaining and expanding this test suite remains a core project priority.
|
||||
|
||||
**Last Updated:** June 2026 (v1.0.2)
|
||||
|
||||
@@ -106,6 +106,23 @@ Expected result:
|
||||
- ChronoSeal does not claim complete prevention
|
||||
- additional application-level controls are required
|
||||
|
||||
## Attacker Classification Boundaries
|
||||
|
||||
### Protected
|
||||
* **Commodity Scrapers:** Simple HTTP clients (`curl`, Python `requests`, Go HTTP clients) that cannot execute JavaScript or WebAssembly.
|
||||
* **Simple Replay Attackers:** Intercepted heartbeat payloads cannot be reused because of the strict hash-chain sequencing and salt rotation.
|
||||
* **Signature Forgers:** Heartbeats without the session's private key will fail Ed25519 verification.
|
||||
|
||||
### Partially Protected
|
||||
* **Headless Automation (Puppeteer, Playwright):** Attackers must load the WASM runtime, execute the VM instructions, calculate gene mutations, and simulate realistic human mouse interactions. This significantly increases CPU and system memory overhead, reducing the scale of bot operations.
|
||||
* **Stealth Automation Frameworks:** Advanced frameworks must maintain state sync across multiple heartbeat cycles, exposing them to timing detection.
|
||||
|
||||
### Unprotected
|
||||
* **WASM Key Extraction:** A reverse engineer with full browser process control can extract the private key from WASM memory.
|
||||
* **Malware Operators:** Keyloggers, screen scrapers, or memory dumpers operating at the OS level are outside the application trust boundary.
|
||||
* **MITM Interceptors (without TLS):** Plaintext traffic can be intercepted. (TLS termination is assumed).
|
||||
* **Insiders / Storage Tampering:** Attackers with direct write access to the SQLite database or Valkey instance can forge or hijack active session states.
|
||||
|
||||
## Attack Vectors and Mitigations
|
||||
|
||||
### Replay
|
||||
@@ -238,3 +255,17 @@ Recommended:
|
||||
## Disclosure
|
||||
|
||||
See [../SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy.
|
||||
|
||||
|
||||
## Additional Mitigations (v1.0.2)
|
||||
|
||||
### Fingerprint Abuse
|
||||
- Bounds enforcement
|
||||
- Numeric sanity validation
|
||||
|
||||
### Resource Exhaustion
|
||||
- Entropy event cap
|
||||
- Rate limiting
|
||||
|
||||
### Browser Hardening
|
||||
- Security response headers
|
||||
@@ -0,0 +1,66 @@
|
||||
# ChronoSeal Debugging & Failure Mode Guide (WHY_IT_FAILS)
|
||||
|
||||
This document provides a technical diagnostic reference for developers, operators, and integration security teams. It explains why a client heartbeat or session initialization fails verification, and how to debug desynchronization issues.
|
||||
|
||||
---
|
||||
|
||||
## 1. Silent Rejections vs. HTTP Failures
|
||||
|
||||
To deny attackers a feedback oracle, the ChronoSeal heartbeat endpoint (`POST /hb`) always returns HTTP status `200 OK` with `{"status": "ok"}` on semantic verification failures.
|
||||
|
||||
* **Successful Attestation:** The JSON response contains the rotated next state information: `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
|
||||
* **Silently Rejected Attestation:** The JSON response *omits* these three fields. The client is expected to roll back the state preview and retry.
|
||||
|
||||
---
|
||||
|
||||
## 2. Common Verification Failure Modes
|
||||
|
||||
### A. Clock Drift (`TimestampDrift`)
|
||||
* **Error Cause:** The client machine's local system time differs from the server's time by more than the configured `max_timestamp_drift_ms` (default 30 seconds).
|
||||
* **Diagnostic Signal:** The `/hb` response omits next state parameters.
|
||||
* **Remediation:** Synchronize both client and server clocks using NTP (Network Time Protocol). On the client, use NTP-synced system clocks or query server timestamp headers during initialization to compute a local clock offset.
|
||||
|
||||
### B. Replay Attempts / Out-of-Sequence (`ChainBroken`)
|
||||
* **Error Cause:** The request `prev_hash` does not match the server-stored `last_hash` for the session.
|
||||
* **Root Causes:**
|
||||
1. The client replayed a previously captured heartbeat payload.
|
||||
2. The client lost the network response containing the rotated next state parameters and retried with stale state.
|
||||
3. A concurrent request succeeded first, updating the session's hash state.
|
||||
* **Remediation:** If network issues cause packet loss, the client must discard the session and initiate a new `/init` handshake. Heartbeats cannot be replayed or resumed from a historical state.
|
||||
|
||||
### C. Signature Failures (`Signature`)
|
||||
* **Error Cause:** The Ed25519 signature over the canonical JSON payload is invalid.
|
||||
* **Root Causes:**
|
||||
1. The client signed a payload that differed in ordering or format from the server's canonical serialization. (Ensure key sorting matches alphabetically: `entropyData`, `fingerprint`, `geneCommitment`, `mutationStep`, `prevHash`, `sessionId`, `stackState`, `timestamp`).
|
||||
2. Different platform engines formatted floats or large numbers differently.
|
||||
3. The public key registered during `/init` does not match the signing key.
|
||||
* **Remediation:** Ensure both frontend and backend use strict canonical serializations (BTreeMap alphabetically sorted keys).
|
||||
|
||||
### D. VM Stack State Mismatch (`VmStackMismatch`)
|
||||
* **Error Cause:** The client's submitted `stack_state` (VM stack and instruction pointer `ip`) does not match the server-side re-execution of the session's random math program.
|
||||
* **Root Causes:**
|
||||
1. An automated client bypassed the VM bytecode interpreter.
|
||||
2. The client VM interpreter diverged mathematically (e.g. word size wrapping or logical op mismatches).
|
||||
* **Remediation:** Check the VM interpreter implementation parity between the client wasm and `shared::vm`.
|
||||
|
||||
### E. Mutation Commitment Mismatch (`MutationCommitmentMismatch`)
|
||||
* **Error Cause:** The client's computed `gene_commitment` does not match the server-applied gene mutation.
|
||||
* **Root Causes:**
|
||||
1. The client used a different number of `mutation_rounds` than the server config.
|
||||
2. The mutation order execution logic diverged.
|
||||
* **Remediation:** Verify that the client wasm correctly parsed `mutation_rounds` from `/init` and passed it to the generator.
|
||||
|
||||
### F. Rate Limiting (`RateLimiter`)
|
||||
* **Error Cause:** The client submitted more requests than allowed by the server's rate-limiting config (e.g., `rate_limit_count` per `rate_limit_window_secs`).
|
||||
* **Diagnostic Signal:** The server returns `200 OK` with `{"status": "ok"}` but no next state data.
|
||||
* **Remediation:** Reduce heartbeat frequency or adjust rate limit parameters in the daemon configuration.
|
||||
|
||||
|
||||
## Additional v1.0.2 Failure Modes
|
||||
|
||||
- Invalid aspect ratio
|
||||
- Invalid device pixel ratio
|
||||
- Invalid hardware concurrency
|
||||
- NaN or infinite fingerprint values
|
||||
- Entropy event count exceeds 500
|
||||
- Client IP rate limited
|
||||
@@ -0,0 +1,41 @@
|
||||
# ChronoSeal Third-Party Wrapper & Integration Guide (WRAPPER_GUIDE)
|
||||
|
||||
This document provides stable guidance for developers building third-party integration wrappers, clients, or SDKs around the `chronoseald` daemon.
|
||||
|
||||
---
|
||||
|
||||
## 1. Public Contract & Stability Guarantees
|
||||
|
||||
As a protocol-first Unix daemon, `chronoseald` guarantees stability on the public network interface.
|
||||
|
||||
### Guaranteed Stable
|
||||
* **Endpoints:** `POST /init` and `POST /hb`.
|
||||
* **JSON Fields:** The structure and naming of request and response keys.
|
||||
* **VM Instruction Set:** The behavior and encoding of the 10 core VM opcodes (`0x00`..=`0x09`).
|
||||
* **Signature Serialization:** Alphabetical key-sorting rules using `BTreeMap` serialization.
|
||||
* **Hash Progression:** Blake3 chain folding rules.
|
||||
|
||||
### Private (Unstable / Subject to Change)
|
||||
* **Database Engines & Schemas:** SQLite table structure, Valkey key formatting, and indexes.
|
||||
* **Daemon CLI Flags:** Internal metrics query formats.
|
||||
* **Memory Structures:** Thread boundaries, session caches, and synchronization locks.
|
||||
|
||||
---
|
||||
|
||||
## 2. API Versioning & Deprecation Policy
|
||||
|
||||
* **Version Format:** API endpoints do not contain version prefixes (e.g., `/v1/hb`). Instead, protocol versioning is coupled to the daemon release version.
|
||||
* **Breaking Protocol Changes:** Any change to the core hash function (Blake3) or the VM instruction set will trigger a major release (e.g., `v2.0.0`).
|
||||
* **Deprecation Cycle:** Deprecated features will be supported for at least one minor release cycle, documented in `docs/PROTOCOL_STABILITY.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Reference Implementation Steps for Wrappers
|
||||
|
||||
To build a client-side wrapper or application adapter for `chronoseald`:
|
||||
|
||||
1. **Handshake:** Send `POST /init` with the hex-encoded Ed25519 public key. Save the returned `session_id`, `salt`, `opcodes_b64`, and `mutation_order_b64`.
|
||||
2. **VM Execution:** Run the math VM program (decoded from `opcodes_b64`) using the client wasm runtime to get the target `stack_state`.
|
||||
3. **Gene Mutation:** Decode `mutation_order_b64`, apply the mutation steps to the local gene buffer, and compute the new commitment hash.
|
||||
4. **Signing:** Build the canonical alphabetical JSON message, sign it, and send `POST /hb`.
|
||||
5. **Chain Advancement:** On success, extract `next_salt` and `next_mutation_order_b64` to prepare the next heartbeat request.
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" width="296.99997mm" viewBox="0 0 1122.5196 793.7008" enable-background="new 0 0 1254 1254" xml:space="preserve" height="210mm" sodipodi:docname="logo1.svg" inkscape:export-filename="logo1.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="1.1654266" inkscape:cx="561.16791" inkscape:cy="396.85039" inkscape:window-width="2048" inkscape:window-height="1205" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="Layer_1"></sodipodi:namedview><defs id="defs44"></defs><path fill="none" opacity="1" stroke="none" d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path2"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path45" style="fill:#e1e1e4;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path46" style="fill:#b0b2b8;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20Line truncated
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
@@ -16,6 +16,7 @@ let minInterval = 12000;
|
||||
let maxInterval = 25000;
|
||||
let pendingMutationStep = 0;
|
||||
let pendingMutationOrderB64 = '';
|
||||
let mutationRounds = 4;
|
||||
|
||||
export async function initHeartbeat() {
|
||||
await init();
|
||||
@@ -32,6 +33,7 @@ export async function initHeartbeat() {
|
||||
}
|
||||
pendingMutationStep = initResp.mutation_step;
|
||||
pendingMutationOrderB64 = initResp.mutation_order_b64;
|
||||
mutationRounds = initResp.mutation_rounds || 4;
|
||||
lastTime = performance.now();
|
||||
scheduleNext();
|
||||
}
|
||||
@@ -56,7 +58,7 @@ async function sendHeartbeat() {
|
||||
const timestamp = Date.now();
|
||||
const entropyData = { events: events.map(e => ({ x: e.x, y: e.y, t: e.t })) };
|
||||
const entropyJson = JSON.stringify(entropyData);
|
||||
const geneCommitment = preview_gene_commitment(pendingMutationOrderB64);
|
||||
const geneCommitment = preview_gene_commitment(pendingMutationOrderB64, session, pendingMutationStep, mutationRounds);
|
||||
if (!geneCommitment) {
|
||||
throw new Error('Unable to compute mutation commitment');
|
||||
}
|
||||
@@ -75,7 +77,6 @@ async function sendHeartbeat() {
|
||||
const sig = sign_message(msg);
|
||||
if (!sig) {
|
||||
discard_gene_preview();
|
||||
console.error('Keypair not initialised — skipping heartbeat');
|
||||
return;
|
||||
}
|
||||
const resp = await sendRequest('/hb', 'POST', {
|
||||
@@ -105,11 +106,9 @@ async function sendHeartbeat() {
|
||||
pendingMutationOrderB64 = resp.next_mutation_order_b64;
|
||||
} else {
|
||||
discard_gene_preview();
|
||||
console.warn('Heartbeat rejected');
|
||||
}
|
||||
} catch (e) {
|
||||
discard_gene_preview();
|
||||
console.error(e);
|
||||
} finally {
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; style-src 'self' 'unsafe-inline'">
|
||||
<title>Anti-Scraper Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { initHeartbeat } from './heartbeat.js';
|
||||
|
||||
(async () => {
|
||||
await initHeartbeat();
|
||||
})();
|
||||
initHeartbeat().catch(() => {});
|
||||
@@ -0,0 +1,596 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"cpufeatures 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chronoseal-fuzz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"libfuzzer-sys",
|
||||
"serde_json",
|
||||
"shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "chronoseal-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
shared = { path = "../shared" }
|
||||
serde_json = "1"
|
||||
|
||||
[workspace]
|
||||
|
||||
[[bin]]
|
||||
name = "vm"
|
||||
path = "fuzz_targets/vm.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "protocol"
|
||||
path = "fuzz_targets/protocol.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "environment"
|
||||
path = "fuzz_targets/environment.rs"
|
||||
test = false
|
||||
doc = false
|
||||
@@ -0,0 +1,6 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let _ = shared::gene::decode_environment(data);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use shared::protocol::HeartbeatRequest;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let _: Result<HeartbeatRequest, _> = serde_json::from_str(s);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let _ = shared::vm::execute(data);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
echo "Starting server with static frontend serving..."
|
||||
cd ../server
|
||||
cargo run --release
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chronoseal-server"
|
||||
version = "0.6.0"
|
||||
version = "1.0.2"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
@@ -30,4 +30,6 @@ hex = "0.4"
|
||||
base64 = "0.22"
|
||||
rand = "0.8"
|
||||
ed25519-dalek = "2"
|
||||
valkey = "0.0.0-alpha5"
|
||||
redis = { version = "0.29", features = ["r2d2"] }
|
||||
dashmap = "6"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::session::AppState;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Runs an infinite background loop that periodically cleans up database and memory resources.
|
||||
///
|
||||
/// Every 60 seconds, this loop performs two tasks:
|
||||
/// 1. Evicts expired session records from the configured database storage backend.
|
||||
/// 2. Evicts stale rate-limiter entries that have outlived the current rate-limiting window.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - Shared reference to the server application state.
|
||||
pub async fn cleanup_loop(state: Arc<AppState>) {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
@@ -12,11 +20,10 @@ 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 map growth.
|
||||
{
|
||||
let window_secs = state.get_config().rate_limit_window_secs;
|
||||
let mut rl = state.rate_limiter.lock().await;
|
||||
rl.evict_stale(window_secs);
|
||||
state.rate_limiter.evict_stale(window_secs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,9 @@ impl Config {
|
||||
size: self.gene_size,
|
||||
});
|
||||
}
|
||||
if !(1..=shared::constants::MAX_MUTATION_ROUNDS).contains(&self.mutation_rounds) {
|
||||
if !(shared::constants::MIN_MUTATION_ROUNDS..=shared::constants::MAX_MUTATION_ROUNDS)
|
||||
.contains(&self.mutation_rounds)
|
||||
{
|
||||
return Err(ConfigError::InvalidMutationRounds {
|
||||
rounds: self.mutation_rounds,
|
||||
});
|
||||
@@ -274,7 +276,8 @@ impl std::fmt::Display for ConfigError {
|
||||
Self::InvalidMutationRounds { rounds } => {
|
||||
write!(
|
||||
f,
|
||||
"invalid mutation rounds {rounds}; expected 1..={}",
|
||||
"invalid mutation rounds {rounds}; expected {}..={}",
|
||||
shared::constants::MIN_MUTATION_ROUNDS,
|
||||
shared::constants::MAX_MUTATION_ROUNDS
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@ use ed25519_dalek::{Signature, VerifyingKey};
|
||||
use shared::protocol::HeartbeatRequest;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Serializes the heartbeat request into a canonical JSON representation for signature verification.
|
||||
///
|
||||
/// Uses `BTreeMap` to order top-level keys alphabetically, matching the JavaScript client's
|
||||
/// sorting algorithm: `JSON.stringify(obj, Object.keys(obj).sort())`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `req` - The heartbeat request to serialize.
|
||||
pub fn canonical_signing_message(
|
||||
req: &HeartbeatRequest,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Build canonical JSON with BTreeMap so keys are sorted alphabetically,
|
||||
// matching the JS client's JSON.stringify(obj, Object.keys(obj).sort()).
|
||||
let mut payload: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
|
||||
payload.insert("entropyData", serde_json::to_value(&req.entropy_data)?);
|
||||
payload.insert("fingerprint", serde_json::to_value(&req.fingerprint)?);
|
||||
@@ -19,6 +24,14 @@ pub fn canonical_signing_message(
|
||||
Ok(serde_json::to_string(&payload)?)
|
||||
}
|
||||
|
||||
/// Verifies the Ed25519 signature of a client's heartbeat request.
|
||||
///
|
||||
/// Decodes the signature and compares it strictly against the canonical JSON message
|
||||
/// using the client's public key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pub_key_bytes` - The client's public key bytes.
|
||||
/// * `req` - The heartbeat request payload containing the signature.
|
||||
pub fn verify_signature(
|
||||
pub_key_bytes: &[u8],
|
||||
req: &HeartbeatRequest,
|
||||
|
||||
@@ -25,12 +25,19 @@ pub enum SessionError {
|
||||
|
||||
#[error("Invalid gene configuration: {0}")]
|
||||
InvalidGeneConfiguration(String),
|
||||
|
||||
#[error("Rate limited")]
|
||||
RateLimited,
|
||||
}
|
||||
|
||||
impl IntoResponse for SessionError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
SessionError::RateLimited => (
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
"Too many requests".to_string(),
|
||||
),
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal server error".to_string(),
|
||||
@@ -86,4 +93,10 @@ pub enum VerificationError {
|
||||
|
||||
#[error("Gene state error: {0}")]
|
||||
GeneState(String),
|
||||
|
||||
#[error("VM execution stack state mismatch")]
|
||||
VmStackMismatch,
|
||||
|
||||
#[error("Concurrent state modification detected (CAS failed)")]
|
||||
ConcurrentUpdate,
|
||||
}
|
||||
@@ -1,16 +1,79 @@
|
||||
use shared::protocol::Fingerprint;
|
||||
|
||||
const MIN_ASPECT_RATIO: f64 = 0.5;
|
||||
const MAX_ASPECT_RATIO: f64 = 3.0;
|
||||
const MAX_DEVICE_PIXEL_RATIO: f64 = 5.0;
|
||||
const MAX_HARDWARE_CONCURRENCY: u32 = 256;
|
||||
|
||||
/// Validates the browser fingerprint fields submitted by the client.
|
||||
///
|
||||
/// Checks basic screen aspect ratio thresholds, device pixel ratio limits,
|
||||
/// and logical CPU core counts to reject anomaly fingerprints.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fp` - The client's hardware and screen layout fingerprint.
|
||||
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
|
||||
if !(0.5..=3.0).contains(&ar) {
|
||||
if !ar.is_finite() || !(MIN_ASPECT_RATIO..=MAX_ASPECT_RATIO).contains(&ar) {
|
||||
return Err("aspect ratio".into());
|
||||
}
|
||||
|
||||
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
|
||||
if dpr <= 0.0 || dpr > 5.0 {
|
||||
if !dpr.is_finite() || dpr <= 0.0 || dpr > MAX_DEVICE_PIXEL_RATIO {
|
||||
return Err("dpr".into());
|
||||
}
|
||||
if fp.hardware_concurrency == 0 {
|
||||
|
||||
if fp.hardware_concurrency == 0 || fp.hardware_concurrency > MAX_HARDWARE_CONCURRENCY {
|
||||
return Err("hw".into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fingerprint(
|
||||
aspect_ratio: impl Into<String>,
|
||||
device_pixel_ratio: impl Into<String>,
|
||||
hardware_concurrency: u32,
|
||||
) -> Fingerprint {
|
||||
Fingerprint {
|
||||
aspect_ratio: aspect_ratio.into(),
|
||||
device_pixel_ratio: device_pixel_ratio.into(),
|
||||
hardware_concurrency,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_fingerprint() {
|
||||
assert!(validate(&fingerprint("1.7777777778", "2", 8)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_boundary_values() {
|
||||
assert!(validate(&fingerprint("0.5", "1", 1)).is_ok());
|
||||
assert!(validate(&fingerprint("3.0", "5.0", MAX_HARDWARE_CONCURRENCY)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_aspect_ratios() {
|
||||
for aspect_ratio in ["not-a-number", "NaN", "inf", "0.49", "3.01"] {
|
||||
assert!(validate(&fingerprint(aspect_ratio, "2", 8)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_device_pixel_ratios() {
|
||||
for device_pixel_ratio in ["not-a-number", "NaN", "inf", "0", "-1", "5.01"] {
|
||||
assert!(validate(&fingerprint("1.77", device_pixel_ratio, 8)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_hardware_concurrency() {
|
||||
assert!(validate(&fingerprint("1.77", "2", 0)).is_err());
|
||||
assert!(validate(&fingerprint("1.77", "2", MAX_HARDWARE_CONCURRENCY + 1)).is_err());
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,6 @@ async fn main() {
|
||||
|
||||
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
if let Some(config_path) = cli.globals.config.as_deref() {
|
||||
std::env::set_var("CHRONOSEAL_CONFIG", config_path);
|
||||
}
|
||||
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
|
||||
let log_file = log_file_for_command(&cli);
|
||||
let _log_guard = init_logging(log_filter, log_file)?;
|
||||
|
||||
@@ -9,3 +9,25 @@ pub async fn log_request(req: Request, next: Next) -> Response {
|
||||
tracing::info!("{} {} -> {}", method, uri, response.status());
|
||||
response
|
||||
}
|
||||
|
||||
/// Injects defensive HTTP response headers on every response.
|
||||
///
|
||||
/// These headers mitigate several classes of attacks:
|
||||
/// - `X-Content-Type-Options: nosniff` — prevents MIME-type sniffing.
|
||||
/// - `X-Frame-Options: DENY` — blocks clickjacking via framing.
|
||||
/// - `Referrer-Policy: no-referrer` — suppresses referrer leakage.
|
||||
/// - `X-XSS-Protection: 0` — disables legacy XSS auditors (can introduce bugs).
|
||||
/// - `Permissions-Policy` — restricts powerful browser features.
|
||||
pub async fn security_headers(req: Request, next: Next) -> Response {
|
||||
let mut response = next.run(req).await;
|
||||
let headers = response.headers_mut();
|
||||
headers.insert("x-content-type-options", "nosniff".parse().unwrap());
|
||||
headers.insert("x-frame-options", "DENY".parse().unwrap());
|
||||
headers.insert("referrer-policy", "no-referrer".parse().unwrap());
|
||||
headers.insert("x-xss-protection", "0".parse().unwrap());
|
||||
headers.insert(
|
||||
"permissions-policy",
|
||||
"camera=(), microphone=(), geolocation=()".parse().unwrap(),
|
||||
);
|
||||
response
|
||||
}
|
||||
@@ -1,34 +1,54 @@
|
||||
use std::collections::HashMap;
|
||||
use dashmap::DashMap;
|
||||
use std::time::Instant;
|
||||
|
||||
/// A lock-free, concurrent sliding-window rate limiter backed by `DashMap`.
|
||||
///
|
||||
/// All public methods take `&self` (no `&mut self`), so the limiter can live in
|
||||
/// an `Arc<AppState>` without a `Mutex` wrapper.
|
||||
pub struct RateLimiter {
|
||||
buckets: HashMap<String, (u32, Instant)>,
|
||||
/// Maps rate-limit keys to request counts and window start timestamps.
|
||||
buckets: DashMap<String, (u32, Instant)>,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
/// Creates a new, empty `RateLimiter`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buckets: HashMap::new(),
|
||||
buckets: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(&mut self, key: &str, limit: u32, window_secs: u64) -> bool {
|
||||
/// Evaluates if a request conforms to the rate limit.
|
||||
///
|
||||
/// Returns `true` if allowed, or `false` if the rate limit is exceeded.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - The unique identifier to rate-limit (e.g., client IP address).
|
||||
/// * `limit` - The maximum number of allowed requests per window.
|
||||
/// * `window_secs` - The length of the sliding-window in seconds.
|
||||
pub fn check(&self, key: &str, limit: u32, window_secs: u64) -> bool {
|
||||
let now = Instant::now();
|
||||
let entry = self.buckets.entry(key.to_string()).or_insert((0, now));
|
||||
if now.duration_since(entry.1).as_secs() >= window_secs {
|
||||
*entry = (1, now);
|
||||
let mut entry = self.buckets.entry(key.to_string()).or_insert((0, now));
|
||||
let (count, ts) = entry.value_mut();
|
||||
if now.duration_since(*ts).as_secs() >= window_secs {
|
||||
*count = 1;
|
||||
*ts = now;
|
||||
true
|
||||
} else if entry.0 >= limit {
|
||||
} else if *count >= limit {
|
||||
false
|
||||
} else {
|
||||
entry.0 += 1;
|
||||
*count += 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove entries whose rate-limit window has fully elapsed.
|
||||
/// Call this periodically (e.g. from the cleanup loop) to bound memory usage.
|
||||
pub fn evict_stale(&mut self, window_secs: u64) {
|
||||
/// Evicts expired rate-limit entries whose time windows have fully elapsed.
|
||||
///
|
||||
/// Intended to be called periodically to bound in-memory map growth.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `window_secs` - The active rate-limiting window duration in seconds.
|
||||
pub fn evict_stale(&self, window_secs: u64) {
|
||||
let now = Instant::now();
|
||||
self.buckets
|
||||
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs);
|
||||
@@ -43,7 +63,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rate_limiter() {
|
||||
let mut rl = RateLimiter::new();
|
||||
let rl = RateLimiter::new();
|
||||
// Limit of 2 requests per 1 second window
|
||||
assert!(rl.check("user1", 2, 1));
|
||||
assert!(rl.check("user1", 2, 1));
|
||||
@@ -57,7 +77,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rate_limiter_eviction() {
|
||||
let mut rl = RateLimiter::new();
|
||||
let rl = RateLimiter::new();
|
||||
assert!(rl.check("user1", 1, 1));
|
||||
assert_eq!(rl.buckets.len(), 1);
|
||||
|
||||
|
||||
@@ -7,15 +7,52 @@ pub async fn handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<HeartbeatRequest>,
|
||||
) -> (StatusCode, Json<HeartbeatResponse>) {
|
||||
// Rate limiting
|
||||
let start_http = std::time::Instant::now();
|
||||
state
|
||||
.heartbeats_total
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Cap entropy events to prevent oversized payloads from exhausting memory.
|
||||
if payload.entropy_data.events.len() > 1000 {
|
||||
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||
state
|
||||
.http_latency_ns
|
||||
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||
state
|
||||
.http_ops_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(HeartbeatResponse {
|
||||
status: "ok".into(),
|
||||
next_salt: None,
|
||||
next_mutation_step: None,
|
||||
next_mutation_order_b64: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Rate limiting (lock-free via DashMap)
|
||||
{
|
||||
let (limit, window_secs) = {
|
||||
let cfg = state.get_config();
|
||||
(cfg.rate_limit_count, cfg.rate_limit_window_secs)
|
||||
};
|
||||
let mut rl = state.rate_limiter.lock().await;
|
||||
if !rl.check(&payload.session_id, limit, window_secs) {
|
||||
if !state
|
||||
.rate_limiter
|
||||
.check(&payload.session_id, limit, window_secs)
|
||||
{
|
||||
tracing::debug!("Rate limit hit: {}", payload.session_id);
|
||||
state
|
||||
.verification_failures_total
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||
state
|
||||
.http_latency_ns
|
||||
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||
state
|
||||
.http_ops_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(HeartbeatResponse {
|
||||
@@ -29,7 +66,17 @@ pub async fn handler(
|
||||
}
|
||||
|
||||
let config = state.get_config();
|
||||
match crate::session::verify_heartbeat(&state.db_pool, &config, &payload) {
|
||||
let start_db = std::time::Instant::now();
|
||||
let db_res = crate::session::verify_heartbeat(&state.db_pool, &config, &payload);
|
||||
let db_dur = start_db.elapsed().as_nanos() as u64;
|
||||
state
|
||||
.storage_latency_ns
|
||||
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
|
||||
state
|
||||
.storage_ops_count
|
||||
.fetch_add(2, std::sync::atomic::Ordering::Relaxed); // read + write
|
||||
|
||||
let outcome = match db_res {
|
||||
Ok(result) => (
|
||||
StatusCode::OK,
|
||||
Json(HeartbeatResponse {
|
||||
@@ -41,6 +88,24 @@ pub async fn handler(
|
||||
),
|
||||
Err(e) => {
|
||||
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
|
||||
state
|
||||
.verification_failures_total
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
match &e {
|
||||
crate::errors::VerificationError::ChainBroken => {
|
||||
state
|
||||
.replay_attempts_total
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
crate::errors::VerificationError::MutationCommitmentMismatch
|
||||
| crate::errors::VerificationError::MutationProgram(_)
|
||||
| crate::errors::VerificationError::GeneState(_) => {
|
||||
state
|
||||
.mutation_failures_total
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(HeartbeatResponse {
|
||||
@@ -51,7 +116,17 @@ pub async fn handler(
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||
state
|
||||
.http_latency_ns
|
||||
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||
state
|
||||
.http_ops_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
outcome
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -59,7 +134,7 @@ mod tests {
|
||||
use super::*;
|
||||
use axum::{extract::State, Json};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use shared::protocol::{EntropyData, Fingerprint, InitResponse, MouseEvent, StackState};
|
||||
use shared::protocol::{EntropyData, Fingerprint, InitResponse, MouseEvent};
|
||||
use std::path::Path;
|
||||
|
||||
fn test_config() -> crate::config::Config {
|
||||
@@ -107,10 +182,12 @@ mod tests {
|
||||
},
|
||||
],
|
||||
};
|
||||
let stack_state = StackState {
|
||||
stack: vec![9, 10, 11],
|
||||
ip: 2,
|
||||
};
|
||||
let program_bytes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)
|
||||
.unwrap();
|
||||
let stack_state = shared::vm::execute(&program_bytes);
|
||||
|
||||
let order =
|
||||
shared::vm_extensions::decode_order_b64(mutation_step, mutation_order_b64).unwrap();
|
||||
@@ -147,8 +224,16 @@ mod tests {
|
||||
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
|
||||
let state = Arc::new(AppState {
|
||||
db_pool: pool.clone(),
|
||||
rate_limiter: tokio::sync::Mutex::new(crate::ratelimit::RateLimiter::new()),
|
||||
rate_limiter: crate::ratelimit::RateLimiter::new(),
|
||||
config: std::sync::RwLock::new(config.clone()),
|
||||
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
|
||||
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
|
||||
mutation_failures_total: std::sync::atomic::AtomicU64::new(0),
|
||||
replay_attempts_total: std::sync::atomic::AtomicU64::new(0),
|
||||
storage_latency_ns: std::sync::atomic::AtomicU64::new(0),
|
||||
storage_ops_count: std::sync::atomic::AtomicU64::new(0),
|
||||
http_latency_ns: std::sync::atomic::AtomicU64::new(0),
|
||||
http_ops_count: std::sync::atomic::AtomicU64::new(0),
|
||||
});
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
@@ -8,7 +8,37 @@ pub async fn handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<InitRequest>,
|
||||
) -> Result<Json<InitResponse>, SessionError> {
|
||||
let start_http = std::time::Instant::now();
|
||||
let config = state.get_config();
|
||||
let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key)?;
|
||||
|
||||
// Rate limit session creation by public key to prevent storage exhaustion.
|
||||
if !state.rate_limiter.check(
|
||||
&payload.public_key,
|
||||
config.rate_limit_count,
|
||||
config.rate_limit_window_secs,
|
||||
) {
|
||||
return Err(SessionError::RateLimited);
|
||||
}
|
||||
|
||||
let start_db = std::time::Instant::now();
|
||||
let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key);
|
||||
let db_dur = start_db.elapsed().as_nanos() as u64;
|
||||
state
|
||||
.storage_latency_ns
|
||||
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
|
||||
state
|
||||
.storage_ops_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let resp = resp?;
|
||||
|
||||
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||
state
|
||||
.http_latency_ns
|
||||
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||
state
|
||||
.http_ops_count
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
Ok(Json(resp))
|
||||
}
|
||||
@@ -5,17 +5,19 @@ use crate::{
|
||||
routes, session,
|
||||
storage::{self, StoreStats},
|
||||
};
|
||||
use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||
use axum::{
|
||||
extract::ConnectInfo, http::StatusCode, response::IntoResponse, routing::get, Json, Router,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read, Write},
|
||||
net::{SocketAddr, TcpStream},
|
||||
net::{IpAddr, SocketAddr, TcpStream},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tokio::sync::Notify;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -144,8 +146,16 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
|
||||
let db_pool = init_db_pool(&config)?;
|
||||
let state = Arc::new(session::AppState {
|
||||
db_pool,
|
||||
rate_limiter: Mutex::new(RateLimiter::new()),
|
||||
rate_limiter: RateLimiter::new(),
|
||||
config: std::sync::RwLock::new(config.clone()),
|
||||
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
|
||||
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
|
||||
mutation_failures_total: std::sync::atomic::AtomicU64::new(0),
|
||||
replay_attempts_total: std::sync::atomic::AtomicU64::new(0),
|
||||
storage_latency_ns: std::sync::atomic::AtomicU64::new(0),
|
||||
storage_ops_count: std::sync::atomic::AtomicU64::new(0),
|
||||
http_latency_ns: std::sync::atomic::AtomicU64::new(0),
|
||||
http_ops_count: std::sync::atomic::AtomicU64::new(0),
|
||||
});
|
||||
|
||||
let bg_state = state.clone();
|
||||
@@ -162,7 +172,11 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
|
||||
tower_http::services::ServeDir::new(&config.frontend_dir),
|
||||
)
|
||||
.layer(tower_http::cors::CorsLayer::permissive())
|
||||
.layer(axum::middleware::from_fn(
|
||||
crate::middleware::security_headers,
|
||||
))
|
||||
.layer(axum::middleware::from_fn(crate::middleware::log_request))
|
||||
.layer(axum::extract::DefaultBodyLimit::max(64 * 1024)) // 64 KiB
|
||||
.with_state(state.clone());
|
||||
|
||||
let addr: SocketAddr = config.bind.parse()?;
|
||||
@@ -170,9 +184,12 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
|
||||
info!(bind = %config.bind, "chronoseal daemon started");
|
||||
|
||||
let shutdown = signal_task(state.clone());
|
||||
let result = axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown)
|
||||
.await;
|
||||
let result = axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(shutdown)
|
||||
.await;
|
||||
|
||||
remove_pid_file(&config.pid_file);
|
||||
result?;
|
||||
@@ -269,8 +286,12 @@ async fn health_handler() -> impl IntoResponse {
|
||||
}
|
||||
|
||||
async fn stats_handler(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||
) -> Result<Json<StoreStats>, (StatusCode, String)> {
|
||||
if !is_loopback(addr.ip()) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
state
|
||||
.db_pool
|
||||
.stats()
|
||||
@@ -279,18 +300,99 @@ async fn stats_handler(
|
||||
}
|
||||
|
||||
async fn metrics_handler(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||
) -> Result<String, (StatusCode, String)> {
|
||||
state
|
||||
if !is_loopback(addr.ip()) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let stats = state
|
||||
.db_pool
|
||||
.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",
|
||||
stats.sessions, stats.expired_sessions, stats.max_chain_length
|
||||
)
|
||||
})
|
||||
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
||||
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||
|
||||
let heartbeats = state
|
||||
.heartbeats_total
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let ver_failures = state
|
||||
.verification_failures_total
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let mut_failures = state
|
||||
.mutation_failures_total
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let replays = state
|
||||
.replay_attempts_total
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let store_ns = state
|
||||
.storage_latency_ns
|
||||
.load(std::sync::atomic::Ordering::Relaxed) as f64;
|
||||
let store_sum = store_ns / 1_000_000_000.0;
|
||||
let store_count = state
|
||||
.storage_ops_count
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let http_ns = state
|
||||
.http_latency_ns
|
||||
.load(std::sync::atomic::Ordering::Relaxed) as f64;
|
||||
let http_sum = http_ns / 1_000_000_000.0;
|
||||
let http_count = state
|
||||
.http_ops_count
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
Ok(format!(
|
||||
"# HELP chronoseal_active_sessions Active ChronoSeal sessions\n\
|
||||
# TYPE chronoseal_active_sessions gauge\n\
|
||||
chronoseal_active_sessions {}\n\
|
||||
# HELP chronoseal_expired_sessions Expired sessions not yet removed\n\
|
||||
# TYPE chronoseal_expired_sessions gauge\n\
|
||||
chronoseal_expired_sessions {}\n\
|
||||
# HELP chronoseal_max_chain_length Maximum heartbeat chain length\n\
|
||||
# TYPE chronoseal_max_chain_length gauge\n\
|
||||
chronoseal_max_chain_length {}\n\
|
||||
# HELP chronoseal_heartbeats_total Total heartbeat requests processed\n\
|
||||
# TYPE chronoseal_heartbeats_total counter\n\
|
||||
chronoseal_heartbeats_total {}\n\
|
||||
# HELP chronoseal_verification_failures_total Total heartbeat verification failures\n\
|
||||
# TYPE chronoseal_verification_failures_total counter\n\
|
||||
chronoseal_verification_failures_total {}\n\
|
||||
# HELP chronoseal_mutation_failures_total Total heartbeat mutation verification failures\n\
|
||||
# TYPE chronoseal_mutation_failures_total counter\n\
|
||||
chronoseal_mutation_failures_total {}\n\
|
||||
# HELP chronoseal_replay_attempts_total Total heartbeat replay attempts detected\n\
|
||||
# TYPE chronoseal_replay_attempts_total counter\n\
|
||||
chronoseal_replay_attempts_total {}\n\
|
||||
# HELP chronoseal_storage_latency_seconds_sum Total time spent in storage operations in seconds\n\
|
||||
# TYPE chronoseal_storage_latency_seconds_sum counter\n\
|
||||
chronoseal_storage_latency_seconds_sum {:.6}\n\
|
||||
# HELP chronoseal_storage_latency_seconds_count Total storage operations count\n\
|
||||
# TYPE chronoseal_storage_latency_seconds_count counter\n\
|
||||
chronoseal_storage_latency_seconds_count {}\n\
|
||||
# HELP chronoseal_http_latency_seconds_sum Total time spent in HTTP request processing in seconds\n\
|
||||
# TYPE chronoseal_http_latency_seconds_sum counter\n\
|
||||
chronoseal_http_latency_seconds_sum {:.6}\n\
|
||||
# HELP chronoseal_http_latency_seconds_count Total HTTP operations count\n\
|
||||
# TYPE chronoseal_http_latency_seconds_count counter\n\
|
||||
chronoseal_http_latency_seconds_count {}\n",
|
||||
stats.sessions,
|
||||
stats.expired_sessions,
|
||||
stats.max_chain_length,
|
||||
heartbeats,
|
||||
ver_failures,
|
||||
mut_failures,
|
||||
replays,
|
||||
store_sum,
|
||||
store_count,
|
||||
http_sum,
|
||||
http_count
|
||||
))
|
||||
}
|
||||
|
||||
fn is_loopback(ip: IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => v4.is_loopback(),
|
||||
IpAddr::V6(v6) => v6.is_loopback(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn signal_task(state: Arc<session::AppState>) {
|
||||
@@ -458,10 +560,16 @@ mod tests {
|
||||
fn test_init_db_pool_valkey_compat_mode() {
|
||||
let mut config = base_config();
|
||||
config.db_type = crate::config::DbType::Valkey;
|
||||
let pool = init_db_pool(&config).unwrap();
|
||||
let stats = pool.stats().unwrap();
|
||||
assert_eq!(stats.sessions, 0);
|
||||
assert_eq!(stats.expired_sessions, 0);
|
||||
assert_eq!(stats.max_chain_length, 0);
|
||||
match init_db_pool(&config) {
|
||||
Ok(pool) => {
|
||||
let stats = pool.stats().unwrap();
|
||||
assert_eq!(stats.sessions, 0);
|
||||
assert_eq!(stats.expired_sessions, 0);
|
||||
assert_eq!(stats.max_chain_length, 0);
|
||||
}
|
||||
Err(_) => {
|
||||
// Valkey not running in the test environment, which is acceptable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
pub struct AppState {
|
||||
pub db_pool: crate::storage::DbPool,
|
||||
pub rate_limiter: tokio::sync::Mutex<crate::ratelimit::RateLimiter>,
|
||||
pub rate_limiter: crate::ratelimit::RateLimiter,
|
||||
pub config: std::sync::RwLock<crate::config::Config>,
|
||||
pub heartbeats_total: std::sync::atomic::AtomicU64,
|
||||
pub verification_failures_total: std::sync::atomic::AtomicU64,
|
||||
pub mutation_failures_total: std::sync::atomic::AtomicU64,
|
||||
pub replay_attempts_total: std::sync::atomic::AtomicU64,
|
||||
pub storage_latency_ns: std::sync::atomic::AtomicU64,
|
||||
pub storage_ops_count: std::sync::atomic::AtomicU64,
|
||||
pub http_latency_ns: std::sync::atomic::AtomicU64,
|
||||
pub http_ops_count: std::sync::atomic::AtomicU64,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -68,6 +76,7 @@ pub fn create_session(
|
||||
environment: environment_blob,
|
||||
pending_mutation: initial_mutation.program,
|
||||
pending_mutation_step: initial_mutation.step,
|
||||
opcodes,
|
||||
};
|
||||
|
||||
db.insert_session(&record)
|
||||
@@ -84,6 +93,7 @@ pub fn create_session(
|
||||
gene_size: config.gene_size as u32,
|
||||
mutation_step: initial_mutation.step,
|
||||
mutation_order_b64: initial_mutation_b64,
|
||||
mutation_rounds: config.mutation_rounds,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,6 +160,12 @@ pub fn verify_heartbeat(
|
||||
fingerprint::validate(&req.fingerprint)
|
||||
.map_err(|e| crate::errors::VerificationError::FingerprintFailed(e.to_string()))?;
|
||||
|
||||
// 5.5 Verify VM execution state
|
||||
let expected_stack = shared::vm::execute(&session.opcodes);
|
||||
if req.stack_state.stack != expected_stack.stack || req.stack_state.ip != expected_stack.ip {
|
||||
return Err(crate::errors::VerificationError::VmStackMismatch);
|
||||
}
|
||||
|
||||
// 6. Compute new hash
|
||||
let new_hash = shared::hashing::next_chain_hash(
|
||||
&prev_hash_bytes,
|
||||
@@ -182,9 +198,16 @@ pub fn verify_heartbeat(
|
||||
environment: next_environment_blob,
|
||||
pending_mutation: next_mutation.program,
|
||||
pending_mutation_step: next_step,
|
||||
opcodes: session.opcodes,
|
||||
};
|
||||
db.update_session(&update_record)
|
||||
.map_err(|e| crate::errors::VerificationError::Storage(e.to_string()))?;
|
||||
db.update_session(&update_record, &session.last_hash)
|
||||
.map_err(|e| {
|
||||
if e.to_string().contains("Concurrent update detected") {
|
||||
crate::errors::VerificationError::ConcurrentUpdate
|
||||
} else {
|
||||
crate::errors::VerificationError::Storage(e.to_string())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(HeartbeatVerificationResult {
|
||||
next_salt_hex,
|
||||
@@ -210,6 +233,7 @@ mod tests {
|
||||
pending_mutation_step: u64,
|
||||
pending_mutation_order_b64: String,
|
||||
committed_gene_state: GeneState,
|
||||
opcodes_b64: String,
|
||||
}
|
||||
|
||||
fn test_config() -> crate::config::Config {
|
||||
@@ -247,13 +271,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_stack() -> StackState {
|
||||
StackState {
|
||||
stack: vec![42, 7, 99],
|
||||
ip: 3,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_fingerprint() -> Fingerprint {
|
||||
Fingerprint {
|
||||
aspect_ratio: "1.77".to_string(),
|
||||
@@ -288,6 +305,7 @@ mod tests {
|
||||
pending_mutation_step: init.mutation_step,
|
||||
pending_mutation_order_b64: init.mutation_order_b64.clone(),
|
||||
committed_gene_state: gene::new_state(init.gene_size as usize).unwrap(),
|
||||
opcodes_b64: init.opcodes_b64.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +322,13 @@ mod tests {
|
||||
vm_extensions::apply_program_clone(&client.committed_gene_state, &order.program)
|
||||
.unwrap();
|
||||
let entropy = test_entropy();
|
||||
let stack = test_stack();
|
||||
|
||||
let program_bytes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&client.opcodes_b64,
|
||||
)
|
||||
.unwrap();
|
||||
let stack = shared::vm::execute(&program_bytes);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: client.session_id.clone(),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::config::Config;
|
||||
use redis::Commands;
|
||||
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 {
|
||||
@@ -20,7 +19,7 @@ pub enum DbPool {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValkeyStore {
|
||||
client: Arc<Mutex<ValkeyClient>>,
|
||||
pool: r2d2::Pool<redis::Client>,
|
||||
index_key: String,
|
||||
}
|
||||
|
||||
@@ -38,6 +37,7 @@ pub struct SessionRecord {
|
||||
pub environment: Vec<u8>,
|
||||
pub pending_mutation: Vec<u8>,
|
||||
pub pending_mutation_step: u64,
|
||||
pub opcodes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DbPool {
|
||||
@@ -54,19 +54,18 @@ impl DbPool {
|
||||
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))
|
||||
}
|
||||
}
|
||||
let connection_string =
|
||||
if addr.starts_with("redis://") || addr.starts_with("rediss://") {
|
||||
addr.clone()
|
||||
} else {
|
||||
format!("redis://{}", addr)
|
||||
};
|
||||
let client = redis::Client::open(connection_string)?;
|
||||
let pool = r2d2::Pool::builder().build(client)?;
|
||||
Ok(DbPool::Valkey(ValkeyStore {
|
||||
pool,
|
||||
index_key: "sessions:ids".to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,8 +78,8 @@ impl DbPool {
|
||||
"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)",
|
||||
pending_mutation, pending_mutation_step, opcodes
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
)?;
|
||||
stmt.execute(rusqlite::params![
|
||||
record.session_id,
|
||||
@@ -95,6 +94,7 @@ impl DbPool {
|
||||
&record.environment,
|
||||
&record.pending_mutation,
|
||||
record.pending_mutation_step,
|
||||
&record.opcodes,
|
||||
])?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -110,7 +110,7 @@ impl DbPool {
|
||||
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
|
||||
"SELECT session_id, public_key, salt, last_hash, chain_length, created_at, last_seen, expires_at, gene, environment, pending_mutation, pending_mutation_step, opcodes
|
||||
FROM sessions WHERE session_id = ?1",
|
||||
)?;
|
||||
let row = stmt.query_row([session_id], |row| {
|
||||
@@ -127,6 +127,7 @@ impl DbPool {
|
||||
environment: row.get(9)?,
|
||||
pending_mutation: row.get(10)?,
|
||||
pending_mutation_step: row.get(11)?,
|
||||
opcodes: row.get(12)?,
|
||||
})
|
||||
});
|
||||
match row {
|
||||
@@ -139,11 +140,15 @@ impl DbPool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn update_session(
|
||||
&self,
|
||||
record: &SessionRecord,
|
||||
old_last_hash: &[u8],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match self {
|
||||
DbPool::Sqlite(pool) => {
|
||||
let conn = pool.get()?;
|
||||
conn.execute(
|
||||
let rows = conn.execute(
|
||||
"UPDATE sessions SET
|
||||
public_key=?1,
|
||||
salt=?2,
|
||||
@@ -155,8 +160,9 @@ impl DbPool {
|
||||
gene=?8,
|
||||
environment=?9,
|
||||
pending_mutation=?10,
|
||||
pending_mutation_step=?11
|
||||
WHERE session_id=?12",
|
||||
pending_mutation_step=?11,
|
||||
opcodes=?12
|
||||
WHERE session_id=?13 AND last_hash=?14",
|
||||
rusqlite::params![
|
||||
&record.public_key,
|
||||
&record.salt,
|
||||
@@ -169,12 +175,20 @@ impl DbPool {
|
||||
&record.environment,
|
||||
&record.pending_mutation,
|
||||
record.pending_mutation_step,
|
||||
&record.opcodes,
|
||||
&record.session_id,
|
||||
old_last_hash,
|
||||
],
|
||||
)?;
|
||||
if rows == 0 {
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::AlreadyExists,
|
||||
"Concurrent update detected (CAS failed)",
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
DbPool::Valkey(store) => store.insert_session(record),
|
||||
DbPool::Valkey(store) => store.update_session_cas(record, old_last_hash),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +251,10 @@ fn init_sqlite_pool(
|
||||
}
|
||||
r2d2_sqlite::SqliteConnectionManager::file(path)
|
||||
};
|
||||
let manager = manager.with_init(|conn| {
|
||||
conn.busy_timeout(std::time::Duration::from_millis(5000))?;
|
||||
Ok(())
|
||||
});
|
||||
let pool = r2d2::Pool::new(manager)?;
|
||||
let conn = pool.get()?;
|
||||
init_schema(&conn)?;
|
||||
@@ -257,7 +275,8 @@ fn init_schema(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
|
||||
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
|
||||
pending_mutation_step INTEGER NOT NULL DEFAULT 0,
|
||||
opcodes BLOB NOT NULL DEFAULT X''
|
||||
);",
|
||||
)?;
|
||||
ensure_column(
|
||||
@@ -280,6 +299,11 @@ fn init_schema(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
|
||||
"pending_mutation_step",
|
||||
"ALTER TABLE sessions ADD COLUMN pending_mutation_step INTEGER NOT NULL DEFAULT 0",
|
||||
)?;
|
||||
ensure_column(
|
||||
conn,
|
||||
"opcodes",
|
||||
"ALTER TABLE sessions ADD COLUMN opcodes BLOB NOT NULL DEFAULT X''",
|
||||
)?;
|
||||
conn.execute_batch(
|
||||
"CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);",
|
||||
)?;
|
||||
@@ -313,67 +337,136 @@ impl ValkeyStore {
|
||||
&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)
|
||||
let mut conn = self.pool.get()?;
|
||||
let key = self.session_key(session_id);
|
||||
let payload: Option<String> = conn.get(&key)?;
|
||||
match payload {
|
||||
Some(p) => Ok(serde_json::from_str(&p)?),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = self.client.lock().unwrap();
|
||||
let mut conn = self.pool.get()?;
|
||||
let key = self.session_key(&record.session_id);
|
||||
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)?;
|
||||
}
|
||||
let now = current_time_ms();
|
||||
let ttl_seconds = (record.expires_at.saturating_sub(now) / 1000).max(1);
|
||||
|
||||
redis::pipe()
|
||||
.atomic()
|
||||
.cmd("SET")
|
||||
.arg(&key)
|
||||
.arg(&value)
|
||||
.arg("EX")
|
||||
.arg(ttl_seconds)
|
||||
.cmd("ZADD")
|
||||
.arg(&self.index_key)
|
||||
.arg(record.expires_at)
|
||||
.arg(&record.session_id)
|
||||
.cmd("ZADD")
|
||||
.arg("sessions:chain_lengths")
|
||||
.arg(record.chain_length)
|
||||
.arg(&record.session_id)
|
||||
.query::<()>(&mut *conn)?;
|
||||
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());
|
||||
}
|
||||
fn update_session_cas(
|
||||
&self,
|
||||
record: &SessionRecord,
|
||||
old_last_hash: &[u8],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut conn = self.pool.get()?;
|
||||
let key = self.session_key(&record.session_id);
|
||||
|
||||
// Watch key for concurrent modification
|
||||
redis::cmd("WATCH").arg(&key).query::<()>(&mut *conn)?;
|
||||
|
||||
// Fetch current and verify last_hash matches
|
||||
let payload: Option<String> = conn.get(&key)?;
|
||||
match payload {
|
||||
Some(p) => {
|
||||
let current_record: SessionRecord = serde_json::from_str(&p)?;
|
||||
if current_record.last_hash != old_last_hash {
|
||||
redis::cmd("UNWATCH").query::<()>(&mut *conn)?;
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::AlreadyExists,
|
||||
"Concurrent update detected (CAS failed in Valkey)",
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
redis::cmd("UNWATCH").query::<()>(&mut *conn)?;
|
||||
return Err(Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"Session not found for update in Valkey",
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let value = serde_json::to_string(record)?;
|
||||
let now = current_time_ms();
|
||||
let ttl_seconds = (record.expires_at.saturating_sub(now) / 1000).max(1);
|
||||
|
||||
let response: Option<()> = redis::pipe()
|
||||
.atomic()
|
||||
.cmd("SET")
|
||||
.arg(&key)
|
||||
.arg(&value)
|
||||
.arg("EX")
|
||||
.arg(ttl_seconds)
|
||||
.cmd("ZADD")
|
||||
.arg(&self.index_key)
|
||||
.arg(record.expires_at)
|
||||
.arg(&record.session_id)
|
||||
.cmd("ZADD")
|
||||
.arg("sessions:chain_lengths")
|
||||
.arg(record.chain_length)
|
||||
.arg(&record.session_id)
|
||||
.query(&mut *conn)?;
|
||||
|
||||
match response {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::AlreadyExists,
|
||||
"Transaction aborted due to concurrent modification",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn purge_expired_sessions(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let now = current_time_ms();
|
||||
let mut conn = self.pool.get()?;
|
||||
// Fetch expired session IDs
|
||||
let expired_ids: Vec<String> = conn.zrangebyscore(&self.index_key, 0, now)?;
|
||||
if !expired_ids.is_empty() {
|
||||
redis::pipe()
|
||||
.atomic()
|
||||
.cmd("ZREM")
|
||||
.arg(&self.index_key)
|
||||
.arg(&expired_ids)
|
||||
.cmd("ZREM")
|
||||
.arg("sessions:chain_lengths")
|
||||
.arg(&expired_ids)
|
||||
.query::<()>(&mut *conn)?;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut conn = self.pool.get()?;
|
||||
let sessions: u64 = conn.zcard(&self.index_key)?;
|
||||
let expired_sessions: u64 = conn.zcount(&self.index_key, 0, now)?;
|
||||
|
||||
let max_chain_length_res: Vec<(String, u64)> =
|
||||
conn.zrevrange_withscores("sessions:chain_lengths", 0, 0)?;
|
||||
let max_chain_length = max_chain_length_res
|
||||
.first()
|
||||
.map(|(_, score)| *score)
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(StoreStats {
|
||||
sessions,
|
||||
expired_sessions,
|
||||
@@ -385,6 +478,212 @@ impl ValkeyStore {
|
||||
pub fn current_time_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.expect("system clock is before UNIX epoch; check system time")
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod valkey_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valkey_store_operations() {
|
||||
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
|
||||
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
|
||||
let connection_string = format!("redis://{}", addr);
|
||||
let client = match redis::Client::open(connection_string) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
let pool = match r2d2::Pool::builder().build(client) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return,
|
||||
};
|
||||
let mut conn = match pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
let _: () = match redis::cmd("PING").query(&mut *conn) {
|
||||
Ok(res) => res,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let store = ValkeyStore {
|
||||
pool,
|
||||
index_key: "test:sessions:ids".to_string(),
|
||||
};
|
||||
|
||||
let _: Result<(), _> = conn.del("test:sessions:ids");
|
||||
|
||||
let session_id = "test_session_123".to_string();
|
||||
let record = SessionRecord {
|
||||
session_id: session_id.clone(),
|
||||
public_key: vec![1, 2, 3],
|
||||
salt: vec![4, 5, 6],
|
||||
last_hash: vec![7, 8, 9],
|
||||
chain_length: 10,
|
||||
created_at: 1000,
|
||||
last_seen: 2000,
|
||||
expires_at: current_time_ms() + 10000,
|
||||
gene: vec![11],
|
||||
environment: vec![12],
|
||||
pending_mutation: vec![13],
|
||||
pending_mutation_step: 14,
|
||||
opcodes: vec![],
|
||||
};
|
||||
|
||||
store.insert_session(&record).unwrap();
|
||||
|
||||
let loaded = store.load_session(&session_id).unwrap().unwrap();
|
||||
assert_eq!(loaded.session_id, session_id);
|
||||
assert_eq!(loaded.chain_length, 10);
|
||||
|
||||
let stats = store.stats().unwrap();
|
||||
assert_eq!(stats.sessions, 1);
|
||||
assert_eq!(stats.max_chain_length, 10);
|
||||
|
||||
store.purge_expired_sessions().unwrap();
|
||||
let stats = store.stats().unwrap();
|
||||
assert_eq!(stats.sessions, 1);
|
||||
|
||||
let _: Result<(), _> = conn.del(store.session_key(&session_id));
|
||||
let _: Result<(), _> = conn.del(&store.index_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valkey_pool_concurrency() {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
|
||||
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
|
||||
let connection_string = format!("redis://{}", addr);
|
||||
let client = match redis::Client::open(connection_string) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
let pool = match r2d2::Pool::builder().build(client) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return,
|
||||
};
|
||||
let mut conn = match pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
let _: () = match redis::cmd("PING").query(&mut *conn) {
|
||||
Ok(res) => res,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let store = ValkeyStore {
|
||||
pool,
|
||||
index_key: "test:concurrent:sessions:ids".to_string(),
|
||||
};
|
||||
let _: Result<(), _> = conn.del("test:concurrent:sessions:ids");
|
||||
|
||||
let store_arc = Arc::new(store);
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for t in 0..10 {
|
||||
let store_clone = store_arc.clone();
|
||||
let session_id = format!("valkey_concurrent_{}", t);
|
||||
let handle = thread::spawn(move || {
|
||||
let record = SessionRecord {
|
||||
session_id: session_id.clone(),
|
||||
public_key: vec![1, 2, 3],
|
||||
salt: vec![4, 5, 6],
|
||||
last_hash: vec![7, 8, 9],
|
||||
chain_length: 1,
|
||||
created_at: 1000,
|
||||
last_seen: 2000,
|
||||
expires_at: current_time_ms() + 10000,
|
||||
gene: vec![11],
|
||||
environment: vec![12],
|
||||
pending_mutation: vec![13],
|
||||
pending_mutation_step: 14,
|
||||
opcodes: vec![],
|
||||
};
|
||||
store_clone.insert_session(&record).unwrap();
|
||||
let loaded = store_clone.load_session(&session_id).unwrap().unwrap();
|
||||
assert_eq!(loaded.session_id, session_id);
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
let stats = store_arc.stats().unwrap();
|
||||
assert_eq!(stats.sessions, 10);
|
||||
|
||||
// Cleanup
|
||||
let mut conn = store_arc.pool.get().unwrap();
|
||||
for t in 0..10 {
|
||||
let _: Result<(), _> =
|
||||
conn.del(store_arc.session_key(&format!("valkey_concurrent_{}", t)));
|
||||
}
|
||||
let _: Result<(), _> = conn.del(&store_arc.index_key);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod sqlite_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn test_sqlite_pool_concurrency() {
|
||||
let db_path = Path::new("target/test_sqlite_concurrency.db");
|
||||
if let Some(parent) = db_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = std::fs::remove_file(db_path);
|
||||
|
||||
let pool = init_pool(db_path).unwrap();
|
||||
let pool_arc = Arc::new(pool);
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for t in 0..10 {
|
||||
let pool_clone = pool_arc.clone();
|
||||
let handle = thread::spawn(move || {
|
||||
let session_id = format!("concurrent_session_{}", t);
|
||||
let record = SessionRecord {
|
||||
session_id: session_id.clone(),
|
||||
public_key: vec![1, 2, 3],
|
||||
salt: vec![4, 5, 6],
|
||||
last_hash: vec![7, 8, 9],
|
||||
chain_length: 1,
|
||||
created_at: 1000,
|
||||
last_seen: 2000,
|
||||
expires_at: current_time_ms() + 10000,
|
||||
gene: vec![11],
|
||||
environment: vec![12],
|
||||
pending_mutation: vec![13],
|
||||
pending_mutation_step: 14,
|
||||
opcodes: vec![],
|
||||
};
|
||||
pool_clone.insert_session(&record).unwrap();
|
||||
let loaded = pool_clone.load_session(&session_id).unwrap().unwrap();
|
||||
assert_eq!(loaded.session_id, session_id);
|
||||
|
||||
let mut updated = loaded;
|
||||
updated.chain_length = 2;
|
||||
let old_hash = updated.last_hash.clone();
|
||||
pool_clone.update_session(&updated, &old_hash).unwrap();
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
let stats = pool_arc.stats().unwrap();
|
||||
assert_eq!(stats.sessions, 10);
|
||||
|
||||
std::mem::drop(pool_arc);
|
||||
let _ = std::fs::remove_file(db_path);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::config::Config;
|
||||
use shared::protocol::EntropyData;
|
||||
|
||||
/// Validates the browser mouse cursor interaction path for bot/automation detection.
|
||||
///
|
||||
/// Evaluates mouse velocity and distance features, checks the total distance traversed,
|
||||
/// checks for cursor pauses (low movement over high time diff), and enforces average cursor speeds.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - The client-supplied interaction entropy events.
|
||||
/// * `config` - The server configuration boundaries.
|
||||
pub fn validate_mouse(
|
||||
data: &EntropyData,
|
||||
config: &Config,
|
||||
|
||||
@@ -4,6 +4,13 @@ use shared::{
|
||||
vm_extensions::{self, ExecutionTrace, MutationError, MutationOrder},
|
||||
};
|
||||
|
||||
/// Generates a randomized VM opcode instruction program within a length range.
|
||||
///
|
||||
/// Builds a program of mathematical and stack ops (e.g. literals, ADD, SUB, XOR, HASH)
|
||||
/// with dynamic depth checking to ensure valid stacks and prevent out of bounds execution.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `len_range` - The inclusive range of instruction counts to generate.
|
||||
pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Vec<u8> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let count = rng.gen_range(len_range);
|
||||
@@ -47,6 +54,11 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
|
||||
ops
|
||||
}
|
||||
|
||||
/// Executes a raw VM mutation program bytecode slice against a `GeneState`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The mutable gene state to mutate.
|
||||
/// * `program` - The raw VM instruction program.
|
||||
#[allow(dead_code)]
|
||||
pub fn execute_mutation_program(
|
||||
state: &mut GeneState,
|
||||
@@ -55,6 +67,11 @@ pub fn execute_mutation_program(
|
||||
vm_extensions::execute_program(state, program)
|
||||
}
|
||||
|
||||
/// Executes a `MutationOrder` program against a `GeneState`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The mutable gene state.
|
||||
/// * `order` - The mutation order.
|
||||
#[allow(dead_code)]
|
||||
pub fn execute_mutation_order(
|
||||
state: &mut GeneState,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
version = "0.6.0"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -12,3 +12,6 @@ base64 = "0.22"
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
@@ -10,3 +10,4 @@ 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;
|
||||
pub const MAX_STACK_DEPTH: usize = 64;
|
||||
@@ -55,6 +55,10 @@ impl std::fmt::Display for GeneError {
|
||||
|
||||
impl std::error::Error for GeneError {}
|
||||
|
||||
/// Creates a new, blank `GeneState` with the specified gene buffer size.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `gene_size` - The length of the gene byte buffer. Must be within `1..=MAX_GENE_SIZE`.
|
||||
pub fn new_state(gene_size: usize) -> Result<GeneState, GeneError> {
|
||||
if !(1..=MAX_GENE_SIZE).contains(&gene_size) {
|
||||
return Err(GeneError::InvalidGeneSize { size: gene_size });
|
||||
@@ -65,6 +69,7 @@ pub fn new_state(gene_size: usize) -> Result<GeneState, GeneError> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new `GeneState` with the default gene buffer size (`DEFAULT_GENE_SIZE`).
|
||||
pub fn default_state() -> GeneState {
|
||||
GeneState {
|
||||
gene: vec![0; DEFAULT_GENE_SIZE],
|
||||
@@ -72,6 +77,10 @@ pub fn default_state() -> GeneState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the structural invariants of the given `GeneState`.
|
||||
///
|
||||
/// Ensures the gene size is within valid bounds and the environment records are
|
||||
/// properly sorted, non-empty, and free of duplicates.
|
||||
pub fn validate_state(state: &GeneState) -> Result<(), GeneError> {
|
||||
if !(1..=MAX_GENE_SIZE).contains(&state.gene.len()) {
|
||||
return Err(GeneError::InvalidGeneSize {
|
||||
@@ -81,6 +90,13 @@ pub fn validate_state(state: &GeneState) -> Result<(), GeneError> {
|
||||
validate_environment(&state.environment)
|
||||
}
|
||||
|
||||
/// Retrieves the quantity associated with a specific environment symbol.
|
||||
///
|
||||
/// Performs a binary search over the sorted environment records. Returns 0 if the symbol is missing.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The gene state to query.
|
||||
/// * `symbol` - The 16-bit key to search for.
|
||||
pub fn get_env_quantity(state: &GeneState, symbol: u16) -> u32 {
|
||||
match state
|
||||
.environment
|
||||
@@ -91,6 +107,14 @@ pub fn get_env_quantity(state: &GeneState, symbol: u16) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the quantity of an environment symbol in a `GeneState`.
|
||||
///
|
||||
/// If quantity is 0, the record is removed. The environment is kept sorted alphabetically by symbol.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The mutable gene state to update.
|
||||
/// * `symbol` - The 16-bit key.
|
||||
/// * `quantity` - The quantity to assign.
|
||||
pub fn set_env_quantity(
|
||||
state: &mut GeneState,
|
||||
symbol: u16,
|
||||
@@ -125,6 +149,12 @@ pub fn set_env_quantity(
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a quantity to an environment symbol with saturating arithmetic.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The mutable gene state.
|
||||
/// * `symbol` - The 16-bit key.
|
||||
/// * `quantity` - The quantity to add.
|
||||
pub fn add_env_quantity(
|
||||
state: &mut GeneState,
|
||||
symbol: u16,
|
||||
@@ -136,6 +166,14 @@ pub fn add_env_quantity(
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
/// Subtracts a quantity from an environment symbol with saturating arithmetic.
|
||||
///
|
||||
/// If the resulting quantity drops to 0, the symbol is removed.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The mutable gene state.
|
||||
/// * `symbol` - The 16-bit key.
|
||||
/// * `quantity` - The quantity to subtract.
|
||||
pub fn sub_env_quantity(
|
||||
state: &mut GeneState,
|
||||
symbol: u16,
|
||||
@@ -147,6 +185,12 @@ pub fn sub_env_quantity(
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
/// Encodes the environment records list into a compact byte slice.
|
||||
///
|
||||
/// Each record is written as a little-endian `u16` symbol followed by a little-endian `u32` quantity.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `records` - The sorted environment records.
|
||||
pub fn encode_environment(records: &[EnvironmentRecord]) -> Result<Vec<u8>, GeneError> {
|
||||
validate_environment(records)?;
|
||||
let mut out = Vec::with_capacity(records.len() * 6);
|
||||
@@ -157,6 +201,12 @@ pub fn encode_environment(records: &[EnvironmentRecord]) -> Result<Vec<u8>, Gene
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Decodes environment records from a byte slice.
|
||||
///
|
||||
/// Validates that the length is a multiple of 6 and that records conform to sorting and quantity invariants.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `blob` - The serialized byte slice.
|
||||
pub fn decode_environment(blob: &[u8]) -> Result<Vec<EnvironmentRecord>, GeneError> {
|
||||
if !blob.len().is_multiple_of(6) {
|
||||
return Err(GeneError::EnvironmentBlobLengthInvalid { len: blob.len() });
|
||||
@@ -180,6 +230,9 @@ pub fn decode_environment(blob: &[u8]) -> Result<Vec<EnvironmentRecord>, GeneErr
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Computes the raw Blake3 cryptographic commitment of the `GeneState`.
|
||||
///
|
||||
/// Includes gene length, gene buffer, environment record count, and individual record key/values.
|
||||
pub fn commitment(state: &GeneState) -> [u8; 32] {
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(b"chronoseal/gene/v1");
|
||||
@@ -193,10 +246,19 @@ pub fn commitment(state: &GeneState) -> [u8; 32] {
|
||||
*h.finalize().as_bytes()
|
||||
}
|
||||
|
||||
/// Computes the hex-encoded cryptographic commitment of the `GeneState`.
|
||||
pub fn commitment_hex(state: &GeneState) -> String {
|
||||
hex::encode(commitment(state))
|
||||
}
|
||||
|
||||
/// Computes a context-bound Blake3 cryptographic commitment of the `GeneState`.
|
||||
///
|
||||
/// Integrates `session_id` and the current `step` index into the hash to bind the commitment.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The gene state.
|
||||
/// * `session_id` - The client session ID.
|
||||
/// * `step` - The mutation step index.
|
||||
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");
|
||||
@@ -206,10 +268,17 @@ pub fn commitment_with_context(state: &GeneState, session_id: &str, step: u64) -
|
||||
*h.finalize().as_bytes()
|
||||
}
|
||||
|
||||
/// Computes a context-bound, hex-encoded Blake3 cryptographic commitment of the `GeneState`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The gene state.
|
||||
/// * `session_id` - The client session ID.
|
||||
/// * `step` - The mutation step index.
|
||||
pub fn commitment_hex_with_context(state: &GeneState, session_id: &str, step: u64) -> String {
|
||||
hex::encode(commitment_with_context(state, session_id, step))
|
||||
}
|
||||
|
||||
/// Helper function to validate sorting, uniqueness, and non-zero properties of environment records.
|
||||
fn validate_environment(records: &[EnvironmentRecord]) -> Result<(), GeneError> {
|
||||
if records.len() > MAX_ENV_RECORDS {
|
||||
return Err(GeneError::TooManyEnvironmentRecords { len: records.len() });
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
use crate::protocol::{EntropyData, StackState};
|
||||
use blake3::Hasher;
|
||||
|
||||
/// Initial hash for a brand-new session: Blake3(session_id || pub_key || salt)
|
||||
/// Computes the initial hash for a brand-new attestation session.
|
||||
///
|
||||
/// The hash is constructed as:
|
||||
/// `Blake3(session_id || pub_key || salt)`
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `session_id` - The unique hex-encoded identifier for the session.
|
||||
/// * `pub_key` - The client's Ed25519 public key.
|
||||
/// * `salt` - The initial server-issued salt.
|
||||
pub fn initial_hash(session_id: &str, pub_key: &[u8], salt: &[u8]) -> Vec<u8> {
|
||||
let mut h = Hasher::new();
|
||||
h.update(session_id.as_bytes());
|
||||
@@ -10,7 +18,18 @@ pub fn initial_hash(session_id: &str, pub_key: &[u8], salt: &[u8]) -> Vec<u8> {
|
||||
h.finalize().as_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Next hash in the chain: Blake3 with the salt mixed in (no keyed mode needed)
|
||||
/// Computes the next hash in the Blake3 attestation chain.
|
||||
///
|
||||
/// This mixes in the previous hash head, the client timestamp, the serialized entropy data,
|
||||
/// the VM stack state, and the server-issued salt. Uses `serde_json::to_vec` to avoid
|
||||
/// intermediate heap string allocations and UTF-8 verification checks.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `prev_hash` - The previous hash-chain head.
|
||||
/// * `timestamp` - The client-supplied heartbeat timestamp.
|
||||
/// * `entropy` - The collected browser interaction entropy.
|
||||
/// * `stack` - The final VM stack state after running the opcode program.
|
||||
/// * `salt` - The server-issued salt for rotation.
|
||||
pub fn next_chain_hash(
|
||||
prev_hash: &[u8],
|
||||
timestamp: u64,
|
||||
@@ -18,14 +37,13 @@ pub fn next_chain_hash(
|
||||
stack: &StackState,
|
||||
salt: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let entropy_json = serde_json::to_string(entropy).unwrap();
|
||||
let stack_json = serde_json::to_string(stack).unwrap();
|
||||
let entropy_bytes = serde_json::to_vec(entropy).unwrap_or_default();
|
||||
let stack_bytes = serde_json::to_vec(stack).unwrap_or_default();
|
||||
|
||||
let entropy_hash = blake3::hash(entropy_json.as_bytes());
|
||||
let stack_hash = blake3::hash(stack_json.as_bytes());
|
||||
let entropy_hash = blake3::hash(&entropy_bytes);
|
||||
let stack_hash = blake3::hash(&stack_bytes);
|
||||
|
||||
let mut h = Hasher::new();
|
||||
// Mix the salt into the hash state
|
||||
h.update(salt);
|
||||
h.update(prev_hash);
|
||||
h.update(×tamp.to_le_bytes());
|
||||
@@ -34,7 +52,12 @@ pub fn next_chain_hash(
|
||||
h.finalize().as_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Hash of all stack items for VM HASH opcode
|
||||
/// Computes a 32-bit FNV-like Blake3 hash of all stack elements.
|
||||
///
|
||||
/// This is used by the VM `HASH` opcode to fold the current stack state into a single value.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stack` - The list of u32 stack elements to hash.
|
||||
pub fn hash_stack(stack: &[u32]) -> u32 {
|
||||
let data: Vec<u8> = stack.iter().flat_map(|x| x.to_le_bytes()).collect();
|
||||
let hash = blake3::hash(&data);
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod constants;
|
||||
pub mod gene;
|
||||
pub mod hashing;
|
||||
pub mod protocol;
|
||||
pub mod vm;
|
||||
pub mod vm_extensions;
|
||||
@@ -1,73 +1,118 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Request payload sent by the client to initialize a new attestation session.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct InitRequest {
|
||||
/// Hex-encoded 32-byte Ed25519 public verifying key generated by the client.
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
/// Response payload returned by the server upon successful session initialization.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InitResponse {
|
||||
/// The unique hex-encoded session identifier.
|
||||
pub session_id: String,
|
||||
/// The initial server-issued salt to be mixed in the first heartbeat's hash.
|
||||
pub salt: String,
|
||||
/// Base64-encoded initial VM program for client stack execution.
|
||||
pub opcodes_b64: String,
|
||||
/// The computed initial hash of the attestation chain.
|
||||
pub initial_hash: String,
|
||||
/// Timestamp in milliseconds indicating when the session expires.
|
||||
pub expires_at: u64,
|
||||
/// Minimum time in milliseconds allowed between subsequent heartbeats.
|
||||
pub heartbeat_min_interval_ms: u64,
|
||||
/// Maximum time in milliseconds allowed between subsequent heartbeats.
|
||||
pub heartbeat_max_interval_ms: u64,
|
||||
/// Size of the synthetic gene byte buffer.
|
||||
pub gene_size: u32,
|
||||
/// The current mutation step index (starts at 1).
|
||||
pub mutation_step: u64,
|
||||
/// Base64-encoded initial gene mutation program.
|
||||
pub mutation_order_b64: String,
|
||||
/// The number of mutation rounds configured on the server.
|
||||
pub mutation_rounds: u8,
|
||||
}
|
||||
|
||||
/// Heartbeat request payload submitted periodically by the client to prove session continuity.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct HeartbeatRequest {
|
||||
/// The session identifier.
|
||||
pub session_id: String,
|
||||
/// The expected hash from the previous heartbeat/initialization step.
|
||||
pub prev_hash: String,
|
||||
/// The client's current system timestamp in milliseconds.
|
||||
pub timestamp: u64,
|
||||
/// The collected client entropy data (such as mouse events).
|
||||
pub entropy_data: EntropyData,
|
||||
/// The final execution state of the client's VM stack program.
|
||||
pub stack_state: StackState,
|
||||
/// The client's browser hardware and layout fingerprint.
|
||||
pub fingerprint: Fingerprint,
|
||||
/// The mutation step index corresponding to the pending mutation.
|
||||
pub mutation_step: u64,
|
||||
/// Hex-encoded commitment of the mutated gene state.
|
||||
pub gene_commitment: String,
|
||||
/// Ed25519 signature of the canonical JSON-serialized payload.
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// Response payload returned by the server for heartbeat submissions.
|
||||
///
|
||||
/// In case of silent rejection, all fields except `status` are omitted.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HeartbeatResponse {
|
||||
/// Attestation status, typically "ok" even on silent failures.
|
||||
pub status: String,
|
||||
/// The next server-issued salt for hash chain progression.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_salt: Option<String>,
|
||||
/// The next expected mutation step index.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_mutation_step: Option<u64>,
|
||||
/// Base64-encoded next mutation program for client gene progression.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_mutation_order_b64: Option<String>,
|
||||
}
|
||||
|
||||
/// Client browser fingerprint metadata used for basic sanity checks.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Fingerprint {
|
||||
/// Aspect ratio of the client screen.
|
||||
#[serde(rename = "aspectRatio")]
|
||||
pub aspect_ratio: String,
|
||||
/// Device pixel ratio of the screen.
|
||||
#[serde(rename = "devicePixelRatio")]
|
||||
pub device_pixel_ratio: String,
|
||||
/// Number of logical processor cores available.
|
||||
#[serde(rename = "hardwareConcurrency")]
|
||||
pub hardware_concurrency: u32,
|
||||
}
|
||||
|
||||
/// Wrapper for browser-side entropy collection.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct EntropyData {
|
||||
/// A chronological list of mouse movement events.
|
||||
pub events: Vec<MouseEvent>,
|
||||
}
|
||||
|
||||
/// Information about a single mouse movement interaction.
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct MouseEvent {
|
||||
/// Absolute horizontal coordinate of the cursor.
|
||||
pub x: f64,
|
||||
/// Absolute vertical coordinate of the cursor.
|
||||
pub y: f64,
|
||||
/// Relative timestamp in milliseconds of the event occurrence.
|
||||
#[serde(rename = "t")]
|
||||
pub timestamp_ms: f64,
|
||||
}
|
||||
|
||||
/// The state of the VM stack machine after executing a program.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StackState {
|
||||
/// The elements remaining on the stack.
|
||||
pub stack: Vec<u32>,
|
||||
/// The final instruction pointer location at program completion or termination.
|
||||
pub ip: u16,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use crate::protocol::StackState;
|
||||
|
||||
/// Executes a raw VM mathematical instruction program bytecode slice.
|
||||
///
|
||||
/// This implements the mathematical stack machine interpreter used by the client
|
||||
/// to generate the attestation stack state.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `program` - The raw VM instruction program bytecode.
|
||||
pub fn execute(program: &[u8]) -> StackState {
|
||||
let mut stack: Vec<u32> = Vec::new();
|
||||
let mut ip: usize = 0;
|
||||
while ip < program.len() {
|
||||
let op = program[ip];
|
||||
ip += 1;
|
||||
match op {
|
||||
0x00 => {
|
||||
if ip + 4 > program.len() {
|
||||
break;
|
||||
}
|
||||
let val = u32::from_le_bytes([
|
||||
program[ip],
|
||||
program[ip + 1],
|
||||
program[ip + 2],
|
||||
program[ip + 3],
|
||||
]);
|
||||
ip += 4;
|
||||
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||
break;
|
||||
}
|
||||
stack.push(val);
|
||||
}
|
||||
0x01..=0x07 => {
|
||||
if stack.len() < 2 {
|
||||
break;
|
||||
}
|
||||
let b = stack.pop().unwrap();
|
||||
let a = stack.pop().unwrap();
|
||||
let r = match op {
|
||||
0x01 => a.wrapping_add(b),
|
||||
0x02 => a.wrapping_sub(b),
|
||||
0x03 => a.wrapping_mul(b),
|
||||
0x04 => a ^ b,
|
||||
0x05 => a & b,
|
||||
0x06 => a | b,
|
||||
0x07 => a.rotate_left(b % 32),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||
break;
|
||||
}
|
||||
stack.push(r);
|
||||
}
|
||||
0x08 => {
|
||||
if stack.is_empty() {
|
||||
break;
|
||||
}
|
||||
let a = stack.pop().unwrap();
|
||||
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||
break;
|
||||
}
|
||||
stack.push(!a);
|
||||
}
|
||||
0x09 => {
|
||||
let r = crate::hashing::hash_stack(&stack);
|
||||
stack.clear();
|
||||
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||
break;
|
||||
}
|
||||
stack.push(r);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
StackState {
|
||||
stack,
|
||||
ip: ip as u16,
|
||||
}
|
||||
}
|
||||
@@ -88,10 +88,18 @@ impl From<GeneError> for MutationError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes a `MutationOrder` into standard Base64 representation of its bytecode.
|
||||
pub fn encode_order_b64(order: &MutationOrder) -> String {
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &order.program)
|
||||
}
|
||||
|
||||
/// Decodes a `MutationOrder` from its Base64 representation.
|
||||
///
|
||||
/// Validates that the decoded program size does not exceed the allowed maximum budget size.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `step` - The step index associated with this mutation order.
|
||||
/// * `b64` - The Base64 string containing the raw bytecode.
|
||||
pub fn decode_order_b64(step: u64, b64: &str) -> Result<MutationOrder, MutationError> {
|
||||
let program = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64)
|
||||
.map_err(MutationError::Base64)?;
|
||||
@@ -101,11 +109,27 @@ pub fn decode_order_b64(step: u64, b64: &str) -> Result<MutationOrder, MutationE
|
||||
Ok(MutationOrder { step, program })
|
||||
}
|
||||
|
||||
/// Generates a randomized `MutationOrder` program for a given step and gene size.
|
||||
///
|
||||
/// Uses thread-local random number generator.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `step` - The step index.
|
||||
/// * `gene_size` - The length of the gene byte buffer.
|
||||
pub fn generate_order(step: u64, gene_size: usize) -> MutationOrder {
|
||||
let mut rng = rand::thread_rng();
|
||||
generate_order_with_rng(&mut rng, step, gene_size)
|
||||
}
|
||||
|
||||
/// Generates a randomized `MutationOrder` program using a specific custom RNG.
|
||||
///
|
||||
/// Builds a program containing between 20 and 36 mutation instructions (e.g. loads, point changes,
|
||||
/// insertions, deletions, env modifications) and ensures a minimum number of finalize hash steps are included.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - The random number generator.
|
||||
/// * `step` - The step index.
|
||||
/// * `gene_size` - The length of the gene byte buffer.
|
||||
pub fn generate_order_with_rng<R: Rng + ?Sized>(
|
||||
rng: &mut R,
|
||||
step: u64,
|
||||
@@ -213,10 +237,12 @@ pub fn generate_order_with_rng<R: Rng + ?Sized>(
|
||||
MutationOrder { step, program }
|
||||
}
|
||||
|
||||
/// Clones the `GeneState` and executes the mutation program for `DEFAULT_MUTATION_ROUNDS`.
|
||||
pub fn apply_program_clone(state: &GeneState, program: &[u8]) -> Result<GeneState, MutationError> {
|
||||
apply_program_clone_with_rounds(state, program, DEFAULT_MUTATION_ROUNDS)
|
||||
}
|
||||
|
||||
/// Clones the `GeneState` and executes the mutation program for a specific number of rounds.
|
||||
pub fn apply_program_clone_with_rounds(
|
||||
state: &GeneState,
|
||||
program: &[u8],
|
||||
@@ -227,6 +253,7 @@ pub fn apply_program_clone_with_rounds(
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
/// Executes the mutation program on the mutable `GeneState` reference for a specific number of rounds.
|
||||
pub fn apply_program_with_rounds(
|
||||
state: &mut GeneState,
|
||||
program: &[u8],
|
||||
@@ -236,11 +263,21 @@ pub fn apply_program_with_rounds(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Executes the mutation program on the mutable `GeneState` reference for `DEFAULT_MUTATION_ROUNDS`.
|
||||
pub fn apply_program(state: &mut GeneState, program: &[u8]) -> Result<(), MutationError> {
|
||||
let _ = execute_program_with_rounds(state, program, DEFAULT_MUTATION_ROUNDS)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Executes the mutation program on the mutable `GeneState` reference for multiple rounds.
|
||||
///
|
||||
/// Implements a soft instruction cost-budget cap check to prevent hostile/inefficient
|
||||
/// programs from lagging the host server thread or client runtime.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The mutable gene state buffer.
|
||||
/// * `program` - The raw bytecode sequence.
|
||||
/// * `rounds` - The requested number of execution rounds.
|
||||
pub fn execute_program_with_rounds(
|
||||
state: &mut GeneState,
|
||||
program: &[u8],
|
||||
@@ -317,6 +354,13 @@ fn estimate_program_cost(program: &[u8]) -> usize {
|
||||
cost.max(1)
|
||||
}
|
||||
|
||||
/// Executes the VM mutation program on the mutable `GeneState` reference.
|
||||
///
|
||||
/// This interprets VM mutation opcodes to modify the gene byte array and environment records.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - The mutable gene state to mutate.
|
||||
/// * `program` - The raw instruction bytecode slice.
|
||||
pub fn execute_program(
|
||||
state: &mut GeneState,
|
||||
program: &[u8],
|
||||
@@ -835,4 +879,25 @@ mod tests {
|
||||
"mutation execution too slow: {elapsed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vm_instruction_budget_soft_cap() {
|
||||
let state = new_state(8).unwrap();
|
||||
// Construct a program with 130 OP_FINALIZE_GENE_HASH instructions.
|
||||
// HASH has HASH_OPCODE_INSTRUCTION_COST = 16.
|
||||
// Total cost will be 130 * 16 = 2080, which exceeds MAX_MUTATION_INSTRUCTION_BUDGET (2048).
|
||||
let program = vec![OP_FINALIZE_GENE_HASH; 130];
|
||||
let cost = estimate_program_cost(&program);
|
||||
assert!(cost >= 2080);
|
||||
|
||||
// Assert that the max allowed rounds is calculated as 1 since cost > budget.
|
||||
let expected_rounds = std::cmp::max(1, MAX_MUTATION_INSTRUCTION_BUDGET / cost);
|
||||
assert_eq!(expected_rounds, 1);
|
||||
|
||||
// Execute the program with a requested 10 rounds.
|
||||
// The runtime should execute it successfully without panic, while applying the round limitation.
|
||||
let mut test_state = state.clone();
|
||||
let trace = execute_program_with_rounds(&mut test_state, &program, 10).unwrap();
|
||||
assert_eq!(trace.final_ip, program.len());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_vm_execute_never_panics(ref program in any::<Vec<u8>>()) {
|
||||
// VM execution should be totally robust and never panic on any random input stream.
|
||||
let state = shared::vm::execute(program);
|
||||
// The instruction pointer (ip) should not exceed the program length
|
||||
assert!(state.ip as usize <= program.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gene_environment_roundtrip_never_panics(ref data in any::<Vec<u8>>()) {
|
||||
// Try to decode random bytes. It should either succeed or fail gracefully, never panic.
|
||||
let _ = shared::gene::decode_environment(data);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chronoseal-wasm"
|
||||
version = "0.6.0"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -51,8 +51,14 @@ pub fn compute_next_hash(
|
||||
) -> String {
|
||||
let prev = hex::decode(prev_hash_hex).unwrap_or_default();
|
||||
let salt = hex::decode(salt_hex).unwrap_or_default();
|
||||
let entropy = serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json).unwrap();
|
||||
let stack = serde_json::from_str::<shared::protocol::StackState>(stack_state_json).unwrap();
|
||||
let entropy = match serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
let stack = match serde_json::from_str::<shared::protocol::StackState>(stack_state_json) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
|
||||
hex::encode(new)
|
||||
}
|
||||
@@ -4,70 +4,19 @@ use wasm_bindgen::prelude::*;
|
||||
#[wasm_bindgen]
|
||||
pub fn run_program(program_b64: &str) -> JsValue {
|
||||
use base64::Engine;
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(program_b64)
|
||||
.unwrap();
|
||||
let bytes = match base64::engine::general_purpose::STANDARD.decode(program_b64) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return JsValue::NULL,
|
||||
};
|
||||
let state = execute(&bytes);
|
||||
serde_wasm_bindgen::to_value(&state).unwrap()
|
||||
match serde_wasm_bindgen::to_value(&state) {
|
||||
Ok(v) => v,
|
||||
Err(_) => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(program: &[u8]) -> StackState {
|
||||
let mut stack: Vec<u32> = Vec::new();
|
||||
let mut ip: usize = 0;
|
||||
while ip < program.len() {
|
||||
let op = program[ip];
|
||||
ip += 1;
|
||||
match op {
|
||||
0x00 => {
|
||||
if ip + 4 > program.len() {
|
||||
break;
|
||||
}
|
||||
let val = u32::from_le_bytes([
|
||||
program[ip],
|
||||
program[ip + 1],
|
||||
program[ip + 2],
|
||||
program[ip + 3],
|
||||
]);
|
||||
ip += 4;
|
||||
stack.push(val);
|
||||
}
|
||||
0x01..=0x07 => {
|
||||
if stack.len() < 2 {
|
||||
break;
|
||||
}
|
||||
let b = stack.pop().unwrap();
|
||||
let a = stack.pop().unwrap();
|
||||
let r = match op {
|
||||
0x01 => a.wrapping_add(b),
|
||||
0x02 => a.wrapping_sub(b),
|
||||
0x03 => a.wrapping_mul(b),
|
||||
0x04 => a ^ b,
|
||||
0x05 => a & b,
|
||||
0x06 => a | b,
|
||||
0x07 => a.rotate_left(b % 32),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
stack.push(r);
|
||||
}
|
||||
0x08 => {
|
||||
if stack.is_empty() {
|
||||
break;
|
||||
}
|
||||
let a = stack.pop().unwrap();
|
||||
stack.push(!a);
|
||||
}
|
||||
0x09 => {
|
||||
let r = shared::hashing::hash_stack(&stack);
|
||||
stack.clear();
|
||||
stack.push(r);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
StackState {
|
||||
stack,
|
||||
ip: ip as u16,
|
||||
}
|
||||
shared::vm::execute(program)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>API Reference | ChronoSeal Documentation</title>
|
||||
<meta name="description" content="ChronoSeal HTTP API Reference, documenting endpoints, JSON request-response shapes, and WASM exports.">
|
||||
<link rel="stylesheet" href="css/chronoseal.css">
|
||||
<link rel="stylesheet" href="css/docs.css">
|
||||
<link rel="stylesheet" href="css/print.css" media="print">
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
|
||||
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Floating Background Spheres -->
|
||||
<div class="bg-animation">
|
||||
<div class="gradient-sphere sphere-1"></div>
|
||||
<div class="gradient-sphere sphere-2"></div>
|
||||
<div class="gradient-sphere sphere-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Header / Navbar -->
|
||||
<nav class="navbar" id="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="index.html" class="logo">
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
|
||||
<span>ChronoSeal</span>
|
||||
</a>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="philosophy.html">Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
<div class="nav-dropdown">
|
||||
<a href="#" class="nav-link">More <small>▼</small></a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a href="api.html" class="active">API Reference</a>
|
||||
<a href="deployment.html">Deployment Guide</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
<a href="testing.html">Testing Strategy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="threat-model.html">Threat Model</a>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</button>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Doc Page Shell -->
|
||||
<div class="doc-page">
|
||||
<div class="doc-layout">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="doc-sidebar">
|
||||
<button class="doc-sidebar-toggle">
|
||||
<i class="fas fa-bars"></i> Sidebar Menu
|
||||
</button>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Core Concepts</div>
|
||||
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
|
||||
<a href="architecture.html" class="doc-nav-item">Architecture Overview</a>
|
||||
<a href="comparison.html" class="doc-nav-item">ChronoSeal vs Others</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Protocol & API</div>
|
||||
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
|
||||
<a href="api.html" class="doc-nav-item active">API Reference</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Operations & Security</div>
|
||||
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
|
||||
<a href="security.html" class="doc-nav-item">Security Policy</a>
|
||||
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
|
||||
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Lifecycle & Dev</div>
|
||||
<a href="deployment.html" class="doc-nav-item">Deployment Guide</a>
|
||||
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
|
||||
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="doc-main">
|
||||
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="index.html">Home</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="#">Protocol & API</a>
|
||||
<span class="sep">/</span>
|
||||
<span>API Reference</span>
|
||||
</nav>
|
||||
|
||||
<article class="doc-content">
|
||||
<h1>ChronoSeal API Reference</h1>
|
||||
<p class="doc-subtitle">ChronoSeal exposes a lightweight HTTP API for session handshakes, heartbeat attestation verification, metrics, and statistics.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Endpoint Summary</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Path</th>
|
||||
<th>Core Purpose</th>
|
||||
<th>Content Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>POST</code></td>
|
||||
<td><code>/init</code></td>
|
||||
<td>Create a new attestation session</td>
|
||||
<td><code>application/json</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST</code></td>
|
||||
<td><code>/hb</code></td>
|
||||
<td>Submit and verify a signed heartbeat</td>
|
||||
<td><code>application/json</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/health</code></td>
|
||||
<td>Check daemon operational health</td>
|
||||
<td><code>application/json</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/stats</code></td>
|
||||
<td>Get runtime database statistics</td>
|
||||
<td><code>application/json</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/metrics</code></td>
|
||||
<td>Prometheus-compatible scraping metrics</td>
|
||||
<td><code>text/plain</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET</code></td>
|
||||
<td><code>/</code></td>
|
||||
<td>Serve static integration files</td>
|
||||
<td>HTML / JS / WASM</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Data Encoding Formats</h2>
|
||||
<ul>
|
||||
<li><strong>Keys & Signatures:</strong> Ed25519 public keys represent 64 hex characters (32 bytes). Signatures represent 128 hex characters (64 bytes).</li>
|
||||
<li><strong>Hashes:</strong> Blake3 digests represent 64 hex characters (32 bytes).</li>
|
||||
<li><strong>Salts:</strong> Random seeds represented as 32 hex characters (16 bytes).</li>
|
||||
<li><strong>Timestamps:</strong> Integer epoch milliseconds.</li>
|
||||
<li><strong>Programs:</strong> Base64-encoded strings (representing VM opcodes or mutation steps).</li>
|
||||
</ul>
|
||||
|
||||
<h2><code>POST /init</code></h2>
|
||||
<p>Registers the browser public key and initiates the session tracker.</p>
|
||||
|
||||
<h3>Request Payload</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">JSON Request</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>{
|
||||
"public_key": "24a1b0cd982fecba45...64 hex chars"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Successful Response (200 OK)</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">JSON Response</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>{
|
||||
"session_id": "8902abc345def678...64 hex chars",
|
||||
"salt": "f51278ba...32 hex chars",
|
||||
"opcodes_b64": "AQAFAwEG...",
|
||||
"initial_hash": "23ab89c0...64 hex chars",
|
||||
"expires_at": 1782390482000,
|
||||
"heartbeat_min_interval_ms": 12000,
|
||||
"heartbeat_max_interval_ms": 25000,
|
||||
"gene_size": 512,
|
||||
"mutation_step": 1,
|
||||
"mutation_order_b64": "YWJjZGVm..."
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h2><code>POST /hb</code></h2>
|
||||
<p>Verifies client compliance for the current step and rolls over session parameters.</p>
|
||||
|
||||
<h3>Request Payload</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">JSON Request</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>{
|
||||
"session_id": "8902abc345def678...64 hex chars",
|
||||
"prev_hash": "23ab89c0...64 hex chars",
|
||||
"timestamp": 1782390494000,
|
||||
"entropy_data": {
|
||||
"events": [
|
||||
{ "x": 124.5, "y": 308.2, "t": 120.4 }
|
||||
]
|
||||
},
|
||||
"stack_state": {
|
||||
"stack": [108429, 3902],
|
||||
"ip": 12
|
||||
},
|
||||
"fingerprint": {
|
||||
"aspectRatio": "1.7777777778",
|
||||
"devicePixelRatio": "2",
|
||||
"hardwareConcurrency": 8
|
||||
},
|
||||
"mutation_step": 1,
|
||||
"gene_commitment": "56ab12cd...64 hex chars",
|
||||
"signature": "ab0921cd56ef...128 hex chars"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Accepted Response</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">JSON Response</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>{
|
||||
"status": "ok",
|
||||
"next_salt": "78abef90...32 hex chars",
|
||||
"next_mutation_step": 2,
|
||||
"next_mutation_order_b64": "cGFzc3dvcmQ..."
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Rejected Response (Silent Rejection)</h3>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">JSON Response</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>{
|
||||
"status": "ok"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="doc-alert doc-alert--warn">
|
||||
<div class="doc-alert-icon">⚠️</div>
|
||||
<div class="doc-alert-body">
|
||||
<strong>Important:</strong> Heartbeat rejections return <code>200 OK</code> with <code>status: ok</code> but OMIT next-state fields. Clients must check for the presence of <code>next_salt</code> before advancing local states.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Canonical Signing Payload</h2>
|
||||
<p>The Ed25519 signature covers a canonical JSON string. The server orders the keys alphabetically using camelCase notation. Ensure serialization matches this format precisely:</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">JSON Payload</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>{
|
||||
"entropyData": { "events": [{ "t": 120.4, "x": 124.5, "y": 308.2 }] },
|
||||
"fingerprint": { "aspectRatio": "1.7777777778", "devicePixelRatio": "2", "hardwareConcurrency": 8 },
|
||||
"geneCommitment": "56ab12cd...",
|
||||
"mutationStep": 1,
|
||||
"prevHash": "23ab89c0...",
|
||||
"sessionId": "8902abc3...",
|
||||
"stackState": { "ip": 12, "stack": [108429, 3902] },
|
||||
"timestamp": 1782390494000
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Operational Probes</h2>
|
||||
|
||||
<h3><code>GET /health</code></h3>
|
||||
<div class="code-block">
|
||||
<pre><code>{
|
||||
"status": "healthy"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3><code>GET /stats</code></h3>
|
||||
<div class="code-block">
|
||||
<pre><code>{
|
||||
"sessions": 412,
|
||||
"expired_sessions": 3,
|
||||
"max_chain_length": 84
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3><code>GET /metrics</code></h3>
|
||||
<div class="code-block">
|
||||
<pre><code># HELP chronoseal_sessions Active ChronoSeal sessions
|
||||
# TYPE chronoseal_sessions gauge
|
||||
chronoseal_sessions 412
|
||||
# HELP chronoseal_expired_sessions Expired sessions not yet removed
|
||||
# TYPE chronoseal_expired_sessions gauge
|
||||
chronoseal_expired_sessions 3
|
||||
# HELP chronoseal_max_chain_length Maximum heartbeat chain length
|
||||
# TYPE chronoseal_max_chain_length gauge
|
||||
chronoseal_max_chain_length 84</code></pre>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Pager -->
|
||||
<nav class="doc-pager" aria-label="Doc navigation Pager">
|
||||
<a href="protocol.html" class="doc-pager-link">
|
||||
<div class="doc-pager-label">Previous</div>
|
||||
<div class="doc-pager-title">Protocol Specification</div>
|
||||
</a>
|
||||
<a href="threat-model.html" class="doc-pager-link doc-pager-link--next">
|
||||
<div class="doc-pager-label">Next</div>
|
||||
<div class="doc-pager-title">Threat Model</div>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="doc-meta">
|
||||
Last Updated: June 2026 (v1.0.2)
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
|
||||
</h4>
|
||||
<p>Next-generation browser attestation daemon built for the modern web.</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Resources</h4>
|
||||
<a href="philosophy.html">Design Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Community</h4>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
|
||||
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
|
||||
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Legal & Security</h4>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Search Overlay -->
|
||||
<div class="search-overlay">
|
||||
<div class="search-box">
|
||||
<div class="search-input-wrap">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
|
||||
<span class="search-kbd">/</span>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to top -->
|
||||
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
|
||||
|
||||
<!-- JS Scripts -->
|
||||
<script src="js/search.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,300 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Architecture Overview | ChronoSeal Documentation</title>
|
||||
<meta name="description" content="ChronoSeal system architecture, state models, components, validation pipelines, and trust boundaries.">
|
||||
<link rel="stylesheet" href="css/chronoseal.css">
|
||||
<link rel="stylesheet" href="css/docs.css">
|
||||
<link rel="stylesheet" href="css/print.css" media="print">
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
|
||||
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Floating Background Spheres -->
|
||||
<div class="bg-animation">
|
||||
<div class="gradient-sphere sphere-1"></div>
|
||||
<div class="gradient-sphere sphere-2"></div>
|
||||
<div class="gradient-sphere sphere-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Header / Navbar -->
|
||||
<nav class="navbar" id="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="index.html" class="logo">
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
|
||||
<span>ChronoSeal</span>
|
||||
</a>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="philosophy.html">Philosophy</a>
|
||||
<a href="architecture.html" class="active">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
<div class="nav-dropdown">
|
||||
<a href="#" class="nav-link">More <small>▼</small></a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="deployment.html">Deployment Guide</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
<a href="testing.html">Testing Strategy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="threat-model.html">Threat Model</a>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</button>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Doc Page Shell -->
|
||||
<div class="doc-page">
|
||||
<div class="doc-layout">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="doc-sidebar">
|
||||
<button class="doc-sidebar-toggle">
|
||||
<i class="fas fa-bars"></i> Sidebar Menu
|
||||
</button>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Core Concepts</div>
|
||||
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
|
||||
<a href="architecture.html" class="doc-nav-item active">Architecture Overview</a>
|
||||
<a href="comparison.html" class="doc-nav-item">ChronoSeal vs Others</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Protocol & API</div>
|
||||
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
|
||||
<a href="api.html" class="doc-nav-item">API Reference</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Operations & Security</div>
|
||||
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
|
||||
<a href="security.html" class="doc-nav-item">Security Policy</a>
|
||||
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
|
||||
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Lifecycle & Dev</div>
|
||||
<a href="deployment.html" class="doc-nav-item">Deployment Guide</a>
|
||||
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
|
||||
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="doc-main">
|
||||
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="index.html">Home</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="#">Core Concepts</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Architecture Overview</span>
|
||||
</nav>
|
||||
|
||||
<article class="doc-content">
|
||||
<h1>ChronoSeal Architecture</h1>
|
||||
<p class="doc-subtitle">ChronoSeal is a Unix-native browser attestation daemon. It validates session continuity by combining cryptographic signatures, hash-chain progression, deterministic VM execution, and a shared Synthetic Gene Mutation Engine.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>System Diagram</h2>
|
||||
<p>The following diagram shows the relationship between browser clients, the daemon process, and the shared protocol library:</p>
|
||||
|
||||
<!-- Inline Architecture Diagram Container -->
|
||||
<div id="diagram-architecture" class="arch-diagram" style="margin-top:24px"></div>
|
||||
|
||||
<h2>Workspace Components</h2>
|
||||
<p>The codebase is structured as a Rust workspace containing three runtime crates and static frontend assets:</p>
|
||||
|
||||
<h3>1. <code>shared/</code> Crate</h3>
|
||||
<p>The <strong>determinism boundary</strong> of ChronoSeal. Any execution logic that must agree precisely between the browser WASM runtime and server verification belongs here. Its responsibilities include:</p>
|
||||
<ul>
|
||||
<li>Wire protocol struct formats for request and response models.</li>
|
||||
<li>Blake3 hash-chain progression functions.</li>
|
||||
<li>Synthetic gene model definitions and commitments.</li>
|
||||
<li>Mutation program bytecode generator, interpreter, and execution tracer.</li>
|
||||
</ul>
|
||||
|
||||
<h3>2. <code>server/</code> Crate</h3>
|
||||
<p>Compiles into the <code>chronoseal</code> daemon binary. It manages the server runtime, HTTP API routes, database backends, and verification logic. Key responsibilities:</p>
|
||||
<ul>
|
||||
<li>CLI command parsing and flag defaults.</li>
|
||||
<li>Axum-based web router and health endpoints.</li>
|
||||
<li>Verification pipeline (signature verification, timestamp drift checks, rate limit validations).</li>
|
||||
<li>Pluggable storage adapters (SQLite memory/disk, Valkey).</li>
|
||||
<li>Background cleanup loop for session purges.</li>
|
||||
</ul>
|
||||
|
||||
<h3>3. <code>wasm/</code> Crate</h3>
|
||||
<p>Compiles into the browser WebAssembly package (using <code>wasm-pack</code>) used by the frontend. Its key functions include:</p>
|
||||
<ul>
|
||||
<li>Secure client-side Ed25519 keypair generation and verification.</li>
|
||||
<li>Message signing for canonical heartbeat payloads.</li>
|
||||
<li>Client-side VM program execution and stack history logging.</li>
|
||||
<li>Previewing, committing, and discarding synthetic gene mutations.</li>
|
||||
</ul>
|
||||
|
||||
<h3>4. <code>frontend/</code> Directory</h3>
|
||||
<p>Contains static JavaScript (<code>heartbeat.js</code>, <code>app.js</code>) and assets served to browsers to orchestrate background attestation calls without blocking UI rendering.</p>
|
||||
|
||||
<h2>Session State Model</h2>
|
||||
<p>The daemon stores a single <code>SessionRecord</code> in the active database per session, structured as follows:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Core Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>session_id</code></td>
|
||||
<td>Random 32-byte session lookup key.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>public_key</code></td>
|
||||
<td>Registered Ed25519 public key. Used to verify all heartbeats.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>salt</code></td>
|
||||
<td>Current server salt required to verify the next heartbeat.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last_hash</code></td>
|
||||
<td>Current accepted Blake3 hash head. Prevents out-of-order repeats.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gene</code></td>
|
||||
<td>Committed synthetic gene byte buffer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pending_mutation</code></td>
|
||||
<td>The mutation program compiled by the server for the next heartbeat step.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pending_mutation_step</code></td>
|
||||
<td>Mismatches between this and request steps cause silent rejection.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="doc-alert doc-alert--warn">
|
||||
<div class="doc-alert-icon">⚠️</div>
|
||||
<div class="doc-alert-body">
|
||||
<strong>State Invariant:</strong> The server-side session state advances ONLY after a heartbeat passes all pipeline validations. Failed heartbeats never alter the stored salt, hash, or gene parameters, preventing desynchronization exploits.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Verification Pipeline</h2>
|
||||
<p>Heartbeat verification follows a strict chronological order. If any check fails, execution immediately halts, returning the silent rejection response. The pipeline is:</p>
|
||||
<ol>
|
||||
<li>Load the session record using the submitted <code>session_id</code>.</li>
|
||||
<li>Assert that the session exists and has not expired (<code>now < expires_at</code>).</li>
|
||||
<li>Verify the Ed25519 signature against the reconstructed canonical JSON payload.</li>
|
||||
<li>Assert the request's <code>prev_hash</code> matches the server-stored <code>last_hash</code>.</li>
|
||||
<li>Compare request step with <code>pending_mutation_step</code>.</li>
|
||||
<li>Apply the stored <code>pending_mutation</code> to a cloned gene buffer.</li>
|
||||
<li>Assert the computed gene commitment matches the request's <code>gene_commitment</code>.</li>
|
||||
<li>Assert that the timestamp drift matches liveness bounds (within 30 seconds).</li>
|
||||
<li>Assert that mouse activity entropy is present and speed falls within thresholds.</li>
|
||||
<li>Assert screen aspect ratio, device pixel ratio, and concurrency parameters match bounds.</li>
|
||||
<li>Advance the session's hash head, rotate the salt, compile the next mutation program, and persist.</li>
|
||||
</ol>
|
||||
</article>
|
||||
|
||||
<!-- Pager -->
|
||||
<nav class="doc-pager" aria-label="Doc navigation Pager">
|
||||
<a href="comparison.html" class="doc-pager-link">
|
||||
<div class="doc-pager-label">Previous</div>
|
||||
<div class="doc-pager-title">ChronoSeal vs Others</div>
|
||||
</a>
|
||||
<a href="protocol.html" class="doc-pager-link doc-pager-link--next">
|
||||
<div class="doc-pager-label">Next</div>
|
||||
<div class="doc-pager-title">Protocol Specification</div>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="doc-meta">
|
||||
Last Updated: June 2026 (v1.0.2)
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
|
||||
</h4>
|
||||
<p>Next-generation browser attestation daemon built for the modern web.</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Resources</h4>
|
||||
<a href="philosophy.html">Design Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Community</h4>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
|
||||
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
|
||||
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Legal & Security</h4>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Search Overlay -->
|
||||
<div class="search-overlay">
|
||||
<div class="search-box">
|
||||
<div class="search-input-wrap">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
|
||||
<span class="search-kbd">/</span>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to top -->
|
||||
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
|
||||
|
||||
<!-- JS Scripts -->
|
||||
<script src="js/search.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/diagrams.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="296.99997mm"
|
||||
viewBox="0 0 1122.5196 793.7008"
|
||||
enable-background="new 0 0 1254 1254"
|
||||
xml:space="preserve"
|
||||
height="210mm"
|
||||
sodipodi:docname="chronoseal.svg"
|
||||
inkscape:export-filename="logo1.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.1654266"
|
||||
inkscape:cx="561.16791"
|
||||
inkscape:cy="396.85039"
|
||||
inkscape:window-width="2048"
|
||||
inkscape:window-height="1205"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><defs
|
||||
id="defs44" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<path
|
||||
fill="none"
|
||||
opacity="0"
|
||||
stroke="none"
|
||||
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path2" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path45"
|
||||
style="fill:#e1e1e4;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path46"
|
||||
style="fill:#b0b2b8;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path47"
|
||||
style="fill:#7b7e85;fill-opacity:1" /><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
|
||||
x="154.54701"
|
||||
y="550.13623"
|
||||
id="text47"><tspan
|
||||
id="tspan47"
|
||||
x="154.54701"
|
||||
y="550.13623" /><tspan
|
||||
id="tspan48"
|
||||
x="154.54701"
|
||||
y="750.13623"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
|
||||
id="tspan49">chrono</tspan>seal</tspan></text><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path1"
|
||||
style="fill:#4a4d53;fill-opacity:1" /></svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="296.99997mm"
|
||||
viewBox="0 0 1122.5196 793.7008"
|
||||
enable-background="new 0 0 1254 1254"
|
||||
xml:space="preserve"
|
||||
height="210mm"
|
||||
sodipodi:docname="chronoseal.svg"
|
||||
inkscape:export-filename="logo1.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.1654266"
|
||||
inkscape:cx="561.16791"
|
||||
inkscape:cy="396.85039"
|
||||
inkscape:window-width="2048"
|
||||
inkscape:window-height="1205"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><defs
|
||||
id="defs44" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<path
|
||||
fill="none"
|
||||
opacity="0"
|
||||
stroke="none"
|
||||
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path2" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path45"
|
||||
style="fill:#e1e1e4;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path46"
|
||||
style="fill:#b0b2b8;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path47"
|
||||
style="fill:#7b7e85;fill-opacity:1" /><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
|
||||
x="154.54701"
|
||||
y="550.13623"
|
||||
id="text47"><tspan
|
||||
id="tspan47"
|
||||
x="154.54701"
|
||||
y="550.13623" /><tspan
|
||||
id="tspan48"
|
||||
x="154.54701"
|
||||
y="750.13623"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
|
||||
id="tspan49">chrono</tspan>seal</tspan></text><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path1"
|
||||
style="fill:#4a4d53;fill-opacity:1" /></svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,356 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>ChronoSeal vs Commercial Anti-Bot Systems | ChronoSeal Documentation</title>
|
||||
<meta name="description" content="Compare ChronoSeal honestly with commercial anti-bot Edge/SaaS platforms like Cloudflare, Akamai, PerimeterX, and reCAPTCHA.">
|
||||
<link rel="stylesheet" href="css/chronoseal.css">
|
||||
<link rel="stylesheet" href="css/docs.css">
|
||||
<link rel="stylesheet" href="css/print.css" media="print">
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
|
||||
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Floating Background Spheres -->
|
||||
<div class="bg-animation">
|
||||
<div class="gradient-sphere sphere-1"></div>
|
||||
<div class="gradient-sphere sphere-2"></div>
|
||||
<div class="gradient-sphere sphere-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Header / Navbar -->
|
||||
<nav class="navbar" id="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="index.html" class="logo">
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
|
||||
<span>ChronoSeal</span>
|
||||
</a>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="philosophy.html">Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="comparison.html" class="active">Comparison</a>
|
||||
<div class="nav-dropdown">
|
||||
<a href="#" class="nav-link">More <small>▼</small></a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="deployment.html">Deployment Guide</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
<a href="testing.html">Testing Strategy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="threat-model.html">Threat Model</a>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</button>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Doc Page Shell -->
|
||||
<div class="doc-page">
|
||||
<div class="doc-layout">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="doc-sidebar">
|
||||
<button class="doc-sidebar-toggle">
|
||||
<i class="fas fa-bars"></i> Sidebar Menu
|
||||
</button>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Core Concepts</div>
|
||||
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
|
||||
<a href="architecture.html" class="doc-nav-item">Architecture Overview</a>
|
||||
<a href="comparison.html" class="doc-nav-item active">ChronoSeal vs Others</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Protocol & API</div>
|
||||
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
|
||||
<a href="api.html" class="doc-nav-item">API Reference</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Operations & Security</div>
|
||||
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
|
||||
<a href="security.html" class="doc-nav-item">Security Policy</a>
|
||||
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
|
||||
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Lifecycle & Dev</div>
|
||||
<a href="deployment.html" class="doc-nav-item">Deployment Guide</a>
|
||||
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
|
||||
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="doc-main">
|
||||
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="index.html">Home</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="#">Core Concepts</a>
|
||||
<span class="sep">/</span>
|
||||
<span>ChronoSeal vs Others</span>
|
||||
</nav>
|
||||
|
||||
<article class="doc-content">
|
||||
<h1>ChronoSeal vs Popular Anti-Bot Systems</h1>
|
||||
<p class="doc-subtitle">ChronoSeal is a self-hosted, cryptographic attestation daemon. This document compares it honestly with leading commercial solutions.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Quick Comparison Matrix</h2>
|
||||
|
||||
<div class="comparison-table-wrap" style="margin-bottom: 2rem;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Solution</th>
|
||||
<th>Type</th>
|
||||
<th>Core Method</th>
|
||||
<th>Privacy</th>
|
||||
<th>Self-Hosted</th>
|
||||
<th>Strength</th>
|
||||
<th>Behavioral</th>
|
||||
<th>Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>ChronoSeal</strong></td>
|
||||
<td>Self-hosted Daemon</td>
|
||||
<td>Ed25519 + Gene Mutation</td>
|
||||
<td><span class="check"><i class="fas fa-check-circle"></i> Excellent</span></td>
|
||||
<td>Yes</td>
|
||||
<td>Very High</td>
|
||||
<td>Light + Tunable</td>
|
||||
<td><span class="check"><i class="fas fa-check-circle"></i> Free</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cloudflare Bot Mgmt</td>
|
||||
<td>Cloud Edge</td>
|
||||
<td>Challenges + Fingerprint</td>
|
||||
<td>Medium</td>
|
||||
<td>No</td>
|
||||
<td>Medium</td>
|
||||
<td>Strong</td>
|
||||
<td>Freemium</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Akamai Bot Manager</td>
|
||||
<td>Enterprise Edge</td>
|
||||
<td>Fingerprinting + Heuristics</td>
|
||||
<td><span class="times"><i class="fas fa-times-circle"></i> Low</span></td>
|
||||
<td>Hybrid</td>
|
||||
<td>Medium</td>
|
||||
<td>Very Strong</td>
|
||||
<td>Very High</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HUMAN (PerimeterX)</td>
|
||||
<td>Cloud SaaS</td>
|
||||
<td>Behavioral Biometrics + ML</td>
|
||||
<td><span class="times"><i class="fas fa-times-circle"></i> Low</span></td>
|
||||
<td>No</td>
|
||||
<td>Medium</td>
|
||||
<td>Very Strong</td>
|
||||
<td>Enterprise</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DataDome</td>
|
||||
<td>Cloud SaaS</td>
|
||||
<td>Real-time ML scoring</td>
|
||||
<td>Medium</td>
|
||||
<td>No</td>
|
||||
<td>Medium</td>
|
||||
<td>Strong</td>
|
||||
<td>Enterprise</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>reCAPTCHA v3</td>
|
||||
<td>Google Service</td>
|
||||
<td>Invisible risk challenges</td>
|
||||
<td><span class="times"><i class="fas fa-times-circle"></i> Poor</span></td>
|
||||
<td>No</td>
|
||||
<td>Low</td>
|
||||
<td>Medium</td>
|
||||
<td>Free → Paid</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kasada</td>
|
||||
<td>Cloud SaaS</td>
|
||||
<td>Proof-of-Work + Obfuscation</td>
|
||||
<td>Medium</td>
|
||||
<td>No</td>
|
||||
<td>High</td>
|
||||
<td>Strong</td>
|
||||
<td>Enterprise</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Detailed Analysis</h2>
|
||||
|
||||
<h3>1. ChronoSeal (v1.0.2)</h3>
|
||||
<p><strong>Strengths:</strong></p>
|
||||
<ul>
|
||||
<li>Strongest cryptographic foundation (Ed25519 signatures + Blake3 hash chain + Synthetic Gene Mutation Engine).</li>
|
||||
<li>Fully deterministic server ↔ WASM execution agreement.</li>
|
||||
<li>Completely invisible to users with silent rejection mechanics.</li>
|
||||
<li>Excellent privacy posture — no third-party tracking, profiling, or persistent databases.</li>
|
||||
<li>Tunable mutation complexity parameters (<code>gene_size</code> and <code>mutation_rounds</code>).</li>
|
||||
<li>100% control, auditability, and local operations.</li>
|
||||
</ul>
|
||||
<p><strong>Weaknesses:</strong></p>
|
||||
<ul>
|
||||
<li>Requires self-hosting, configuration management, and server capacity.</li>
|
||||
<li>No global threat intelligence network or shared IP reputation lists.</li>
|
||||
</ul>
|
||||
|
||||
<h3>2. Cloudflare Bot Management</h3>
|
||||
<p><strong>Strengths:</strong></p>
|
||||
<ul>
|
||||
<li>Extremely easy to deploy for domains already routed through Cloudflare.</li>
|
||||
<li>Excellent scale and global threat reputation database.</li>
|
||||
</ul>
|
||||
<p><strong>Weaknesses vs ChronoSeal:</strong></p>
|
||||
<ul>
|
||||
<li>Relies heavily on browser fingerprint heuristics and invasive JS challenges.</li>
|
||||
<li>Sends visitor metadata to Cloudflare (privacy impact).</li>
|
||||
<li>Vendor lock-in and zero visibility into decision algorithms.</li>
|
||||
</ul>
|
||||
|
||||
<h3>3. Enterprise Solutions (Akamai, HUMAN, DataDome, Kasada)</h3>
|
||||
<p><strong>Strengths:</strong></p>
|
||||
<ul>
|
||||
<li>Sophisticated machine learning modeling of biometrics and timing.</li>
|
||||
<li>Professional support, operational SLAs, and security response teams.</li>
|
||||
</ul>
|
||||
<p><strong>Weaknesses vs ChronoSeal:</strong></p>
|
||||
<ul>
|
||||
<li>Extremely expensive enterprise licensing models.</li>
|
||||
<li>Black-box systems with limited logging and transparency.</li>
|
||||
<li>Heavy user data collection, causing privacy and compliance overhead.</li>
|
||||
</ul>
|
||||
|
||||
<h3>4. reCAPTCHA v3</h3>
|
||||
<p><strong>Strengths:</strong></p>
|
||||
<ul>
|
||||
<li>Free tier with wide community adoption.</li>
|
||||
<li>Quick to integrate in basic web forms.</li>
|
||||
</ul>
|
||||
<p><strong>Weaknesses vs ChronoSeal:</strong></p>
|
||||
<ul>
|
||||
<li>Heavy Google user tracking cookies and profiling.</li>
|
||||
<li>Fails to block modern, stateful automated browsers and headless runs.</li>
|
||||
<li>High rate of bypasses by standard captcha-solving farms.</li>
|
||||
</ul>
|
||||
|
||||
<h2>When to Choose ChronoSeal</h2>
|
||||
<p>Choose <strong>ChronoSeal</strong> if you want:</p>
|
||||
<ul>
|
||||
<li>Maximum visitor privacy.</li>
|
||||
<li>Strong, deterministic cryptographic guarantees.</li>
|
||||
<li>Complete control over your server infrastructure and logs.</li>
|
||||
<li>Configurable and tunable verification strength.</li>
|
||||
<li>Zero dependency on third-party SaaS vendors.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Technical Differentiation</h2>
|
||||
<p>ChronoSeal's primary advantage is the <strong>Synthetic Gene Mutation Engine</strong>. Instead of just checking static fingerprint values or browser headers, the server issues dynamic mutation programs that both the server and client WASM execute in sync. This establishes a second stateful channel that is extremely difficult for automation clients to spoof at scale without implementing the complete state model.</p>
|
||||
</article>
|
||||
|
||||
<!-- Pager -->
|
||||
<nav class="doc-pager" aria-label="Doc navigation Pager">
|
||||
<a href="security.html" class="doc-pager-link">
|
||||
<div class="doc-pager-label">Previous</div>
|
||||
<div class="doc-pager-title">Security Policy</div>
|
||||
</a>
|
||||
<a href="architecture.html" class="doc-pager-link doc-pager-link--next">
|
||||
<div class="doc-pager-label">Next</div>
|
||||
<div class="doc-pager-title">Architecture Overview</div>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="doc-meta">
|
||||
Last Updated: June 2026 (v1.0.2)
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
|
||||
</h4>
|
||||
<p>Next-generation browser attestation daemon built for the modern web.</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Resources</h4>
|
||||
<a href="philosophy.html">Design Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Community</h4>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
|
||||
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
|
||||
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Legal & Security</h4>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Search Overlay -->
|
||||
<div class="search-overlay">
|
||||
<div class="search-box">
|
||||
<div class="search-input-wrap">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
|
||||
<span class="search-kbd">/</span>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to top -->
|
||||
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
|
||||
|
||||
<!-- JS Scripts -->
|
||||
<script src="js/search.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,984 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Design System — chronoseal.css
|
||||
Vibrant, premium, glassmorphism theme matching the target site
|
||||
============================================================ */
|
||||
|
||||
/* ---- Custom Properties ---- */
|
||||
:root {
|
||||
--bg-primary: #0a0e27;
|
||||
--bg-secondary: #11162e;
|
||||
--bg-card: rgba(18, 24, 48, 0.7);
|
||||
--bg-card-solid: #121830;
|
||||
--border: rgba(56, 78, 135, 0.3);
|
||||
--border-glow: rgba(0, 255, 255, 0.2);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a0a8c3;
|
||||
--text-muted: #5a6490;
|
||||
|
||||
--accent-cyan: #00e5ff;
|
||||
--accent-purple: #b84eff;
|
||||
--accent-green: #00ff88;
|
||||
--accent-red: #ff4757;
|
||||
|
||||
--gradient-1: linear-gradient(135deg, #00e5ff 0%, #b84eff 100%);
|
||||
--gradient-2: linear-gradient(135deg, #00ff88 0%, #00e5ff 100%);
|
||||
--shadow-glow: 0 0 30px rgba(0, 229, 255, 0.1);
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
|
||||
--max-width: 1400px;
|
||||
--header-h: 75px;
|
||||
--r-sm: 8px;
|
||||
--r-md: 12px;
|
||||
--r-lg: 20px;
|
||||
--ease: cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
--tr: .3s ease;
|
||||
}
|
||||
|
||||
/* ---- Reset ---- */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all var(--tr);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
img, svg {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* ---- Layout ---- */
|
||||
.container, .main-content {
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding-top: var(--header-h);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 6rem 0;
|
||||
scroll-margin-top: 100px;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.arch-diagram {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 2rem auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.arch-diagram svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.section--alt {
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACKGROUND ANIMATION
|
||||
============================================================ */
|
||||
.bg-animation {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gradient-sphere {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.4;
|
||||
animation: float 20s infinite ease-in-out;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.sphere-1 { width: 600px; height: 600px; background: var(--accent-cyan); top: -200px; right: -200px; animation-delay: 0s; }
|
||||
.sphere-2 { width: 500px; height: 500px; background: var(--accent-purple); bottom: -150px; left: -150px; animation-delay: -5s; }
|
||||
.sphere-3 { width: 400px; height: 400px; background: var(--accent-green); top: 40%; left: 30%; animation-delay: -10s; opacity: 0.2; }
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(30px, -30px) scale(1.05); }
|
||||
66% { transform: translate(-20px, 20px) scale(0.95); }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
NAVBAR / HEADER
|
||||
============================================================ */
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(10, 14, 39, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: 1.25rem 2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar.scrolled {
|
||||
padding: 0.75rem 2rem;
|
||||
background: rgba(10, 14, 39, 0.98);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
background: var(--gradient-1);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logobar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
color: transparent;
|
||||
background: var(--gradient-1);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 2.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.nav-links a:hover, .nav-links a.active {
|
||||
color: var(--text-primary);
|
||||
text-shadow: 0 0 10px rgba(0, 229, 255, 0.4);
|
||||
}
|
||||
|
||||
.github-btn {
|
||||
background: var(--bg-card-solid);
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.github-btn:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 15px rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Nav Dropdown */
|
||||
.nav-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 15px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
min-width: 220px;
|
||||
background: var(--bg-card-solid);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-md);
|
||||
padding: 6px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--tr), transform var(--tr);
|
||||
box-shadow: 0 16px 48px rgba(0,0,0,.4);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.nav-dropdown:hover .nav-dropdown-menu {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.nav-dropdown-menu a {
|
||||
display: block;
|
||||
padding: 8px 14px;
|
||||
font-size: .88rem;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--r-sm);
|
||||
transition: all var(--tr);
|
||||
}
|
||||
|
||||
.nav-dropdown-menu a:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
HERO SECTION
|
||||
============================================================ */
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 6rem 0;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
border: 1px solid var(--border-glow);
|
||||
border-radius: 50px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--accent-cyan);
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: clamp(3rem, 5vw, 4.5rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--gradient-1);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-secondary);
|
||||
max-width: 700px;
|
||||
margin: 0 auto 2.5rem;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BUTTONS
|
||||
============================================================ */
|
||||
.btn {
|
||||
padding: 0.875rem 2rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all var(--tr);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-1);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(0, 229, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border);
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
STATS ROW
|
||||
============================================================ */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 6rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem 2rem;
|
||||
border: 1px solid var(--border);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 800;
|
||||
background: var(--gradient-1);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
margin-top: 1rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SECTION HEADER
|
||||
============================================================ */
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
display: inline-block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-cyan);
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--gradient-2);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
color: var(--text-secondary);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
FEATURES GRID / CARDS
|
||||
============================================================ */
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
transition: all 0.4s var(--ease);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-10px);
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: rgba(0, 229, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(0, 229, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-icon i {
|
||||
font-size: 1.75rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 0.35rem 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
STEPS / HOW IT WORKS
|
||||
============================================================ */
|
||||
.steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-md);
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
transition: all var(--tr);
|
||||
}
|
||||
|
||||
.step:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.step-number {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: var(--gradient-1);
|
||||
color: var(--bg-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.step-content h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-content p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
COMPARISON TABLE
|
||||
============================================================ */
|
||||
.comparison-table-wrap {
|
||||
overflow-x: auto;
|
||||
background: var(--bg-card);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 1rem;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.comparison-table-wrap table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 700px;
|
||||
}
|
||||
|
||||
.comparison-table-wrap th, .comparison-table-wrap td {
|
||||
padding: 1.25rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.comparison-table-wrap th {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
}
|
||||
|
||||
.comparison-table-wrap tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.check {
|
||||
color: var(--accent-green);
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
td.check {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
td.check i {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.times {
|
||||
color: var(--accent-red);
|
||||
opacity: 0.7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
td.times {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
td.times i {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CODE BLOCKS
|
||||
============================================================ */
|
||||
.code-block {
|
||||
background: #080b1a;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.9rem;
|
||||
margin: 1.5rem 0;
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-block-center {
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
color: #a0a8c3;
|
||||
line-height: 1.5;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.code-lang {
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.code-copy-btn {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
transition: all var(--tr);
|
||||
}
|
||||
.code-copy-btn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
FOOTER
|
||||
============================================================ */
|
||||
.footer {
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 5rem 2rem 2rem;
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.footer-section h4 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.footer-section p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footer-section a {
|
||||
display: inline-block;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
margin-bottom: 0.75rem;
|
||||
transition: all var(--tr);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.footer-section a:hover {
|
||||
color: var(--accent-cyan);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
padding-top: 3rem;
|
||||
margin-top: 3rem;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SEARCH OVERLAY
|
||||
============================================================ */
|
||||
.search-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background: rgba(10, 14, 39, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--tr);
|
||||
}
|
||||
|
||||
.search-overlay.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
max-width: 580px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-lg);
|
||||
overflow: hidden;
|
||||
transform: translateY(16px);
|
||||
transition: transform var(--tr);
|
||||
box-shadow: 0 24px 64px rgba(0,0,0,.5);
|
||||
}
|
||||
|
||||
.search-overlay.open .search-box {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-input-wrap svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-kbd {
|
||||
font-size: .7rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-card-solid);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
display: block;
|
||||
padding: 10px 16px;
|
||||
border-radius: var(--r-sm);
|
||||
transition: background var(--tr);
|
||||
}
|
||||
|
||||
.search-result:hover, .search-result.selected {
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
}
|
||||
|
||||
.search-result-title {
|
||||
font-size: .9rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.search-result-desc {
|
||||
font-size: .8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BACK TO TOP
|
||||
============================================================ */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-card-solid);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 50%;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all var(--tr);
|
||||
}
|
||||
|
||||
.back-to-top.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
color: var(--accent-cyan);
|
||||
border-color: var(--accent-cyan);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
ANIMATION INTERSECT
|
||||
============================================================ */
|
||||
.animate {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
}
|
||||
.animate.in-view {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
RESPONSIVE
|
||||
============================================================ */
|
||||
@media (max-width: 900px) {
|
||||
.nav-links {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(10, 14, 39, 0.98);
|
||||
backdrop-filter: blur(15px);
|
||||
flex-direction: column;
|
||||
padding: 2rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: 1.5rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.nav-links.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
/* ============================================================
|
||||
Documentation Pages — docs.css
|
||||
Layout, sidebar, content rendering for documentation pages
|
||||
============================================================ */
|
||||
|
||||
/* ---- Doc page shell ---- */
|
||||
.doc-page {
|
||||
padding-top: var(--header-h);
|
||||
}
|
||||
|
||||
.doc-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 0;
|
||||
min-height: calc(100vh - var(--header-h));
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ---- Sidebar ---- */
|
||||
.doc-sidebar {
|
||||
position: sticky;
|
||||
top: var(--header-h);
|
||||
height: calc(100vh - var(--header-h));
|
||||
overflow-y: auto;
|
||||
padding: 2.5rem 1.5rem;
|
||||
background: rgba(17, 22, 46, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.doc-sidebar-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.doc-nav-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.doc-nav-label {
|
||||
font-size: .75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.doc-nav-item {
|
||||
display: block;
|
||||
padding: 8px 14px;
|
||||
font-size: .9rem;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--r-sm);
|
||||
transition: all var(--tr);
|
||||
border-left: 2px solid transparent;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.doc-nav-item:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
}
|
||||
|
||||
.doc-nav-item.active {
|
||||
color: var(--accent-cyan);
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
border-left-color: var(--accent-cyan);
|
||||
text-shadow: 0 0 10px rgba(0, 229, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ---- Content area ---- */
|
||||
.doc-main {
|
||||
padding: 3rem 4rem 6rem;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
/* ---- Breadcrumb ---- */
|
||||
.doc-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.doc-breadcrumb a {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.doc-breadcrumb a:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.doc-breadcrumb .sep {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ---- Markdown content rendering ---- */
|
||||
.doc-content h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -.03em;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.15;
|
||||
background: var(--gradient-1);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.doc-content .doc-subtitle {
|
||||
font-size: 1.15rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.doc-content h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -.02em;
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
scroll-margin-top: calc(var(--header-h) + 24px);
|
||||
background: var(--gradient-2);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.doc-content h3 {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
scroll-margin-top: calc(var(--header-h) + 24px);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.doc-content h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
scroll-margin-top: calc(var(--header-h) + 24px);
|
||||
}
|
||||
|
||||
.doc-content p {
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.doc-content ul, .doc-content ol {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.doc-content ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.doc-content ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.doc-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.doc-content li strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.doc-content code {
|
||||
padding: 2px 6px;
|
||||
font-size: .88em;
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
border: 1px solid rgba(0, 229, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.doc-content pre {
|
||||
background: #080b1a;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0 2rem;
|
||||
overflow-x: auto;
|
||||
font-size: .88rem;
|
||||
line-height: 1.6;
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.doc-content pre code {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: #a0a8c3;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.doc-content strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.doc-content em {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.doc-content a {
|
||||
color: var(--accent-cyan);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: all var(--tr);
|
||||
}
|
||||
|
||||
.doc-content a:hover {
|
||||
color: var(--text-primary);
|
||||
border-bottom-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.doc-content blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-left: 4px solid var(--accent-purple);
|
||||
background: var(--bg-card);
|
||||
border-radius: 0 var(--r-md) var(--r-md) 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.doc-content blockquote p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.doc-content table {
|
||||
width: 100%;
|
||||
margin: 1.5rem 0 2rem;
|
||||
border-collapse: collapse;
|
||||
font-size: .88rem;
|
||||
}
|
||||
|
||||
.doc-content table th {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
font-size: .78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: var(--text-primary);
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.doc-content table td {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.doc-content table tr:hover td {
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
.doc-content hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
/* ---- Alert boxes ---- */
|
||||
.doc-alert {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 1.5rem 0 2rem;
|
||||
border-radius: var(--r-md);
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.doc-alert-icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.doc-alert-body {
|
||||
font-size: .9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.doc-alert-body p {
|
||||
margin-bottom: 4px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.doc-alert-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.doc-alert--note {
|
||||
background: rgba(0, 229, 255, 0.05);
|
||||
border-color: rgba(0, 229, 255, 0.2);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.doc-alert--tip {
|
||||
background: rgba(0, 255, 136, 0.05);
|
||||
border-color: rgba(0, 255, 136, 0.2);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.doc-alert--warn {
|
||||
background: rgba(251, 191, 36, 0.05);
|
||||
border-color: rgba(251, 191, 36, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.doc-alert--danger {
|
||||
background: rgba(255, 71, 87, 0.05);
|
||||
border-color: rgba(255, 71, 87, 0.2);
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
/* ---- Prev / Next navigation ---- */
|
||||
.doc-pager {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.doc-pager-link {
|
||||
padding: 1.5rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-md);
|
||||
transition: all var(--tr);
|
||||
}
|
||||
|
||||
.doc-pager-link:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background: rgba(0, 229, 255, 0.03);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.doc-pager-label {
|
||||
font-size: .75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.doc-pager-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.doc-pager-link--next {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ---- Last updated ---- */
|
||||
.doc-meta {
|
||||
font-size: .8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
RESPONSIVE
|
||||
============================================================ */
|
||||
@media(max-width:900px) {
|
||||
.doc-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.doc-sidebar {
|
||||
position: fixed;
|
||||
top: var(--header-h);
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
z-index: 998;
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--tr);
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.doc-sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.doc-sidebar-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
font-size: .85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-sm);
|
||||
margin-bottom: 16px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.doc-sidebar-toggle:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.doc-main {
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
|
||||
.doc-pager {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* ============================================================
|
||||
Print Stylesheet — print.css
|
||||
Clean print layout for documentation pages
|
||||
============================================================ */
|
||||
|
||||
@media print {
|
||||
*{color:#111!important;background:white!important;box-shadow:none!important;text-shadow:none!important}
|
||||
|
||||
body{font-size:11pt;line-height:1.6}
|
||||
|
||||
.site-header,.site-footer,.nav-toggle,.back-to-top,
|
||||
.search-overlay,.doc-sidebar,.doc-toc,.doc-pager,
|
||||
.doc-sidebar-toggle,.doc-breadcrumb,
|
||||
.hero-bg,.hero-grid,.hero-badge,.hero-actions,
|
||||
.hero-terminal,.cta-section,
|
||||
.btn,.nav-cta{display:none!important}
|
||||
|
||||
.doc-layout,.doc-layout--toc{display:block!important}
|
||||
.doc-main{padding:0!important;max-width:100%!important}
|
||||
.doc-page{padding-top:0!important}
|
||||
|
||||
a{text-decoration:underline}
|
||||
a[href^="http"]::after{content:" (" attr(href) ")";font-size:9pt;color:#666}
|
||||
a[href^="#"]::after{content:""}
|
||||
|
||||
pre,code{font-size:9pt;border:1px solid #ddd;padding:8px;page-break-inside:avoid}
|
||||
table{border-collapse:collapse;width:100%}
|
||||
th,td{border:1px solid #ccc;padding:6px 10px;font-size:9pt}
|
||||
th{background:#eee!important;font-weight:700}
|
||||
|
||||
h1{font-size:20pt;border-bottom:2px solid #111;padding-bottom:6pt;margin-bottom:12pt}
|
||||
h2{font-size:16pt;border-bottom:1px solid #999;padding-bottom:4pt;margin-top:24pt;page-break-after:avoid}
|
||||
h3{font-size:13pt;margin-top:18pt;page-break-after:avoid}
|
||||
|
||||
img{max-width:100%!important}
|
||||
.section{padding:24pt 0!important}
|
||||
|
||||
@page{margin:1.5cm 2cm}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Deployment Guide | ChronoSeal Documentation</title>
|
||||
<meta name="description" content="ChronoSeal Deployment Guide, detailing compiler requirements, native installation script, environment variables, Nginx configurations, and Docker integration.">
|
||||
<link rel="stylesheet" href="css/chronoseal.css">
|
||||
<link rel="stylesheet" href="css/docs.css">
|
||||
<link rel="stylesheet" href="css/print.css" media="print">
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
|
||||
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Floating Background Spheres -->
|
||||
<div class="bg-animation">
|
||||
<div class="gradient-sphere sphere-1"></div>
|
||||
<div class="gradient-sphere sphere-2"></div>
|
||||
<div class="gradient-sphere sphere-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Header / Navbar -->
|
||||
<nav class="navbar" id="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="index.html" class="logo">
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
|
||||
<span>ChronoSeal</span>
|
||||
</a>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="philosophy.html">Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
<div class="nav-dropdown">
|
||||
<a href="#" class="nav-link">More <small>▼</small></a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="deployment.html" class="active">Deployment Guide</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
<a href="testing.html">Testing Strategy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="threat-model.html">Threat Model</a>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</button>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Doc Page Shell -->
|
||||
<div class="doc-page">
|
||||
<div class="doc-layout">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="doc-sidebar">
|
||||
<button class="doc-sidebar-toggle">
|
||||
<i class="fas fa-bars"></i> Sidebar Menu
|
||||
</button>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Core Concepts</div>
|
||||
<a href="philosophy.html" class="doc-nav-item">Design Philosophy</a>
|
||||
<a href="architecture.html" class="doc-nav-item">Architecture Overview</a>
|
||||
<a href="comparison.html" class="doc-nav-item">ChronoSeal vs Others</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Protocol & API</div>
|
||||
<a href="protocol.html" class="doc-nav-item">Protocol Specification</a>
|
||||
<a href="api.html" class="doc-nav-item">API Reference</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Operations & Security</div>
|
||||
<a href="threat-model.html" class="doc-nav-item">Threat Model</a>
|
||||
<a href="security.html" class="doc-nav-item">Security Policy</a>
|
||||
<a href="privacy.html" class="doc-nav-item">Privacy Policy</a>
|
||||
<a href="performance.html" class="doc-nav-item">Performance Tuning</a>
|
||||
</div>
|
||||
<div class="doc-nav-group">
|
||||
<div class="doc-nav-label">Lifecycle & Dev</div>
|
||||
<a href="deployment.html" class="doc-nav-item active">Deployment Guide</a>
|
||||
<a href="operations.html" class="doc-nav-item">Operations Handbook</a>
|
||||
<a href="testing.html" class="doc-nav-item">Testing Strategy</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="doc-main">
|
||||
<nav class="doc-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="index.html">Home</a>
|
||||
<span class="sep">/</span>
|
||||
<a href="#">Lifecycle & Dev</a>
|
||||
<span class="sep">/</span>
|
||||
<span>Deployment Guide</span>
|
||||
</nav>
|
||||
|
||||
<article class="doc-content">
|
||||
<h1>ChronoSeal Deployment Guide</h1>
|
||||
<p class="doc-subtitle">ChronoSeal runs as a native Unix daemon behind TLS, serving WebAssembly assets. This guide covers building, configuring, and service installation options.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>System Requirements</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Requirement</th>
|
||||
<th>Min Version</th>
|
||||
<th>Core Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Rust (cargo)</td>
|
||||
<td>1.87 stable</td>
|
||||
<td>Compile server binary and shared libraries</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>wasm-pack</td>
|
||||
<td>0.13</td>
|
||||
<td>Generate the browser WebAssembly module package</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>wasm32 target</td>
|
||||
<td>stable</td>
|
||||
<td>Required target for cargo wasm32 compilation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>systemd</td>
|
||||
<td>248+</td>
|
||||
<td>Service management and sandbox isolation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker</td>
|
||||
<td>24.x</td>
|
||||
<td>Containerization support</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Building from Source</h2>
|
||||
<p>Install the Rust WASM build targets and tools:</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">Shell</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>rustup target add wasm32-unknown-unknown
|
||||
cargo install wasm-pack</code></pre>
|
||||
</div>
|
||||
|
||||
<p>Build the browser WASM package and compile the server daemon:</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">Shell</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code># Build WASM package and copy to frontend assets
|
||||
wasm-pack build wasm --target web --release
|
||||
rm -rf frontend/pkg
|
||||
mv wasm/pkg frontend/pkg
|
||||
|
||||
# Build release server daemon
|
||||
cargo build -p chronoseal-server --bin chronoseal --release</code></pre>
|
||||
</div>
|
||||
<p>The compiled binary is written to <code>target/release/chronoseal</code>.</p>
|
||||
|
||||
<h2>Native Service Installation</h2>
|
||||
<p>The easiest way to deploy ChronoSeal on Linux is using our installer script. This creates a dedicated system user, configures directory paths, copies assets, and sets up systemd sandboxing:</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">Shell</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>sudo bash scripts/install.sh</code></pre>
|
||||
</div>
|
||||
<p>Verify installation operational status:</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">Shell</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>sudo systemctl status chronoseal
|
||||
chronoseal health
|
||||
sudo journalctl -u chronoseal -f</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Configuration Precedence</h2>
|
||||
<p>ChronoSeal resolves configuration variables in this order:
|
||||
<code>CLI parameters</code> > <code>CHRONOSEAL_* Environment Variables</code> > <code>TOML file</code> > <code>Defaults</code>.
|
||||
</p>
|
||||
<p>The TOML configuration is searched at:
|
||||
<code>$CHRONOSEAL_CONFIG</code>, <code>/etc/chronoseal/config.toml</code>, <code>~/.config/chronoseal/config.toml</code>.
|
||||
</p>
|
||||
|
||||
<h3>Common Environment overrides</h3>
|
||||
<ul>
|
||||
<li><code>CHRONOSEAL_BIND</code>: Binding address (e.g. <code>127.0.0.1:3000</code>).</li>
|
||||
<li><code>CHRONOSEAL_DB_TYPE</code>: <code>sqlite-in-memory</code>, <code>sqlite-in-disk</code>, or <code>valkey</code>.</li>
|
||||
<li><code>CHRONOSEAL_VALKEY_ADDR</code>: Address of Valkey server (defaults to <code>127.0.0.1:6666</code>).</li>
|
||||
<li><code>CHRONOSEAL_FRONTEND_DIR</code>: Directory containing frontend static assets.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Reverse Proxy Configuration (Nginx)</h2>
|
||||
<p>Ensure ChronoSeal runs behind TLS in production. Secure proxy traffic to the local daemon port:</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">Nginx</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Docker Compose Deployment</h2>
|
||||
<p>Build and run the container service (configured for non-root execution by default):</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">Shell</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>bash scripts/build.sh
|
||||
docker compose up -d --build</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Production Checklist</h2>
|
||||
<ul>
|
||||
<li>Ensure WASM modules in <code>frontend/pkg</code> are rebuilt with the <code>--release</code> flag.</li>
|
||||
<li>Serve all ChronoSeal endpoints exclusively over HTTPS.</li>
|
||||
<li>Restict server bind address to localhost (<code>127.0.0.1</code>) behind a reverse proxy.</li>
|
||||
<li>Run the daemon service under a dedicated unprivileged user account.</li>
|
||||
<li>Disable debug logging (avoid <code>CHRONOSEAL_LOG=debug</code>) in production to protect credentials.</li>
|
||||
<li>Configure Valkey or persistent SQLite for production session storage.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<!-- Pager -->
|
||||
<nav class="doc-pager" aria-label="Doc navigation Pager">
|
||||
<a href="operations.html" class="doc-pager-link">
|
||||
<div class="doc-pager-label">Previous</div>
|
||||
<div class="doc-pager-title">Operations Handbook</div>
|
||||
</a>
|
||||
<a href="testing.html" class="doc-pager-link doc-pager-link--next">
|
||||
<div class="doc-pager-label">Next</div>
|
||||
<div class="doc-pager-title">Testing Strategy</div>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="doc-meta">
|
||||
Last Updated: June 2026 (v1.0.2)
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
|
||||
</h4>
|
||||
<p>Next-generation browser attestation daemon built for the modern web.</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Resources</h4>
|
||||
<a href="philosophy.html">Design Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Community</h4>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a>
|
||||
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a>
|
||||
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Legal & Security</h4>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Search Overlay -->
|
||||
<div class="search-overlay">
|
||||
<div class="search-box">
|
||||
<div class="search-input-wrap">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
|
||||
<span class="search-kbd">/</span>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to top -->
|
||||
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
|
||||
|
||||
<!-- JS Scripts -->
|
||||
<script src="js/search.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 192 KiB |
@@ -0,0 +1,43 @@
|
||||
name: chronoseal-www
|
||||
services:
|
||||
chronoseal:
|
||||
cpu_shares: 90
|
||||
command: []
|
||||
container_name: chronoseal-www
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: "33491517440"
|
||||
hostname: chronoseal-www
|
||||
image: nginx:alpine
|
||||
labels:
|
||||
icon: https://github.com/thakares/chronoseal-rs/raw/main/logo/chronoseal.svg
|
||||
networks:
|
||||
default: null
|
||||
ports:
|
||||
- mode: ingress
|
||||
target: 80
|
||||
published: "8383"
|
||||
protocol: tcp
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /DATA/AppData/chronoseal-www
|
||||
target: /usr/share/nginx/html
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: true
|
||||
networks:
|
||||
default:
|
||||
name: chronoseal-www_default
|
||||
x-casaos:
|
||||
author: self
|
||||
category: self
|
||||
hostname: ""
|
||||
icon: https://github.com/thakares/chronoseal-rs/raw/main/logo/chronoseal.svg
|
||||
index: /
|
||||
is_uncontrolled: false
|
||||
port_map: "8383"
|
||||
scheme: http
|
||||
title:
|
||||
custom: chronoseal-www
|
||||
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="296.99997mm"
|
||||
viewBox="0 0 1122.5196 793.7008"
|
||||
enable-background="new 0 0 1254 1254"
|
||||
xml:space="preserve"
|
||||
height="210mm"
|
||||
sodipodi:docname="chronoseal.svg"
|
||||
inkscape:export-filename="logo1.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.1654266"
|
||||
inkscape:cx="561.16791"
|
||||
inkscape:cy="396.85039"
|
||||
inkscape:window-width="2048"
|
||||
inkscape:window-height="1205"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><defs
|
||||
id="defs44" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<path
|
||||
fill="none"
|
||||
opacity="0"
|
||||
stroke="none"
|
||||
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path2" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path45"
|
||||
style="fill:#e1e1e4;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path46"
|
||||
style="fill:#b0b2b8;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path47"
|
||||
style="fill:#7b7e85;fill-opacity:1" /><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
|
||||
x="154.54701"
|
||||
y="550.13623"
|
||||
id="text47"><tspan
|
||||
id="tspan47"
|
||||
x="154.54701"
|
||||
y="550.13623" /><tspan
|
||||
id="tspan48"
|
||||
x="154.54701"
|
||||
y="750.13623"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
|
||||
id="tspan49">chrono</tspan>seal</tspan></text><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path1"
|
||||
style="fill:#4a4d53;fill-opacity:1" /></svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" width="296.99997mm" viewBox="0 0 1122.5196 793.7008" enable-background="new 0 0 1254 1254" xml:space="preserve" height="210mm" sodipodi:docname="logo1.svg" inkscape:export-filename="logo1.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="1.1654266" inkscape:cx="561.16791" inkscape:cy="396.85039" inkscape:window-width="2048" inkscape:window-height="1205" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="Layer_1"></sodipodi:namedview><defs id="defs44"></defs><path fill="none" opacity="1" stroke="none" d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path2"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path45" style="fill:#e1e1e4;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path46" style="fill:#b0b2b8;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20Line truncated
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" width="296.99997mm" viewBox="0 0 1122.5196 793.7008" enable-background="new 0 0 1254 1254" xml:space="preserve" height="210mm" sodipodi:docname="logo1.svg" inkscape:export-filename="logo1.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="1.1654266" inkscape:cx="561.16791" inkscape:cy="396.85039" inkscape:window-width="2048" inkscape:window-height="1205" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="Layer_1"></sodipodi:namedview><defs id="defs44"></defs><path fill="none" opacity="1" stroke="none" d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path2"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path45" style="fill:#e1e1e4;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path46" style="fill:#b0b2b8;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20Line truncated
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,424 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>ChronoSeal | Browser Attestation Daemon</title>
|
||||
<meta name="description" content="ChronoSeal is a Unix-native browser attestation daemon combining cryptographic signatures, deterministic state machines, and a synthetic gene mutation engine for session integrity.">
|
||||
<link rel="stylesheet" href="css/chronoseal.css">
|
||||
<link rel="stylesheet" href="css/print.css" media="print">
|
||||
<link rel="manifest" href="site.webmanifest">
|
||||
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
|
||||
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Floating Background Spheres -->
|
||||
<div class="bg-animation">
|
||||
<div class="gradient-sphere sphere-1"></div>
|
||||
<div class="gradient-sphere sphere-2"></div>
|
||||
<div class="gradient-sphere sphere-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Header / Navbar -->
|
||||
<nav class="navbar" id="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="index.html" class="logo">
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo">
|
||||
<span>ChronoSeal</span>
|
||||
</a>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="philosophy.html">Philosophy</a>
|
||||
<a href="architecture.html">Architecture</a>
|
||||
<a href="protocol.html">Protocol</a>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
<div class="nav-dropdown">
|
||||
<a href="#" class="nav-link">More <small>▼</small></a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a href="api.html">API Reference</a>
|
||||
<a href="deployment.html">Deployment Guide</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
<a href="testing.html">Testing Strategy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="threat-model.html">Threat Model</a>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm btn-icon search-trigger" aria-label="Search" onclick="ChronoSearch.open()">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
</button>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" class="github-btn" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
<button class="nav-toggle" id="menuBtn" aria-label="Toggle Navigation">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section id="home" class="hero animate">
|
||||
<div class="hero-badge">
|
||||
<i class="fas fa-code-branch"></i> v1.0.2 · Production Hardened
|
||||
</div>
|
||||
<h1>Browser Attestation<br>Reimagined</h1>
|
||||
<p class="hero-subtitle">
|
||||
ChronoSeal is a Unix-native browser attestation daemon combining cryptographic signatures,
|
||||
deterministic state machines, and a synthetic gene mutation engine for unparalleled session integrity.
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
<a href="deployment.html" class="btn btn-primary">
|
||||
<i class="fas fa-terminal"></i> Get Started
|
||||
</a>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" class="btn btn-secondary" target="_blank" rel="noopener">
|
||||
<i class="fab fa-github"></i> View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="stats-grid animate">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">100</div>
|
||||
<div class="stat-label">Tests Passing</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">3</div>
|
||||
<div class="stat-label">Storage Backends</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">10</div>
|
||||
<div class="stat-label">VM Opcodes</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">30s</div>
|
||||
<div class="stat-label">Max Drift</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core Features Section -->
|
||||
<section id="features" class="section animate">
|
||||
<div class="section-header">
|
||||
<h2>Core Features</h2>
|
||||
<p>Everything you need for robust, enterprise-grade browser attestation</p>
|
||||
</div>
|
||||
<div class="cards-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-dna"></i></div>
|
||||
<h3>Synthetic Gene Engine</h3>
|
||||
<p>Deterministic mutation sequence that both server and browser WASM must execute in sync—creating an unprecedented second state channel.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag">Mutation Rounds</span>
|
||||
<span class="tag">Gene Size 512-4096</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-link"></i></div>
|
||||
<h3>Cryptographic Chain</h3>
|
||||
<p>Ed25519 signatures and a Blake3 hash chain progression with rotating salts ensures replay resistance and continuity verification.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag">Ed25519</span>
|
||||
<span class="tag">Blake3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-microchip"></i></div>
|
||||
<h3>Deterministic VM</h3>
|
||||
<p>A lightweight 10-opcode virtual machine executing server-issued programs with exact stack state verification between client and server.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag">Stack Verification</span>
|
||||
<span class="tag">Custom Instruction Set</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-user-secret"></i></div>
|
||||
<h3>Silent Rejection</h3>
|
||||
<p>Failed heartbeats return identical HTTP 200 responses—providing zero oracle feedback and making automated probing impossible.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag">No Oracle</span>
|
||||
<span class="tag">Security First</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-database"></i></div>
|
||||
<h3>Pluggable Storage</h3>
|
||||
<p>Use SQLite in-memory for testing, SQLite disk for standalone, or Valkey/Redis for massive distributed cluster deployments.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag">SQLite</span>
|
||||
<span class="tag">Valkey/Redis</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-server"></i></div>
|
||||
<h3>Production Ready</h3>
|
||||
<p>Built-in Prometheus metrics, liveness/readiness probes, structured logging, systemd integration, and graceful shutdown.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag">Prometheus</span>
|
||||
<span class="tag">Observability</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Architecture Section -->
|
||||
<section id="architecture-sec" class="section animate">
|
||||
<div class="section-header">
|
||||
<h2>System Architecture</h2>
|
||||
<p>A look inside how ChronoSeal protects your sessions in real-time</p>
|
||||
</div>
|
||||
<div class="architecture-container">
|
||||
<!-- Inline diagram container dynamically drawn by diagrams.js -->
|
||||
<div id="diagram-architecture" class="arch-diagram"></div>
|
||||
</div>
|
||||
<div class="text-center" style="margin-top:24px">
|
||||
<a href="architecture.html" class="btn btn-secondary btn-sm">Read Architecture Docs</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Protocol Section -->
|
||||
<section id="protocol-sec" class="section animate">
|
||||
<div class="section-header">
|
||||
<h2>Protocol Flow</h2>
|
||||
<p>Secure handshake and continuous background verification</p>
|
||||
</div>
|
||||
<div class="code-block code-block-center">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">Sequence Flow Diagram</span>
|
||||
<button class="code-copy-btn">Copy</button>
|
||||
</div>
|
||||
<pre><code>┌─────────────┐ ┌─────────────┐
|
||||
│ Browser │ │ Server │
|
||||
│ (WASM) │ │ (ChronoSeal)│
|
||||
└──────┬──────┘ └──────┬──────┘
|
||||
│ │
|
||||
│ POST /init { public_key } │
|
||||
│─────────────────────────────────────────────────>│
|
||||
│ │
|
||||
│ 200 { session_id, salt, opcodes, │
|
||||
│ initial_hash, mutation_order } │
|
||||
│<─────────────────────────────────────────────────│
|
||||
│ │
|
||||
│ [Execute VM, Preview Mutation] │
|
||||
│ │
|
||||
│ POST /hb { prev_hash, timestamp, entropy, │
|
||||
│ stack_state, gene_commitment, │
|
||||
│ signature } │
|
||||
│─────────────────────────────────────────────────>│
|
||||
│ │
|
||||
│ [Verify: Signature → Hash → Mutation → Drift] │
|
||||
│ │
|
||||
│ 200 { status: "ok", next_salt, │
|
||||
│ next_mutation_step, next_order } │
|
||||
│<─────────────────────────────────────────────────│
|
||||
│ │
|
||||
│ [Commit Preview, Rotate State] │
|
||||
│ │
|
||||
▼ ▼</code></pre>
|
||||
</div>
|
||||
<div class="text-center" style="margin-top:24px">
|
||||
<a href="protocol.html" class="btn btn-secondary btn-sm">Read Protocol Docs</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Comparison Section -->
|
||||
<section id="comparison-sec" class="section animate">
|
||||
<div class="section-header">
|
||||
<h2>vs Popular Solutions</h2>
|
||||
<p>Why modern privacy-conscious teams choose ChronoSeal</p>
|
||||
</div>
|
||||
<div class="comparison-table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>ChronoSeal</th>
|
||||
<th>Cloudflare Turnstile</th>
|
||||
<th>reCAPTCHA v3</th>
|
||||
<th>Enterprise WAFs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Self-hosted & Air-gapped</td>
|
||||
<td class="check"><i class="fas fa-check-circle"></i> Yes</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Privacy Focused</td>
|
||||
<td class="check"><i class="fas fa-check-circle"></i> Excellent</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Medium</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Poor</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Low</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cryptographic Continuity</td>
|
||||
<td class="check"><i class="fas fa-check-circle"></i> Very High</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Medium</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Low</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Medium</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cost Structure</td>
|
||||
<td class="check"><i class="fas fa-check-circle"></i> Free (FOSS)</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Freemium</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Free → Paid</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> Extremely High</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WASM Mutation Engine</td>
|
||||
<td class="check"><i class="fas fa-check-circle"></i> Unique</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Silent Rejection Architecture</td>
|
||||
<td class="check"><i class="fas fa-check-circle"></i> Yes</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
<td class="times"><i class="fas fa-times-circle"></i> No</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center" style="margin-top:24px">
|
||||
<a href="comparison.html" class="btn btn-secondary btn-sm">Detailed Comparison</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Deployment Section -->
|
||||
<section id="deployment-sec" class="section animate">
|
||||
<div class="section-header">
|
||||
<h2>Quick Deployment</h2>
|
||||
<p>Spin up the daemon in under a minute</p>
|
||||
</div>
|
||||
<div class="cards-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fab fa-docker"></i></div>
|
||||
<h3>Docker</h3>
|
||||
<div class="code-block" style="margin-top: 1rem; border: none; padding: 1rem; background: rgba(0,0,0,0.3);">
|
||||
<pre><code>docker run -d -p 3000:3000 \
|
||||
chronoseal/chronoseal:latest</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-cubes"></i></div>
|
||||
<h3>Docker Compose</h3>
|
||||
<div class="code-block" style="margin-top: 1rem; border: none; padding: 1rem; background: rgba(0,0,0,0.3);">
|
||||
<pre><code>bash scripts/build.sh
|
||||
docker compose up -d --build</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fab fa-linux"></i></div>
|
||||
<h3>Native (systemd)</h3>
|
||||
<div class="code-block" style="margin-top: 1rem; border: none; padding: 1rem; background: rgba(0,0,0,0.3);">
|
||||
<pre><code>curl -sL https://chronoseal.io/install.sh | sudo bash
|
||||
sudo systemctl enable --now chronoseal</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-terminal"></i></div>
|
||||
<h3>From Source</h3>
|
||||
<div class="code-block" style="margin-top: 1rem; border: none; padding: 1rem; background: rgba(0,0,0,0.3);">
|
||||
<pre><code>git clone https://github.com/thakares/chronoseal-rs
|
||||
cd chronoseal && cargo run --release</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-network-wired"></i></div>
|
||||
<h3>Nginx Proxy</h3>
|
||||
<div class="code-block" style="margin-top: 1rem; border: none; padding: 1rem; background: rgba(0,0,0,0.3);">
|
||||
<pre><code>location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fas fa-heartbeat"></i></div>
|
||||
<h3>Verification</h3>
|
||||
<div class="code-block" style="margin-top: 1rem; border: none; padding: 1rem; background: rgba(0,0,0,0.3);">
|
||||
<pre><code># Check daemon health status
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# Query daemon CLI metrics
|
||||
chronoseal status --format json</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h4>
|
||||
<img class="logobar" src="assets/logo.svg" alt="ChronoSeal Logo" style="width: 24px; height: 24px; vertical-align: middle;"> ChronoSeal
|
||||
</h4>
|
||||
<p>Next-generation browser attestation daemon built for the modern web.</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Resources</h4>
|
||||
<a href="philosophy.html">Design Philosophy</a><br/>
|
||||
<a href="architecture.html">Architecture</a><br/>
|
||||
<a href="protocol.html">Protocol</a><br/>
|
||||
<a href="api.html">API Reference</a><br/>
|
||||
<a href="comparison.html">Comparison</a>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Community</h4>
|
||||
<a href="https://github.com/thakares/chronoseal-rs" target="_blank" rel="noopener"><i class="fab fa-github"></i> GitHub</a><br/>
|
||||
<a href="deployment.html"><i class="fas fa-rocket"></i> Deployment</a><br/>
|
||||
<a href="testing.html"><i class="fas fa-vial"></i> Testing</a><br/>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>Legal & Security</h4>
|
||||
<a href="security.html">Security Policy</a>
|
||||
<a href="privacy.html">Privacy Policy</a>
|
||||
<a href="performance.html">Performance Tuning</a>
|
||||
<a href="operations.html">Operations Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 ChronoSeal Project. Built with 🦀 Rust and ❤️ for open source.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Search Overlay -->
|
||||
<div class="search-overlay">
|
||||
<div class="search-box">
|
||||
<div class="search-input-wrap">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="Search documentation... (Esc to close)">
|
||||
<span class="search-kbd">/</span>
|
||||
</div>
|
||||
<div class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to top -->
|
||||
<a href="#" class="back-to-top" aria-label="Back to top">▲</a>
|
||||
|
||||
<!-- JS Scripts -->
|
||||
<script src="js/search.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/pwa.js"></script>
|
||||
<script src="js/diagrams.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,184 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — app.js
|
||||
Core runtime: header scroll, mobile nav, scroll animations,
|
||||
code copy, back-to-top, keyboard shortcuts
|
||||
============================================================ */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* ---- Header scroll state ---- */
|
||||
var header = document.querySelector('.navbar');
|
||||
if (header) {
|
||||
var onScroll = function () {
|
||||
header.classList.toggle('scrolled', window.scrollY > 20);
|
||||
};
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
}
|
||||
|
||||
/* ---- Enhanced Mobile nav toggle ---- */
|
||||
var toggle = document.querySelector('.nav-toggle');
|
||||
var navLinks = document.querySelector('.nav-links');
|
||||
if (toggle && navLinks) {
|
||||
var menuIcon = toggle.querySelector('i');
|
||||
toggle.addEventListener('click', function () {
|
||||
var isActive = navLinks.classList.contains('active');
|
||||
if (isActive) {
|
||||
navLinks.classList.remove('active');
|
||||
if (menuIcon) {
|
||||
menuIcon.classList.remove('fa-times');
|
||||
menuIcon.classList.add('fa-bars');
|
||||
}
|
||||
} else {
|
||||
navLinks.classList.add('active');
|
||||
if (menuIcon) {
|
||||
menuIcon.classList.remove('fa-bars');
|
||||
menuIcon.classList.add('fa-times');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Close nav on link click
|
||||
navLinks.querySelectorAll('a').forEach(function (a) {
|
||||
a.addEventListener('click', function () {
|
||||
navLinks.classList.remove('active');
|
||||
if (menuIcon) {
|
||||
menuIcon.classList.remove('fa-times');
|
||||
menuIcon.classList.add('fa-bars');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Doc sidebar mobile toggle ---- */
|
||||
var sidebarToggle = document.querySelector('.doc-sidebar-toggle');
|
||||
var sidebar = document.querySelector('.doc-sidebar');
|
||||
if (sidebarToggle && sidebar) {
|
||||
sidebarToggle.addEventListener('click', function () {
|
||||
sidebar.classList.toggle('open');
|
||||
});
|
||||
// Close sidebar on link click (mobile)
|
||||
sidebar.querySelectorAll('.doc-nav-item').forEach(function (a) {
|
||||
a.addEventListener('click', function () {
|
||||
if (window.innerWidth <= 900) sidebar.classList.remove('open');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Intersection Observer for Scroll Animations ---- */
|
||||
if ('IntersectionObserver' in window) {
|
||||
var observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' };
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('in-view');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all animated elements
|
||||
document.querySelectorAll('.animate, .card, .stat-card, .step').forEach(function (el) {
|
||||
if (!el.classList.contains('animate')) {
|
||||
el.classList.add('animate');
|
||||
}
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Code block copy buttons ---- */
|
||||
document.querySelectorAll('.code-copy-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var pre = btn.closest('.code-block').querySelector('pre');
|
||||
if (!pre) return;
|
||||
var text = pre.textContent;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
|
||||
});
|
||||
} else {
|
||||
// Fallback
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;left:-999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- Back to top ---- */
|
||||
var btt = document.querySelector('.back-to-top');
|
||||
if (btt) {
|
||||
window.addEventListener('scroll', function () {
|
||||
btt.classList.toggle('visible', window.scrollY > 400);
|
||||
}, { passive: true });
|
||||
btt.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Keyboard shortcut: / or Ctrl+K for search ---- */
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === '/' && !isInput(e.target)) {
|
||||
e.preventDefault();
|
||||
if (typeof ChronoSearch !== 'undefined') ChronoSearch.open();
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (typeof ChronoSearch !== 'undefined') ChronoSearch.open();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (typeof ChronoSearch !== 'undefined') ChronoSearch.close();
|
||||
}
|
||||
});
|
||||
|
||||
function isInput(el) {
|
||||
var tag = el.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
||||
}
|
||||
|
||||
/* ---- Nav dropdown (desktop hover + mobile tap) ---- */
|
||||
document.querySelectorAll('.nav-dropdown').forEach(function (dd) {
|
||||
var trigger = dd.querySelector('.nav-link');
|
||||
if (!trigger) return;
|
||||
trigger.addEventListener('click', function (e) {
|
||||
if (window.innerWidth <= 768) {
|
||||
e.preventDefault();
|
||||
dd.classList.toggle('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- Active nav link highlight ---- */
|
||||
var currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
||||
document.querySelectorAll('.nav-links a, .doc-nav-item').forEach(function (link) {
|
||||
var href = link.getAttribute('href');
|
||||
if (!href) return;
|
||||
var linkPage = href.split('/').pop().split('#')[0] || 'index.html';
|
||||
if (linkPage === currentPage) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- Smooth anchor scroll ---- */
|
||||
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
var id = a.getAttribute('href').substring(1);
|
||||
var target = document.getElementById(id);
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.pushState(null, '', '#' + id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,160 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — diagrams.js
|
||||
Generates SVG architecture & protocol flow diagrams inline
|
||||
============================================================ */
|
||||
|
||||
var ChronoDiagrams = (function () {
|
||||
'use strict';
|
||||
|
||||
function drawArchitecture(containerId) {
|
||||
var el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
|
||||
var svg = '<svg viewBox="0 0 760 420" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;max-width:760px">';
|
||||
|
||||
/* Background */
|
||||
svg += '<rect width="760" height="420" rx="12" fill="#131620"/>';
|
||||
|
||||
/* Browser box */
|
||||
svg += '<rect x="40" y="30" width="280" height="170" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="180" y="55" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">BROWSER</text>';
|
||||
|
||||
svg += '<rect x="60" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="115" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">WASM Runtime</text>';
|
||||
|
||||
svg += '<rect x="190" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="245" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">heartbeat.js</text>';
|
||||
|
||||
svg += '<rect x="60" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="115" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Ed25519 Keys</text>';
|
||||
|
||||
svg += '<rect x="190" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="245" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Gene Engine</text>';
|
||||
|
||||
svg += '<text x="180" y="185" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">Entropy · Signing · VM · Mutation</text>';
|
||||
|
||||
/* Arrow: Browser → Server */
|
||||
svg += '<line x1="320" y1="115" x2="430" y2="115" stroke="#7b7e85" stroke-width="1.5" stroke-dasharray="6 3"/>';
|
||||
svg += '<polygon points="428,110 438,115 428,120" fill="#7b7e85"/>';
|
||||
svg += '<text x="375" y="105" fill="#6b6f7a" font-size="8" text-anchor="middle" font-family="Inter,sans-serif">POST /hb</text>';
|
||||
svg += '<text x="375" y="135" fill="#6b6f7a" font-size="8" text-anchor="middle" font-family="Inter,sans-serif">POST /init</text>';
|
||||
|
||||
/* Server box */
|
||||
svg += '<rect x="440" y="30" width="280" height="170" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="580" y="55" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">SERVER (chronoseal)</text>';
|
||||
|
||||
svg += '<rect x="460" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="515" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Axum Router</text>';
|
||||
|
||||
svg += '<rect x="590" y="70" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="645" y="93" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Rate Limiter</text>';
|
||||
|
||||
svg += '<rect x="460" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="515" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Verifier</text>';
|
||||
|
||||
svg += '<rect x="590" y="120" width="110" height="36" rx="5" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="645" y="143" fill="#e1e1e4" font-size="10" text-anchor="middle" font-family="Inter,sans-serif">Gene Engine</text>';
|
||||
|
||||
svg += '<text x="580" y="185" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">Verify · Advance · Trust · CAS</text>';
|
||||
|
||||
/* Shared box (bottom center) */
|
||||
svg += '<rect x="230" y="240" width="300" height="60" rx="8" fill="#1e2230" stroke="#b0b2b8" stroke-width="1" stroke-dasharray="4 3"/>';
|
||||
svg += '<text x="380" y="266" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">shared crate</text>';
|
||||
svg += '<text x="380" y="284" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">Protocol · Hashing · Gene · VM · Constants</text>';
|
||||
|
||||
/* Arrows to shared */
|
||||
svg += '<line x1="180" y1="200" x2="310" y2="244" stroke="#7b7e85" stroke-width="1" stroke-dasharray="4 3"/>';
|
||||
svg += '<line x1="580" y1="200" x2="450" y2="244" stroke="#7b7e85" stroke-width="1" stroke-dasharray="4 3"/>';
|
||||
|
||||
/* Storage box */
|
||||
svg += '<rect x="440" y="340" width="280" height="55" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="580" y="365" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">STORAGE</text>';
|
||||
svg += '<text x="580" y="382" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">SQLite In-Memory · SQLite Disk · Valkey</text>';
|
||||
|
||||
/* Server → Storage arrow */
|
||||
svg += '<line x1="580" y1="200" x2="580" y2="340" stroke="#7b7e85" stroke-width="1.5" stroke-dasharray="6 3"/>';
|
||||
svg += '<polygon points="575,338 580,348 585,338" fill="#7b7e85"/>';
|
||||
|
||||
/* CLI box */
|
||||
svg += '<rect x="40" y="340" width="170" height="55" rx="8" fill="#1e2230" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<text x="125" y="365" fill="#b0b2b8" font-size="11" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">CLI</text>';
|
||||
svg += '<text x="125" y="382" fill="#6b6f7a" font-size="9" text-anchor="middle" font-family="Inter,sans-serif">status · health · metrics · stats</text>';
|
||||
|
||||
svg += '</svg>';
|
||||
el.innerHTML = svg;
|
||||
}
|
||||
|
||||
function drawProtocolFlow(containerId) {
|
||||
var el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
|
||||
var svg = '<svg viewBox="0 0 600 520" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;max-width:600px">';
|
||||
svg += '<rect width="600" height="520" rx="12" fill="#131620"/>';
|
||||
|
||||
/* Columns */
|
||||
svg += '<text x="150" y="30" fill="#b0b2b8" font-size="12" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">Browser</text>';
|
||||
svg += '<text x="450" y="30" fill="#b0b2b8" font-size="12" font-weight="700" text-anchor="middle" font-family="Inter,sans-serif">Server</text>';
|
||||
|
||||
/* Lifelines */
|
||||
svg += '<line x1="150" y1="44" x2="150" y2="500" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
svg += '<line x1="450" y1="44" x2="450" y2="500" stroke="#2a2f40" stroke-width="1.5"/>';
|
||||
|
||||
var y = 70;
|
||||
var arrows = [
|
||||
{ from: 150, to: 450, label: 'Generate Ed25519 keypair', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'POST /init { public_key }', note: '', dir: 'right' },
|
||||
{ from: 450, to: 150, label: '{ session_id, salt, opcodes, gene_size, mutation_order }', note: '', dir: 'left' },
|
||||
{ from: 150, to: 450, label: 'Execute VM program', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'Collect entropy + sign payload', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'Preview gene commitment', note: '', dir: 'self' },
|
||||
{ from: 150, to: 450, label: 'POST /hb { session_id, hash, signature, gene_commitment, … }', note: '', dir: 'right' },
|
||||
{ from: 450, to: 150, label: 'Verify chain + signature + gene + trust', note: '', dir: 'selfr' },
|
||||
{ from: 450, to: 150, label: '{ next_salt, next_mutation_step, next_mutation_order }', note: 'accepted', dir: 'left' },
|
||||
{ from: 450, to: 150, label: '{ status: "ok" }', note: 'rejected (silent)', dir: 'left' },
|
||||
];
|
||||
|
||||
for (var i = 0; i < arrows.length; i++) {
|
||||
var a = arrows[i];
|
||||
if (a.dir === 'right') {
|
||||
svg += '<line x1="155" y1="' + y + '" x2="445" y2="' + y + '" stroke="#7b7e85" stroke-width="1.2"/>';
|
||||
svg += '<polygon points="443,' + (y - 4) + ' 450,' + y + ' 443,' + (y + 4) + '" fill="#7b7e85"/>';
|
||||
svg += '<text x="300" y="' + (y - 8) + '" fill="#e1e1e4" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
} else if (a.dir === 'left') {
|
||||
svg += '<line x1="445" y1="' + y + '" x2="155" y2="' + y + '" stroke="#7b7e85" stroke-width="1.2"/>';
|
||||
svg += '<polygon points="157,' + (y - 4) + ' 150,' + y + ' 157,' + (y + 4) + '" fill="#7b7e85"/>';
|
||||
svg += '<text x="300" y="' + (y - 8) + '" fill="#e1e1e4" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
if (a.note) {
|
||||
svg += '<text x="300" y="' + (y + 14) + '" fill="#6b6f7a" font-size="8" text-anchor="middle" font-style="italic" font-family="Inter,sans-serif">' + a.note + '</text>';
|
||||
y += 8;
|
||||
}
|
||||
} else if (a.dir === 'self') {
|
||||
svg += '<rect x="60" y="' + (y - 12) + '" width="180" height="22" rx="4" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="150" y="' + (y + 3) + '" fill="#9a9da6" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
} else if (a.dir === 'selfr') {
|
||||
svg += '<rect x="360" y="' + (y - 12) + '" width="180" height="22" rx="4" fill="#252a3a" stroke="#2a2f40"/>';
|
||||
svg += '<text x="450" y="' + (y + 3) + '" fill="#9a9da6" font-size="8.5" text-anchor="middle" font-family="Inter,sans-serif">' + a.label + '</text>';
|
||||
}
|
||||
y += 44;
|
||||
}
|
||||
|
||||
svg += '</svg>';
|
||||
el.innerHTML = svg;
|
||||
}
|
||||
|
||||
/* ---- Auto-init ---- */
|
||||
function init() {
|
||||
drawArchitecture('diagram-architecture');
|
||||
drawProtocolFlow('diagram-protocol');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return {
|
||||
drawArchitecture: drawArchitecture,
|
||||
drawProtocolFlow: drawProtocolFlow
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,41 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — pwa.js
|
||||
Progressive Web App: service worker registration + install
|
||||
============================================================ */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/sw.js').then(function (reg) {
|
||||
/* Update found — silent refresh */
|
||||
reg.addEventListener('updatefound', function () {
|
||||
var worker = reg.installing;
|
||||
if (!worker) return;
|
||||
worker.addEventListener('statechange', function () {
|
||||
if (worker.state === 'activated' && navigator.serviceWorker.controller) {
|
||||
/* New version available — user can refresh */
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch(function () {
|
||||
/* SW registration failed — app still works */
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Installable PWA banner (A2HS) ---- */
|
||||
var deferredPrompt = null;
|
||||
|
||||
window.addEventListener('beforeinstallprompt', function (e) {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
/* Could show a custom install banner here */
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', function () {
|
||||
deferredPrompt = null;
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,144 @@
|
||||
/* ============================================================
|
||||
ChronoSeal Website — search.js
|
||||
Client-side full-text search across all pages
|
||||
============================================================ */
|
||||
|
||||
var ChronoSearch = (function () {
|
||||
'use strict';
|
||||
|
||||
/* ---- Search index ---- */
|
||||
var pages = [
|
||||
{ title: 'Home', url: 'index.html', desc: 'ChronoSeal overview, features, and quick start', keywords: 'home overview features quick start install' },
|
||||
{ title: 'Architecture', url: 'architecture.html', desc: 'System architecture, crate layout, and data flow', keywords: 'architecture crates workspace wasm shared server' },
|
||||
{ title: 'Protocol', url: 'protocol.html', desc: 'Heartbeat protocol, hash chain, and mutation engine', keywords: 'protocol heartbeat hash chain blake3 ed25519 mutation gene' },
|
||||
{ title: 'API Reference', url: 'api.html', desc: 'HTTP endpoints: /init, /hb, /health, /metrics, /stats', keywords: 'api http post init heartbeat health metrics stats json' },
|
||||
{ title: 'Deployment', url: 'deployment.html', desc: 'Installation, systemd, Docker, nginx reverse proxy', keywords: 'deploy install systemd docker nginx proxy production' },
|
||||
{ title: 'Threat Model', url: 'threat-model.html', desc: 'Security threat model and defense boundaries', keywords: 'threat model security attack replay automation defense' },
|
||||
{ title: 'Performance', url: 'performance.html', desc: 'Performance tuning, benchmarks, and optimization', keywords: 'performance tuning benchmark latency throughput' },
|
||||
{ title: 'Philosophy', url: 'philosophy.html', desc: 'Design philosophy and engineering priorities', keywords: 'philosophy design unix privacy operator control' },
|
||||
{ title: 'Privacy Policy', url: 'privacy.html', desc: 'Data handling, storage, and privacy commitments', keywords: 'privacy policy data collection storage session' },
|
||||
{ title: 'Security', url: 'security.html', desc: 'Security assumptions, hardening, and response headers', keywords: 'security assumptions headers hardening csp' },
|
||||
{ title: 'Operations', url: 'operations.html', desc: 'Monitoring, health checks, metrics, and logging', keywords: 'operations health status metrics stats logging monitor' },
|
||||
{ title: 'Testing', url: 'testing.html', desc: 'Test suite, fuzzing, and validation coverage', keywords: 'testing tests cargo fuzz validation fingerprint' },
|
||||
{ title: 'Comparison', url: 'comparison.html', desc: 'ChronoSeal vs commercial anti-bot solutions', keywords: 'comparison cloudflare akamai recaptcha datadome kasada' },
|
||||
];
|
||||
|
||||
var overlay = null;
|
||||
var input = null;
|
||||
var results = null;
|
||||
|
||||
function init() {
|
||||
overlay = document.querySelector('.search-overlay');
|
||||
input = document.querySelector('.search-input');
|
||||
results = document.querySelector('.search-results');
|
||||
if (!overlay || !input || !results) return;
|
||||
|
||||
input.addEventListener('input', debounce(onInput, 150));
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) close();
|
||||
});
|
||||
|
||||
// Keyboard nav inside results
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
navigateResults(e.key === 'ArrowDown' ? 1 : -1);
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
var sel = results.querySelector('.search-result.selected');
|
||||
if (sel) { window.location.href = sel.getAttribute('href'); close(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!overlay) init();
|
||||
if (!overlay) return;
|
||||
overlay.classList.add('open');
|
||||
input.value = '';
|
||||
results.innerHTML = renderHints();
|
||||
setTimeout(function () { input.focus(); }, 100);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (overlay) overlay.classList.remove('open');
|
||||
}
|
||||
|
||||
function onInput() {
|
||||
var q = input.value.trim().toLowerCase();
|
||||
if (!q) { results.innerHTML = renderHints(); return; }
|
||||
|
||||
var matches = [];
|
||||
for (var i = 0; i < pages.length; i++) {
|
||||
var p = pages[i];
|
||||
var hay = (p.title + ' ' + p.desc + ' ' + p.keywords).toLowerCase();
|
||||
if (hay.indexOf(q) !== -1) {
|
||||
matches.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
results.innerHTML = '<div class="search-empty">No results for “' + escHtml(q) + '”</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
for (var j = 0; j < matches.length; j++) {
|
||||
var m = matches[j];
|
||||
html += '<a class="search-result' + (j === 0 ? ' selected' : '') + '" href="' + m.url + '">';
|
||||
html += '<div class="search-result-title">' + highlightMatch(m.title, q) + '</div>';
|
||||
html += '<div class="search-result-desc">' + highlightMatch(m.desc, q) + '</div>';
|
||||
html += '</a>';
|
||||
}
|
||||
results.innerHTML = html;
|
||||
}
|
||||
|
||||
function navigateResults(dir) {
|
||||
var items = results.querySelectorAll('.search-result');
|
||||
if (!items.length) return;
|
||||
var idx = -1;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].classList.contains('selected')) { idx = i; break; }
|
||||
}
|
||||
if (idx >= 0) items[idx].classList.remove('selected');
|
||||
idx = Math.max(0, Math.min(items.length - 1, idx + dir));
|
||||
items[idx].classList.add('selected');
|
||||
items[idx].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function renderHints() {
|
||||
return '<div class="search-empty">Type to search documentation…<br><span style="font-size:.78rem;color:var(--text-muted)">↑↓ Navigate · ↵ Open · Esc Close</span></div>';
|
||||
}
|
||||
|
||||
function highlightMatch(text, q) {
|
||||
var idx = text.toLowerCase().indexOf(q);
|
||||
if (idx === -1) return escHtml(text);
|
||||
return escHtml(text.substring(0, idx)) +
|
||||
'<mark style="background:rgba(176,178,184,.2);color:var(--text-primary);border-radius:2px;padding:0 2px">' +
|
||||
escHtml(text.substring(idx, idx + q.length)) + '</mark>' +
|
||||
escHtml(text.substring(idx + q.length));
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(s));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
var t;
|
||||
return function () {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(fn, ms);
|
||||
};
|
||||
}
|
||||
|
||||
/* ---- Auto-init ---- */
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return { open: open, close: close };
|
||||
})();
|
||||
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="296.99997mm"
|
||||
viewBox="0 0 1122.5196 793.7008"
|
||||
enable-background="new 0 0 1254 1254"
|
||||
xml:space="preserve"
|
||||
height="210mm"
|
||||
sodipodi:docname="logo1.svg"
|
||||
inkscape:export-filename="logo1.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.1654266"
|
||||
inkscape:cx="561.16791"
|
||||
inkscape:cy="396.85039"
|
||||
inkscape:window-width="2048"
|
||||
inkscape:window-height="1205"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><defs
|
||||
id="defs44" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<path
|
||||
fill="none"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path2" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path45"
|
||||
style="fill:#e1e1e4;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path46"
|
||||
style="fill:#b0b2b8;fill-opacity:1" /><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path47"
|
||||
style="fill:#7b7e85;fill-opacity:1" /><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;fill:#e1e1e4;fill-opacity:1;stroke-width:39.1"
|
||||
x="154.54701"
|
||||
y="550.13623"
|
||||
id="text47"><tspan
|
||||
id="tspan47"
|
||||
x="154.54701"
|
||||
y="550.13623" /><tspan
|
||||
id="tspan48"
|
||||
x="154.54701"
|
||||
y="750.13623"
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:160px;font-family:'URW Gothic';-inkscape-font-specification:'URW Gothic, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1f2024;fill-opacity:1"
|
||||
id="tspan49">chrono</tspan>seal</tspan></text><path
|
||||
fill="#4b4d51"
|
||||
opacity="1"
|
||||
stroke="none"
|
||||
d="m 791.89376,306.36264 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z"
|
||||
id="path1"
|
||||
style="fill:#4a4d53;fill-opacity:1" /></svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |