34 Commits
Author SHA1 Message Date
thakares 480d8e642d Enhance website branding, assets and search indexing
Rust / build (push) Canceled after 0s
2026-06-05 15:02:01 +05:30
thakares 414b74b95f Improve README formatting for daemon and runtime sizes
Updated formatting for size and security features in README.
2026-06-05 14:01:42 +05:30
thakares 227f5ff922 Update README with performance and security details
Added performance metrics and security features to README.
2026-06-05 13:59:42 +05:30
thakares 72ae2f4b06 Add runtime footprint section to README
Added a section on runtime footprint detailing deployment sizes and benefits.
2026-06-05 13:57:59 +05:30
thakares 3397ca121b Enhance deployment documentation with security checks
Added sections for binary hardening verification, runtime verification, and deployment footprint details.
2026-06-05 13:55:16 +05:30
thakares 1e038901b3 Add ChronoSeal product website 2026-06-05 12:40:10 +05:30
thakares 6c11495892 Fix formatting for protocol request examples 2026-06-04 20:54:41 +05:30
thakares a1a2e9d571 Update v1.0.2 documentation and security guidance 2026-06-04 20:47:22 +05:30
thakares f4c4beb6b5 Update testing documentation for v1.0.2 2026-06-04 20:21:39 +05:30
thakares 1004a39667 Bump version to 1.0.2 in Cargo.toml 2026-06-04 20:14:20 +05:30
thakares a7cc533ed4 Update architecture documentation for v1.0.2 2026-06-04 20:11:43 +05:30
thakares 40877be160 Update README for v1.0.2 release 2026-06-04 20:05:55 +05:30
thakares 8d119ac00e Harden validation, improve runtime security and refactor core services
Rust / build (push) Canceled after 0s
2026-06-04 19:58:21 +05:30
thakares aebef4c623 Update version badge to v1.0.1 in README.md 2026-05-30 21:49:11 +05:30
thakares b81b7b0e15 Remove .personal from .gitignore
Remove .personal from .gitignore
2026-05-30 21:31:27 +05:30
thakares 3f445eead5 docs: update TESTING.md for v1.0.1 — 95 tests, proptest section, known gaps 2026-05-30 21:24:29 +05:30
thakares 3225509713 Begin post-v1.0.1 protocol and runtime improvements 2026-05-30 21:00:22 +05:30
thakares ecb1721ff4 Align crate versions with v1.0.1 release 2026-05-30 20:58:19 +05:30
thakares d965451d4f Add missing proptest dev dependency
Rust / build (push) Canceled after 0s
2026-05-30 20:48:46 +05:30
thakares 1a58ef9796 Release v1.0.0: protocol freeze, replay testing, fuzzing and audit readiness
Rust / build (push) Canceled after 0s
2026-05-30 20:09:24 +05:30
thakares c7873b429d Enhance v0.6.1 testing documentation
- Expand TESTING.md with comprehensive test coverage details
- Document server, WASM, and shared crate test suites
- Highlight critical security and parity tests
- Improve testing philosophy and contributor guidance
- Record current 89-test validation baseline
2026-05-29 23:04:42 +05:30
thakares 3679e6808b Expand v0.6.1 documentation suite
Rust / build (push) Canceled after 0s
- Add COMPARISON.md for architecture and positioning analysis
- Add PERFORMANCE-TUNING.md for mutation engine optimization guidance
- Add TESTING.md documenting the 89-test security-focused test suite
- Document server, WASM, and shared crate test coverage
- Add operational guidance for tuning, validation, and verification
- Improve project maintainability and contributor onboarding
2026-05-29 22:56:08 +05:30
thakares fc2d693518 Add comparison and performance tuning documentation
Rust / build (push) Canceled after 0s
2026-05-29 22:34:13 +05:30
thakares 0ed3cb444d Refactor attestation engine and synchronize project documentation
- Refine session and storage lifecycle handling
- Improve VM extension architecture across server, shared, and WASM runtimes
- Enhance synthetic gene mutation engine integration and parity guarantees
- Align deterministic state progression between server and browser execution paths
- Update configuration examples and deployment guidance
- Expand architecture, API, threat model, privacy, and WASM build documentation
- Refresh README with comprehensive project overview, operational workflows,
  browser integration details, storage backend documentation, and security model
- Document v0.6.0 refactoring outcomes and design rationale
- Improve consistency across documentation, configuration, and implementation

This commit consolidates the v0.6.0 architectural refactoring effort,
strengthening deterministic browser/server parity while improving
maintainability, operational clarity, and project documentation.
2026-05-29 21:55:08 +05:30
thakares 2b8afd54e0 docs: update README and docs for v0.6.0 architecture and deployment, preserve current server/shared/wasm updates 2026-05-29 21:27:11 +05:30
thakares 6067746898 Suppress dead_code warnings for VM execution helpers 2026-05-29 18:13:33 +05:30
thakares 3d2a1a0ad7 Delete LICENSE-MIT 2026-05-29 17:47:49 +05:30
thakares 815d29af4c fix: correct license badge to MIT OR Apache-2.0 2026-05-29 16:11:40 +05:30
thakares b2835f454f docs: update ARCHITECTURE.md for v0.6.0 gene mutation system 2026-05-29 15:55:33 +05:30
thakares 4a64a57347 Update README.md 2026-05-29 15:52:00 +05:30
thakares 9d52828bb6 fix: correct license badge to MIT OR Apache-2.0 2026-05-29 15:44:07 +05:30
thakares 19666bd608 fix: correct license badge to MIT OR Apache-2.0 2026-05-29 15:42:58 +05:30
thakares 089a834f96 docs: comprehensive README with v0.6.0 gene mutation system, contributing & security policy
- Add v0.6.0 synthetic gene mutation system section (from REFRACTORING-v0.6.0.md)
- Add contributing guidelines, security policy, language breakdown
- Add badge bar, mutation threat row, topics tags
- No existing content removed
2026-05-29 15:39:39 +05:30
thakares ba768da58e feat: implement v0.6.0 mutation engine and db_type runtime selection
Rust / build (push) Canceled after 0s
2026-05-29 14:50:45 +05:30
122 changed files with 17079 additions and 2124 deletions

No files matched your search

Generated
+1056 -16
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -4,7 +4,8 @@ resolver = "2"
members = [ members = [
"shared", "shared",
"server", "server",
"wasm" "wasm",
"chronoseal-replay"
] ]
[workspace.package] [workspace.package]
+5
View File
@@ -24,4 +24,9 @@ ENV CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend
ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid 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"] CMD ["chronoseal", "run"]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Sunil Purushottam Thakare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+761 -411
View File
File diff suppressed because it is too large. Load diff
+15
View File
@@ -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"] }
+638
View File
@@ -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(&current_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(())
}
+1
View File
@@ -27,6 +27,7 @@ RestrictSUIDSGID=yes
LockPersonality=yes LockPersonality=yes
SystemCallArchitectures=native SystemCallArchitectures=native
ReadWritePaths=/run/chronoseal.pid ReadWritePaths=/run/chronoseal.pid
ReadWritePaths=/var/lib/chronoseal
# Logging # Logging
StandardOutput=journal StandardOutput=journal
+278 -128
View File
@@ -1,20 +1,58 @@
# ChronoSeal — API Reference # ChronoSeal API Reference
ChronoSeal exposes a small HTTP API for browser attestation, heartbeat verification, health checks, metrics, and runtime statistics.
This document describes the wire format and acceptance semantics. The internal state model is covered in [ARCHITECTURE.md](ARCHITECTURE.md).
## Base URL ## Base URL
All endpoints are relative to the server root. In development: `http://localhost:3000`. All paths are relative to the ChronoSeal server root.
In production: your HTTPS domain via reverse proxy.
--- - Development default: `http://127.0.0.1:3000`
- Production: the HTTPS origin or reverse-proxy path used by the protected site
## Endpoints Production deployments should use HTTPS. The daemon itself can run behind a local reverse proxy.
### `POST /init` ## Content Type
Initialise a new session. Called once per page load, immediately after the JSON endpoints expect:
WASM module generates an Ed25519 keypair.
#### Request ```http
Content-Type: application/json
```
Responses are JSON except `/metrics`, which returns Prometheus text format.
## Endpoint Summary
| Method | Path | Purpose |
|---|---|---|
| `POST` | `/init` | Create a browser attestation session |
| `POST` | `/hb` | Submit and verify a signed heartbeat |
| `GET` | `/health` | Return daemon health |
| `GET` | `/stats` | Return storage/session statistics |
| `GET` | `/metrics` | Return Prometheus-compatible metrics |
| `GET` | `/` | Serve static frontend assets from `frontend_dir` |
## Data Types
Common encodings:
| Value | Encoding |
|---|---|
| Ed25519 public key | 32 raw bytes encoded as 64 hex characters |
| Ed25519 signature | 64 raw bytes encoded as 128 hex characters |
| `session_id` | 32 random bytes encoded as 64 hex characters |
| `salt` | 16 random bytes encoded as 32 hex characters |
| `initial_hash`, `prev_hash`, `gene_commitment` | 32-byte digest encoded as 64 hex characters |
| `opcodes_b64`, `mutation_order_b64` | standard base64 |
| timestamps | Unix time in milliseconds unless otherwise stated |
## `POST /init`
Creates a new attestation session.
### Request
```http ```http
POST /init POST /init
@@ -27,42 +65,58 @@ Content-Type: application/json
} }
``` ```
| Field | Type | Description | | Field | Type | Required | Description |
|---|---|---| |---|---|---:|---|
| `public_key` | `string` | Hex-encoded 32-byte Ed25519 verifying key generated by the WASM module | | `public_key` | string | yes | Browser-generated Ed25519 public key as 64 hex characters |
#### Response `200 OK` The private key is generated and retained by the browser WASM runtime. It is not sent to the server.
### Successful Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json ```json
{ {
"session_id": "64-char hex string (32 bytes)", "session_id": "64-char hex string",
"salt": "32-char hex string (16 bytes)", "salt": "32-char hex string",
"opcodes_b64": "base64-encoded VM program (8–16 opcodes)", "opcodes_b64": "base64-encoded VM program",
"initial_hash": "64-char hex string (32 bytes Blake3)", "initial_hash": "64-char hex string",
"expires_at": 1234567890123 "expires_at": 1234567890123,
"heartbeat_min_interval_ms": 12000,
"heartbeat_max_interval_ms": 25000,
"gene_size": 512,
"mutation_step": 1,
"mutation_order_b64": "base64-encoded mutation program"
} }
``` ```
| Field | Type | Description | | Field | Type | Description |
|---|---|---| |---|---|---|
| `session_id` | `string` | Opaque session identifier; include in every heartbeat | | `session_id` | string | Opaque session identifier |
| `salt` | `string` | Initial salt; used to compute `H(0)` and first `H(1)` | | `salt` | string | Current server salt for the first heartbeat hash computation |
| `opcodes_b64` | `string` | Base64 VM program; execute with `run_program()` on every heartbeat | | `opcodes_b64` | string | Randomized VM program executed by the browser runtime |
| `initial_hash` | `string` | `H(0) = Blake3(session_id ║ pub_key ║ salt)`; the first `prev_hash` | | `initial_hash` | string | Initial chain head used as `prev_hash` for the first heartbeat |
| `expires_at` | `number` | Unix timestamp in milliseconds; session expires after 30 minutes of inactivity | | `expires_at` | number | Session expiration timestamp in milliseconds |
| `heartbeat_min_interval_ms` | number | Minimum heartbeat delay recommended by the server |
| `heartbeat_max_interval_ms` | number | Maximum heartbeat delay recommended by the server |
| `gene_size` | number | Initial synthetic gene buffer size |
| `mutation_step` | number | Mutation step expected on the first heartbeat |
| `mutation_order_b64` | string | Server-authored mutation order for the first heartbeat |
#### Error ### Error Behavior
Returns `500 Internal Server Error` only on server-side failures (DB errors, `/init` uses normal route-level error handling for invalid payloads or server failures. Invalid public key length, invalid configured gene size, or storage failure can prevent session creation.
invalid public key length). No meaningful error body is returned.
--- Unlike `/hb`, initialization failures are not part of the silent heartbeat rejection model.
### `POST /hb` ## `POST /hb`
Submit a heartbeat. Called every 12–25 seconds with uniform random jitter. Submits one heartbeat for an existing session.
#### Request ### Request
```http ```http
POST /hb POST /hb
@@ -71,13 +125,12 @@ Content-Type: application/json
```json ```json
{ {
"session_id": "64-char hex", "session_id": "64-char hex",
"prev_hash": "64-char hex", "prev_hash": "64-char hex",
"timestamp": 1234567890123, "timestamp": 1234567890123,
"entropy_data": { "entropy_data": {
"events": [ "events": [
{ "x": 412.0, "y": 308.5, "t": 1234.567 }, { "x": 412.0, "y": 308.5, "t": 1234.567 }
{ "x": 415.2, "y": 310.1, "t": 1285.123 }
] ]
}, },
"stack_state": { "stack_state": {
@@ -85,132 +138,229 @@ Content-Type: application/json
"ip": 42 "ip": 42
}, },
"fingerprint": { "fingerprint": {
"aspectRatio": "1.7777777778", "aspectRatio": "1.7777777778",
"devicePixelRatio": "2", "devicePixelRatio": "2",
"hardwareConcurrency": 8 "hardwareConcurrency": 8
}, },
"signature": "128-char hex Ed25519 signature" "mutation_step": 1,
"gene_commitment": "64-char hex",
"signature": "128-char hex"
}
```
| Field | Type | Required | Description |
|---|---|---:|---|
| `session_id` | string | yes | Session ID from `/init` |
| `prev_hash` | string | yes | Current browser view of the accepted hash-chain head |
| `timestamp` | number | yes | Browser wall-clock timestamp in milliseconds |
| `entropy_data.events` | array | yes | Mouse samples since the previous heartbeat |
| `entropy_data.events[].x` | number | yes | Mouse x coordinate |
| `entropy_data.events[].y` | number | yes | Mouse y coordinate |
| `entropy_data.events[].t` | number | yes | Event timestamp in milliseconds relative to the browser sampling window |
| `stack_state.stack` | array | yes | VM stack output as unsigned 32-bit values |
| `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 | 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 |
### Canonical Signing Payload
The signature covers a canonical JSON object with sorted top-level keys:
```json
{
"entropyData": { "events": [{ "t": 1234.567, "x": 412.0, "y": 308.5 }] },
"fingerprint": {
"aspectRatio": "1.7777777778",
"devicePixelRatio": "2",
"hardwareConcurrency": 8
},
"geneCommitment": "64-char hex",
"mutationStep": 1,
"prevHash": "64-char hex",
"sessionId": "64-char hex",
"stackState": { "ip": 42, "stack": [2971406957, 1234567890] },
"timestamp": 1234567890123
}
```
Important details:
- The transport payload uses snake_case for several fields.
- The signed payload uses camelCase names.
- Top-level keys must be serialized deterministically in lexical order.
- The `signature` field is not part of the signed payload.
- Nested serialization must match the server's `serde_json` representation.
The server reconstructs the canonical message from the received request before verifying the Ed25519 signature.
### Accepted Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
```
```json
{
"status": "ok",
"next_salt": "32-char hex string",
"next_mutation_step": 2,
"next_mutation_order_b64": "base64-encoded mutation program"
} }
``` ```
| Field | Type | Description | | Field | Type | Description |
|---|---|---| |---|---|---|
| `session_id` | `string` | Session ID from `/init` | | `status` | string | Always `ok` |
| `prev_hash` | `string` | Hash chain head from previous heartbeat (or `initial_hash` for the first) | | `next_salt` | string | Server salt for the next heartbeat |
| `timestamp` | `number` | `Date.now()` in milliseconds; must be within ±30s of server time | | `next_mutation_step` | number | Mutation step expected on the next heartbeat |
| `entropy_data.events` | `array` | Mouse events since previous heartbeat; each has `x`, `y` (px), `t` (performance.now ms) | | `next_mutation_order_b64` | string | Server-authored mutation order for the next heartbeat |
| `stack_state.stack` | `array` | `u32[]` result of executing the VM program |
| `stack_state.ip` | `number` | Instruction pointer after execution |
| `fingerprint.aspectRatio` | `string` | `(screen.width / screen.height).toFixed(10)` |
| `fingerprint.devicePixelRatio` | `string` | `String(window.devicePixelRatio)` |
| `fingerprint.hardwareConcurrency` | `number` | `navigator.hardwareConcurrency \|\| 1` |
| `signature` | `string` | Hex-encoded 64-byte Ed25519 signature over the canonical payload |
#### Canonical Signing Payload Clients should treat the heartbeat as accepted only when all next-state fields are present.
The client signs the following JSON object. Top-level keys must be sorted ### Rejected Response
alphabetically. Nested object keys follow their natural serialisation order.
```json ```http
{ HTTP/1.1 200 OK
"entropyData": { "events": [{ "t": …, "x": …, "y": … }] }, Content-Type: application/json
"fingerprint": { "aspectRatio": "…", "devicePixelRatio": "…", "hardwareConcurrency": … },
"prevHash": "…",
"sessionId": "…",
"stackState": { "ip": …, "stack": […] },
"timestamp": …
}
``` ```
Note: field names in the signing payload use camelCase (`sessionId`,
`prevHash`, `entropyData`, `stackState`) while the request body uses
snake_case (`session_id`, `prev_hash`, `entropy_data`, `stack_state`).
#### Response `200 OK` — Accepted
```json
{
"status": "ok",
"next_salt": "32-char hex string (16 bytes)"
}
```
The client must:
1. Capture `sentSalt = currentSalt` before updating.
2. Set `currentSalt = next_salt`.
3. Compute `prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt)`.
#### Response `200 OK` — Rejected
```json ```json
{ {
"status": "ok" "status": "ok"
} }
``` ```
`next_salt` is absent. The response body is intentionally identical in Rejected heartbeats omit:
structure. Rejections are silent — the caller cannot distinguish a validation
failure from a rate limit hit or an expired session.
The client should log a warning and continue scheduling heartbeats (they will - `next_salt`
continue to fail until the page is reloaded and a new session is established). - `next_mutation_step`
- `next_mutation_order_b64`
--- This response shape is intentional. The server does not reveal which validation stage failed.
## Validation Rules (Server-Side) ### Heartbeat Validation Order
Heartbeats are rejected (silently) if any of the following checks fail: The server currently validates heartbeats in this order:
| Check | Condition for rejection | 1. Rate-limit check in the route handler.
|---|---| 2. Load session by `session_id`.
| Rate limit | > 5 requests per 10-second window for this `session_id` | 3. Check session expiration.
| Session not found | `session_id` not in SQLite | 4. Verify Ed25519 signature.
| Session expired | `current_time_ms > expires_at` | 5. Compare `prev_hash` with stored `last_hash`.
| Signature invalid | Ed25519 verification fails against stored public key | 6. Compare `mutation_step` with stored `pending_mutation_step`.
| Hash chain broken | `hex(prev_hash) ≠ stored last_hash` | 7. Apply stored pending mutation to a cloned gene state.
| Timestamp drift | `\|server_now_ms - timestamp\| > 30 000` | 8. Compare expected and submitted `gene_commitment`.
| Insufficient mouse events | `events.len() < 3` | 9. Enforce timestamp drift.
| Insufficient mouse distance | `total_dist < 10.0 px` | 10. Validate mouse entropy.
| Mouse speed too high | `total_dist / total_time_ms > 2.0 px/ms` | 11. Validate fingerprint fields.
| No mouse pauses | `pause_count < 1` | 12. Compute next hash-chain head.
| Invalid aspect ratio | `ar < 0.5` or `ar > 3.0` | 13. Generate next mutation order and salt.
| Invalid devicePixelRatio | `dpr ≤ 0.0` or `dpr > 5.0` | 14. Persist advanced session state.
| Zero hardwareConcurrency | `hardware_concurrency == 0` |
--- Any failure after route-level JSON decoding returns the silent rejection body.
## Hash Chain Specification ## `GET /health`
``` Returns a basic health response.
H(0) = Blake3( session_id_bytes ║ pub_key_bytes ║ salt₀_bytes )
H(n) = Blake3( ```http
saltₙ₋₁_bytes GET /health
║ H(n-1)_bytes
║ timestamp_u64_le_bytes
║ Blake3( UTF-8( JSON(entropy_data) ) )
║ Blake3( UTF-8( JSON(stack_state) ) )
)
``` ```
All inputs are concatenated in the order shown. `timestamp` is encoded as a ```json
64-bit unsigned integer in little-endian byte order. JSON serialisation of {
`entropy_data` and `stack_state` uses the field order defined by the shared "status": "healthy"
Rust types (serde derive, no custom ordering). }
```
--- ## `GET /stats`
## WASM API Returns storage-derived session statistics.
The WASM module (`antibot_wasm`) exports the following functions to JavaScript: ```http
GET /stats
```
```json
{
"sessions": 1,
"expired_sessions": 0,
"max_chain_length": 4
}
```
| Field | Type | Description |
|---|---|---|
| `sessions` | number | Stored session count |
| `expired_sessions` | number | Expired sessions not yet purged |
| `max_chain_length` | number | Highest stored heartbeat chain length |
## `GET /metrics`
Returns Prometheus-compatible text.
```http
GET /metrics
```
```text
# HELP chronoseal_sessions Active ChronoSeal sessions
# TYPE chronoseal_sessions gauge
chronoseal_sessions 1
# HELP chronoseal_expired_sessions Expired sessions not yet removed
# TYPE chronoseal_expired_sessions gauge
chronoseal_expired_sessions 0
# HELP chronoseal_max_chain_length Maximum heartbeat chain length
# TYPE chronoseal_max_chain_length gauge
chronoseal_max_chain_length 4
```
## Client State Rules
After `/init`, the client stores:
- `session_id`
- `initial_hash` as the first `prev_hash`
- current `salt`
- VM opcode program
- committed gene state
- pending mutation step
- pending mutation order
On accepted `/hb`:
1. Commit the local gene preview.
2. Compute the next local hash using the old salt that was active when the heartbeat was sent.
3. Replace current salt with `next_salt`.
4. Replace pending mutation step and order with server-provided values.
On rejected `/hb`:
1. Discard the local gene preview.
2. Do not advance hash-chain state.
3. Do not advance mutation state.
4. Treat the session as suspect or restart attestation.
## WASM Runtime Exports
The generated `chronoseal_wasm` package exposes:
| Function | Signature | Description | | Function | Signature | Description |
|---|---|---| |---|---|---|
| `generate_keypair()` | `() → string` | Generate Ed25519 keypair; return hex public key. Private key stored in WASM memory. | | `generate_keypair()` | `() -> string` | Generate an Ed25519 keypair and return public key hex |
| `get_public_key()` | `() → string` | Return hex public key, or `""` if not initialised. | | `get_public_key()` | `() -> string` | Return current public key hex, or `""` if no keypair exists |
| `sign_message(msg)` | `(string) → string` | Sign UTF-8 string; return hex signature, or `""` if not initialised. | | `sign_message(msg)` | `(string) -> string` | Sign a UTF-8 payload and return hex signature, or `""` on failure |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) → string` | Compute next Blake3 chain hash; all inputs/output hex or JSON strings. | | `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | Compute next Blake3 chain hash |
| `run_program(b64)` | `(string) → JsValue` | Execute base64 VM program; return `{ stack: u32[], ip: number }`. | | `run_program(b64)` | `(string) -> JsValue` | Execute a base64 VM program and return stack state |
| `init_gene_state(gene_size)` | `(u32) -> bool` | Initialize the browser gene state |
| `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` | `(string, string, u64, u8) -> string` | Preview next mutation commitment |
| `commit_gene_preview()` | `() -> bool` | Commit the preview after accepted heartbeat |
| `discard_gene_preview()` | `() -> void` | Discard preview after rejection or error |
| `current_gene_commitment(session_id, mutation_step)` | `(string, u64) -> string` | Return current committed gene commitment |
All functions return empty strings on error rather than panicking. String-returning functions use `""` to signal failure. Callers must handle empty strings explicitly.
Callers must check for empty return values before using the result.
+504 -322
View File
@@ -1,381 +1,563 @@
# ChronoSeal — Architecture # ChronoSeal Architecture
## Overview ChronoSeal is a Unix-native browser attestation daemon. It validates browser session continuity by combining signed heartbeats, Blake3 hash-chain progression, deterministic VM execution, behavioral sanity checks, and a shared Synthetic Gene Mutation Engine that runs on both the server and the browser WASM runtime.
ChronoSeal is a stateless, cryptographic browser attestation framework. Its This document describes the system architecture, state model, validation pipeline, trust boundaries, and operational assumptions. The API wire format is documented separately in [API.md](API.md), and deployment guidance is documented in [DEPLOYMENT.md](DEPLOYMENT.md).
purpose is to make automated clients (headless browsers, AI scrapers, API
harvesters) computationally expensive and operationally complex to operate,
while remaining completely invisible to real human users.
The design is inspired by the heartbeat model used in embedded IoT firmware: ## Architectural Goals
a device that stops sending signed, chained attestations is assumed to be
offline or compromised. ChronoSeal applies the same principle to browser
sessions.
--- ChronoSeal is designed as infrastructure software rather than a consumer-facing widget. The main goals are:
## Design Principles - Keep the server small, inspectable, and operable as a normal Unix daemon.
- Use deterministic client/server computation so the server can verify browser-side progression without trusting browser claims blindly.
- Make replay, stale state reuse, and incomplete automation expensive.
- Preserve privacy by using short-lived session state instead of persistent identity tracking.
- Avoid attacker feedback oracles by returning indistinguishable success-shaped responses for rejected heartbeats.
- Keep browser integration lightweight: static JavaScript plus a Rust-generated WASM package.
**Stateless per request.** The server carries no per-request state beyond what ChronoSeal does not attempt to prove that a human is present. It attempts to prove that a client is maintaining the expected live browser-side cryptographic and mutation state.
is stored in SQLite keyed on `session_id`. Every HTTP request is independently
verifiable.
**Silent failure.** Validation failures never return an error status or an ## System Context
error body. The server always responds `{"status":"ok"}` and simply omits
`next_salt`. The client degrades gracefully. Attackers cannot enumerate
validation rules by probing error responses.
**Private key isolation.** The Ed25519 signing key is generated inside the ```text
WASM module and never serialised, never exposed to the JavaScript environment, Protected browser origin
and never transmitted. It exists only in WASM linear memory for the lifetime |
of the page. | static files and API calls
v
**Layered validation.** A heartbeat must pass five independent checks: session +------------------------------+
existence, expiry, signature, hash chain, and behavioral signals. Bypassing | Browser |
one layer is not sufficient. | - frontend JavaScript |
| - chronoseal_wasm runtime |
**Cost asymmetry.** Each heartbeat requires a real browser environment, mouse | - Ed25519 session key |
activity, correct WASM execution, chain state synchronisation, and a valid | - VM and gene state |
Ed25519 signature over a time-windowed payload. For an automated client, the +---------------+--------------+
synchronisation burden alone makes scaled operation expensive. |
| POST /init
### High-Level Design | POST /hb
v
- **Core**: Rust + Axum (async web framework) +------------------------------+
- **Storage**: In-memory SQLite (fast, ephemeral per process — restarts are clean) | ChronoSeal daemon |
- **Client**: WASM + Rust (runs in browser for proof generation) | - Axum HTTP routes |
- **Security Model**: Behavioral analysis + hash chaining + entropy scoring | - session verifier |
- **Deployment**: Static musl binary, systemd service, optional Docker | - storage abstraction |
| - metrics and health |
### Key Components +---------------+--------------+
|
- `shared/` — Types, constants, crypto primitives used by server and WASM | SessionRecord
- `server/` — Axum routes, session management, trust engine, rate limiting, cleanup tasks v
- `wasm/` — Client-side proof generation +------------------------------+
- `frontend/` — Static assets served by the application | Storage backend |
| - sqlite-in-memory |
### Unix-Native Design Decisions | - sqlite-in-disk |
| - valkey |
- Runs as a proper systemd service with strict sandboxing +------------------------------+
- All state is either in-memory or in standard locations (`/run/`, `/var/log/`, `/etc/`)
- Graceful shutdown and reload support via signals
- Logging designed for `journalctl` and structured parsing
- Configuration will be fully runtime (no recompile needed)
### Design Goal
ChronoSeal should feel as natural to use as `nginx` or `redis-server` on a Linux system.
---
## Component Map
```
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ entropy.js │ │ heartbeat.js │ │ transport.js│ │
│ │ │ │ │ │ │ │
│ │ mousemove │──►│ orchestrates │──►│ fetch POST │ │
│ │ event ring │ │ init + HB │ │ /init /hb │ │
│ └─────────────┘ └──────┬───────┘ └─────────────┘ │
│ │ │
│ ┌──────▼───────────────────────┐ │
│ │ WASM Module (antibot_wasm) │ │
│ │ │ │
│ │ crypto.rs vm.rs │ │
│ │ ├ generate_keypair() │ │
│ │ ├ sign_message() │ │
│ │ ├ compute_next_hash() │ │
│ │ └ run_program() │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ HTTPS
┌─────────────────────────▼───────────────────────────────┐
│ Server (Axum) │
│ │
│ routes/init.rs routes/heartbeat.rs │
│ │ │ │
│ └──────────┬───────────────┘ │
│ ▼ │
│ session.rs │
│ ├ create_session() │
│ └ verify_heartbeat() │
│ │ │
│ ┌──────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ crypto.rs trust.rs fingerprint.rs │
│ (sig verify) (mouse (aspect ratio, │
│ speed) DPR, HW conc.) │
│ │ │
│ ▼ │
│ shared::hashing (Blake3 hash chain) │
│ │ │
│ ▼ │
│ storage.rs (in-memory SQLite) │
│ │
│ ratelimit.rs cleanup.rs vm.rs middleware.rs │
└─────────────────────────────────────────────────────────┘
``` ```
--- ChronoSeal can serve the frontend files itself or sit behind a reverse proxy. TLS termination should happen before traffic reaches the daemon in production.
## Session Lifecycle ## Workspace Components
### 1. Initialisation — `POST /init` The repository is a Rust workspace with three runtime crates and one static frontend directory.
``` ### `shared/`
Client Server
│ │ `shared/` contains protocol and deterministic runtime code used by both the server and WASM crates.
│ generate Ed25519 keypair (in WASM) │
│ pub_key = verifying_key.to_bytes() │ Responsibilities:
│ │
├─── { public_key: hex(pub_key) } ──────►│ - wire protocol structs for `/init` and `/hb`
│ │ session_id = rand::random::<[u8;32]>() - Blake3 hash-chain helpers
│ │ salt₀ = rand::random::<[u8;16]>() - synthetic gene state representation
│ │ H(0) = Blake3(session_id║pub_key║salt₀) - environment encoding and validation
│ │ opcodes = generate_random_program(8..=16) - mutation program generation, encoding, decoding, and execution
│ │ INSERT INTO sessions … - deterministic VM extension opcode semantics
│ │
│◄── { session_id, salt, opcodes_b64, │ Important files:
│ initial_hash, expires_at } ───────┤
│ │ | File | Responsibility |
│ prevHash = initial_hash │ |---|---|
│ currentSalt = salt │ | `protocol.rs` | `InitRequest`, `InitResponse`, `HeartbeatRequest`, `HeartbeatResponse`, and supporting payload types |
│ opcodesB64 = opcodes_b64 │ | `hashing.rs` | initial and next hash-chain computation |
| `gene.rs` | gene state, environment records, validation, and context-bound commitment |
| `vm_extensions.rs` | mutation order generation, opcode interpreter, execution tracing, and tests |
| `constants.rs` | protocol and execution bounds |
`shared/` is the determinism boundary. Any logic that must agree between server and browser belongs here rather than in server-only or frontend-only code.
### `server/`
`server/` builds the `chronoseal` binary. It owns daemon lifecycle, HTTP routing, session verification, storage, metrics, configuration, and CLI behavior.
Important files:
| File | Responsibility |
|---|---|
| `main.rs` | CLI command dispatch |
| `cli.rs` | command, flag, and environment variable definitions |
| `config.rs` | defaults, TOML loading, environment overrides, validation |
| `runtime.rs` | daemon startup, Axum router, health, metrics, stats, graceful shutdown |
| `routes/init.rs` | `POST /init` handler |
| `routes/heartbeat.rs` | `POST /hb` handler and silent rejection response shape |
| `session.rs` | session creation, heartbeat verification, state advancement |
| `crypto.rs` | canonical signing payload and Ed25519 signature verification |
| `storage.rs` | `DbPool`, SQLite, Valkey compatibility, session persistence, stats |
| `trust.rs` | mouse entropy validation |
| `fingerprint.rs` | browser signal validation, bounds enforcement, and fingerprint sanity checks |
| `ratelimit.rs` | per-session rate limiting |
| `cleanup.rs` | expired session removal |
The server treats the browser as untrusted. Browser-supplied values are accepted only after signature, continuity, timing, behavioral, and mutation checks pass.
### `wasm/`
`wasm/` compiles to the browser runtime package with `wasm-pack --target web`.
Responsibilities:
- generate and hold the browser-local Ed25519 keypair
- sign canonical heartbeat payloads
- compute hash-chain values used by the browser integration
- execute randomized VM programs
- maintain committed and preview synthetic gene state
- preview mutation commitments before a heartbeat is submitted
- commit or discard preview state after server response
Important files:
| File | Responsibility |
|---|---|
| `crypto.rs` | key generation, public key export, message signing |
| `vm.rs` | base VM program execution |
| `vm_extensions.rs` | gene initialization, mutation preview, commit, discard, current commitment |
The WASM runtime is not a trusted execution environment. It is useful because it forces a browser client to implement the same state transitions as the server and makes simple HTTP automation insufficient.
### `frontend/`
`frontend/` contains static JavaScript and browser assets. It loads `frontend/pkg/chronoseal_wasm.js`, calls `/init`, periodically sends `/hb`, and coordinates browser-side state transitions.
The frontend is intentionally thin. Durable protocol rules live in Rust, not in handwritten JavaScript.
## Runtime Topology
The daemon builds a single Axum application with:
| Route | Method | Purpose |
|---|---|---|
| `/init` | `POST` | create a new attestation session |
| `/hb` | `POST` | verify and advance a heartbeat |
| `/health` | `GET` | health probe |
| `/metrics` | `GET` | Prometheus-compatible metrics |
| `/stats` | `GET` | storage/session statistics |
| `/` | `GET` | static frontend assets from `frontend_dir` |
Shared runtime state is held in `AppState`:
- `db_pool`: storage backend handle
- `rate_limiter`: process-local DashMap-backed concurrent rate limiter
- `config`: runtime configuration snapshot behind an `RwLock`
Configuration is resolved in this order:
1. CLI flags
2. `CHRONOSEAL_*` environment variables
3. TOML configuration file
4. built-in defaults
## Session State Model
The server persists one `SessionRecord` per active session.
| Field | Meaning |
|---|---|
| `session_id` | random 32-byte session identifier encoded as hex |
| `public_key` | browser-generated Ed25519 verifying key |
| `salt` | current server salt for hash-chain progression |
| `last_hash` | current accepted hash-chain head |
| `chain_length` | number of accepted chain states including initialization |
| `created_at` | creation timestamp in milliseconds |
| `last_seen` | timestamp of last accepted heartbeat |
| `expires_at` | session expiration timestamp in milliseconds |
| `gene` | committed synthetic gene byte buffer |
| `environment` | encoded environment records |
| `pending_mutation` | server-issued mutation program for the next heartbeat |
| `pending_mutation_step` | mutation step expected on the next heartbeat |
The committed server state advances only after a heartbeat passes all validation checks. Failed heartbeats do not update `last_hash`, `salt`, `gene`, `environment`, `pending_mutation`, or `pending_mutation_step`.
## Initialization Flow
```text
Browser/WASM Server
------------ ------
generate_keypair()
public key
|
| POST /init { public_key }
v
validate public key length
create GeneState
generate session_id
generate salt
compute initial_hash
generate VM opcodes
generate mutation step 1
persist SessionRecord
^
| InitResponse
|
store session_id, salt,
initial_hash, opcodes,
gene_size, mutation order
``` ```
### 2. Heartbeat — `POST /hb` Initialization creates the first server-side commitment state but does not prove liveness. Liveness begins with accepted heartbeats.
Fired every 12–25 seconds with uniform random jitter. The initial response contains:
``` - `session_id`
Client Server - `salt`
│ │ - `opcodes_b64`
│ stackState = run_program(opcodesB64) │ - `initial_hash`
│ events = collectEntropy(lastTime) │ - `expires_at`
│ ts = Date.now() │ - heartbeat interval bounds
│ │ - `gene_size`
│ signable = { │ - `mutation_step`
│ entropyData, fingerprint, │ ← keys sorted alphabetically - `mutation_order_b64`
│ prevHash, sessionId, │
│ stackState, timestamp │ ## Heartbeat Flow
│ } │
│ sig = sign_message( │ ```text
│ JSON.stringify(signable, keys.sort))│ Browser/WASM Server
│ │ ------------ ------
├─── { session_id, prev_hash, timestamp, │ execute VM program
│ entropy_data, stack_state, │ collect entropy and fingerprint data
│ fingerprint, signature } ────────►│ preview pending gene mutation
│ │ 1. Rate limit check build canonical signing payload
│ │ 2. Lookup session, check expiry sign with Ed25519 private key
│ │ 3. Verify Ed25519 signature |
│ │ 4. Verify hash chain continuity | POST /hb HeartbeatRequest
│ │ 5. Validate timestamp window ±30s v
│ │ 6. Validate mouse behavior load session
│ │ 7. Validate fingerprint signals check expiration
│ │ 8. Compute H(n), rotate salt verify signature
│ │ 9. UPDATE sessions … check hash continuity
│ │ check mutation step
│◄── { status: "ok", next_salt } ────────┤ apply pending mutation
│ │ compare gene commitment
│ sentSalt = currentSalt ◄── captured BEFORE rotation check timestamp drift
│ currentSalt = next_salt │ validate mouse entropy
│ prevHash = compute_next_hash( │ validate fingerprint
│ prevHash, ts, entropy, │ compute next hash
│ stackState, sentSalt) │ generate next mutation
generate next salt
persist advanced state
^
| accepted: status + next salt + next mutation
| rejected: { "status": "ok" }
|
commit preview on accepted response
discard or stop on rejected response
``` ```
### 3. Failure Path Accepted heartbeats return `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
On any validation failure the server returns `{"status":"ok"}` with no Rejected heartbeats return only:
`next_salt`. The client logs a warning and continues scheduling heartbeats.
The chain is broken — subsequent heartbeats will also fail silently.
No error is surfaced to the page or its visitors.
---
## Cryptographic Protocol
### Key Generation
```
Ed25519 keypair generated via ed25519-dalek + rand::thread_rng (OS-seeded)
Private key: stored in WASM thread_local, never leaves WASM memory
Public key: 32 bytes, hex-encoded, sent to server at init
```
### Hash Chain
```
H(0) = Blake3( session_id ║ pub_key ║ salt₀ )
H(n) = Blake3(
saltₙ₋₁ ← server-side only, rotated each heartbeat
║ H(n-1) ← must match stored last_hash
║ timestamp_u64_le
║ Blake3( JSON(entropy_data) )
║ Blake3( JSON(stack_state) )
)
```
Salt rotation means an attacker who intercepts a heartbeat cannot compute
future chain links without also intercepting every subsequent server response.
### Canonical Signing Payload
The signed message is a JSON object with top-level keys sorted alphabetically,
serialised with no extra whitespace:
```json ```json
{ {
"entropyData": { "events": [{"t":…,"x":…,"y":…}] }, "status": "ok"
"fingerprint": { "aspectRatio":"…","devicePixelRatio":"…","hardwareConcurrency":… },
"prevHash": "hex…",
"sessionId": "hex…",
"stackState": { "ip":…,"stack":[…] },
"timestamp": 1234567890123
} }
``` ```
The server reconstructs this using `std::collections::BTreeMap` (alphabetical This silent rejection behavior is part of the security model. It prevents the API from acting as an oracle for signature, timing, mutation, or behavior failures.
key order) before calling `VerifyingKey::verify_strict`. Any field mismatch,
key order difference, or whitespace difference causes a signature failure.
### Hashing Algorithm ## Verification Pipeline
Blake3 is used throughout: hash chain links, entropy data digest, stack state Heartbeat verification occurs in `server/src/session.rs`.
digest, and the VM HASH opcode. Blake3 is chosen for speed in WASM,
resistance to length-extension attacks, and a clean Rust API.
--- The current validation order is:
## Stack Machine 1. Load the session by `session_id`.
2. Reject if the session is missing.
3. Reject if `now > expires_at`.
4. Verify the Ed25519 signature over the canonical payload.
5. Decode and compare `prev_hash` with the stored `last_hash`.
6. Compare request `mutation_step` with stored `pending_mutation_step`.
7. Decode the stored gene environment.
8. Apply the stored `pending_mutation` to a cloned server gene state.
9. Compute the expected `gene_commitment` with session and step context.
10. Compare the request `gene_commitment` with the expected commitment.
11. Enforce timestamp drift bounds.
12. Validate mouse entropy.
13. Validate browser fingerprint fields.
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.
17. Persist the advanced session state.
The server generates a random program on session init. The client executes it The verifier performs state mutation only after validation succeeds. This preserves replay resistance and avoids desynchronizing the server after invalid requests.
on every heartbeat and includes the resulting `StackState { stack, ip }` in
the signed payload. This ensures each heartbeat carries unique, verifiable
computation without additional round-trips.
### Instruction Set ## Canonical Signing Boundary
| Opcode | Mnemonic | Operand | Stack effect | Description | The heartbeat signature covers a canonical JSON payload built from:
|--------|----------|---------------|--------------|-------------|
| `0x00` | PUSH | u32 (4B LE) | +1 | Push literal |
| `0x01` | ADD | — | −1 | `a + b` wrapping |
| `0x02` | SUB | — | −1 | `a - b` wrapping |
| `0x03` | MUL | — | −1 | `a * b` wrapping |
| `0x04` | XOR | — | −1 | `a ^ b` |
| `0x05` | AND | — | −1 | `a & b` |
| `0x06` | OR | — | −1 | `a \| b` |
| `0x07` | ROT | — | −1 | `a.rotate_left(b % 32)` |
| `0x08` | NOT | — | 0 | `!a` (unary) |
| `0x09` | HASH | — | -(depth-1) | Blake3 of all stack items → single u32 |
The generator ensures ≥ 2 items on the stack before any binary opcode. - `entropyData`
NOT (0x08) does not change depth. HASH resets depth to 1. - `fingerprint`
- `geneCommitment`
- `mutationStep`
- `prevHash`
- `sessionId`
- `stackState`
- `timestamp`
--- The server constructs this payload using a `BTreeMap`, which orders top-level keys deterministically before serializing. The transport request uses snake_case field names, while the signed payload uses camelCase names that match the browser-side canonical message.
## Behavioral Validation The signature does not cover the `signature` field itself.
### Mouse Entropy ## Hash-Chain Boundary
Every heartbeat includes the mouse events collected since the previous Each accepted heartbeat advances a Blake3 hash chain.
heartbeat. Server checks:
| Check | Threshold | Inputs include:
|---|---|
| Minimum event count | ≥ 3 |
| Minimum cumulative distance | ≥ 10 px |
| Maximum average speed | ≤ 2.0 px/ms (distance / elapsed ms) |
| Minimum pause count | ≥ 1 (movement < 0.2 px over > 50 ms) |
### Browser Fingerprint - previous hash-chain head
- heartbeat timestamp
- entropy data
- VM stack state
- current server salt
| Signal | Valid range | The server stores only the current accepted head as `last_hash`. A replayed heartbeat with an old `prev_hash` fails because the stored `last_hash` has already advanced.
|---|---|
| `aspectRatio` (width / height) | 0.5 – 3.0 |
| `devicePixelRatio` | 0 < dpr ≤ 5.0 |
| `hardwareConcurrency` | ≥ 1 |
--- The salt rotates after every accepted heartbeat. The next salt is returned only on acceptance, so rejected clients do not receive the material needed for the next valid chain step.
## Rate Limiting ## Synthetic Gene Mutation Engine
Token bucket per `session_id`: 5 requests / 10-second window. The Synthetic Gene Mutation Engine provides an additional deterministic continuity check.
Stale entries evicted every 60 seconds by the cleanup task.
Rate-limited responses are indistinguishable from validation failures.
--- Core concepts:
## SQLite Schema - `GeneState`: committed gene byte buffer plus environment records.
- `MutationOrder`: mutation step plus encoded mutation program.
- `pending_mutation`: the server-authored program expected on the next heartbeat.
- `gene_commitment`: context-bound commitment over the candidate gene state, `session_id`, and `mutation_step`.
```sql The server and WASM runtime both execute the same mutation semantics from `shared/vm_extensions.rs`.
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY, Mutation lifecycle:
public_key BLOB NOT NULL, -- 32-byte Ed25519 verifying key
salt BLOB NOT NULL, -- 16-byte current salt 1. Server stores a pending mutation program and step.
last_hash BLOB NOT NULL, -- 32-byte Blake3 chain head 2. Browser previews that mutation against its committed gene state.
chain_length INTEGER NOT NULL DEFAULT 1, 3. Browser sends the resulting `gene_commitment`.
created_at INTEGER NOT NULL, -- Unix ms 4. Server applies the same mutation to a clone of its committed gene state.
last_seen INTEGER NOT NULL, -- Unix ms 5. Server compares the expected commitment with the browser commitment.
expires_at INTEGER NOT NULL -- Unix ms 6. On success, server commits the candidate state and issues the next mutation.
); 7. Browser commits its preview only after receiving an accepted response.
This design prevents a client from advancing mutation state independently of the server. The mutation order is server-authored, step-bound, and accepted only once.
## Behavioral Trust Checks
ChronoSeal includes lightweight behavioral checks. These checks are not a complete human verification system; they are an automation cost signal.
Current checks include:
- minimum mouse activity, when enabled
- minimum total mouse movement distance
- maximum average mouse speed
- minimum pause count
- timestamp drift bound
- basic fingerprint field validation
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
Storage is abstracted by `DbPool`.
| Backend | `db_type` | Characteristics |
|---|---|---|
| SQLite memory | `sqlite-in-memory` | default, process-local, ephemeral |
| SQLite disk | `sqlite-in-disk` | persisted SQLite file at `db_path` |
| Valkey | `valkey` | Valkey-compatible session store utilizing thread-safe connection pooling |
The storage layer must support:
- insert session
- load session
- update session
- delete expired sessions
- report statistics
`valkey` mode reads `CHRONOSEAL_VALKEY_ADDR`, defaulting to `127.0.0.1:6666`. 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
ChronoSeal exposes two operational surfaces:
- CLI commands: `status`, `health`, `metrics`, `stats`, `config check`
- HTTP endpoints: `/health`, `/metrics`, `/stats`
The metrics endpoint reports storage-derived counters including:
- active sessions
- expired sessions
- maximum observed chain length
The daemon uses structured tracing and can log to journald through normal systemd operation. Operators should avoid debug logging in production because internal identifiers may appear in logs.
ChronoSeal applies security response headers including:
- Content-Security-Policy
- X-Frame-Options
- X-Content-Type-Options
- Referrer-Policy
- Permissions-Policy
## Trust Boundaries
### Browser Boundary
The browser is untrusted. It may lie about entropy, fingerprint values, VM output, mutation commitment, timing, and session identifiers.
Mitigation:
- signature verification binds payloads to the browser session key
- hash-chain checks reject stale state
- mutation commitment checks reject incorrect gene progression
- timing and behavioral checks reject implausible requests
### WASM Boundary
WASM code runs in the browser and is therefore not trusted as secure enclave code.
Mitigation:
- the server independently recomputes critical deterministic state
- private key custody raises automation cost but is not treated as hardware-backed secrecy
- failures do not reveal detailed reasons to callers
### Storage Boundary
Storage is trusted for session continuity. If storage is lost, sessions cannot continue. If storage is tampered with, attestation integrity can be affected.
Mitigation:
- use proper filesystem permissions for SQLite disk mode
- deploy Valkey on a trusted network or protected socket
- keep ChronoSeal behind normal host and service hardening
### Network Boundary
ChronoSeal expects production traffic to be protected by TLS. Plaintext deployment weakens confidentiality and makes traffic analysis easier.
Mitigation:
- terminate TLS at a reverse proxy or load balancer
- keep `/init` and `/hb` same-origin with protected content when possible
- avoid exposing internal metrics broadly
## Failure Semantics
ChronoSeal intentionally separates transport success from attestation success.
| Failure class | HTTP behavior | State mutation |
|---|---|---|
| malformed route-level request | normal HTTP error handling | no session advancement |
| invalid heartbeat semantics | `200 OK` with `{"status":"ok"}` | no session advancement |
| rejected heartbeat | `200 OK` with `{"status":"ok"}` | no session advancement |
| accepted heartbeat | `200 OK` with next-state fields | session state advances from the verifier's perspective |
This ambiguity reduces attacker feedback. Application integrations must check for the presence of `next_salt`, `next_mutation_step`, and `next_mutation_order_b64` rather than treating any `status: ok` as an accepted heartbeat.
## Invariants
The architecture relies on these invariants:
- A session has exactly one expected `pending_mutation_step` at a time.
- A pending mutation is consumed only by an accepted heartbeat.
- `last_hash` changes only after a heartbeat passes verification.
- `salt` changes only after a heartbeat passes verification.
- `gene` and `environment` change only after mutation commitment validation succeeds.
- The next mutation order is generated only from an accepted candidate state.
- Rejected heartbeats do not reveal the failed validation stage.
- Browser-side preview state is committed only after an accepted heartbeat response.
Breaking these invariants can introduce replay acceptance, client/server desynchronization, or oracle behavior.
## Concurrency Notes
ChronoSeal currently verifies a heartbeat by loading a session, computing candidate state, and writing the updated record back to storage. The intended operational model is one live heartbeat stream per browser session.
Concurrent heartbeats for the same `session_id` should naturally collapse to at most one accepted progression because both requests present the same `prev_hash` and `mutation_step`; after the first accepted update, the second request becomes stale. Storage backends must preserve update visibility strongly enough for this assumption to hold.
## Deployment Shape
Typical production topology:
```text
Internet
|
v
TLS reverse proxy
|
v
chronoseal daemon on 127.0.0.1:3000
|
v
SQLite disk or Valkey storage
``` ```
In-memory SQLite — all sessions lost on server restart by design. Recommended deployment properties:
Clients re-initialise transparently on the next page load.
--- - 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
- monitor `/health`, `/metrics`, and `/stats`
- use `sqlite-in-memory` for ephemeral local sessions
- use `sqlite-in-disk` or `valkey` when sessions must survive process restarts
## Threat Model ## Security Hardening (v1.0.2)
### In Scope Recent hardening improvements include:
| Threat | Mitigation | - fingerprint bounds validation
|---|---| - VM stack depth protection
| Playwright / Puppeteer / Selenium | Mouse entropy + behavioral validation | - panic-resistant hashing paths
| Puppeteer Stealth, undetected-chromedriver | Signature over VM execution state | - panic-resistant WASM helpers
| Heartbeat replay | Hash chain + ±30s timestamp window | - DashMap-backed concurrent rate limiting
| Signature forgery | Private key isolated in WASM memory | - security response headers
| Parallel session sharing | Each session bound to a unique keypair | - non-root container execution
| Brute-forced session IDs | 256-bit random entropy |
| Flooding with fake session IDs | Rate limiter + periodic HashMap eviction |
| Traffic analysis | Uniform `{"status":"ok"}` on all failure paths |
### Out of Scope ## Limitations
| Threat | Reason | ChronoSeal is not:
|---|---|
| Real browser with real human input | Indistinguishable from a legitimate user |
| WASM reverse engineering | Obfuscation is not a security primitive |
| Server-side compromise | Outside the scope of client attestation |
ChronoSeal raises cost and complexity of automated access. It is not a - a user authentication system
cryptographic proof of humanity and does not claim to be. - a CAPTCHA
- a fraud scoring engine
- a hardware attestation system
- a persistent identity framework
- a complete defense against fully resourced browser farms
--- It is a protocol layer that makes browser automation and replay more expensive by requiring correct, continuous, stateful execution.
## Module Reference ## Related Documents
| Path | Purpose | - [API Reference](API.md)
|---|---| - [Deployment Guide](DEPLOYMENT.md)
| `shared/src/protocol.rs` | Shared types: `InitRequest`, `HeartbeatRequest`, `StackState`, … | - [Threat Model](THREAT_MODEL.md)
| `shared/src/hashing.rs` | `initial_hash`, `next_chain_hash`, `hash_stack` | - [WASM Build Guide](WASM_BUILD.md)
| `shared/src/constants.rs` | All tunable parameters | - [Design Philosophy](DESIGN-PHILOSOPHY.md)
| `server/src/routes/init.rs` | `POST /init` handler | - [Privacy Policy](PRIVACY%20POLICY.md)
| `server/src/routes/heartbeat.rs` | `POST /hb` handler |
| `server/src/session.rs` | `create_session`, `verify_heartbeat` |
| `server/src/crypto.rs` | `verify_signature` — BTreeMap canonical JSON |
| `server/src/trust.rs` | `validate_mouse` — speed, distance, pauses |
| `server/src/fingerprint.rs` | `validate` — aspect ratio, DPR, HW concurrency |
| `server/src/vm.rs` | `generate_random_program` |
| `server/src/ratelimit.rs` | `RateLimiter::check`, `evict_stale` |
| `server/src/cleanup.rs` | Background loop: expire sessions + evict rate limiter |
| `server/src/storage.rs` | SQLite init, `current_time_ms` |
| `wasm/src/crypto.rs` | `generate_keypair`, `sign_message`, `compute_next_hash` |
| `wasm/src/vm.rs` | `run_program` — stack machine executor |
| `frontend/heartbeat.js` | Session init, heartbeat loop, chain advancement |
| `frontend/entropy.js` | Mouse event ring buffer, `collectEntropy` |
| `frontend/transport.js` | `sendRequest` fetch wrapper |
+129
View File
@@ -0,0 +1,129 @@
# ChronoSeal vs Popular Anti-Bot Systems (2026)
ChronoSeal is a **self-hosted, cryptographic attestation daemon**. This document compares it honestly with leading commercial solutions.
## Quick Comparison
| Solution | Type | Core Method | Privacy | Self-Hosted | Crypto Strength | Behavioral Analysis | Cost | Best For |
|----------------------------|-------------------|--------------------------------------|---------|-------------|-----------------|---------------------|---------------|------------------------------|
| **ChronoSeal** | Self-hosted Daemon| Ed25519 + Blake3 + **Gene Mutation** | Excellent | Yes | Very High | Light + Tunable | Free | Privacy + Control |
| Cloudflare Bot Management | Cloud Edge | JS Challenges + ML Fingerprinting | Medium | No | Medium | Strong | Freemium | Easy mass protection |
| Akamai Bot Manager | Enterprise Edge | Behavioral + Device Fingerprinting | Low | Hybrid | Medium | Very Strong | Very High | Large enterprises |
| HUMAN (PerimeterX) | Cloud SaaS | Behavioral Biometrics + ML | Low | No | Medium | Very Strong | Enterprise | Sophisticated bot defense |
| DataDome | Cloud SaaS | Real-time ML + Behavioral | Medium | No | Medium | Strong | Enterprise | E-commerce scraping |
| reCAPTCHA v3 | Google Service | Risk scoring + invisible challenges | Poor | No | Low | Medium | Free → Paid | Simple bot filtering |
| Kasada | Cloud SaaS | Proof-of-Work + Behavioral | Medium | No | High | Strong | Enterprise | Advanced automation |
## Detailed Analysis
### 1. ChronoSeal (v1.0.2)
**Strengths:**
- Strongest **cryptographic foundation** (Ed25519 signatures + Blake3 hash chain + Synthetic Gene Mutation Engine)
- Fully **deterministic** server ↔ WASM parity
- Completely **invisible** to users with silent rejection
- Excellent **privacy** — no third-party tracking or fingerprint databases
- Highly **tunable** mutation strength (`gene_size` + `mutation_rounds`)
- Full control and auditability
**Weaknesses:**
- Requires self-hosting and maintenance
- No global threat intelligence network like Cloudflare
---
### 2. Cloudflare Bot Management
**Strengths:**
- Extremely easy to deploy
- Excellent scale and global threat intelligence
- Good detection rates
**Weaknesses vs ChronoSeal:**
- Relies heavily on fingerprinting and JS challenges
- Sends data to Cloudflare (privacy impact)
- Less transparent and auditable
- Vendor lock-in
**Winner:** ChronoSeal for privacy-conscious teams
---
### 3. Enterprise Solutions (Akamai, HUMAN, DataDome, Kasada)
**Strengths:**
- Sophisticated ML + behavioral analysis
- Large threat intelligence databases
- Professional support
**Weaknesses vs ChronoSeal:**
- Extremely expensive
- Black-box systems (limited visibility)
- Heavy data collection (privacy concerns)
- Vendor dependency
**Winner:** ChronoSeal for teams wanting transparency and control
---
### 4. reCAPTCHA v3
**Strengths:**
- Free tier available
- Easy integration
**Weaknesses:**
- Heavy Google tracking
- Increasingly bypassed
- Poor privacy
**Winner:** ChronoSeal by a large margin
---
## When to Choose ChronoSeal
**Choose ChronoSeal if you want:**
- Maximum privacy
- Strong cryptographic guarantees
- Full control over your infrastructure
- Tunable defense strength
- No third-party data sharing
- Open source transparency
**Choose Commercial Solutions if you want:**
- Zero maintenance
- Massive global threat intelligence
- Enterprise support & SLAs
- Quick deployment at huge scale
## Technical Differentiation
ChronoSeal’s unique advantage is the **Synthetic Gene Mutation Engine** — a deterministic, server-controlled mutation sequence that both server and browser WASM must execute in sync. This creates a second synchronized state channel that is extremely difficult for automation to maintain at scale.
No commercial solution currently offers equivalent cryptographic + mutation-based attestation in a self-hosted package.
---
## Conclusion
**ChronoSeal** is currently one of the strongest **open-source/self-hosted** anti-bot solutions available. It trades ease-of-use and global scale for **privacy, transparency, cryptographic strength, and control**.
It is particularly well-suited for:
- Privacy-focused organizations
- High-value content platforms
- Teams that want to avoid vendor lock-in
- 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
+483 -249
View File
@@ -1,205 +1,453 @@
# ChronoSeal — Deployment Guide # ChronoSeal Deployment Guide
## Prerequisites ChronoSeal is intended to run as a small Unix daemon behind TLS, with static browser assets served either by the daemon or by the same protected origin. This guide covers native, service, and container deployment.
| Tool | Minimum version | Purpose | ## Deployment Model
|---|---|---|
| Rust | 1.87 stable | Server + WASM compilation |
| wasm-pack | 0.13 | WASM build and packaging |
| Docker + Compose | 24 / 2.x | Container deployment |
| nginx / NPM / HAProxy | any | TLS termination, reverse proxy |
Install Rust: https://rustup.rs Typical production topology:
Install wasm-pack: `cargo install wasm-pack`
--- ```text
Internet
|
v
TLS reverse proxy
|
v
chronoseal daemon on 127.0.0.1:3000
|
v
sqlite-in-disk or valkey storage
```
For local evaluation, the daemon can bind directly to `0.0.0.0:3000` or `127.0.0.1:3000`.
## Requirements
| Tool | Minimum | Purpose |
|---|---:|---|
| Rust | 1.87 stable | Build server and shared crates |
| `wasm32-unknown-unknown` target | current stable | Compile WASM runtime |
| `wasm-pack` | 0.13 | Generate browser WASM package |
| systemd | 248+ | Native service management |
| Docker | 24.x | Optional container image |
| Docker Compose | 2.x | Optional local orchestration |
Install Rust from rustup, then install the WASM tooling:
```bash
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
```
## Build ## Build
### 1. Build the WASM module Use the repository build script:
```bash
wasm-pack build wasm --target web --release
mv wasm/pkg frontend/pkg
```
This produces `frontend/pkg/antibot_wasm.js` and `frontend/pkg/antibot_wasm_bg.wasm`,
which are loaded by `frontend/main.js` at runtime.
### 2. Build the server
```bash
cargo build -p server --release
```
Binary output: `target/release/server`
### 3. Build both (convenience script)
```bash ```bash
bash scripts/build.sh bash scripts/build.sh
``` ```
--- The script:
## Running 1. Builds `wasm/` with `wasm-pack build --target web --release`.
2. Replaces `frontend/pkg` with the generated package.
3. Builds the release daemon binary.
### Development Manual equivalent:
```bash ```bash
bash scripts/dev.sh wasm-pack build wasm --target web --release
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
cargo build -p chronoseal-server --bin chronoseal --release
``` ```
Runs the server with `cargo run --release`. The server serves the `frontend/` Release binary:
directory statically at `/` via tower-http `ServeDir`.
Open `http://localhost:3000` in a browser. Open DevTools console — heartbeats ```text
should appear every 12–25 seconds. No visible UI is rendered; the protection target/release/chronoseal
is entirely silent. ```
## Binary Hardening Verification
### Production (native binary) 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 ```bash
cargo build -p server --release checksec file target/release/chronoseal
sudo cp target/release/server /usr/local/bin/chronoseal
``` ```
Set environment variables before running: 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 ```bash
export RUST_LOG=info # or warn for quieter output strip target/release/chronoseal -o chronoseal.stripped
chronoseal
nm -D chronoseal.stripped | wc -l
``` ```
The server binds to `0.0.0.0:3000` by default. Place behind a reverse proxy A stripped production binary should expose only a small dynamic symbol set.
for TLS — do not expose port 3000 directly.
--- Check for remaining debug sections:
## systemd
### Service file
The provided `chronoseal.service` includes hardened systemd sandboxing:
```
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
SystemCallArchitectures=native
```
### Install
```bash ```bash
# Create a dedicated system user readelf -S chronoseal.stripped | grep debug
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chronoseal
# Install binary and frontend
sudo cp target/release/server /usr/local/bin/chronoseal
sudo mkdir -p /opt/chronoseal/frontend
sudo cp -r frontend/ /opt/chronoseal/frontend/
sudo chown -R chronoseal:chronoseal /opt/chronoseal
# Install and enable service
sudo cp chronoseal.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now chronoseal
``` ```
### Verify 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
The installer builds, installs, enables, and starts the service:
```bash
sudo bash scripts/install.sh
```
Installer actions:
- create the `chronoseal` system user if missing
- build WASM and server artifacts
- install `target/release/chronoseal` to `/usr/local/bin/chronoseal`
- copy `frontend/` to `/opt/chronoseal/frontend`
- install `chronoseal.service` to `/etc/systemd/system/chronoseal.service`
- reload systemd
- enable and start the service
Verify:
```bash ```bash
sudo systemctl status chronoseal sudo systemctl status chronoseal
journalctl -u chronoseal -f chronoseal status --format json
chronoseal health
sudo journalctl -u chronoseal -f
``` ```
--- ## Running Without Install
## Docker For local development:
### Build and run
```bash ```bash
docker compose up -d --build bash scripts/build.sh
cargo run -p chronoseal-server --bin chronoseal -- run \
--bind 127.0.0.1:3000 \
--frontend-dir frontend
``` ```
### docker-compose.yml overview Probe the daemon:
```yaml
services:
chronoseal:
build: .
restart: unless-stopped
ports:
- "3000:3000"
environment:
RUST_LOG: info
tmpfs:
- /tmp
```
The `tmpfs` mount ensures the in-memory SQLite database is never written to
disk, even if Docker's storage driver were to flush the container filesystem.
### Dockerfile stages
The Dockerfile uses a two-stage build:
1. `rust:1.87-bookworm` — compiles the server binary
2. `debian:bookworm-slim` — minimal runtime image with only `ca-certificates`
The WASM module and frontend must be built separately (wasm-pack requires a
browser toolchain not present in the server image) and mounted or copied into
the container at `/opt/chronoseal/frontend/`.
```bash ```bash
# Build WASM first curl http://127.0.0.1:3000/health
wasm-pack build wasm --target web --release curl http://127.0.0.1:3000/stats
mv wasm/pkg frontend/pkg curl http://127.0.0.1:3000/metrics
# Then build and run the container
docker compose up -d --build
``` ```
Or mount the pre-built frontend as a volume: ## Configuration
```yaml ChronoSeal resolves configuration in this order:
volumes:
- ./frontend:/opt/chronoseal/frontend:ro 1. CLI flags
2. `CHRONOSEAL_*` environment variables
3. TOML config file
4. built-in defaults
Default config discovery:
1. `CHRONOSEAL_CONFIG`, if it points to an existing file
2. `/etc/chronoseal/config.toml`
3. `$XDG_CONFIG_HOME/chronoseal/config.toml`
4. `~/.config/chronoseal/config.toml`
Validate effective configuration:
```bash
chronoseal config check --format yaml
``` ```
--- Example:
## Reverse Proxy ```toml
bind = "127.0.0.1:3000"
db_type = "sqlite-in-disk"
pid_file = "/run/chronoseal.pid"
db_path = "/var/lib/chronoseal/chronoseal.sqlite"
frontend_dir = "/usr/share/chronoseal/frontend"
log_file = "/var/log/chronoseal/chronoseal.jsonl"
ChronoSeal must be served over HTTPS. The heartbeat payload contains a heartbeat_min_interval_ms = 12000
timestamp; if traffic is observable in plaintext, timing attacks become heartbeat_max_interval_ms = 25000
easier. TLS 1.3 is strongly recommended. expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
### nginx min_mouse_total_dist = 10.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
gene_size = 512
mutation_rounds = 4
```
## Storage Backends
| Backend | `db_type` | Use case |
|---|---|---|
| SQLite memory | `sqlite-in-memory` | ephemeral local or stateless deployment |
| SQLite disk | `sqlite-in-disk` | persisted session continuity across restarts |
| Valkey | `valkey` | external session storage |
For disk persistence:
```bash
sudo mkdir -p /var/lib/chronoseal
sudo chown -R chronoseal:chronoseal /var/lib/chronoseal
```
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
The supplied service file is intended as the baseline unit. Keep the daemon under a dedicated user and restrict filesystem access to the paths it needs.
Useful commands:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now chronoseal
sudo systemctl restart chronoseal
sudo systemctl status chronoseal
sudo journalctl -u chronoseal -f
```
Recommended hardening properties include:
- `NoNewPrivileges=true`
- `PrivateTmp=true`
- `ProtectSystem=strict`
- `ProtectHome=true`
- `ProtectKernelTunables=true`
- `ProtectKernelModules=true`
- `ProtectControlGroups=true`
- `MemoryDenyWriteExecute=true`
- `RestrictRealtime=true`
- `RestrictSUIDSGID=true`
- `SystemCallArchitectures=native`
Any hardening must still allow access to:
- the binary
- frontend assets
- PID file directory
- optional log file directory
- SQLite database directory, if using `sqlite-in-disk`
## Reverse Proxy and TLS
ChronoSeal should be served over HTTPS in production. Terminate TLS at a reverse proxy or load balancer and proxy to the local daemon.
Minimal nginx example:
```nginx ```nginx
server { server {
listen 443 ssl http2; listen 443 ssl http2;
server_name your.domain.com; server_name example.com;
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Tight timeouts — heartbeat interval is 12–25s proxy_read_timeout 35s;
proxy_read_timeout 35s; proxy_send_timeout 10s;
proxy_send_timeout 10s;
location / { location / {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -213,134 +461,120 @@ server {
server { server {
listen 80; listen 80;
server_name your.domain.com; server_name example.com;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
``` ```
### Nginx Proxy Manager Keep `/init`, `/hb`, and frontend assets on the same origin when possible. If you split origins, configure CORS and cookie/application policy deliberately.
1. Add a new Proxy Host pointing to `http://chronoseal:3000` ## Docker
2. Enable SSL, Request Let's Encrypt certificate
3. Enable HTTP/2, Force SSL
4. Under Advanced, add:
```
proxy_read_timeout 35s;
proxy_send_timeout 10s;
```
### HAProxy Build and run:
```haproxy ```bash
frontend https_front bash scripts/build.sh
bind *:443 ssl crt /etc/haproxy/certs/your.domain.pem alpn h2,http/1.1 docker compose up -d --build
default_backend chronoseal_back
backend chronoseal_back
server chronoseal 127.0.0.1:3000 check
timeout connect 5s
timeout server 35s
``` ```
--- The Compose file exposes port `3000`.
## Integration into an Existing Site ```bash
curl http://127.0.0.1:3000/health
ChronoSeal is designed to run as a sidecar — its `/init` and `/hb` endpoints
can be proxied from any existing web server. The frontend assets (`pkg/`) need
to be served from the same origin as the protected page (or CORS must be
configured).
### Option A — Serve everything from ChronoSeal
ChronoSeal serves `frontend/` statically. Put your protected HTML inside
`frontend/` and let ChronoSeal serve it directly.
### Option B — Proxy only the API endpoints
Keep your existing server. Proxy `/init` and `/hb` to ChronoSeal, and serve
the WASM and JS assets from your CDN or existing static file server.
```nginx
# On your existing server:
location ~ ^/(init|hb)$ {
proxy_pass http://127.0.0.1:3000;
}
``` ```
Add to your protected pages: The Dockerfile copies `frontend/` from the working tree. Build `frontend/pkg` before building the image when the browser WASM runtime is required inside the container.
```html
<script type="module" src="/pkg/antibot_wasm.js"></script>
<script type="module" src="/main.js"></script>
```
---
## Configuration
All parameters are in `shared/src/constants.rs`. Recompile after changes.
| Constant | Default | Notes |
|---|---|---|
| `SESSION_ID_LEN` | 32 bytes | 256-bit entropy — do not reduce |
| `SALT_LEN` | 16 bytes | Per-heartbeat salt |
| `HEARTBEAT_MIN_INTERVAL_MS` | 12 000 ms | Increase to reduce server load |
| `HEARTBEAT_MAX_INTERVAL_MS` | 25 000 ms | Jitter upper bound |
| `EXPIRATION_MINUTES` | 30 min | Session TTL after last heartbeat |
| `RATE_LIMIT_COUNT` | 5 | Max heartbeats per window per session |
| `RATE_LIMIT_WINDOW_SECS` | 10 s | Rate limit window |
| `MAX_TIMESTAMP_DRIFT_MS` | 30 000 ms | Anti-replay window; account for NTP skew |
| `MIN_MOUSE_TOTAL_DIST` | 10.0 px | Lower for low-activity pages |
| `MAX_MOUSE_AVG_SPEED` | 2.0 px/ms | Raise if legitimate users are rejected |
| `MIN_PAUSE_COUNT` | 1 | Minimum natural pause events |
---
## Observability ## Observability
ChronoSeal uses `tracing` with `tracing-subscriber`. Log levels: CLI:
| Level | Events |
|---|---|
| `INFO` | Server start, request method + path + status |
| `WARN` | Heartbeat validation failures (with session ID and reason) |
| `DEBUG` | Rate limit hits |
```bash ```bash
RUST_LOG=info chronoseal # production chronoseal status --format json
RUST_LOG=debug chronoseal # development chronoseal health
RUST_LOG=warn chronoseal # minimal output chronoseal stats --format json
chronoseal metrics
``` ```
Log format is plain text to stdout. Pipe to `journald`, `fluentd`, or any HTTP:
log aggregator via stdout capture.
---
## Health Check
The server has no dedicated `/health` endpoint. Use a TCP check on port 3000,
or a lightweight HTTP check on `GET /` (which serves `index.html`).
```bash ```bash
# Docker health check (add to docker-compose.yml if needed) curl http://127.0.0.1:3000/health
healthcheck: curl http://127.0.0.1:3000/stats
test: ["CMD", "curl", "-sf", "http://localhost:3000/"] curl http://127.0.0.1:3000/metrics
interval: 30s
timeout: 5s
retries: 3
``` ```
--- Prometheus metrics:
## Security Checklist - `chronoseal_sessions`
- `chronoseal_expired_sessions`
- `chronoseal_max_chain_length`
- [ ] TLS 1.3 enabled, TLS 1.0/1.1 disabled ## Logging
- [ ] HTTP/2 enabled
- [ ] Port 3000 not exposed to the public internet (only via reverse proxy) Use info-level logs for production:
- [ ] `RUST_LOG=warn` or `info` in production (not `debug` — session IDs appear in logs)
- [ ] systemd service running as `chronoseal` user with hardened sandbox ```bash
- [ ] `MemoryDenyWriteExecute=true` in service file (prevents JIT in process) CHRONOSEAL_LOG=info chronoseal run
- [ ] CORS `CorsLayer::permissive()` replaced with origin-restricted policy for production ```
- [ ] Frontend assets served over the same HTTPS origin as protected pages
or with systemd:
```bash
sudo systemctl edit chronoseal
```
Avoid debug logging in production because internal identifiers may be written to logs.
## Production Checklist
- Build `frontend/pkg` before packaging.
- Serve ChronoSeal traffic over HTTPS.
- Bind the daemon to localhost behind a reverse proxy unless direct exposure is required.
- Use a dedicated service user.
- Keep debug logs disabled.
- Choose storage intentionally: `sqlite-in-memory`, `sqlite-in-disk`, or `valkey`.
- 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.
+111 -27
View File
@@ -1,41 +1,125 @@
# ChronoSeal Design Philosophy # ChronoSeal Design Philosophy
**"Everything is a File" — Unix-Native Software Design** ChronoSeal is designed for operators who want a local, inspectable, Unix-native browser attestation layer rather than a hosted anti-bot black box.
ChronoSeal is intentionally built as a **first-class citizen of Linux**. The entire application is designed to behave like a well-engineered native file within the Unix filesystem. ## Core Position
### Why This Philosophy Matters ChronoSeal is infrastructure software. It should feel closer to `nginx`, `redis-server`, or a small system daemon than to a third-party analytics platform.
ChronoSeal is designed so that administrators can operate, monitor, configure, and integrate it using the same reliable, transparent, and trusted tools and patterns they already use on Linux systems — without fighting the operating environment. Design priorities:
### Core Principles - CLI-first operation
- explicit configuration
- deterministic protocol behavior
- small runtime surface
- privacy-preserving state
- observable health and metrics
- no hidden telemetry
- no persistent user profiling
- **Everything is a File**: The application must be controllable, inspectable, and composable through standard Unix interfaces (CLI, files, signals, pipes, and environment). ## What ChronoSeal Optimizes For
- **CLI as Source of Truth**: All operations — starting, stopping, configuring, monitoring, and debugging — must be possible from the command line with excellent discoverability.
- **Behave Like a Native File**: Predictable lifecycle management through commands, signals (`SIGHUP`, `SIGTERM`, `SIGUSR1`), logs, configuration files, and standard process semantics.
- **Composability**: Must work naturally with pipes, redirection, scripts, systemd, Ansible, Docker, and orchestration tools.
- **Observability by Default**: All important state and metrics should be accessible as text or structured data.
- **Minimal Friction, Maximum Durability**: One-line installer, world-class `--help`, proper man pages, and decades-long maintainability are non-negotiable.
- **Respect for the OS**: Follows Linux Filesystem Hierarchy Standard (FHS), XDG Base Directory specification, and hardened systemd practices.
### Non-Goals ### Operator Control
ChronoSeal is **not** designed to be: Operators should be able to build, run, inspect, configure, monitor, and stop the service with ordinary Unix tools.
- Cloud-first or vendor-specific
- Browser-first or JavaScript-heavy
- Dependency-heavy or framework-driven
- GUI-centric (any graphical interface must be a thin wrapper)
- Telemetry-oriented or privacy-invasive
- Optimized for rapid prototyping at the cost of long-term reliability
These non-goals help keep the project focused on stability, simplicity, security, and deep Unix integration. This is why ChronoSeal provides:
### Development Mindset - `chronoseal run`
- `chronoseal status`
- `chronoseal health`
- `chronoseal config check`
- `chronoseal metrics`
- `chronoseal stats`
- shell completions
- systemd integration
- Production robustness, security, and long-term sustainability take clear precedence over development speed. ### Determinism
- Every design decision is evaluated against one question:
**“Does this make ChronoSeal feel like it naturally belongs in `/usr/bin/`?”**
This philosophy guided the complete refactoring of ChronoSeal and continues to drive all future development. The protocol depends on deterministic agreement between server Rust and browser WASM.
**Status**: Core architecture and systemd integration completed. Rich CLI, runtime configuration system, and one-line installer are in active development. Shared logic belongs in `shared/` when divergence would create security or correctness risk. This includes:
- protocol structs
- hash-chain semantics
- synthetic gene model
- mutation opcode behavior
- mutation order encoding
### Cost Escalation
ChronoSeal does not claim impossible security. It raises the cost of automation by making clients maintain:
- a browser-local signing key
- a signed canonical heartbeat payload
- a Blake3 hash chain
- VM execution output
- server-issued mutation progression
- plausible timing and interaction signals
The objective is to make cheap automation brittle and expensive automation more complex.
### Silent Rejection
Heartbeat rejection is intentionally ambiguous. Invalid heartbeats receive the same `status` value as accepted heartbeats, but accepted responses include next-state fields.
This avoids turning the API into a validation oracle. Integrators must check for `next_salt`, `next_mutation_step`, and `next_mutation_order_b64`.
### Privacy
ChronoSeal should not become a surveillance system.
It avoids:
- long-term user identifiers
- browser history
- cross-site identity graphs
- fingerprint databases
- behavioral profiling as a product feature
It stores only the session state required for continuity.
## Non-Goals
ChronoSeal is not:
- a CAPTCHA
- a fraud scoring engine
- an authentication provider
- a hosted SaaS product
- a persistent fingerprinting system
- a replacement for authorization checks
- a complete defense against real browser farms
## Operational Assumptions
ChronoSeal assumes:
- Linux or a Unix-like host
- systemd for production service management
- TLS in production
- browser clients can execute WASM
- operators can manage config files and service users
- application owners decide how attestation status gates protected resources
## Engineering Biases
When the project faces tradeoffs, prefer:
- explicit configuration over implicit magic
- server-side recomputation over browser trust
- bounded deterministic execution over unbounded heuristics
- clear CLI output over hidden dashboards
- local deployment over mandatory cloud dependencies
- privacy by data minimization over privacy by policy alone
## Success Criteria
ChronoSeal is succeeding when:
- legitimate browser sessions advance without user friction
- simple scrapers cannot pass the protocol
- automation requires a full stateful implementation
- operators can debug deployments with normal Unix tools
- stored data remains minimal and short-lived
- documentation reflects the implementation precisely
+100
View File
@@ -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.
+292
View File
@@ -0,0 +1,292 @@
# ChronoSeal Performance Tuning Guide
ChronoSeal uses a deterministic Synthetic Gene Mutation Engine to strengthen browser session continuity validation. This guide explains how to tune the mutation engine for an appropriate balance between security strength, resource consumption, and user experience.
The primary tuning parameters are:
* `gene_size` — size of the synthetic gene buffer
* `mutation_rounds` — number of mutation iterations executed per heartbeat
---
## Understanding the Mutation Engine
For every accepted heartbeat, ChronoSeal executes a server-generated mutation program against a synthetic gene buffer.
Increasing mutation complexity raises the computational cost of reproducing valid session state while also increasing CPU utilization on both the server and browser runtime.
General effects:
* Larger `gene_size` increases mutation state complexity.
* Higher `mutation_rounds` increase computational work per heartbeat.
* Both increase memory access and CPU consumption.
* Excessive values may negatively impact lower-end mobile devices.
The optimal values depend on your threat model and expected client hardware.
---
## Recommended Configurations
| Profile | `gene_size` | `mutation_rounds` | Security Level | Recommended Usage |
| ----------------- | ----------- | ----------------- | ---------------- | -------------------------------- |
| Default | 512 | 4 | Moderate | Development and testing |
| 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 = 4
```
This configuration provides a strong balance between security and runtime overhead for most deployments.
---
## Configuration
Edit your configuration file:
```toml
# Mutation Engine Settings
gene_size = 2048
mutation_rounds = 4
```
Common configuration locations:
```text
/etc/chronoseal/config.toml
~/.config/chronoseal/config.toml
```
Restart ChronoSeal:
```bash
sudo systemctl restart chronoseal
```
Validate the effective configuration:
```bash
chronoseal config check --format yaml
```
---
## Performance Monitoring
### Server-Side Monitoring
View service logs:
```bash
sudo journalctl -u chronoseal -f
```
Inspect runtime statistics:
```bash
chronoseal stats --format json
```
Enable additional diagnostics when required:
```bash
CHRONOSEAL_LOG=debug chronoseal run
```
Avoid debug logging in production environments.
---
### Browser-Side Monitoring
Measure mutation execution time:
```javascript
console.time("gene-mutation");
const commitment = preview_gene_commitment(
order_b64,
session_id,
mutation_step,
mutation_rounds
);
console.timeEnd("gene-mutation");
```
Browser developer tools can also be used to monitor:
* JavaScript execution time
* WASM execution time
* CPU utilization
* Memory consumption
---
## Tuning Strategy
### Step 1: Start with Recommended Values
```toml
gene_size = 2048
mutation_rounds = 4
```
Deploy and observe normal usage patterns.
### Step 2: Monitor Heartbeat Success Rates
Watch for:
* heartbeat failures
* increased browser CPU usage
* elevated mobile device latency
* increased battery consumption
### Step 3: Increase Gradually
Increase one parameter at a time.
Recommended progression:
```text
2048 / 4
4096 / 4
4096 / 8
4096 / 10
```
This makes it easier to identify performance bottlenecks.
### Step 4: Test Mobile Devices
Always test on:
* Android devices
* iPhones
* older laptops
* low-power CPUs
Desktop-only validation can be misleading.
---
## Advanced Deployment Strategies
### Risk-Based Mutation Strength
Future deployments may choose to dynamically increase mutation strength based on:
* session age
* failed heartbeat history
* suspicious behavior signals
* protected resource sensitivity
Example policy:
```text
New session → 2048 / 4
Suspicious session → 4096 / 8
Elevated-risk action → 4096 / 10
```
---
## Performance Recommendations
### WASM Builds
Always use optimized builds:
```bash
wasm-pack build wasm --target web --release
```
### General Guidance
* 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.
* Re-evaluate settings after major algorithm changes.
### Storage Performance
For maximum throughput:
```toml
db_type = "sqlite-in-memory"
```
For persistence:
```toml
db_type = "sqlite-in-disk"
```
Choose based on operational requirements rather than mutation settings.
---
## Security Considerations
Higher values increase the cost of reproducing valid session state but do not provide absolute protection against determined attackers.
ChronoSeal remains a cost-raising attestation layer rather than a complete anti-abuse solution.
Mutation tuning should be considered alongside:
* heartbeat timing controls
* signature validation
* hash-chain continuity
* behavioral trust checks
* rate limiting
* session expiration
---
## Recommended Starting Point
For most production deployments:
```toml
gene_size = 2048
mutation_rounds = 4
```
This configuration provides a strong balance between security, performance, and compatibility across desktop and mobile devices.
---
## Related Documentation
* `docs/ARCHITECTURE.md`
* `docs/API.md`
* `docs/THREAT_MODEL.md`
* `docs/REFRACTORING-v0.6.0.md`
For diagnostics:
```bash
chronoseal config check
chronoseal stats
chronoseal health
```
## v1.0.2 Limits
Current supported range:
```toml
gene_size = 1..=4096
mutation_rounds = 1..=10
```
+71 -243
View File
@@ -1,278 +1,106 @@
# ChronoSeal Privacy & Design Principles # ChronoSeal Privacy Policy
## Privacy-First Browser Attestation Framework ChronoSeal is a privacy-oriented browser attestation system. It is designed to validate short-lived session continuity without creating persistent user profiles.
ChronoSeal is a lightweight, privacy-first browser attestation framework designed to resist: This document describes what ChronoSeal itself collects and stores. Applications that integrate ChronoSeal may collect additional data under their own policies.
- automated bots ## Data ChronoSeal Processes
- AI-driven browser automation
- scripted abuse
- browser surveillance ecosystems
Unlike conventional anti-bot systems, ChronoSeal is intentionally designed to operate **without collecting or storing client identity data**. ChronoSeal processes the minimum protocol data needed to validate a live browser session.
--- | Data | Purpose |
# Core Philosophy
ChronoSeal verifies:
- session continuity
- runtime coherence
- cryptographic synchronization
It does **not** verify:
- personal identity
- browsing history
- behavioral profiles
- long-term reputation
The framework is built around one principle:
> Verify live browser participation without turning users into telemetry.
---
# Privacy-First By Architecture
ChronoSeal is intentionally engineered to avoid becoming:
- a tracking platform
- a fingerprinting database
- a telemetry pipeline
- a surveillance system
## ChronoSeal Does NOT Store
- IP addresses
- Browser history
- Persistent fingerprints
- User profiles
- Behavioral telemetry
- Tracking identifiers
- Device databases
- Long-term session history
- Cross-site correlation data
No client-side personal information is persisted.
---
# Stateless Trust Model
ChronoSeal focuses on:
- ephemeral runtime verification
- cryptographic continuity
- synchronized challenge progression
- live execution integrity
The server only validates:
- whether the current browser session behaves like a coherent participant *right now*
ChronoSeal does not maintain:
- user identity databases
- reputation systems
- persistent surveillance records
---
# Anti-Bot Without Surveillance
Most modern anti-bot systems rely heavily on:
- fingerprinting
- behavioral tracking
- telemetry aggregation
- centralized analytics
ChronoSeal deliberately rejects this model.
Instead, ChronoSeal uses:
- synchronized cryptographic chains
- WASM-isolated signing
- protocol continuity
- transient verification state
This provides bot resistance while preserving user privacy.
---
# Lightweight By Design
ChronoSeal is intentionally engineered to remain:
- compact
- dependency-light
- operationally simple
- Unix-native
## Current Footprint
### Server Binary
Compiled x86_64 Linux server binary:
- ~8.4 MB
### WASM Runtime
`chronoseal_wasm_bg.wasm`
- ~218 KB
### Full WASM Package
Entire generated WASM package:
- ~720 KB
Includes:
- WASM runtime
- JavaScript glue code
- Type definitions
---
# No Frontend Framework Dependency
ChronoSeal does not depend on:
- React
- Angular
- Vue
- Electron
- Node.js runtime
- Browser bundler ecosystems
The browser runtime uses:
- native ES modules
- direct WebAssembly loading
- lightweight JavaScript glue
This minimizes:
- dependency complexity
- supply-chain risk
- build fragility
- browser overhead
---
# Clean Repository Philosophy
ChronoSeal keeps generated artefacts out of version control.
## What Is NOT Stored In The Repository
| Path | Reason |
|---|---| |---|---|
| `wasm/pkg/` | Generated build output | | `session_id` | Opaque session lookup key |
| `frontend/pkg/` | Generated serve-time artefacts | | public key | Verify signed heartbeats for the session |
| `target/` | Standard Rust build artefacts | | `salt` | Hash-chain progression |
| `initial_hash` / `prev_hash` / `last_hash` | Replay-resistant continuity |
| `timestamp` | Drift and liveness validation |
| mouse event samples | Behavioral plausibility checks |
| VM stack state | Input to hash-chain progression |
| basic fingerprint fields | Sanity validation |
| gene bytes and environment records | Mutation continuity |
| pending mutation program and step | Next heartbeat verification |
| expiration and last-seen timestamps | Session lifecycle and cleanup |
Generated binaries change frequently and are reproducible from source. Basic fingerprint fields currently include:
The repository intentionally stores: - aspect ratio
- device pixel ratio
- hardware concurrency
- source code ## Data ChronoSeal Does Not Intentionally Collect
- architecture
- reproducible build logic only
--- ChronoSeal does not intentionally collect or build:
# Unix-Native Operational Model - browser history
- page content history
- account identity
- email addresses
- names
- payment data
- location history
- cross-site tracking identifiers
- persistent fingerprint databases
- long-term behavioral profiles
ChronoSeal is designed as: ChronoSeal is not intended for analytics, advertising, or identity graph construction.
- infrastructure software ## Session Lifetime
- not browser-centric SaaS
Core operational principles: Sessions are short-lived and expire according to `expiration_minutes`, which defaults to 30 minutes.
- CLI-first operation Expired sessions are removed by cleanup behavior. In-memory storage is lost when the process exits.
- systemd-native deployment
- structured logs
- explicit configuration
- inspectable runtime behavior
- minimal hidden state
ChronoSeal should feel natural on Linux systems: ## Storage Modes and Persistence
- simple to deploy | Mode | Persistence |
- easy to audit |---|---|
- understandable years later | `sqlite-in-memory` | process lifetime only |
| `sqlite-in-disk` | persisted to the configured SQLite file |
| `valkey` | persisted according to the Valkey deployment configuration |
--- Persistent state is operator-selected. The default backend is `sqlite-in-memory`.
# Security Through Operational Asymmetry ## Client-Side Key Handling
ChronoSeal increases attacker cost through: The browser WASM runtime generates an Ed25519 keypair for the session.
- synchronization burden - The public key is sent to `/init`.
- runtime continuity requirements - The private key is not sent to the server.
- WASM-isolated cryptographic execution - Heartbeat payloads are signed in the browser runtime.
- chained session progression
It does not attempt: This is a continuity mechanism, not a long-term identity mechanism.
- invasive tracking ## Silent Rejection
- permanent identification
- surveillance-driven scoring
--- ChronoSeal returns the same basic heartbeat status for accepted and rejected heartbeat requests:
# Design Goals ```json
{
"status": "ok"
}
```
ChronoSeal prioritizes: Accepted responses additionally include next-state fields. Rejected responses omit them.
- Privacy This reduces attacker feedback and avoids returning detailed failure classifications to clients.
- Simplicity
- Transparency
- Operational clarity
- Long-term maintainability
- Minimalism
- Unix-native behavior
- Low deployment friction
--- ## Logs
# Non-Goals Operators control logging through `CHRONOSEAL_LOG`, `RUST_LOG`, and optional log-file configuration.
ChronoSeal is intentionally NOT: Production deployments should avoid debug logging because internal session identifiers or validation context may appear in logs.
- A surveillance platform ## Operator Responsibilities
- A telemetry collection system
- A browser fingerprinting database
- An analytics engine
- A cloud lock-in service
- A JavaScript-heavy frontend platform
- An advertising or tracking framework
--- Operators should:
# Summary - serve traffic over HTTPS
- protect SQLite, Valkey, and log storage
- restrict access to metrics and stats endpoints
- choose persistence mode deliberately
- disclose any application-level data collection separately
ChronoSeal is designed to prove: ## Summary
> “A live browser session is coherently participating right now.” ChronoSeal validates live session continuity using short-lived cryptographic and deterministic state. It is designed to raise automation cost without becoming a persistent tracking or profiling system.
without storing:
- who the user is
- where they came from
- what they previously did
It is a lightweight, privacy-preserving, Unix-native browser attestation framework focused on:
- anti-bot resistance
- anti-automation
- operational simplicity
without compromising user privacy.
+98
View File
@@ -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
+37
View File
@@ -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.
+191
View File
@@ -0,0 +1,191 @@
# ChronoSeal v0.6.0 Refactoring and System Upgrade
ChronoSeal v0.6.0 changed the project from a lightweight heartbeat prototype into a Unix-native attestation daemon with shared server/WASM protocol logic, deterministic mutation parity, operational CLI commands, and pluggable storage modes.
This document summarizes the architectural changes introduced in the v0.6.0 line.
## Summary
Major changes:
- introduced the Synthetic Gene Mutation Engine
- added server-side validation of `mutation_step` and `gene_commitment`
- moved protocol and deterministic mutation logic into `shared/`
- added `chronoseal-wasm` browser runtime support for mutation preview and commit
- expanded persisted session state with gene and pending mutation fields
- added storage modes: `sqlite-in-memory`, `sqlite-in-disk`, and `valkey`
- added health, metrics, stats, config, status, completion, and version CLI surfaces
- added PID file handling, structured logging, and graceful shutdown behavior
- preserved silent heartbeat rejection semantics
## Motivation
The earlier model relied mainly on:
- heartbeat timing
- behavioral entropy
- hash-chain continuity
- signature verification
v0.6.0 added a second deterministic state channel: a server-authored synthetic gene mutation sequence. This makes successful automation maintain both:
- the cryptographic hash/signature chain
- the synthetic mutation state expected by the server
## Shared Crate Refactor
`shared/` now owns the parts of the protocol that must remain identical across server and browser runtime:
- request and response structs
- hashing helpers
- synthetic gene state
- mutation environment encoding
- mutation order generation and encoding
- opcode execution semantics
- protocol constants
This reduces the risk of server/WASM drift.
## Mutation Handshake
New protocol fields:
- `gene_size`
- `mutation_step`
- `mutation_order_b64`
- `gene_commitment`
- `next_mutation_step`
- `next_mutation_order_b64`
Lifecycle:
1. `/init` returns mutation step 1 and a server-authored mutation order.
2. The browser previews the mutation in WASM.
3. The browser signs and submits the resulting `gene_commitment`.
4. The server applies the same pending mutation to its committed state.
5. The server compares commitments.
6. On success, server commits the candidate state and issues the next mutation.
7. The browser commits its preview only after receiving the accepted response.
## Session Schema Changes
The persisted session record now includes:
- committed gene bytes
- encoded environment records
- pending mutation program
- pending mutation step
State advances only after a heartbeat is accepted. Rejected heartbeats do not rotate salt, update hash state, commit gene state, or consume the pending mutation.
## WASM Runtime Changes
The WASM crate now supports:
- `generate_keypair()`
- `get_public_key()`
- `sign_message()`
- `compute_next_hash()`
- `run_program()`
- `init_gene_state()`
- `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)`
- `commit_gene_preview()`
- `discard_gene_preview()`
- `current_gene_commitment(session_id, mutation_step)`
The generated package uses the `chronoseal_wasm` prefix.
## Storage Refactor
The storage layer is abstracted behind `DbPool`.
Supported modes:
| Mode | Behavior |
|---|---|
| `sqlite-in-memory` | default ephemeral in-process SQLite |
| `sqlite-in-disk` | persisted SQLite database at `db_path` |
| `valkey` | Valkey-compatible external store |
The storage interface supports insert, load, update, delete expired sessions, and stats.
## CLI and Runtime Changes
The `chronoseal` binary now provides:
- `run`
- `status`
- `health`
- `config check`
- `generate keypair`
- `version`
- `db-type`
- `metrics`
- `stats`
- `completion`
The daemon exposes:
- `POST /init`
- `POST /hb`
- `GET /health`
- `GET /metrics`
- `GET /stats`
- static frontend serving at `/`
## Validation Improvements
The heartbeat verifier now checks:
- session presence
- expiration
- signature
- hash-chain continuity
- mutation step
- mutation commitment parity
- timestamp drift
- behavioral mouse checks
- fingerprint ranges
- rate limiting at the route layer
Accepted heartbeats return next-state fields. Rejected heartbeats return only `{"status":"ok"}`.
## Testing Impact
The refactor added or strengthened tests for:
- gene environment encoding and validation
- mutation opcode behavior
- mutation order round-trips
- deterministic mutation generation with seeded RNG
- server/client mutation parity
- random program divergence resistance
- replay rejection
- mutation step mismatch rejection
- mutation commitment tamper rejection
- storage backend stats
- route-level silent rejection behavior
## Operational Impact
v0.6.0 makes ChronoSeal more suitable for deployment as a real service:
- explicit daemon lifecycle
- CLI-first operations
- systemd-oriented install path
- health and metrics endpoints
- configurable persistence
- shared protocol implementation
- clearer docs and threat model
## Compatibility Notes
Important names in the current implementation:
- binary: `chronoseal`
- server crate: `chronoseal-server`
- WASM crate: `chronoseal-wasm`
- generated WASM module prefix: `chronoseal_wasm`
- persistent SQLite mode: `sqlite-in-disk`
Older docs or integrations may refer to `sqlite-disk`, `server`, or `antibot_wasm`; those names are stale for the current codebase.
+40
View File
@@ -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.
+395
View File
@@ -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)
+215 -149
View File
@@ -1,205 +1,271 @@
# ChronoSeal — Threat Model # ChronoSeal Threat Model
## Purpose ChronoSeal is a cost-raising browser attestation layer. It makes replay, stale state reuse, and incomplete automation more expensive by requiring signed, continuous, deterministic browser-side state progression.
This document defines what ChronoSeal is designed to protect against, what It is not a perfect bot blocker, CAPTCHA replacement, hardware attestation system, fraud engine, or identity provider.
it explicitly does not protect against, and the reasoning behind each
design decision in security terms.
ChronoSeal is a **cost-raising mechanism**. It does not claim to make ## Security Objectives
automated access impossible. It makes automated access expensive, complex
to maintain, and operationally fragile at scale.
--- ChronoSeal aims to:
## Assets Being Protected - reject stale or replayed heartbeat payloads
- reject heartbeats that do not maintain the server-issued mutation sequence
- bind heartbeat payloads to a browser-local Ed25519 session key
- make basic HTTP clients insufficient
- make browser automation maintain multiple synchronized state channels
- avoid detailed rejection feedback
- preserve privacy by avoiding persistent user identity state
| Asset | Description | ## Protected Assets
| Asset | Protection focus |
|---|---| |---|---|
| Web page content | HTML, rendered data, scraped text | | Protected page/API access | Require live attestation before allowing continued access |
| API responses | JSON endpoints that serve structured data | | Session continuity | Ensure each accepted heartbeat advances from the last accepted state |
| Server compute | CPU and bandwidth consumed by automated clients | | Server compute | Rate-limit and reject invalid clients without expensive application work |
| Rate-limited resources | Endpoints with per-user quotas | | Protocol state | Protect hash-chain, salt, and mutation progression |
| Behavioral analytics | Metrics polluted by bot traffic | | User privacy | Avoid long-term tracking and detailed failure disclosure |
--- ## Trust Assumptions
## Attacker Profiles ChronoSeal assumes:
### Level 1 — Script Kiddie / Commodity Scraper - the server host and daemon process are trusted
- storage is trusted for session continuity
- TLS protects traffic in production
- browser clients can run JavaScript and WASM
- operators configure reverse proxy, filesystem permissions, and logs appropriately
**Tools:** `curl`, `requests`, `scrapy`, simple HTTP clients. ChronoSeal does not assume:
**Capability:** No browser environment. Cannot execute JavaScript or WASM.
**ChronoSeal response:** Session never initialises. No `session_id` is ever
presented to `/hb`. Content gated behind session validation is never served.
### Level 2 — Headless Browser Operator - the browser is honest
- WASM is a secure enclave
- mouse data proves human presence
- fingerprint values are unforgeable
- attackers cannot run a full browser
**Tools:** Playwright, Puppeteer, Selenium, undetected-chromedriver. ## Attacker Levels
**Capability:** Full browser environment. Can execute JavaScript and WASM.
Cannot easily synthesise realistic mouse entropy or maintain hash chain state
across concurrent sessions.
**ChronoSeal response:** Mouse entropy validation rejects absent or synthetic
movement. Hash chain requires per-session state synchronisation. Scaling to
hundreds of concurrent sessions requires proportional infrastructure.
### Level 3 — Stealth Automation ### Level 1: Commodity HTTP Client
**Tools:** Puppeteer Stealth, rebrowser-patches, custom CDP clients with Examples:
evasion patches.
**Capability:** Patches `navigator.webdriver`, spoofs browser fingerprints,
can inject synthetic mouse events. May partially pass behavioral checks.
**ChronoSeal response:** Ed25519 signature over the full payload (including
behavioral state and VM execution result) means the attacker must also
correctly execute the WASM program and maintain chain continuity. The private
key is generated fresh per page load and never exposed — it cannot be
extracted from a legitimate session and reused.
### Level 4 — Sophisticated Adversary - `curl`
- `requests`
- scraper scripts without browser or WASM execution
**Tools:** Full browser farm with real input devices, WASM reverse engineering, Expected result:
custom chain maintenance infrastructure.
**Capability:** Can pass all current ChronoSeal checks given sufficient
engineering effort.
**ChronoSeal response:** Significantly increases operational cost. A browser
farm with real input devices costs orders of magnitude more than a commodity
scraper fleet. ChronoSeal is not designed to stop this attacker — no client-
side protection can.
--- - cannot produce valid signatures
- cannot maintain hash-chain state
- cannot execute mutation preview
- cannot produce accepted heartbeats
### Level 2: Basic Headless Browser
Examples:
- Playwright
- Puppeteer
- Selenium
Expected result:
- can load JavaScript and WASM
- must preserve keypair, hash chain, salt, VM, and mutation state
- must generate plausible timing and mouse event windows
- silent rejection complicates debugging and scaling
### Level 3: Stealth Automation
Examples:
- patched browser runtime
- synthetic event generation
- custom protocol client with WASM or Rust reimplementation
Expected result:
- can attempt full protocol implementation
- must still match canonical signing, hash progression, mutation parity, and timing
- must handle changing server-issued mutation programs
- receives limited failure feedback
### Level 4: Resourced Browser Farm
Examples:
- real browsers
- realistic input devices
- human-assisted workflows
- distributed session management
Expected result:
- ChronoSeal raises cost and complexity
- 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 ## Attack Vectors and Mitigations
### Replay Attack ### Replay
**Attack:** Capture a valid heartbeat payload and retransmit it. Attack: resend a previously accepted heartbeat.
**Mitigation:**
- Timestamp window (±30 seconds): replayed payloads are rejected after 30s. Mitigations:
- Hash chain: each heartbeat must present `H(n-1)` matching the server's
stored state. A replayed heartbeat presents a stale hash that no longer - stored `last_hash` must match request `prev_hash`
matches after one successful heartbeat has advanced the chain. - accepted heartbeats rotate salt
- mutation step advances after acceptance
- timestamp drift is bounded
### Signature Forgery ### Signature Forgery
**Attack:** Construct a valid-looking heartbeat payload without the private key. Attack: submit a heartbeat without the browser session private key.
**Mitigation:** Ed25519 with 128-bit security. The private key is generated
inside WASM `thread_local` memory, never serialised, never passed to
JavaScript, never transmitted. Forgery requires breaking Ed25519 or
extracting the key from WASM memory — neither is practical.
### Key Extraction Mitigations:
**Attack:** Inspect WASM linear memory to extract the private signing key. - Ed25519 signature over canonical payload
**Mitigation:** The key is stored in a Rust `thread_local! { RefCell<Option<SigningKey>> }`. - public key registered during `/init`
It has no exported symbol and is not referenced by any exported WASM function - signature verified on every heartbeat
that returns raw memory. An attacker with full DevTools access to the WASM - signature covers mutation step and gene commitment
memory can extract it from one session, but it is useless for other sessions
(fresh keypair per page load) and expires with the session.
### Hash Chain Forgery ### Hash-Chain Desynchronization
**Attack:** Compute a valid `H(n)` without the server-side salt. Attack: submit a heartbeat from stale client state.
**Mitigation:** Each chain link incorporates `saltₙ₋₁`, which is a 16-byte
random value known only to the server and returned (once) in the heartbeat
response. An attacker cannot compute `H(n+1)` without first receiving
`saltₙ` from a successful heartbeat response, which requires a valid signature
and all other checks to pass.
### Session Hijacking Mitigations:
**Attack:** Steal a `session_id` and use it from a different client. - server compares request `prev_hash` to stored `last_hash`
**Mitigation:** `session_id` alone is insufficient — the attacker also needs - server computes the next hash only after all validation passes
the private key (to produce valid signatures) and the current chain state - rejected heartbeats do not advance server state
(to present the correct `prev_hash`). All three are required simultaneously.
### Enumeration of Validation Rules ### Mutation Tampering
**Attack:** Send malformed heartbeats and analyse error responses to map Attack: forge or skip synthetic gene mutations.
validation logic.
**Mitigation:** All failure paths return `{"status":"ok"}` with no `next_salt`.
There is no error code, no error message, and no status difference between
a rate limit hit, an invalid signature, a broken chain, and a behavioral
rejection.
### DoS via Session Flooding Mitigations:
**Attack:** Open thousands of sessions to exhaust the rate limiter's HashMap - server stores the pending mutation program
memory. - request must include the expected `mutation_step`
**Mitigation:** Rate limiter entries are evicted every 60 seconds by the - server applies the mutation independently
cleanup task. Each entry is a small `(u32, Instant)` tuple; even at 100,000 - commitment includes candidate gene state, `session_id`, and step
concurrent fake sessions, the HashMap occupies roughly 10–15 MB, which is - mismatch causes silent rejection
well within normal server memory budgets. Sessions themselves expire after 30
minutes of inactivity and are purged from SQLite.
### Clock Manipulation ### Session Identifier Theft
**Attack:** Manipulate the client's `Date.now()` to bypass the timestamp Attack: reuse a stolen `session_id`.
window.
**Mitigation:** The timestamp is included in the signed payload. Manipulating
it requires also forging the signature. The server validates against its own
clock — client-side clock manipulation cannot help without the private key.
### Synthetic Mouse Events Mitigations:
**Attack:** Inject programmatic `mousemove` events via `dispatchEvent` or - `session_id` alone is insufficient
CDP input simulation. - attacker also needs current private key, hash state, salt, mutation step, and mutation state
**Mitigation:** Synthetic events often fail the pause check (no natural dwell - stale attempts fail after the real session advances
periods), produce unrealistically uniform speed profiles, or fail the minimum
distance threshold. Generating convincingly human mouse traces at scale
requires either real input devices or sophisticated probabilistic models —
both significantly increase operational cost.
--- ### Failure Oracle Probing
## What ChronoSeal Does Not Protect Against Attack: send malformed requests and inspect responses to infer validation rules.
| Limitation | Explanation | Mitigations:
|---|---|
| Real browsers with real users acting as bots | A human operating a browser manually is indistinguishable from a legitimate visitor. ChronoSeal cannot address this. |
| Server-side vulnerabilities | ChronoSeal is a client attestation layer. It does not protect the server from injection, authentication bypass, or other backend vulnerabilities. |
| Highly resourced nation-state actors | Out of scope for a client-side protection layer. |
| Content visible before session establishment | If the protected content is rendered before the first heartbeat, it can be scraped without a session. Gate content on session validity server-side. |
| Perfect bot prevention | No client-side mechanism can be. WASM can be reverse engineered. ChronoSeal raises cost, not an impenetrable barrier. |
--- - heartbeat semantic failures return `200 OK` with `{"status":"ok"}`
- accepted heartbeats are distinguished only by next-state fields
- detailed validation errors are not returned to the client
## Operational Security Notes ### Storage Tampering
### Log Level Attack: alter persisted session state.
Do not run with `RUST_LOG=debug` in production. The debug log includes Mitigations:
`session_id` values, which are sensitive identifiers. Use `warn` or `info`.
### CORS Policy - run the daemon under a dedicated user
- restrict SQLite database permissions
- protect Valkey behind trusted network boundaries
- use normal host hardening and backups where persistence matters
The default `CorsLayer::permissive()` is suitable for development only. Storage is trusted. If an attacker can modify storage, they can affect session continuity.
In production, restrict allowed origins to your own domain:
```rust ## Behavioral Checks
CorsLayer::new()
.allow_origin("https://your.domain.com".parse::<HeaderValue>().unwrap())
.allow_methods([Method::POST])
.allow_headers([header::CONTENT_TYPE])
```
### TLS ChronoSeal validates:
Serve exclusively over TLS 1.3. The heartbeat payload contains timestamps - minimum event count
and behavioral signals. While each payload is signed and cannot be forged, - minimum movement distance
plaintext transmission leaks behavioral patterns and timing information that - maximum average speed
could assist a sophisticated attacker. - pause count
- timestamp drift
- basic fingerprint field ranges
### In-Memory SQLite These checks are cost signals. They are not proof of humanity and should not be the only security layer for high-risk actions.
All session state is lost on server restart. This is intentional — there is ## Privacy Constraints
no persistent state to steal. Clients transparently re-initialise. If your
deployment restarts frequently (e.g. rolling deploys), sessions will be lost
more often; tune `HEARTBEAT_MIN_INTERVAL_MS` and `EXPIRATION_MINUTES`
accordingly so clients recover quickly.
--- ChronoSeal intentionally avoids:
## Security Disclosure - persistent user identifiers
- browser history collection
- device fingerprint databases
- cross-session identity graphs
- long-term behavioral profiles
See [SECURITY.md](../SECURITY.md) for the vulnerability disclosure policy Session data is short-lived by default. Persistent storage is operator-selected through `sqlite-in-disk` or `valkey`.
and contact details.
## Limitations
ChronoSeal does not protect against:
- real users intentionally automating or abusing access
- complete browser farms with realistic input
- compromised server hosts
- tampered storage
- server-side application vulnerabilities
- credential theft outside ChronoSeal
- policy decisions that require identity, risk scoring, or business context
## Operational Security
Recommended:
- serve all traffic over HTTPS
- keep `/init` and `/hb` same-origin with protected content when possible
- run behind a reverse proxy
- keep debug logs disabled in production
- protect storage and log directories
- monitor health and metrics
- use `sqlite-in-memory` for ephemeral sessions
- use `sqlite-in-disk` or `valkey` only when persistence is required
## 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
+102 -240
View File
@@ -1,301 +1,163 @@
# ChronoSeal — WASM Build Guide # ChronoSeal WASM Build Guide
## Overview ChronoSeal uses a Rust-generated WASM package for browser-side attestation. The package is built from `wasm/` and copied into `frontend/pkg`.
The client-side cryptographic core of ChronoSeal is written in Rust and ## Responsibilities
compiled to WebAssembly (WASM). The JavaScript frontend (`heartbeat.js`)
imports functions from this WASM module to generate keypairs, sign heartbeat
payloads, compute hash chain links, and execute the stack machine program.
The import line in `heartbeat.js`: The WASM runtime:
```js - generates a browser-local Ed25519 keypair
import init, { generate_keypair, sign_message, compute_next_hash, run_program } - signs canonical heartbeat payloads
from './pkg/antibot_wasm.js'; - computes Blake3 hash-chain progression
``` - executes server-issued VM opcode programs
- initializes synthetic gene state
- previews gene mutation commitments
- commits or discards preview state after heartbeat response
`./pkg/antibot_wasm.js` is a **generated file**. It does not exist in the The WASM runtime is not treated as a secure enclave. The server independently recomputes deterministic state.
repository and must be produced by building the `wasm/` crate before running
the server.
--- ## Requirements
## How the WASM Module is Built
The tool that compiles Rust to WASM and generates the JavaScript glue is
[`wasm-pack`](https://rustwasm.github.io/wasm-pack/).
When you run:
```bash
wasm-pack build wasm --target web --release
```
wasm-pack does the following in sequence:
1. Compiles `wasm/src/lib.rs` (and its submodules) to a `.wasm` binary using
the `wasm32-unknown-unknown` target.
2. Runs `wasm-bindgen` to inspect every `#[wasm_bindgen]`-annotated function
and struct and generate a JavaScript wrapper for each one.
3. Optionally runs `wasm-opt` (from Binaryen) to size-optimise the binary.
4. Writes all output to `wasm/pkg/`.
---
## Output: `wasm/pkg/`
After a successful build, `wasm/pkg/` contains:
```
wasm/pkg/
├── antibot_wasm.js ← ES module; the file heartbeat.js imports
├── antibot_wasm_bg.wasm ← compiled WASM binary (~300–800 KB release)
├── antibot_wasm_bg.js ← internal memory bridge (do not import directly)
├── antibot_wasm.d.ts ← TypeScript type declarations
├── antibot_wasm_bg.d.ts ← TypeScript declarations for the bg module
└── package.json
```
### `antibot_wasm.js`
This is the public entry point. It contains:
- An `init()` function that fetches and instantiates the `.wasm` binary.
- One JavaScript wrapper function for each `#[wasm_bindgen]` export in
`wasm/src/`:
| Rust export | JS wrapper | Description |
|---|---|---|
| `generate_keypair()` | `generate_keypair()` | Generate Ed25519 keypair; return hex public key |
| `get_public_key()` | `get_public_key()` | Return hex public key, or `""` if not initialised |
| `sign_message(msg)` | `sign_message(msg)` | Sign string; return hex signature, or `""` if not initialised |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `compute_next_hash(...)` | Compute next Blake3 chain hash |
| `run_program(b64)` | `run_program(b64)` | Execute base64 VM program; return `{ stack, ip }` |
### `antibot_wasm_bg.wasm`
The compiled binary. The `.bg` suffix means "background" — this is the raw
WASM that `antibot_wasm.js` loads internally. You should not reference this
file directly in your HTML.
---
## Step-by-Step Build
### 1. Install the Rust WASM target
```bash ```bash
rustup target add wasm32-unknown-unknown rustup target add wasm32-unknown-unknown
```
This is a one-time step. Without it, the Rust compiler cannot produce WASM
output.
### 2. Install wasm-pack
```bash
cargo install wasm-pack cargo install wasm-pack
``` ```
Or via the installer script:
```bash
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
Verify: Verify:
```bash ```bash
wasm-pack --version wasm-pack --version
# wasm-pack 0.13.x
``` ```
### 3. Build the WASM module ## Build
From the project root: From the repository root:
```bash ```bash
wasm-pack build wasm --target web --release wasm-pack build wasm --target web --release
```
`--target web` produces an ES module (`import`/`export` syntax) suitable for
use directly in a browser without a bundler. Other targets (`bundler`,
`nodejs`, `no-modules`) produce different output formats and are not
compatible with the ChronoSeal frontend as written.
`--release` enables Rust's release optimisations (inlining, dead code
elimination, size reduction). Omit it during development for faster builds
and better panic messages.
### 4. Move the output to the frontend
```bash
rm -rf frontend/pkg rm -rf frontend/pkg
mv wasm/pkg frontend/pkg mv wasm/pkg frontend/pkg
``` ```
The frontend expects the WASM module at `frontend/pkg/antibot_wasm.js` `--target web` emits native ES modules compatible with the static frontend.
because `heartbeat.js` imports from `./pkg/antibot_wasm.js` relative to
the `frontend/` directory, which is where the server's static file handler
is rooted.
--- Development build:
## Using the Build Script ```bash
wasm-pack build wasm --target web
rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
```
The convenience script at `scripts/build.sh` performs all steps in order: Full project build:
```bash ```bash
bash scripts/build.sh bash scripts/build.sh
``` ```
This builds the WASM module, moves it to `frontend/pkg/`, and then builds ## Output Files
the server binary. Run this for a clean full build before deployment.
For development iteration where you are only changing Rust WASM code: The package name comes from the crate name `chronoseal-wasm`, so generated files use the `chronoseal_wasm` prefix.
```bash Expected `frontend/pkg/` contents include:
wasm-pack build wasm --target web # (omit --release for speed)
rm -rf frontend/pkg && mv wasm/pkg frontend/pkg
```
For development where you are only changing server code: - `chronoseal_wasm.js`
- `chronoseal_wasm_bg.wasm`
- `chronoseal_wasm.d.ts`
- `package.json`
```bash Generated files in `wasm/pkg/` and `frontend/pkg/` are build artifacts and should be regenerated during release.
cargo build -p server
```
--- ## Browser Import
## How `heartbeat.js` Loads the Module
`heartbeat.js` uses a standard ES module dynamic import pattern:
```js ```js
import init, { generate_keypair, sign_message, compute_next_hash, run_program } import init, {
from './pkg/antibot_wasm.js'; generate_keypair,
get_public_key,
export async function initHeartbeat() { sign_message,
// 1. Fetch and instantiate the .wasm binary compute_next_hash,
await init(); run_program,
init_gene_state,
// 2. Generate keypair — private key stored in WASM memory only preview_gene_commitment,
const pubKeyHex = generate_keypair(); commit_gene_preview,
discard_gene_preview,
// 3. Send public key to server, receive session_id and chain seed current_gene_commitment
// ... } from './pkg/chronoseal_wasm.js';
}
``` ```
`init()` is the default export from `antibot_wasm.js`. It fetches Call `await init()` before using any exported function.
`antibot_wasm_bg.wasm` (from the same `pkg/` directory) via `fetch()`,
compiles it in the browser's WASM engine, and links it to the JS glue
layer. After `await init()` returns, all the named exports
(`generate_keypair`, `sign_message`, etc.) are ready to call.
The `init()` call must complete before any other WASM function is called. ## Exported Functions
Calling `sign_message()` or `compute_next_hash()` before `await init()`
returns will produce an empty string (the module is not yet instantiated).
--- | Function | Signature | Failure value |
|---|---|---|
| `generate_keypair()` | `() -> string` | `""` only on unexpected failure |
| `get_public_key()` | `() -> string` | `""` if no keypair exists |
| `sign_message(msg)` | `(string) -> string` | `""` if no keypair exists or signing fails |
| `compute_next_hash(prev, ts, entropy, stack, salt)` | `(string, u64, string, string, string) -> string` | panic/error path should be avoided by valid inputs |
| `run_program(b64)` | `(string) -> JsValue` | returns empty/default stack state on invalid execution path |
| `init_gene_state(gene_size)` | `(u32) -> bool` | `false` |
| `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` | `(string, string, u64, u8) -> string` | `""` |
| `commit_gene_preview()` | `() -> bool` | `false` |
| `discard_gene_preview()` | `() -> void` | none |
| `current_gene_commitment(session_id, mutation_step)` | `(string, u64) -> string` | `""` if no committed state exists |
## Serving the WASM Binary `rounds = 0` in `preview_gene_commitment` selects the shared default mutation round count.
Browsers require WASM files to be served with the correct MIME type: ## Mutation State Lifecycle
The browser must keep two gene states:
- committed state: the last accepted state
- preview state: candidate state for the heartbeat currently being sent
Expected sequence:
1. Call `init_gene_state(gene_size)` after `/init`.
2. Call `preview_gene_commitment(order_b64, session_id, mutation_step, rounds)` before signing `/hb`.
3. Include the returned commitment and mutation step in the signed heartbeat.
4. If the response contains next-state fields, call `commit_gene_preview()`.
5. If the heartbeat is rejected or errors, call `discard_gene_preview()`.
Never commit preview state before the server accepts the heartbeat.
## Hash-Chain Ordering
After an accepted heartbeat, compute the next local hash with the salt that was active when the heartbeat was sent. Then replace the local salt with `next_salt`.
Correct order:
```js
const sentSalt = currentSalt;
currentSalt = resp.next_salt;
prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackStateJson, sentSalt);
``` ```
This mirrors the server, which computes and stores the new hash before rotating to the next salt.
## Serving WASM
The `.wasm` file must be served with:
```text
Content-Type: application/wasm Content-Type: application/wasm
``` ```
Most web servers set this automatically for `.wasm` files. If you see the ChronoSeal's built-in static file service handles this for normal deployments.
error:
``` ## Validation
WebAssembly.instantiate(): Response has unsupported MIME type
```
Add the MIME type to your server configuration: Recommended checks after WASM changes:
**nginx:**
```nginx
types {
application/wasm wasm;
}
```
**Apache `.htaccess`:**
```apache
AddType application/wasm .wasm
```
The Axum `ServeDir` handler used by ChronoSeal's built-in static server
sets the correct MIME type automatically via `tower-http`.
---
## What Is Not in the Repository
| Path | Why excluded |
|---|---|
| `wasm/pkg/` | Generated build output — changes on every build |
| `frontend/pkg/` | Same generated output, moved to serve location |
| `target/` | Standard Rust build artefacts |
Both `wasm/pkg/` and `frontend/pkg/` are listed in `.gitignore`. Committing
them would bloat the repository (the `.wasm` binary alone is 300–800 KB),
create noisy diffs on every rebuild, and give a false impression that the
WASM module is pre-built and ready to use without a build step.
---
## Troubleshooting
### `wasm32-unknown-unknown` target not found
```
error[E0463]: can't find crate for `std`
```
Fix:
```bash ```bash
rustup target add wasm32-unknown-unknown cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
wasm-pack build wasm --target web
``` ```
### `wasm-pack` not found Then refresh `frontend/pkg`:
```bash ```bash
cargo install wasm-pack rm -rf frontend/pkg
mv wasm/pkg frontend/pkg
``` ```
### `wasm-opt` not found (warning, not an error)
wasm-pack prints a warning if `wasm-opt` is not installed. The build still
succeeds; the binary is just not size-optimised.
```bash
# On Debian/Ubuntu/Arch
sudo apt install binaryen # Debian/Ubuntu
sudo pacman -S binaryen # Arch
```
### `antibot_wasm_bg.wasm` fetch fails (404)
The `.wasm` file is not being served from `frontend/pkg/`. Verify:
```bash
ls /mnt/Programs/ChronoSeal/frontend/pkg/
# Should list: antibot_wasm.js antibot_wasm_bg.wasm ...
```
If the directory is empty or missing, re-run the build steps above.
### MIME type error in browser
See the "Serving the WASM Binary" section above.
### `sign_message` or `generate_keypair` returns empty string
The WASM keypair has not been initialised. Ensure `await init()` and
`generate_keypair()` are called (and awaited) before any other WASM
function. Check the browser console for any errors during `init()`.
+66
View File
@@ -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
+41
View File
@@ -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.
+22
View File
@@ -1,5 +1,27 @@
bind = "0.0.0.0:3000" bind = "0.0.0.0:3000"
# Storage backend: sqlite-in-memory (default), sqlite-in-disk, or valkey.
db_type = "sqlite-in-memory"
pid_file = "/run/chronoseal.pid" pid_file = "/run/chronoseal.pid"
db_path = "/var/lib/chronoseal/chronoseal.sqlite" db_path = "/var/lib/chronoseal/chronoseal.sqlite"
frontend_dir = "/usr/share/chronoseal/frontend" frontend_dir = "/usr/share/chronoseal/frontend"
log_file = "/var/log/chronoseal/chronoseal.jsonl" log_file = "/var/log/chronoseal/chronoseal.jsonl"
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 10.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
# Synthetic gene size. Current valid range: 1..=4096. Default: 512.
gene_size = 512
# Current valid range: 1..=10. Default: 4.
mutation_rounds = 4
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -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

+21
View File
@@ -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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+39 -7
View File
@@ -1,10 +1,22 @@
import init, { generate_keypair, sign_message, compute_next_hash, run_program } from './pkg/chronoseal_wasm.js'; import init, {
generate_keypair,
sign_message,
compute_next_hash,
run_program,
init_gene_state,
preview_gene_commitment,
commit_gene_preview,
discard_gene_preview
} from './pkg/chronoseal_wasm.js';
import { collectEntropy } from './entropy.js'; import { collectEntropy } from './entropy.js';
import { sendRequest } from './transport.js'; import { sendRequest } from './transport.js';
let session, prevHash, currentSalt, opcodesB64, lastTime; let session, prevHash, currentSalt, opcodesB64, lastTime;
let minInterval = 12000; let minInterval = 12000;
let maxInterval = 25000; let maxInterval = 25000;
let pendingMutationStep = 0;
let pendingMutationOrderB64 = '';
let mutationRounds = 4;
export async function initHeartbeat() { export async function initHeartbeat() {
await init(); await init();
@@ -16,6 +28,12 @@ export async function initHeartbeat() {
opcodesB64 = initResp.opcodes_b64; opcodesB64 = initResp.opcodes_b64;
minInterval = initResp.heartbeat_min_interval_ms || 12000; minInterval = initResp.heartbeat_min_interval_ms || 12000;
maxInterval = initResp.heartbeat_max_interval_ms || 25000; maxInterval = initResp.heartbeat_max_interval_ms || 25000;
if (!init_gene_state(initResp.gene_size || 512)) {
throw new Error('Failed to initialize gene state');
}
pendingMutationStep = initResp.mutation_step;
pendingMutationOrderB64 = initResp.mutation_order_b64;
mutationRounds = initResp.mutation_rounds || 4;
lastTime = performance.now(); lastTime = performance.now();
scheduleNext(); scheduleNext();
} }
@@ -40,6 +58,10 @@ async function sendHeartbeat() {
const timestamp = Date.now(); const timestamp = Date.now();
const entropyData = { events: events.map(e => ({ x: e.x, y: e.y, t: e.t })) }; const entropyData = { events: events.map(e => ({ x: e.x, y: e.y, t: e.t })) };
const entropyJson = JSON.stringify(entropyData); const entropyJson = JSON.stringify(entropyData);
const geneCommitment = preview_gene_commitment(pendingMutationOrderB64, session, pendingMutationStep, mutationRounds);
if (!geneCommitment) {
throw new Error('Unable to compute mutation commitment');
}
const signable = { const signable = {
sessionId: session, sessionId: session,
@@ -47,12 +69,14 @@ async function sendHeartbeat() {
timestamp: timestamp, timestamp: timestamp,
entropyData: entropyData, entropyData: entropyData,
stackState: JSON.parse(stackState), stackState: JSON.parse(stackState),
fingerprint: fingerprint fingerprint: fingerprint,
mutationStep: pendingMutationStep,
geneCommitment: geneCommitment
}; };
const msg = JSON.stringify(signable, Object.keys(signable).sort()); const msg = JSON.stringify(signable, Object.keys(signable).sort());
const sig = sign_message(msg); const sig = sign_message(msg);
if (!sig) { if (!sig) {
console.error('Keypair not initialised — skipping heartbeat'); discard_gene_preview();
return; return;
} }
const resp = await sendRequest('/hb', 'POST', { const resp = await sendRequest('/hb', 'POST', {
@@ -62,22 +86,30 @@ async function sendHeartbeat() {
entropy_data: entropyData, entropy_data: entropyData,
stack_state: JSON.parse(stackState), stack_state: JSON.parse(stackState),
fingerprint, fingerprint,
mutation_step: pendingMutationStep,
gene_commitment: geneCommitment,
signature: sig signature: sig
}); });
if (resp.next_salt) { if (resp.next_salt && resp.next_mutation_step && resp.next_mutation_order_b64) {
if (!commit_gene_preview()) {
discard_gene_preview();
throw new Error('Failed to commit local mutation preview');
}
// IMPORTANT: capture the salt that was active when this heartbeat was sent. // IMPORTANT: capture the salt that was active when this heartbeat was sent.
// The server computes new_hash = H(prev, ts, entropy, stack, OLD_salt) and stores it, // The server computes new_hash = H(prev, ts, entropy, stack, OLD_salt) and stores it,
// then rotates to next_salt. We must mirror that using the same old salt, then rotate. // then rotates to next_salt. We must mirror that using the same old salt, then rotate.
const sentSalt = currentSalt; const sentSalt = currentSalt;
currentSalt = resp.next_salt; currentSalt = resp.next_salt;
prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackState, sentSalt); prevHash = compute_next_hash(prevHash, timestamp, entropyJson, stackState, sentSalt);
pendingMutationStep = resp.next_mutation_step;
pendingMutationOrderB64 = resp.next_mutation_order_b64;
} else { } else {
console.warn('Heartbeat rejected'); discard_gene_preview();
} }
} catch (e) { } catch (e) {
console.error(e); discard_gene_preview();
} finally { } finally {
scheduleNext(); scheduleNext();
} }
} }
+1
View File
@@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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> <title>Anti-Scraper Demo</title>
</head> </head>
<body> <body>
+1 -3
View File
@@ -1,5 +1,3 @@
import { initHeartbeat } from './heartbeat.js'; import { initHeartbeat } from './heartbeat.js';
(async () => { initHeartbeat().catch(() => {});
await initHeartbeat();
})();
+596
View File
@@ -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"
+30
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let _ = shared::gene::decode_environment(data);
});
+9
View File
@@ -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);
}
});
+6
View File
@@ -0,0 +1,6 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let _ = shared::vm::execute(data);
});
+1
View File
@@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
set -euo pipefail
echo "Starting server with static frontend serving..." echo "Starting server with static frontend serving..."
cd ../server cd ../server
cargo run --release cargo run --release
+4 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "chronoseal-server" name = "chronoseal-server"
version = "0.5.0" version = "1.0.2"
edition = "2021" edition = "2021"
[[bin]] [[bin]]
@@ -30,3 +30,6 @@ hex = "0.4"
base64 = "0.22" base64 = "0.22"
rand = "0.8" rand = "0.8"
ed25519-dalek = "2" ed25519-dalek = "2"
redis = { version = "0.29", features = ["r2d2"] }
dashmap = "6"
+13 -12
View File
@@ -1,28 +1,29 @@
use crate::session::AppState; use crate::session::AppState;
use std::sync::Arc; 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>) { pub async fn cleanup_loop(state: Arc<AppState>) {
loop { loop {
tokio::time::sleep(std::time::Duration::from_secs(60)).await; tokio::time::sleep(std::time::Duration::from_secs(60)).await;
// Evict expired sessions from SQLite. // Evict expired sessions from the configured storage backend.
{ {
if let Ok(conn) = state.db_pool.get() { if let Err(err) = state.db_pool.delete_expired_sessions() {
let now = crate::storage::current_time_ms(); tracing::error!("Failed to evict expired sessions: {}", err);
let _ = conn.execute(
"DELETE FROM sessions WHERE expires_at < ?1",
rusqlite::params![now],
);
} else {
tracing::error!("Failed to get database connection from pool for cleanup");
} }
} }
// 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 window_secs = state.get_config().rate_limit_window_secs;
let mut rl = state.rate_limiter.lock().await; state.rate_limiter.evict_stale(window_secs);
rl.evict_stale(window_secs);
} }
} }
} }
+14 -1
View File
@@ -53,7 +53,7 @@ impl GlobalArgs {
pub enum Command { pub enum Command {
/// Run the ChronoSeal daemon. /// Run the ChronoSeal daemon.
#[command( #[command(
after_help = "Examples:\n chronoseal run\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run" after_help = "Examples:\n chronoseal run\n chronoseal run --db-type sqlite-in-memory\n chronoseal run --bind 127.0.0.1:3000 --frontend-dir /srv/chronoseal/frontend\n CHRONOSEAL_BIND=0.0.0.0:3000 chronoseal run"
)] )]
Run(RunArgs), Run(RunArgs),
@@ -81,6 +81,10 @@ pub enum Command {
#[command(after_help = "Examples:\n chronoseal version\n chronoseal version --format json")] #[command(after_help = "Examples:\n chronoseal version\n chronoseal version --format json")]
Version, Version,
/// List database backend types and implementation status.
#[command(after_help = "Examples:\n chronoseal db-type\n chronoseal db-type --format json")]
DbType,
/// Print Prometheus metrics from the running daemon. /// Print Prometheus metrics from the running daemon.
#[command( #[command(
after_help = "Examples:\n chronoseal metrics\n chronoseal metrics --bind 127.0.0.1:3000" after_help = "Examples:\n chronoseal metrics\n chronoseal metrics --bind 127.0.0.1:3000"
@@ -103,6 +107,11 @@ pub struct RunArgs {
#[command(flatten)] #[command(flatten)]
pub runtime: RuntimeArgs, pub runtime: RuntimeArgs,
/// Database backend selection.
/// sqlite-in-memory is active. sqlite-in-disk and valkey are planned (TODO).
#[arg(long, env = "CHRONOSEAL_DB_TYPE", value_enum)]
pub db_type: Option<crate::config::DbType>,
/// SQLite database path. Use ':memory:' for ephemeral state. /// SQLite database path. Use ':memory:' for ephemeral state.
#[arg(long, env = "CHRONOSEAL_DB_PATH")] #[arg(long, env = "CHRONOSEAL_DB_PATH")]
pub db_path: Option<PathBuf>, pub db_path: Option<PathBuf>,
@@ -114,6 +123,10 @@ pub struct RunArgs {
/// Optional structured JSON log file. /// Optional structured JSON log file.
#[arg(long, env = "CHRONOSEAL_LOG_FILE")] #[arg(long, env = "CHRONOSEAL_LOG_FILE")]
pub log_file: Option<PathBuf>, pub log_file: Option<PathBuf>,
/// Number of mutation rounds to execute per program.
#[arg(long, env = "CHRONOSEAL_MUTATION_ROUNDS")]
pub mutation_rounds: Option<u8>,
} }
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
+136
View File
@@ -1,4 +1,5 @@
use crate::cli::{RunArgs, RuntimeArgs}; use crate::cli::{RunArgs, RuntimeArgs};
use clap::ValueEnum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
env, fs, io, env, fs, io,
@@ -6,10 +7,30 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
#[value(rename_all = "kebab-case")]
pub enum DbType {
SqliteInMemory,
SqliteInDisk,
Valkey,
}
impl DbType {
pub fn as_str(self) -> &'static str {
match self {
Self::SqliteInMemory => "sqlite-in-memory",
Self::SqliteInDisk => "sqlite-in-disk",
Self::Valkey => "valkey",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct Config { pub struct Config {
pub bind: String, pub bind: String,
pub db_type: DbType,
pub pid_file: PathBuf, pub pid_file: PathBuf,
pub db_path: PathBuf, pub db_path: PathBuf,
pub frontend_dir: PathBuf, pub frontend_dir: PathBuf,
@@ -24,12 +45,15 @@ pub struct Config {
pub max_mouse_avg_speed: f64, pub max_mouse_avg_speed: f64,
pub min_pause_count: u32, pub min_pause_count: u32,
pub require_mouse_activity: bool, pub require_mouse_activity: bool,
pub gene_size: usize,
pub mutation_rounds: u8,
} }
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
bind: "0.0.0.0:3000".to_string(), bind: "0.0.0.0:3000".to_string(),
db_type: DbType::SqliteInMemory,
pid_file: PathBuf::from("/run/chronoseal.pid"), pid_file: PathBuf::from("/run/chronoseal.pid"),
db_path: default_state_dir().join("chronoseal.sqlite"), db_path: default_state_dir().join("chronoseal.sqlite"),
frontend_dir: PathBuf::from("/usr/share/chronoseal/frontend"), frontend_dir: PathBuf::from("/usr/share/chronoseal/frontend"),
@@ -44,6 +68,8 @@ impl Default for Config {
max_mouse_avg_speed: 2.0, max_mouse_avg_speed: 2.0,
min_pause_count: 1, min_pause_count: 1,
require_mouse_activity: true, require_mouse_activity: true,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
mutation_rounds: shared::constants::DEFAULT_MUTATION_ROUNDS,
} }
} }
} }
@@ -82,6 +108,9 @@ impl Config {
pub fn apply_run_args(&mut self, args: &RunArgs) { pub fn apply_run_args(&mut self, args: &RunArgs) {
self.apply_runtime_args(&args.runtime); self.apply_runtime_args(&args.runtime);
if let Some(db_type) = args.db_type {
self.db_type = db_type;
}
if let Some(db_path) = &args.db_path { if let Some(db_path) = &args.db_path {
self.db_path = db_path.clone(); self.db_path = db_path.clone();
} }
@@ -91,6 +120,9 @@ impl Config {
if let Some(log_file) = &args.log_file { if let Some(log_file) = &args.log_file {
self.log_file = Some(log_file.clone()); self.log_file = Some(log_file.clone());
} }
if let Some(mutation_rounds) = args.mutation_rounds {
self.mutation_rounds = mutation_rounds;
}
} }
pub fn validate(&self) -> Result<(), ConfigError> { pub fn validate(&self) -> Result<(), ConfigError> {
@@ -100,6 +132,18 @@ impl Config {
bind: self.bind.clone(), bind: self.bind.clone(),
source, source,
})?; })?;
if !(1..=shared::constants::MAX_GENE_SIZE).contains(&self.gene_size) {
return Err(ConfigError::InvalidGeneSize {
size: self.gene_size,
});
}
if !(shared::constants::MIN_MUTATION_ROUNDS..=shared::constants::MAX_MUTATION_ROUNDS)
.contains(&self.mutation_rounds)
{
return Err(ConfigError::InvalidMutationRounds {
rounds: self.mutation_rounds,
});
}
Ok(()) Ok(())
} }
@@ -107,6 +151,14 @@ impl Config {
if let Ok(value) = env::var("CHRONOSEAL_BIND") { if let Ok(value) = env::var("CHRONOSEAL_BIND") {
self.bind = value; self.bind = value;
} }
if let Ok(value) = env::var("CHRONOSEAL_DB_TYPE") {
self.db_type = match value.as_str() {
"sqlite-in-memory" => DbType::SqliteInMemory,
"sqlite-in-disk" => DbType::SqliteInDisk,
"valkey" => DbType::Valkey,
_ => self.db_type,
};
}
if let Ok(value) = env::var("CHRONOSEAL_PID_FILE") { if let Ok(value) = env::var("CHRONOSEAL_PID_FILE") {
self.pid_file = PathBuf::from(value); self.pid_file = PathBuf::from(value);
} }
@@ -169,6 +221,16 @@ impl Config {
self.require_mouse_activity = val; self.require_mouse_activity = val;
} }
} }
if let Ok(value) = env::var("CHRONOSEAL_GENE_SIZE") {
if let Ok(val) = value.parse() {
self.gene_size = val;
}
}
if let Ok(value) = env::var("CHRONOSEAL_MUTATION_ROUNDS") {
if let Ok(val) = value.parse() {
self.mutation_rounds = val;
}
}
} }
} }
@@ -186,6 +248,12 @@ pub enum ConfigError {
bind: String, bind: String,
source: std::net::AddrParseError, source: std::net::AddrParseError,
}, },
InvalidGeneSize {
size: usize,
},
InvalidMutationRounds {
rounds: u8,
},
} }
impl std::fmt::Display for ConfigError { impl std::fmt::Display for ConfigError {
@@ -198,6 +266,21 @@ impl std::fmt::Display for ConfigError {
Self::InvalidBind { bind, source } => { Self::InvalidBind { bind, source } => {
write!(f, "invalid bind address {bind}: {source}") write!(f, "invalid bind address {bind}: {source}")
} }
Self::InvalidGeneSize { size } => {
write!(
f,
"invalid gene size {size}; expected 1..={}",
shared::constants::MAX_GENE_SIZE
)
}
Self::InvalidMutationRounds { rounds } => {
write!(
f,
"invalid mutation rounds {rounds}; expected {}..={}",
shared::constants::MIN_MUTATION_ROUNDS,
shared::constants::MAX_MUTATION_ROUNDS
)
}
} }
} }
} }
@@ -238,3 +321,56 @@ fn default_state_dir() -> PathBuf {
} }
PathBuf::from("/var/lib/chronoseal") PathBuf::from("/var/lib/chronoseal")
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_db_type_is_sqlite_in_memory() {
let cfg = Config::default();
assert_eq!(cfg.db_type, DbType::SqliteInMemory);
}
#[test]
fn test_apply_run_args_overrides_db_type() {
let mut cfg = Config::default();
let args = crate::cli::RunArgs {
runtime: crate::cli::RuntimeArgs {
bind: None,
pid_file: None,
},
db_type: Some(DbType::SqliteInDisk),
db_path: None,
frontend_dir: None,
log_file: None,
mutation_rounds: None,
};
cfg.apply_run_args(&args);
assert_eq!(cfg.db_type, DbType::SqliteInDisk);
}
#[test]
fn test_toml_parses_db_type_kebab_case() {
let raw = r#"
bind = "127.0.0.1:3000"
db_type = "valkey"
pid_file = "/tmp/pid"
db_path = "/tmp/db.sqlite"
frontend_dir = "."
heartbeat_min_interval_ms = 12000
heartbeat_max_interval_ms = 25000
expiration_minutes = 30
rate_limit_count = 5
rate_limit_window_secs = 10
max_timestamp_drift_ms = 30000
min_mouse_total_dist = 1.0
max_mouse_avg_speed = 2.0
min_pause_count = 1
require_mouse_activity = true
gene_size = 512
"#;
let cfg: Config = toml::from_str(raw).unwrap();
assert_eq!(cfg.db_type, DbType::Valkey);
}
}
+31 -12
View File
@@ -2,6 +2,36 @@ use ed25519_dalek::{Signature, VerifyingKey};
use shared::protocol::HeartbeatRequest; use shared::protocol::HeartbeatRequest;
use std::collections::BTreeMap; 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>> {
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)?)
}
/// 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 fn verify_signature(
pub_key_bytes: &[u8], pub_key_bytes: &[u8],
req: &HeartbeatRequest, req: &HeartbeatRequest,
@@ -9,18 +39,7 @@ pub fn verify_signature(
let pk = VerifyingKey::from_bytes(&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?)?; let pk = VerifyingKey::from_bytes(&pub_key_bytes.try_into().map_err(|_| "invalid pubkey")?)?;
let sig_bytes = hex::decode(&req.signature)?; let sig_bytes = hex::decode(&req.signature)?;
let sig = Signature::from_slice(&sig_bytes)?; let sig = Signature::from_slice(&sig_bytes)?;
let message = canonical_signing_message(req)?;
// Build canonical JSON with BTreeMap so keys are sorted alphabetically,
// matching the JS client's JSON.stringify(obj, Object.keys(obj).sort()).
// Sorted order: entropyData, fingerprint, prevHash, sessionId, stackState, timestamp
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("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));
let message = serde_json::to_string(&payload)?;
pk.verify_strict(message.as_bytes(), &sig)?; pk.verify_strict(message.as_bytes(), &sig)?;
Ok(()) Ok(())
+34
View File
@@ -14,17 +14,30 @@ pub enum SessionError {
#[error("Database error: {0}")] #[error("Database error: {0}")]
Database(#[from] rusqlite::Error), Database(#[from] rusqlite::Error),
#[error("Storage error: {0}")]
Storage(String),
#[error("R2D2 pool error: {0}")] #[error("R2D2 pool error: {0}")]
Pool(#[from] r2d2::Error), Pool(#[from] r2d2::Error),
#[error("Invalid public key length")] #[error("Invalid public key length")]
InvalidPublicKeyLength, InvalidPublicKeyLength,
#[error("Invalid gene configuration: {0}")]
InvalidGeneConfiguration(String),
#[error("Rate limited")]
RateLimited,
} }
impl IntoResponse for SessionError { impl IntoResponse for SessionError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let (status, error_message) = match self { let (status, error_message) = match self {
SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()), SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()),
SessionError::RateLimited => (
StatusCode::TOO_MANY_REQUESTS,
"Too many requests".to_string(),
),
_ => ( _ => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(), "Internal server error".to_string(),
@@ -45,6 +58,9 @@ pub enum VerificationError {
#[error("Database error: {0}")] #[error("Database error: {0}")]
Database(#[from] rusqlite::Error), Database(#[from] rusqlite::Error),
#[error("Storage error: {0}")]
Storage(String),
#[error("Hex decoding error: {0}")] #[error("Hex decoding error: {0}")]
Hex(#[from] hex::FromHexError), Hex(#[from] hex::FromHexError),
@@ -65,4 +81,22 @@ pub enum VerificationError {
#[error("Fingerprint validation failed: {0}")] #[error("Fingerprint validation failed: {0}")]
FingerprintFailed(String), FingerprintFailed(String),
#[error("Mutation step mismatch: expected {expected}, got {got}")]
MutationStepMismatch { expected: u64, got: u64 },
#[error("Mutation commitment mismatch")]
MutationCommitmentMismatch,
#[error("Mutation program error: {0}")]
MutationProgram(String),
#[error("Gene state error: {0}")]
GeneState(String),
#[error("VM execution stack state mismatch")]
VmStackMismatch,
#[error("Concurrent state modification detected (CAS failed)")]
ConcurrentUpdate,
} }
+66 -3
View File
@@ -1,16 +1,79 @@
use shared::protocol::Fingerprint; 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>> { pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?; 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()); return Err("aspect ratio".into());
} }
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?; 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()); return Err("dpr".into());
} }
if fp.hardware_concurrency == 0 {
if fp.hardware_concurrency == 0 || fp.hardware_concurrency > MAX_HARDWARE_CONCURRENCY {
return Err("hw".into()); return Err("hw".into());
} }
Ok(()) 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());
}
}
+3 -3
View File
@@ -30,9 +30,6 @@ async fn main() {
async fn try_main() -> Result<(), Box<dyn std::error::Error>> { async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse(); 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_filter = cli.globals.log.as_deref().unwrap_or("info");
let log_file = log_file_for_command(&cli); let log_file = log_file_for_command(&cli);
let _log_guard = init_logging(log_filter, log_file)?; let _log_guard = init_logging(log_filter, log_file)?;
@@ -75,6 +72,9 @@ async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
Some(Command::Version) => { Some(Command::Version) => {
output::print(cli.globals.output_format(), &runtime::version())?; output::print(cli.globals.output_format(), &runtime::version())?;
} }
Some(Command::DbType) => {
output::print(cli.globals.output_format(), &runtime::db_type_report())?;
}
Some(Command::Metrics(args)) => { Some(Command::Metrics(args)) => {
let mut config = Config::load(cli.globals.config.as_deref())?; let mut config = Config::load(cli.globals.config.as_deref())?;
config.apply_runtime_args(args); config.apply_runtime_args(args);
+22
View File
@@ -9,3 +9,25 @@ pub async fn log_request(req: Request, next: Next) -> Response {
tracing::info!("{} {} -> {}", method, uri, response.status()); tracing::info!("{} {} -> {}", method, uri, response.status());
response 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
}
+34 -14
View File
@@ -1,34 +1,54 @@
use std::collections::HashMap; use dashmap::DashMap;
use std::time::Instant; 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 { 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 { impl RateLimiter {
/// Creates a new, empty `RateLimiter`.
pub fn new() -> Self { pub fn new() -> Self {
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 now = Instant::now();
let entry = self.buckets.entry(key.to_string()).or_insert((0, now)); let mut entry = self.buckets.entry(key.to_string()).or_insert((0, now));
if now.duration_since(entry.1).as_secs() >= window_secs { let (count, ts) = entry.value_mut();
*entry = (1, now); if now.duration_since(*ts).as_secs() >= window_secs {
*count = 1;
*ts = now;
true true
} else if entry.0 >= limit { } else if *count >= limit {
false false
} else { } else {
entry.0 += 1; *count += 1;
true true
} }
} }
/// Remove entries whose rate-limit window has fully elapsed. /// Evicts expired rate-limit entries whose time windows have fully elapsed.
/// Call this periodically (e.g. from the cleanup loop) to bound memory usage. ///
pub fn evict_stale(&mut self, window_secs: u64) { /// 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(); let now = Instant::now();
self.buckets self.buckets
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs); .retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs);
@@ -43,7 +63,7 @@ mod tests {
#[test] #[test]
fn test_rate_limiter() { fn test_rate_limiter() {
let mut rl = RateLimiter::new(); let rl = RateLimiter::new();
// Limit of 2 requests per 1 second window // Limit of 2 requests per 1 second window
assert!(rl.check("user1", 2, 1)); assert!(rl.check("user1", 2, 1));
assert!(rl.check("user1", 2, 1)); assert!(rl.check("user1", 2, 1));
@@ -57,7 +77,7 @@ mod tests {
#[test] #[test]
fn test_rate_limiter_eviction() { fn test_rate_limiter_eviction() {
let mut rl = RateLimiter::new(); let rl = RateLimiter::new();
assert!(rl.check("user1", 1, 1)); assert!(rl.check("user1", 1, 1));
assert_eq!(rl.buckets.len(), 1); assert_eq!(rl.buckets.len(), 1);
+247 -19
View File
@@ -7,56 +7,284 @@ pub async fn handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(payload): Json<HeartbeatRequest>, Json(payload): Json<HeartbeatRequest>,
) -> (StatusCode, Json<HeartbeatResponse>) { ) -> (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 (limit, window_secs) = {
let cfg = state.get_config(); let cfg = state.get_config();
(cfg.rate_limit_count, cfg.rate_limit_window_secs) (cfg.rate_limit_count, cfg.rate_limit_window_secs)
}; };
let mut rl = state.rate_limiter.lock().await; if !state
if !rl.check(&payload.session_id, limit, window_secs) { .rate_limiter
.check(&payload.session_id, limit, window_secs)
{
tracing::debug!("Rate limit hit: {}", payload.session_id); 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 ( return (
StatusCode::OK, StatusCode::OK,
Json(HeartbeatResponse { Json(HeartbeatResponse {
status: "ok".into(), status: "ok".into(),
next_salt: None, next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}), }),
); );
} }
} }
let config = state.get_config(); let config = state.get_config();
let conn = match state.db_pool.get() { let start_db = std::time::Instant::now();
Ok(c) => c, let db_res = crate::session::verify_heartbeat(&state.db_pool, &config, &payload);
Err(e) => { let db_dur = start_db.elapsed().as_nanos() as u64;
tracing::error!("Db pool error: {}", e); state
return ( .storage_latency_ns
StatusCode::INTERNAL_SERVER_ERROR, .fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
Json(HeartbeatResponse { state
status: "error".into(), .storage_ops_count
next_salt: None, .fetch_add(2, std::sync::atomic::Ordering::Relaxed); // read + write
}),
); let outcome = match db_res {
} Ok(result) => (
};
match crate::session::verify_heartbeat(&conn, &config, &payload) {
Ok(next_salt) => (
StatusCode::OK, StatusCode::OK,
Json(HeartbeatResponse { Json(HeartbeatResponse {
status: "ok".into(), status: "ok".into(),
next_salt: Some(next_salt), next_salt: Some(result.next_salt_hex),
next_mutation_step: Some(result.next_mutation_step),
next_mutation_order_b64: Some(result.next_mutation_order_b64),
}), }),
), ),
Err(e) => { Err(e) => {
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, 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, StatusCode::OK,
Json(HeartbeatResponse { Json(HeartbeatResponse {
status: "ok".into(), status: "ok".into(),
next_salt: None, next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}), }),
) )
} }
};
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)]
mod tests {
use super::*;
use axum::{extract::State, Json};
use ed25519_dalek::{Signer, SigningKey};
use shared::protocol::{EntropyData, Fingerprint, InitResponse, MouseEvent};
use std::path::Path;
fn test_config() -> crate::config::Config {
crate::config::Config {
expiration_minutes: 30,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 4.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: 64,
rate_limit_count: 20,
rate_limit_window_secs: 10,
..crate::config::Config::default()
}
}
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) {
let msg = crate::crypto::canonical_signing_message(req).unwrap();
req.signature = hex::encode(sk.sign(msg.as_bytes()).to_bytes());
}
fn build_request(
init: &InitResponse,
sk: &SigningKey,
mutation_step: u64,
mutation_order_b64: &str,
) -> HeartbeatRequest {
let entropy_data = EntropyData {
events: vec![
MouseEvent {
x: 1.0,
y: 1.0,
timestamp_ms: 1.0,
},
MouseEvent {
x: 3.0,
y: 1.0,
timestamp_ms: 2.0,
},
MouseEvent {
x: 3.0,
y: 1.0,
timestamp_ms: 120.0,
},
],
};
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();
let committed = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate =
shared::vm_extensions::apply_program_clone(&committed, &order.program).unwrap();
let mut req = HeartbeatRequest {
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
timestamp: crate::storage::current_time_ms(),
entropy_data,
stack_state,
fingerprint: Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
},
mutation_step,
gene_commitment: shared::gene::commitment_hex_with_context(
&candidate,
&init.session_id,
mutation_step,
),
signature: String::new(),
};
sign_request(sk, &mut req);
req
}
async fn setup_state_and_session(
config: crate::config::Config,
) -> (Arc<AppState>, InitResponse, SigningKey) {
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
let state = Arc::new(AppState {
db_pool: pool.clone(),
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();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init = crate::session::create_session(&pool, &config, &pk_hex).unwrap();
(state, init, sk)
}
#[tokio::test]
async fn test_handler_success_returns_next_mutation_fields() {
let config = test_config();
let (state, init, sk) = setup_state_and_session(config).await;
let req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_some());
assert!(body.next_mutation_step.is_some());
assert!(body.next_mutation_order_b64.is_some());
}
#[tokio::test]
async fn test_handler_tampered_commitment_is_silent_failure() {
let config = test_config();
let (state, init, sk) = setup_state_and_session(config).await;
let mut req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
req.gene_commitment = "00".repeat(32);
sign_request(&sk, &mut req);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_none());
assert!(body.next_mutation_step.is_none());
assert!(body.next_mutation_order_b64.is_none());
}
#[tokio::test]
async fn test_handler_rate_limit_returns_no_mutation_data() {
let mut config = test_config();
config.rate_limit_count = 0;
let (state, init, sk) = setup_state_and_session(config).await;
let req = build_request(&init, &sk, init.mutation_step, &init.mutation_order_b64);
let (status, Json(body)) = handler(State(state), Json(req)).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.status, "ok");
assert!(body.next_salt.is_none());
assert!(body.next_mutation_step.is_none());
assert!(body.next_mutation_order_b64.is_none());
} }
} }
+31 -2
View File
@@ -8,8 +8,37 @@ pub async fn handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Json(payload): Json<InitRequest>, Json(payload): Json<InitRequest>,
) -> Result<Json<InitResponse>, SessionError> { ) -> Result<Json<InitResponse>, SessionError> {
let start_http = std::time::Instant::now();
let config = state.get_config(); let config = state.get_config();
let conn = state.db_pool.get()?;
let resp = crate::session::create_session(&conn, &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)) Ok(Json(resp))
} }
+258 -24
View File
@@ -5,17 +5,19 @@ use crate::{
routes, session, routes, session,
storage::{self, StoreStats}, 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 serde::Serialize;
use std::{ use std::{
fs, fs,
io::{Read, Write}, io::{Read, Write},
net::{SocketAddr, TcpStream}, net::{IpAddr, SocketAddr, TcpStream},
path::Path, path::Path,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use tokio::sync::{Mutex, Notify}; use tokio::sync::Notify;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -80,18 +82,51 @@ impl TextOutput for KeypairReport {
} }
} }
#[derive(Debug, Serialize)]
pub struct DbTypeEntry {
pub name: &'static str,
pub implemented: bool,
pub notes: &'static str,
}
#[derive(Debug, Serialize)]
pub struct DbTypeReport {
pub default: &'static str,
pub backends: Vec<DbTypeEntry>,
}
impl TextOutput for DbTypeReport {
fn to_text(&self) -> String {
let mut out = format!("default={}\n", self.default);
for backend in &self.backends {
let status = if backend.implemented {
"implemented"
} else {
"todo"
};
out.push_str(&format!(
"db_type={} status={} notes={}\n",
backend.name, status, backend.notes
));
}
out
}
}
impl TextOutput for Config { impl TextOutput for Config {
fn to_text(&self) -> String { fn to_text(&self) -> String {
format!( format!(
"bind={}\npid_file={}\ndb_path={}\nfrontend_dir={}\nlog_file={}", "bind={}\ndb_type={}\npid_file={}\ndb_path={}\nfrontend_dir={}\nlog_file={}\ngene_size={}",
self.bind, self.bind,
self.db_type.as_str(),
self.pid_file.display(), self.pid_file.display(),
self.db_path.display(), self.db_path.display(),
self.frontend_dir.display(), self.frontend_dir.display(),
self.log_file self.log_file
.as_ref() .as_ref()
.map(|path| path.display().to_string()) .map(|path| path.display().to_string())
.unwrap_or_else(|| "none".to_string()) .unwrap_or_else(|| "none".to_string()),
self.gene_size
) )
} }
} }
@@ -108,11 +143,19 @@ impl TextOutput for StoreStats {
pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>> { pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>> {
install_pid_file(&config.pid_file)?; install_pid_file(&config.pid_file)?;
let db_pool = storage::init_pool(&config.db_path)?; let db_pool = init_db_pool(&config)?;
let state = Arc::new(session::AppState { let state = Arc::new(session::AppState {
db_pool, db_pool,
rate_limiter: Mutex::new(RateLimiter::new()), rate_limiter: RateLimiter::new(),
config: std::sync::RwLock::new(config.clone()), 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(); let bg_state = state.clone();
@@ -129,7 +172,11 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
tower_http::services::ServeDir::new(&config.frontend_dir), tower_http::services::ServeDir::new(&config.frontend_dir),
) )
.layer(tower_http::cors::CorsLayer::permissive()) .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::middleware::from_fn(crate::middleware::log_request))
.layer(axum::extract::DefaultBodyLimit::max(64 * 1024)) // 64 KiB
.with_state(state.clone()); .with_state(state.clone());
let addr: SocketAddr = config.bind.parse()?; let addr: SocketAddr = config.bind.parse()?;
@@ -137,9 +184,12 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
info!(bind = %config.bind, "chronoseal daemon started"); info!(bind = %config.bind, "chronoseal daemon started");
let shutdown = signal_task(state.clone()); let shutdown = signal_task(state.clone());
let result = axum::serve(listener, app) let result = axum::serve(
.with_graceful_shutdown(shutdown) listener,
.await; app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown)
.await;
remove_pid_file(&config.pid_file); remove_pid_file(&config.pid_file);
result?; result?;
@@ -147,6 +197,33 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
Ok(()) Ok(())
} }
pub fn db_type_report() -> DbTypeReport {
DbTypeReport {
default: crate::config::DbType::SqliteInMemory.as_str(),
backends: vec![
DbTypeEntry {
name: crate::config::DbType::SqliteInMemory.as_str(),
implemented: true,
notes: "default runtime backend",
},
DbTypeEntry {
name: crate::config::DbType::SqliteInDisk.as_str(),
implemented: true,
notes: "persistent SQLite backend (uses --db-path)",
},
DbTypeEntry {
name: crate::config::DbType::Valkey.as_str(),
implemented: true,
notes: "compatibility mode: falls back to sqlite-in-memory",
},
],
}
}
fn init_db_pool(config: &Config) -> Result<storage::DbPool, Box<dyn std::error::Error>> {
storage::DbPool::init(config)
}
pub fn probe_health(config: &Config) -> HealthReport { pub fn probe_health(config: &Config) -> HealthReport {
if http_get(&config.bind, "/health").is_ok() { if http_get(&config.bind, "/health").is_ok() {
HealthReport { HealthReport {
@@ -209,32 +286,113 @@ async fn health_handler() -> impl IntoResponse {
} }
async fn stats_handler( async fn stats_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>, axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<Json<StoreStats>, (StatusCode, String)> { ) -> Result<Json<StoreStats>, (StatusCode, String)> {
let db = state if !is_loopback(addr.ip()) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
state
.db_pool .db_pool
.get() .stats()
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
storage::stats(&db)
.map(Json) .map(Json)
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string())) .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
} }
async fn metrics_handler( async fn metrics_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>, axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<String, (StatusCode, String)> { ) -> Result<String, (StatusCode, String)> {
let db = state if !is_loopback(addr.ip()) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let stats = state
.db_pool .db_pool
.get() .stats()
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?; .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
storage::stats(&db)
.map(|stats| { let heartbeats = state
format!( .heartbeats_total
"# 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", .load(std::sync::atomic::Ordering::Relaxed);
stats.sessions, stats.expired_sessions, stats.max_chain_length let ver_failures = state
) .verification_failures_total
}) .load(std::sync::atomic::Ordering::Relaxed);
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string())) 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>) { async fn signal_task(state: Arc<session::AppState>) {
@@ -339,3 +497,79 @@ fn http_get(bind: &str, path: &str) -> Result<String, Box<dyn std::error::Error>
.ok_or("daemon returned an invalid HTTP response")?; .ok_or("daemon returned an invalid HTTP response")?;
Ok(body.to_string()) Ok(body.to_string())
} }
#[cfg(test)]
mod tests {
use super::*;
fn base_config() -> Config {
Config {
bind: "127.0.0.1:0".to_string(),
db_type: crate::config::DbType::SqliteInMemory,
pid_file: std::path::PathBuf::from("/tmp/chronoseal-test.pid"),
db_path: std::path::PathBuf::from("/tmp/chronoseal-test.sqlite"),
frontend_dir: std::path::PathBuf::from("."),
log_file: None,
heartbeat_min_interval_ms: 12_000,
heartbeat_max_interval_ms: 25_000,
expiration_minutes: 30,
rate_limit_count: 5,
rate_limit_window_secs: 10,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 5.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: shared::constants::DEFAULT_GENE_SIZE,
mutation_rounds: shared::constants::DEFAULT_MUTATION_ROUNDS,
}
}
#[test]
fn test_db_type_report_lists_backends() {
let report = db_type_report();
assert_eq!(report.default, "sqlite-in-memory");
assert_eq!(report.backends.len(), 3);
assert!(report.backends.iter().any(|b| b.name == "valkey"));
}
#[test]
fn test_init_db_pool_sqlite_in_memory() {
let config = base_config();
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);
}
#[test]
fn test_init_db_pool_sqlite_in_disk() {
let mut config = base_config();
config.db_type = crate::config::DbType::SqliteInDisk;
config.db_path = std::path::PathBuf::from("/tmp/chronoseal-db-type-disk.sqlite");
let _ = std::fs::remove_file(&config.db_path);
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);
}
#[test]
fn test_init_db_pool_valkey_compat_mode() {
let mut config = base_config();
config.db_type = crate::config::DbType::Valkey;
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
}
}
}
}
+486 -113
View File
@@ -1,7 +1,15 @@
pub struct AppState { pub struct AppState {
pub db_pool: crate::storage::DbPool, 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 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 { impl AppState {
@@ -15,11 +23,21 @@ impl AppState {
} }
use crate::{crypto, fingerprint, storage, trust, vm}; use crate::{crypto, fingerprint, storage, trust, vm};
use rusqlite::params; use shared::{
use shared::protocol::{HeartbeatRequest, InitResponse}; gene::{self, GeneState},
protocol::{HeartbeatRequest, InitResponse},
vm_extensions,
};
#[derive(Debug, Clone)]
pub struct HeartbeatVerificationResult {
pub next_salt_hex: String,
pub next_mutation_step: u64,
pub next_mutation_order_b64: String,
}
pub fn create_session( pub fn create_session(
conn: &rusqlite::Connection, db: &storage::DbPool,
config: &crate::config::Config, config: &crate::config::Config,
pub_key_hex: &str, pub_key_hex: &str,
) -> Result<InitResponse, crate::errors::SessionError> { ) -> Result<InitResponse, crate::errors::SessionError> {
@@ -27,22 +45,43 @@ pub fn create_session(
if pub_key.len() != shared::constants::SESSION_ID_LEN { if pub_key.len() != shared::constants::SESSION_ID_LEN {
return Err(crate::errors::SessionError::InvalidPublicKeyLength); return Err(crate::errors::SessionError::InvalidPublicKeyLength);
} }
let gene_state = gene::new_state(config.gene_size)
.map_err(|err| crate::errors::SessionError::InvalidGeneConfiguration(err.to_string()))?;
let environment_blob = gene::encode_environment(&gene_state.environment)
.map_err(|err| crate::errors::SessionError::InvalidGeneConfiguration(err.to_string()))?;
let session_id = hex::encode(rand::random::<[u8; shared::constants::SESSION_ID_LEN]>()); let session_id = hex::encode(rand::random::<[u8; shared::constants::SESSION_ID_LEN]>());
let salt = rand::random::<[u8; shared::constants::SALT_LEN]>(); let salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
let now = storage::current_time_ms(); let now = storage::current_time_ms();
let expires_at = now + (config.expiration_minutes as u64) * 60 * 1000; let expires_at = now + (config.expiration_minutes as u64) * 60 * 1000;
let initial_hash = shared::hashing::initial_hash(&session_id, &pub_key, &salt); let initial_hash = shared::hashing::initial_hash(&session_id, &pub_key, &salt);
conn.execute(
"INSERT INTO sessions (session_id, public_key, salt, last_hash, created_at, last_seen, expires_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![session_id, pub_key, salt.to_vec(), initial_hash, now, now, expires_at],
)?;
let opcodes = vm::generate_random_program(8..=16); let opcodes = vm::generate_random_program(8..=16);
let opcodes_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &opcodes); let opcodes_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &opcodes);
let initial_mutation = vm_extensions::generate_order(1, config.gene_size);
let initial_mutation_b64 = vm_extensions::encode_order_b64(&initial_mutation);
let record = storage::SessionRecord {
session_id: session_id.clone(),
public_key: pub_key,
salt: salt.to_vec(),
last_hash: initial_hash.clone(),
chain_length: 1,
created_at: now,
last_seen: now,
expires_at,
gene: gene_state.gene,
environment: environment_blob,
pending_mutation: initial_mutation.program,
pending_mutation_step: initial_mutation.step,
opcodes,
};
db.insert_session(&record)
.map_err(|err| crate::errors::SessionError::Storage(err.to_string()))?;
Ok(InitResponse { Ok(InitResponse {
session_id, session_id,
salt: hex::encode(salt), salt: hex::encode(salt),
@@ -51,174 +90,508 @@ pub fn create_session(
expires_at, expires_at,
heartbeat_min_interval_ms: config.heartbeat_min_interval_ms, heartbeat_min_interval_ms: config.heartbeat_min_interval_ms,
heartbeat_max_interval_ms: config.heartbeat_max_interval_ms, heartbeat_max_interval_ms: config.heartbeat_max_interval_ms,
gene_size: config.gene_size as u32,
mutation_step: initial_mutation.step,
mutation_order_b64: initial_mutation_b64,
mutation_rounds: config.mutation_rounds,
}) })
} }
pub fn verify_heartbeat( pub fn verify_heartbeat(
conn: &rusqlite::Connection, db: &storage::DbPool,
config: &crate::config::Config, config: &crate::config::Config,
req: &HeartbeatRequest, req: &HeartbeatRequest,
) -> Result<String, crate::errors::VerificationError> { ) -> Result<HeartbeatVerificationResult, crate::errors::VerificationError> {
let mut stmt = conn.prepare( let session = db
"SELECT public_key, salt, last_hash, expires_at FROM sessions WHERE session_id = ?1", .load_session(&req.session_id)
)?; .map_err(|e| crate::errors::VerificationError::Storage(e.to_string()))?;
let (pub_key, salt, stored_last_hash, expires_at): (Vec<u8>, Vec<u8>, Vec<u8>, u64) = stmt let session = session.ok_or(crate::errors::VerificationError::SessionNotFound)?;
.query_row(params![req.session_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})
.map_err(|e| {
if matches!(e, rusqlite::Error::QueryReturnedNoRows) {
crate::errors::VerificationError::SessionNotFound
} else {
crate::errors::VerificationError::Database(e)
}
})?;
let now = storage::current_time_ms(); let now = storage::current_time_ms();
if now > expires_at { if now > session.expires_at {
return Err(crate::errors::VerificationError::Expired); return Err(crate::errors::VerificationError::Expired);
} }
// 1. Verify signature // 1. Verify signature
crypto::verify_signature(&pub_key, req) crypto::verify_signature(&session.public_key, req)
.map_err(|e| crate::errors::VerificationError::Signature(e.to_string()))?; .map_err(|e| crate::errors::VerificationError::Signature(e.to_string()))?;
// 2. Check chain continuity // 2. Check chain continuity
if stored_last_hash != hex::decode(&req.prev_hash)? { let prev_hash_bytes = hex::decode(&req.prev_hash)?;
if session.last_hash != prev_hash_bytes {
return Err(crate::errors::VerificationError::ChainBroken); return Err(crate::errors::VerificationError::ChainBroken);
} }
// 3. Time window // 3. Mutation step and deterministic mutation parity
if req.mutation_step != session.pending_mutation_step {
return Err(crate::errors::VerificationError::MutationStepMismatch {
expected: session.pending_mutation_step,
got: req.mutation_step,
});
}
let environment = gene::decode_environment(&session.environment)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
let server_state = GeneState {
gene: session.gene.clone(),
environment,
};
let candidate_state = vm_extensions::apply_program_clone_with_rounds(
&server_state,
&session.pending_mutation,
config.mutation_rounds,
)
.map_err(|e| crate::errors::VerificationError::MutationProgram(e.to_string()))?;
let expected_gene_commitment =
gene::commitment_hex_with_context(&candidate_state, &req.session_id, req.mutation_step);
if req.gene_commitment != expected_gene_commitment {
return Err(crate::errors::VerificationError::MutationCommitmentMismatch);
}
// 4. Time window
let diff = (now as i64) - (req.timestamp as i64); let diff = (now as i64) - (req.timestamp as i64);
if diff.abs() > config.max_timestamp_drift_ms { if diff.abs() > config.max_timestamp_drift_ms {
return Err(crate::errors::VerificationError::TimestampDrift); return Err(crate::errors::VerificationError::TimestampDrift);
} }
// 4. Trusted mouse & fingerprint // 5. Trusted mouse & fingerprint
trust::validate_mouse(&req.entropy_data, config) trust::validate_mouse(&req.entropy_data, config)
.map_err(|e| crate::errors::VerificationError::TrustFailed(e.to_string()))?; .map_err(|e| crate::errors::VerificationError::TrustFailed(e.to_string()))?;
fingerprint::validate(&req.fingerprint) fingerprint::validate(&req.fingerprint)
.map_err(|e| crate::errors::VerificationError::FingerprintFailed(e.to_string()))?; .map_err(|e| crate::errors::VerificationError::FingerprintFailed(e.to_string()))?;
// 5. Compute new hash // 5.5 Verify VM execution state
let prev_hash_bytes = hex::decode(&req.prev_hash)?; 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( let new_hash = shared::hashing::next_chain_hash(
&prev_hash_bytes, &prev_hash_bytes,
req.timestamp, req.timestamp,
&req.entropy_data, &req.entropy_data,
&req.stack_state, &req.stack_state,
&salt, &session.salt,
); );
// 6. New salt for client // 7. Prepare next mutation order and salt
let next_step = session.pending_mutation_step + 1;
let next_mutation = vm_extensions::generate_order(next_step, candidate_state.gene.len());
let next_mutation_b64 = vm_extensions::encode_order_b64(&next_mutation);
let next_salt = rand::random::<[u8; shared::constants::SALT_LEN]>(); let next_salt = rand::random::<[u8; shared::constants::SALT_LEN]>();
let next_salt_hex = hex::encode(next_salt); let next_salt_hex = hex::encode(next_salt);
let next_environment_blob = gene::encode_environment(&candidate_state.environment)
.map_err(|e| crate::errors::VerificationError::GeneState(e.to_string()))?;
conn.execute( let update_record = storage::SessionRecord {
"UPDATE sessions SET last_hash=?1, salt=?2, chain_length=chain_length+1, last_seen=?3 WHERE session_id=?4", session_id: req.session_id.clone(),
params![new_hash, next_salt.to_vec(), now, req.session_id], public_key: session.public_key,
)?; salt: next_salt.to_vec(),
last_hash: new_hash.clone(),
chain_length: session.chain_length + 1,
created_at: session.created_at,
last_seen: now,
expires_at: session.expires_at,
gene: candidate_state.gene,
environment: next_environment_blob,
pending_mutation: next_mutation.program,
pending_mutation_step: next_step,
opcodes: session.opcodes,
};
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(next_salt_hex) Ok(HeartbeatVerificationResult {
next_salt_hex,
next_mutation_step: next_step,
next_mutation_order_b64: next_mutation_b64,
})
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ed25519_dalek::{Signer, SigningKey}; use ed25519_dalek::{Signer, SigningKey};
use shared::protocol::{EntropyData, Fingerprint, HeartbeatRequest, StackState}; use rusqlite::params;
use shared::protocol::{EntropyData, Fingerprint, HeartbeatRequest, MouseEvent, StackState};
use std::path::Path; use std::path::Path;
#[derive(Clone)]
struct SimulatedClient {
signing_key: SigningKey,
session_id: String,
prev_hash: String,
current_salt: String,
pending_mutation_step: u64,
pending_mutation_order_b64: String,
committed_gene_state: GeneState,
opcodes_b64: String,
}
fn test_config() -> crate::config::Config {
crate::config::Config {
expiration_minutes: 30,
max_timestamp_drift_ms: 30_000,
min_mouse_total_dist: 1.0,
max_mouse_avg_speed: 4.0,
min_pause_count: 0,
require_mouse_activity: false,
gene_size: 64,
..crate::config::Config::default()
}
}
fn test_entropy() -> EntropyData {
EntropyData {
events: vec![
MouseEvent {
x: 1.0,
y: 1.0,
timestamp_ms: 1.0,
},
MouseEvent {
x: 2.0,
y: 1.0,
timestamp_ms: 2.0,
},
MouseEvent {
x: 2.0,
y: 1.0,
timestamp_ms: 120.0,
},
],
}
}
fn test_fingerprint() -> Fingerprint {
Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
}
}
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) { fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) {
let mut payload: std::collections::BTreeMap<&str, serde_json::Value> = let message = crate::crypto::canonical_signing_message(req).unwrap();
std::collections::BTreeMap::new();
payload.insert(
"entropyData",
serde_json::to_value(&req.entropy_data).unwrap(),
);
payload.insert(
"fingerprint",
serde_json::to_value(&req.fingerprint).unwrap(),
);
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).unwrap(),
);
payload.insert("timestamp", serde_json::json!(req.timestamp));
let message = serde_json::to_string(&payload).unwrap();
let sig = sk.sign(message.as_bytes()); let sig = sk.sign(message.as_bytes());
req.signature = hex::encode(sig.to_bytes()); req.signature = hex::encode(sig.to_bytes());
} }
fn create_test_session(
db: &storage::DbPool,
config: &crate::config::Config,
) -> (InitResponse, SigningKey) {
let mut rng = rand::thread_rng();
let sk = SigningKey::generate(&mut rng);
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init = create_session(db, config, &pk_hex).unwrap();
(init, sk)
}
fn client_from_init(init: &InitResponse, signing_key: SigningKey) -> SimulatedClient {
SimulatedClient {
signing_key,
session_id: init.session_id.clone(),
prev_hash: init.initial_hash.clone(),
current_salt: init.salt.clone(),
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(),
}
}
fn build_request(
client: &SimulatedClient,
timestamp: u64,
) -> (HeartbeatRequest, GeneState, EntropyData, StackState) {
let order = vm_extensions::decode_order_b64(
client.pending_mutation_step,
&client.pending_mutation_order_b64,
)
.unwrap();
let candidate_state =
vm_extensions::apply_program_clone(&client.committed_gene_state, &order.program)
.unwrap();
let entropy = test_entropy();
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(),
prev_hash: client.prev_hash.clone(),
timestamp,
entropy_data: entropy.clone(),
stack_state: stack.clone(),
fingerprint: test_fingerprint(),
mutation_step: client.pending_mutation_step,
gene_commitment: gene::commitment_hex_with_context(
&candidate_state,
&client.session_id,
client.pending_mutation_step,
),
signature: String::new(),
};
sign_request(&client.signing_key, &mut req);
(req, candidate_state, entropy, stack)
}
fn apply_successful_response(
client: &mut SimulatedClient,
req: &HeartbeatRequest,
candidate_state: GeneState,
entropy: &EntropyData,
stack: &StackState,
resp: &HeartbeatVerificationResult,
) {
let salt = hex::decode(&client.current_salt).unwrap();
let prev_hash = hex::decode(&req.prev_hash).unwrap();
let next_hash =
shared::hashing::next_chain_hash(&prev_hash, req.timestamp, entropy, stack, &salt);
client.prev_hash = hex::encode(next_hash);
client.current_salt = resp.next_salt_hex.clone();
client.pending_mutation_step = resp.next_mutation_step;
client.pending_mutation_order_b64 = resp.next_mutation_order_b64.clone();
client.committed_gene_state = candidate_state;
}
fn load_server_gene_state(db: &storage::DbPool, session_id: &str) -> GeneState {
let session = db.load_session(session_id).unwrap().unwrap();
GeneState {
gene: session.gene,
environment: gene::decode_environment(&session.environment).unwrap(),
}
}
fn run_successful_heartbeat(
db: &storage::DbPool,
config: &crate::config::Config,
client: &mut SimulatedClient,
) -> HeartbeatRequest {
let timestamp = storage::current_time_ms();
let (req, candidate_state, entropy, stack) = build_request(client, timestamp);
let result = verify_heartbeat(db, config, &req).unwrap();
apply_successful_response(client, &req, candidate_state, &entropy, &stack, &result);
req
}
#[test] #[test]
fn test_session_lifecycle_and_verification() { fn test_session_lifecycle_and_verification() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap(); let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = pool.get().unwrap(); let config = test_config();
let config = crate::config::Config { let (init, signing_key) = create_test_session(&pool, &config);
expiration_minutes: 30, assert_eq!(init.gene_size, config.gene_size as u32);
max_timestamp_drift_ms: 30000, assert!(!init.mutation_order_b64.is_empty());
min_mouse_total_dist: 10.0, assert_eq!(init.mutation_step, 1);
max_mouse_avg_speed: 2.0,
min_pause_count: 1,
require_mouse_activity: false, // simpler for tests
..crate::config::Config::default()
};
// Generate Ed25519 keypair let mut client = client_from_init(&init, signing_key);
let mut rng = rand::thread_rng(); for _ in 0..5 {
let sk = SigningKey::generate(&mut rng); run_successful_heartbeat(&pool, &config, &mut client);
let pk = sk.verifying_key(); }
let pub_key_hex = hex::encode(pk.to_bytes());
// 1. Create Session let stats = pool.stats().unwrap();
let start_time = storage::current_time_ms();
let init_resp = create_session(&conn, &config, &pub_key_hex).unwrap();
assert!(init_resp.expires_at >= start_time + 30 * 60 * 1000);
assert!(init_resp.expires_at <= storage::current_time_ms() + 30 * 60 * 1000);
// Verify stats
let stats = storage::stats(&conn).unwrap();
assert_eq!(stats.sessions, 1); assert_eq!(stats.sessions, 1);
assert_eq!(stats.expired_sessions, 0); assert_eq!(stats.max_chain_length, 6);
}
// 2. Heartbeat Verification #[test]
let now = storage::current_time_ms(); fn test_deterministic_server_client_parity_across_many_heartbeats() {
let entropy_data = EntropyData { events: vec![] }; let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let stack_state = StackState { let config = test_config();
stack: vec![42], let (init, signing_key) = create_test_session(&pool, &config);
ip: 5, let mut client = client_from_init(&init, signing_key);
};
let fingerprint = Fingerprint {
aspect_ratio: "1.77".to_string(),
device_pixel_ratio: "2.0".to_string(),
hardware_concurrency: 8,
};
let mut req = HeartbeatRequest { for _ in 0..12 {
session_id: init_resp.session_id.clone(), run_successful_heartbeat(&pool, &config, &mut client);
prev_hash: init_resp.initial_hash.clone(), let server_state = load_server_gene_state(&pool, &client.session_id);
timestamp: now, assert_eq!(server_state, client.committed_gene_state);
entropy_data, }
stack_state, }
fingerprint,
signature: "".to_string(),
};
sign_request(&sk, &mut req); #[test]
fn test_replay_attack_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
// Verify successful heartbeat let timestamp = storage::current_time_ms();
let next_salt = verify_heartbeat(&conn, &config, &req).unwrap(); let (req, candidate_state, entropy, stack) = build_request(&client, timestamp);
assert!(!next_salt.is_empty()); let result = verify_heartbeat(&pool, &config, &req).unwrap();
apply_successful_response(
&mut client,
&req,
candidate_state,
&entropy,
&stack,
&result,
);
// Try duplicate/broken hash chain (prev_hash unchanged but expected next hash in DB) let replay = verify_heartbeat(&pool, &config, &req);
let res = verify_heartbeat(&conn, &config, &req);
assert!(res.is_err());
assert!(matches!( assert!(matches!(
res.unwrap_err(), replay.unwrap_err(),
crate::errors::VerificationError::ChainBroken crate::errors::VerificationError::ChainBroken
)); ));
} }
#[test]
fn test_mutation_step_mismatch_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.mutation_step += 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
));
}
#[test]
fn test_mutation_commitment_tamper_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.gene_commitment = "00".repeat(32);
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationCommitmentMismatch
));
}
#[test]
fn test_malformed_server_mutation_program_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = match &pool {
storage::DbPool::Sqlite(pool) => pool.get().unwrap(),
_ => panic!("expected sqlite pool for test"),
};
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
"UPDATE sessions SET pending_mutation=?1 WHERE session_id=?2",
params![vec![0xFFu8], client.session_id.clone()],
)
.unwrap();
let updated: Vec<u8> = conn
.query_row(
"SELECT pending_mutation FROM sessions WHERE session_id=?1",
params![client.session_id.clone()],
|row| row.get(0),
)
.unwrap();
assert_eq!(updated, vec![0xFFu8]);
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationProgram(_)
));
}
#[test]
fn test_expired_session_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let conn = match &pool {
storage::DbPool::Sqlite(pool) => pool.get().unwrap(),
_ => panic!("expected sqlite pool for test"),
};
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let client = client_from_init(&init, signing_key);
conn.execute(
"UPDATE sessions SET expires_at=?1 WHERE session_id=?2",
params![0u64, client.session_id.clone()],
)
.unwrap();
let timestamp = storage::current_time_ms();
let (req, _, _, _) = build_request(&client, timestamp);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(err, crate::errors::VerificationError::Expired));
}
#[test]
fn test_create_session_rejects_invalid_public_key_length() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let err = create_session(&pool, &config, "00ff").unwrap_err();
assert!(matches!(
err,
crate::errors::SessionError::InvalidPublicKeyLength
));
}
#[test]
fn test_stale_mutation_step_after_success_is_rejected() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let config = test_config();
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
run_successful_heartbeat(&pool, &config, &mut client);
let timestamp = storage::current_time_ms();
let (mut req, _, _, _) = build_request(&client, timestamp);
req.mutation_step -= 1;
sign_request(&client.signing_key, &mut req);
let err = verify_heartbeat(&pool, &config, &req).unwrap_err();
assert!(matches!(
err,
crate::errors::VerificationError::MutationStepMismatch { .. }
));
}
#[test]
fn test_repeated_simulation_keeps_server_and_client_commitments_equal() {
let pool = storage::init_pool(Path::new(":memory:")).unwrap();
let mut config = test_config();
config.gene_size = 128;
let (init, signing_key) = create_test_session(&pool, &config);
let mut client = client_from_init(&init, signing_key);
for _ in 0..10 {
run_successful_heartbeat(&pool, &config, &mut client);
let server_state = load_server_gene_state(&pool, &client.session_id);
assert_eq!(
gene::commitment(&server_state),
gene::commitment(&client.committed_gene_state)
);
}
}
} }
+638 -21
View File
@@ -1,4 +1,5 @@
use rusqlite::Connection; use crate::config::Config;
use redis::Commands;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@@ -10,9 +11,238 @@ pub struct StoreStats {
pub max_chain_length: u64, pub max_chain_length: u64,
} }
pub type DbPool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>; #[derive(Debug, Clone)]
pub enum DbPool {
Sqlite(r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>),
Valkey(ValkeyStore),
}
#[derive(Debug, Clone)]
pub struct ValkeyStore {
pool: r2d2::Pool<redis::Client>,
index_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRecord {
pub session_id: String,
pub public_key: Vec<u8>,
pub salt: Vec<u8>,
pub last_hash: Vec<u8>,
pub chain_length: u64,
pub created_at: u64,
pub last_seen: u64,
pub expires_at: u64,
pub gene: Vec<u8>,
pub environment: Vec<u8>,
pub pending_mutation: Vec<u8>,
pub pending_mutation_step: u64,
pub opcodes: Vec<u8>,
}
impl DbPool {
pub fn init(config: &Config) -> Result<Self, Box<dyn std::error::Error>> {
match config.db_type {
crate::config::DbType::SqliteInMemory => {
let pool = init_sqlite_pool(Path::new(":memory:"))?;
Ok(DbPool::Sqlite(pool))
}
crate::config::DbType::SqliteInDisk => {
let pool = init_sqlite_pool(&config.db_path)?;
Ok(DbPool::Sqlite(pool))
}
crate::config::DbType::Valkey => {
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
.unwrap_or_else(|_| "127.0.0.1:6666".to_string());
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(),
}))
}
}
}
pub fn insert_session(&self, record: &SessionRecord) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"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, opcodes
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
)?;
stmt.execute(rusqlite::params![
record.session_id,
&record.public_key,
&record.salt,
&record.last_hash,
record.chain_length,
record.created_at,
record.last_seen,
record.expires_at,
&record.gene,
&record.environment,
&record.pending_mutation,
record.pending_mutation_step,
&record.opcodes,
])?;
Ok(())
}
DbPool::Valkey(store) => store.insert_session(record),
}
}
pub fn load_session(
&self,
session_id: &str,
) -> Result<Option<SessionRecord>, Box<dyn std::error::Error>> {
match self {
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, opcodes
FROM sessions WHERE session_id = ?1",
)?;
let row = stmt.query_row([session_id], |row| {
Ok(SessionRecord {
session_id: row.get(0)?,
public_key: row.get(1)?,
salt: row.get(2)?,
last_hash: row.get(3)?,
chain_length: row.get(4)?,
created_at: row.get(5)?,
last_seen: row.get(6)?,
expires_at: row.get(7)?,
gene: row.get(8)?,
environment: row.get(9)?,
pending_mutation: row.get(10)?,
pending_mutation_step: row.get(11)?,
opcodes: row.get(12)?,
})
});
match row {
Ok(rec) => Ok(Some(rec)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(Box::new(err)),
}
}
DbPool::Valkey(store) => store.load_session(session_id),
}
}
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()?;
let rows = conn.execute(
"UPDATE sessions SET
public_key=?1,
salt=?2,
last_hash=?3,
chain_length=?4,
created_at=?5,
last_seen=?6,
expires_at=?7,
gene=?8,
environment=?9,
pending_mutation=?10,
pending_mutation_step=?11,
opcodes=?12
WHERE session_id=?13 AND last_hash=?14",
rusqlite::params![
&record.public_key,
&record.salt,
&record.last_hash,
record.chain_length,
record.created_at,
record.last_seen,
record.expires_at,
&record.gene,
&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.update_session_cas(record, old_last_hash),
}
}
pub fn delete_expired_sessions(&self) -> Result<(), Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
conn.execute(
"DELETE FROM sessions WHERE expires_at < ?1",
rusqlite::params![current_time_ms()],
)?;
Ok(())
}
DbPool::Valkey(store) => store.purge_expired_sessions(),
}
}
pub fn stats(&self) -> Result<StoreStats, Box<dyn std::error::Error>> {
match self {
DbPool::Sqlite(pool) => {
let conn = pool.get()?;
let now = current_time_ms();
let sessions =
conn.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))?;
let expired_sessions = conn.query_row(
"SELECT COUNT(*) FROM sessions WHERE expires_at < ?1",
[now],
|row| row.get(0),
)?;
let max_chain_length = conn.query_row(
"SELECT COALESCE(MAX(chain_length), 0) FROM sessions",
[],
|row| row.get(0),
)?;
Ok(StoreStats {
sessions,
expired_sessions,
max_chain_length,
})
}
DbPool::Valkey(store) => store.stats(),
}
}
}
#[cfg(test)]
pub fn init_pool(path: &Path) -> Result<DbPool, Box<dyn std::error::Error>> { pub fn init_pool(path: &Path) -> Result<DbPool, Box<dyn std::error::Error>> {
let pool = init_sqlite_pool(path)?;
Ok(DbPool::Sqlite(pool))
}
fn init_sqlite_pool(
path: &Path,
) -> Result<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>, Box<dyn std::error::Error>> {
let manager = if path == Path::new(":memory:") { let manager = if path == Path::new(":memory:") {
r2d2_sqlite::SqliteConnectionManager::memory() r2d2_sqlite::SqliteConnectionManager::memory()
} else { } else {
@@ -21,7 +251,10 @@ pub fn init_pool(path: &Path) -> Result<DbPool, Box<dyn std::error::Error>> {
} }
r2d2_sqlite::SqliteConnectionManager::file(path) 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 pool = r2d2::Pool::new(manager)?;
let conn = pool.get()?; let conn = pool.get()?;
init_schema(&conn)?; init_schema(&conn)?;
@@ -38,35 +271,419 @@ fn init_schema(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
chain_length INTEGER NOT NULL DEFAULT 1, chain_length INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
last_seen INTEGER NOT NULL, last_seen INTEGER NOT NULL,
expires_at INTEGER NOT NULL expires_at INTEGER NOT NULL,
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,
opcodes BLOB NOT NULL DEFAULT X''
);", );",
)?; )?;
ensure_column(
conn,
"gene",
"ALTER TABLE sessions ADD COLUMN gene BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"environment",
"ALTER TABLE sessions ADD COLUMN environment BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"pending_mutation",
"ALTER TABLE sessions ADD COLUMN pending_mutation BLOB NOT NULL DEFAULT X''",
)?;
ensure_column(
conn,
"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);",
)?;
Ok(()) Ok(())
} }
pub fn stats(conn: &Connection) -> Result<StoreStats, rusqlite::Error> { fn ensure_column(
let now = current_time_ms(); conn: &rusqlite::Connection,
let sessions = conn.query_row("SELECT COUNT(*) FROM sessions", [], |row| row.get(0))?; column: &str,
let expired_sessions = conn.query_row( alter_sql: &str,
"SELECT COUNT(*) FROM sessions WHERE expires_at < ?1", ) -> Result<(), rusqlite::Error> {
[now], let exists: bool = conn.query_row(
"SELECT EXISTS(
SELECT 1 FROM pragma_table_info('sessions') WHERE name = ?1
)",
[column],
|row| row.get(0), |row| row.get(0),
)?; )?;
let max_chain_length = conn.query_row( if !exists {
"SELECT COALESCE(MAX(chain_length), 0) FROM sessions", conn.execute_batch(alter_sql)?;
[], }
|row| row.get(0), Ok(())
)?; }
Ok(StoreStats {
sessions, impl ValkeyStore {
expired_sessions, fn session_key(&self, session_id: &str) -> String {
max_chain_length, format!("session:{}", session_id)
}) }
fn load_session(
&self,
session_id: &str,
) -> Result<Option<SessionRecord>, Box<dyn std::error::Error>> {
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 conn = self.pool.get()?;
let key = self.session_key(&record.session_id);
let value = serde_json::to_string(record)?;
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 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)?;
}
Ok(())
}
fn stats(&self) -> Result<StoreStats, Box<dyn std::error::Error>> {
let now = current_time_ms();
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,
max_chain_length,
})
}
} }
pub fn current_time_ms() -> u64 { pub fn current_time_ms() -> u64 {
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .expect("system clock is before UNIX epoch; check system time")
.as_millis() as u64 .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);
}
}
+8
View File
@@ -1,6 +1,14 @@
use crate::config::Config; use crate::config::Config;
use shared::protocol::EntropyData; 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( pub fn validate_mouse(
data: &EntropyData, data: &EntropyData,
config: &Config, config: &Config,
+72 -3
View File
@@ -1,5 +1,16 @@
use rand::Rng; use rand::Rng;
use shared::{
gene::GeneState,
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> { pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Vec<u8> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let count = rng.gen_range(len_range); let count = rng.gen_range(len_range);
@@ -9,7 +20,7 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
if depth < 2 { if depth < 2 {
// Not enough operands for any binary op — push a literal. // Not enough operands for any binary op — push a literal.
ops.push(0x00); ops.push(0x00);
let val = rng.gen::<u32>(); let val = rng.r#gen::<u32>();
ops.extend_from_slice(&val.to_le_bytes()); ops.extend_from_slice(&val.to_le_bytes());
depth += 1; depth += 1;
} else { } else {
@@ -18,7 +29,7 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
0x00 => { 0x00 => {
// PUSH literal // PUSH literal
ops.push(0x00); ops.push(0x00);
let val = rng.gen::<u32>(); let val = rng.r#gen::<u32>();
ops.extend_from_slice(&val.to_le_bytes()); ops.extend_from_slice(&val.to_le_bytes());
depth += 1; depth += 1;
} }
@@ -43,4 +54,62 @@ pub fn generate_random_program(len_range: std::ops::RangeInclusive<usize>) -> Ve
ops ops
} }
// Server does not need to execute the program; client does. /// 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,
program: &[u8],
) -> Result<ExecutionTrace, MutationError> {
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,
order: &MutationOrder,
) -> Result<ExecutionTrace, MutationError> {
vm_extensions::execute_program(state, &order.program)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use shared::gene::{commitment, new_state};
#[test]
fn test_execute_mutation_program_wraps_shared_engine() {
let mut state = new_state(8).unwrap();
let program = vec![vm_extensions::OP_MUTATE_POINT, 0, 0, 1];
let trace = execute_mutation_program(&mut state, &program).unwrap();
assert_eq!(state.gene[0], 1);
assert_eq!(trace.final_ip, program.len());
}
#[test]
fn test_execute_mutation_order_determinism() {
let mut rng_a = rand::rngs::StdRng::seed_from_u64(101);
let mut rng_b = rand::rngs::StdRng::seed_from_u64(101);
let order_a = vm_extensions::generate_order_with_rng(&mut rng_a, 9, 64);
let order_b = vm_extensions::generate_order_with_rng(&mut rng_b, 9, 64);
assert_eq!(order_a, order_b);
let mut state_a = new_state(64).unwrap();
let mut state_b = new_state(64).unwrap();
let trace_a = execute_mutation_order(&mut state_a, &order_a).unwrap();
let trace_b = execute_mutation_order(&mut state_b, &order_b).unwrap();
assert_eq!(state_a, state_b);
assert_eq!(trace_a.final_stack, trace_b.final_stack);
assert_eq!(commitment(&state_a), commitment(&state_b));
}
}
+6 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "shared" name = "shared"
version = "0.5.0" version = "1.0.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@@ -10,4 +10,8 @@ blake3 = "1"
hex = "0.4" hex = "0.4"
base64 = "0.22" base64 = "0.22"
rand = "0.8" rand = "0.8"
ed25519-dalek = { version = "2", features = ["rand_core"] } ed25519-dalek = { version = "2", features = ["rand_core"] }
tracing = "0.1"
[dev-dependencies]
proptest = "1"
+11
View File
@@ -1,2 +1,13 @@
pub const SESSION_ID_LEN: usize = 32; pub const SESSION_ID_LEN: usize = 32;
pub const SALT_LEN: usize = 16; pub const SALT_LEN: usize = 16;
pub const DEFAULT_GENE_SIZE: usize = 512;
pub const MAX_GENE_SIZE: usize = 4096;
pub const MAX_ENV_RECORDS: usize = 48;
pub const MAX_MUTATION_PROGRAM_BYTES: usize = 256;
pub const DEFAULT_MUTATION_ROUNDS: u8 = 4;
pub const MIN_MUTATION_ROUNDS: u8 = 3;
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;
+463
View File
@@ -0,0 +1,463 @@
use crate::constants::{DEFAULT_GENE_SIZE, MAX_ENV_RECORDS, MAX_GENE_SIZE};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvironmentRecord {
pub symbol: u16,
pub quantity: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneState {
pub gene: Vec<u8>,
pub environment: Vec<EnvironmentRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeneError {
InvalidGeneSize { size: usize },
TooManyEnvironmentRecords { len: usize },
EnvironmentNotSorted,
DuplicateEnvironmentSymbol(u16),
ZeroQuantitySymbol(u16),
EnvironmentFull,
EnvironmentBlobLengthInvalid { len: usize },
EnvironmentBlobTooLarge { records: usize },
}
impl std::fmt::Display for GeneError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidGeneSize { size } => write!(f, "invalid gene size: {size}"),
Self::TooManyEnvironmentRecords { len } => {
write!(f, "too many environment records: {len}")
}
Self::EnvironmentNotSorted => write!(f, "environment records are not sorted"),
Self::DuplicateEnvironmentSymbol(symbol) => {
write!(f, "duplicate environment symbol: {symbol}")
}
Self::ZeroQuantitySymbol(symbol) => {
write!(f, "environment quantity cannot be zero for symbol {symbol}")
}
Self::EnvironmentFull => write!(f, "environment is at maximum capacity"),
Self::EnvironmentBlobLengthInvalid { len } => {
write!(
f,
"environment blob length must be a multiple of 6, got {len}"
)
}
Self::EnvironmentBlobTooLarge { records } => {
write!(f, "environment blob contains too many records: {records}")
}
}
}
}
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 });
}
Ok(GeneState {
gene: vec![0; gene_size],
environment: Vec::new(),
})
}
/// 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],
environment: Vec::new(),
}
}
/// 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 {
size: state.gene.len(),
});
}
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
.binary_search_by_key(&symbol, |record| record.symbol)
{
Ok(i) => state.environment[i].quantity,
Err(_) => 0,
}
}
/// 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,
quantity: u32,
) -> Result<(), GeneError> {
let idx = state
.environment
.binary_search_by_key(&symbol, |record| record.symbol);
match (idx, quantity) {
(Ok(i), 0) => {
state.environment.remove(i);
Ok(())
}
(Ok(i), qty) => {
state.environment[i].quantity = qty;
Ok(())
}
(Err(_), 0) => Ok(()),
(Err(i), qty) => {
if state.environment.len() >= MAX_ENV_RECORDS {
return Err(GeneError::EnvironmentFull);
}
state.environment.insert(
i,
EnvironmentRecord {
symbol,
quantity: qty,
},
);
Ok(())
}
}
}
/// 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,
quantity: u32,
) -> Result<u32, GeneError> {
let current = get_env_quantity(state, symbol);
let next = current.saturating_add(quantity);
set_env_quantity(state, symbol, next)?;
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,
quantity: u32,
) -> Result<u32, GeneError> {
let current = get_env_quantity(state, symbol);
let next = current.saturating_sub(quantity);
set_env_quantity(state, symbol, next)?;
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);
for record in records {
out.extend_from_slice(&record.symbol.to_le_bytes());
out.extend_from_slice(&record.quantity.to_le_bytes());
}
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() });
}
let records_len = blob.len() / 6;
if records_len > MAX_ENV_RECORDS {
return Err(GeneError::EnvironmentBlobTooLarge {
records: records_len,
});
}
let mut records = Vec::with_capacity(records_len);
let mut i = 0;
while i < blob.len() {
let symbol = u16::from_le_bytes([blob[i], blob[i + 1]]);
let quantity = u32::from_le_bytes([blob[i + 2], blob[i + 3], blob[i + 4], blob[i + 5]]);
records.push(EnvironmentRecord { symbol, quantity });
i += 6;
}
validate_environment(&records)?;
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");
h.update(&(state.gene.len() as u32).to_le_bytes());
h.update(&state.gene);
h.update(&(state.environment.len() as u16).to_le_bytes());
for record in &state.environment {
h.update(&record.symbol.to_le_bytes());
h.update(&record.quantity.to_le_bytes());
}
*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");
h.update(session_id.as_bytes());
h.update(&step.to_le_bytes());
h.update(&commitment(state));
*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() });
}
let mut prev_symbol: Option<u16> = None;
for record in records {
if record.quantity == 0 {
return Err(GeneError::ZeroQuantitySymbol(record.symbol));
}
if let Some(prev) = prev_symbol {
if record.symbol < prev {
return Err(GeneError::EnvironmentNotSorted);
}
if record.symbol == prev {
return Err(GeneError::DuplicateEnvironmentSymbol(record.symbol));
}
}
prev_symbol = Some(record.symbol);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{Rng, SeedableRng};
#[test]
fn test_new_state_with_default_size() {
let state = new_state(DEFAULT_GENE_SIZE).unwrap();
assert_eq!(state.gene.len(), DEFAULT_GENE_SIZE);
assert!(state.environment.is_empty());
}
#[test]
fn test_new_state_rejects_invalid_sizes() {
assert!(matches!(
new_state(0).unwrap_err(),
GeneError::InvalidGeneSize { .. }
));
assert!(matches!(
new_state(MAX_GENE_SIZE + 1).unwrap_err(),
GeneError::InvalidGeneSize { .. }
));
}
#[test]
fn test_set_and_get_env_quantity() {
let mut state = new_state(8).unwrap();
set_env_quantity(&mut state, 42, 7).unwrap();
assert_eq!(get_env_quantity(&state, 42), 7);
set_env_quantity(&mut state, 42, 0).unwrap();
assert_eq!(get_env_quantity(&state, 42), 0);
}
#[test]
fn test_add_env_quantity_saturates() {
let mut state = new_state(8).unwrap();
set_env_quantity(&mut state, 1, u32::MAX - 3).unwrap();
let next = add_env_quantity(&mut state, 1, 99).unwrap();
assert_eq!(next, u32::MAX);
}
#[test]
fn test_sub_env_quantity_removes_symbol() {
let mut state = new_state(8).unwrap();
set_env_quantity(&mut state, 7, 10).unwrap();
let next = sub_env_quantity(&mut state, 7, 100).unwrap();
assert_eq!(next, 0);
assert!(state.environment.is_empty());
}
#[test]
fn test_environment_capacity_limit_is_enforced() {
let mut state = new_state(8).unwrap();
for symbol in 0..(MAX_ENV_RECORDS as u16) {
set_env_quantity(&mut state, symbol, 1).unwrap();
}
let err = set_env_quantity(&mut state, 500, 1).unwrap_err();
assert_eq!(err, GeneError::EnvironmentFull);
}
#[test]
fn test_encode_decode_environment_roundtrip() {
let records = vec![
EnvironmentRecord {
symbol: 3,
quantity: 9,
},
EnvironmentRecord {
symbol: 11,
quantity: 999,
},
];
let blob = encode_environment(&records).unwrap();
let decoded = decode_environment(&blob).unwrap();
assert_eq!(decoded, records);
}
#[test]
fn test_decode_environment_rejects_unsorted_records() {
let mut blob = Vec::new();
blob.extend_from_slice(&7u16.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
blob.extend_from_slice(&2u16.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes());
let err = decode_environment(&blob).unwrap_err();
assert_eq!(err, GeneError::EnvironmentNotSorted);
}
#[test]
fn test_decode_environment_rejects_zero_quantity() {
let mut blob = Vec::new();
blob.extend_from_slice(&9u16.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
let err = decode_environment(&blob).unwrap_err();
assert_eq!(err, GeneError::ZeroQuantitySymbol(9));
}
#[test]
fn test_commitment_changes_when_gene_or_environment_changes() {
let mut state_a = new_state(16).unwrap();
let mut state_b = state_a.clone();
assert_eq!(commitment_hex(&state_a), commitment_hex(&state_b));
state_b.gene[0] = 1;
assert_ne!(commitment_hex(&state_a), commitment_hex(&state_b));
set_env_quantity(&mut state_a, 7, 3).unwrap();
assert_ne!(commitment_hex(&state_a), commitment_hex(&state_b));
}
#[test]
fn test_validate_state_rejects_duplicate_environment_symbols() {
let state = GeneState {
gene: vec![0; 10],
environment: vec![
EnvironmentRecord {
symbol: 1,
quantity: 1,
},
EnvironmentRecord {
symbol: 1,
quantity: 2,
},
],
};
assert_eq!(
validate_state(&state).unwrap_err(),
GeneError::DuplicateEnvironmentSymbol(1)
);
}
#[test]
fn test_table_driven_randomized_environment_roundtrip() {
for seed in 0..32u64 {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut state = new_state(32).unwrap();
for _ in 0..128 {
let symbol = rng.gen_range(0u16..200u16);
let qty = if rng.gen_bool(0.15) {
0
} else {
rng.gen_range(1u32..100_000u32)
};
if let Err(err) = set_env_quantity(&mut state, symbol, qty) {
assert_eq!(err, GeneError::EnvironmentFull);
}
validate_state(&state).unwrap();
}
let blob = encode_environment(&state.environment).unwrap();
let decoded = decode_environment(&blob).unwrap();
assert_eq!(decoded, state.environment);
let commitment_a = commitment(&state);
let commitment_b = commitment(&state.clone());
assert_eq!(commitment_a, commitment_b);
}
}
}
+31 -8
View File
@@ -1,7 +1,15 @@
use crate::protocol::{EntropyData, StackState}; use crate::protocol::{EntropyData, StackState};
use blake3::Hasher; 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> { pub fn initial_hash(session_id: &str, pub_key: &[u8], salt: &[u8]) -> Vec<u8> {
let mut h = Hasher::new(); let mut h = Hasher::new();
h.update(session_id.as_bytes()); 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() 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( pub fn next_chain_hash(
prev_hash: &[u8], prev_hash: &[u8],
timestamp: u64, timestamp: u64,
@@ -18,14 +37,13 @@ pub fn next_chain_hash(
stack: &StackState, stack: &StackState,
salt: &[u8], salt: &[u8],
) -> Vec<u8> { ) -> Vec<u8> {
let entropy_json = serde_json::to_string(entropy).unwrap(); let entropy_bytes = serde_json::to_vec(entropy).unwrap_or_default();
let stack_json = serde_json::to_string(stack).unwrap(); let stack_bytes = serde_json::to_vec(stack).unwrap_or_default();
let entropy_hash = blake3::hash(entropy_json.as_bytes()); let entropy_hash = blake3::hash(&entropy_bytes);
let stack_hash = blake3::hash(stack_json.as_bytes()); let stack_hash = blake3::hash(&stack_bytes);
let mut h = Hasher::new(); let mut h = Hasher::new();
// Mix the salt into the hash state
h.update(salt); h.update(salt);
h.update(prev_hash); h.update(prev_hash);
h.update(&timestamp.to_le_bytes()); h.update(&timestamp.to_le_bytes());
@@ -34,7 +52,12 @@ pub fn next_chain_hash(
h.finalize().as_bytes().to_vec() 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 { pub fn hash_stack(stack: &[u32]) -> u32 {
let data: Vec<u8> = stack.iter().flat_map(|x| x.to_le_bytes()).collect(); let data: Vec<u8> = stack.iter().flat_map(|x| x.to_le_bytes()).collect();
let hash = blake3::hash(&data); let hash = blake3::hash(&data);
+3
View File
@@ -1,3 +1,6 @@
pub mod constants; pub mod constants;
pub mod gene;
pub mod hashing; pub mod hashing;
pub mod protocol; pub mod protocol;
pub mod vm;
pub mod vm_extensions;
+60 -6
View File
@@ -1,64 +1,118 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)] /// Request payload sent by the client to initialize a new attestation session.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InitRequest { pub struct InitRequest {
/// Hex-encoded 32-byte Ed25519 public verifying key generated by the client.
pub public_key: String, pub public_key: String,
} }
#[derive(Serialize)] /// Response payload returned by the server upon successful session initialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitResponse { pub struct InitResponse {
/// The unique hex-encoded session identifier.
pub session_id: String, pub session_id: String,
/// The initial server-issued salt to be mixed in the first heartbeat's hash.
pub salt: String, pub salt: String,
/// Base64-encoded initial VM program for client stack execution.
pub opcodes_b64: String, pub opcodes_b64: String,
/// The computed initial hash of the attestation chain.
pub initial_hash: String, pub initial_hash: String,
/// Timestamp in milliseconds indicating when the session expires.
pub expires_at: u64, pub expires_at: u64,
/// Minimum time in milliseconds allowed between subsequent heartbeats.
pub heartbeat_min_interval_ms: u64, pub heartbeat_min_interval_ms: u64,
/// Maximum time in milliseconds allowed between subsequent heartbeats.
pub heartbeat_max_interval_ms: u64, 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,
} }
#[derive(Deserialize, Serialize)] /// Heartbeat request payload submitted periodically by the client to prove session continuity.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HeartbeatRequest { pub struct HeartbeatRequest {
/// The session identifier.
pub session_id: String, pub session_id: String,
/// The expected hash from the previous heartbeat/initialization step.
pub prev_hash: String, pub prev_hash: String,
/// The client's current system timestamp in milliseconds.
pub timestamp: u64, pub timestamp: u64,
/// The collected client entropy data (such as mouse events).
pub entropy_data: EntropyData, pub entropy_data: EntropyData,
/// The final execution state of the client's VM stack program.
pub stack_state: StackState, pub stack_state: StackState,
/// The client's browser hardware and layout fingerprint.
pub fingerprint: 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, pub signature: String,
} }
#[derive(Serialize)] /// 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 { pub struct HeartbeatResponse {
/// Attestation status, typically "ok" even on silent failures.
pub status: String, pub status: String,
/// The next server-issued salt for hash chain progression.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub next_salt: Option<String>, 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>,
} }
#[derive(Deserialize, Serialize)] /// Client browser fingerprint metadata used for basic sanity checks.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Fingerprint { pub struct Fingerprint {
/// Aspect ratio of the client screen.
#[serde(rename = "aspectRatio")] #[serde(rename = "aspectRatio")]
pub aspect_ratio: String, pub aspect_ratio: String,
/// Device pixel ratio of the screen.
#[serde(rename = "devicePixelRatio")] #[serde(rename = "devicePixelRatio")]
pub device_pixel_ratio: String, pub device_pixel_ratio: String,
/// Number of logical processor cores available.
#[serde(rename = "hardwareConcurrency")] #[serde(rename = "hardwareConcurrency")]
pub hardware_concurrency: u32, pub hardware_concurrency: u32,
} }
#[derive(Deserialize, Serialize)] /// Wrapper for browser-side entropy collection.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EntropyData { pub struct EntropyData {
/// A chronological list of mouse movement events.
pub events: Vec<MouseEvent>, pub events: Vec<MouseEvent>,
} }
/// Information about a single mouse movement interaction.
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MouseEvent { pub struct MouseEvent {
/// Absolute horizontal coordinate of the cursor.
pub x: f64, pub x: f64,
/// Absolute vertical coordinate of the cursor.
pub y: f64, pub y: f64,
/// Relative timestamp in milliseconds of the event occurrence.
#[serde(rename = "t")] #[serde(rename = "t")]
pub timestamp_ms: f64, pub timestamp_ms: f64,
} }
/// The state of the VM stack machine after executing a program.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackState { pub struct StackState {
/// The elements remaining on the stack.
pub stack: Vec<u32>, pub stack: Vec<u32>,
/// The final instruction pointer location at program completion or termination.
pub ip: u16, pub ip: u16,
} }
+79
View File
@@ -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,
}
}
+903
View File
@@ -0,0 +1,903 @@
use crate::{
constants::{
DEFAULT_MUTATION_ROUNDS, HASH_OPCODE_INSTRUCTION_COST, MAX_GENE_SIZE,
MAX_MUTATION_INSTRUCTION_BUDGET, MAX_MUTATION_PROGRAM_BYTES, SOFT_CAP_DURATION_MS,
},
gene::{
add_env_quantity, get_env_quantity, sub_env_quantity, validate_state, GeneError, GeneState,
},
};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::time::Instant;
// Stack-machine mutation opcodes (v0.6.0).
//
// NOTE: stack effect notation:
// +1 => pushes one u32
// -1 => pops one u32
// 0 => net-zero (or no stack interaction)
//
// Security/performance notes:
// - All index operands are normalized with modulo to avoid panics.
// - Program size is bounded by MAX_MUTATION_PROGRAM_BYTES.
// - Environment arithmetic is saturating and deterministic.
// - Hashing uses fixed BLAKE3 commitment and fixed transcription algorithm.
pub const OP_GENE_LOAD: u8 = 0x23; // +1
pub const OP_GENE_STORE: u8 = 0x24; // -1
pub const OP_MUTATE_POINT: u8 = 0x25; // 0
pub const OP_INSERT: u8 = 0x26; // -1
pub const OP_DELETE: u8 = 0x27; // +1
pub const OP_TRANSCRIBE: u8 = 0x28; // +1
pub const OP_APPLY_MUTAGEN: u8 = 0x29; // -1
pub const OP_FINALIZE_GENE_HASH: u8 = 0x2A; // +1
pub const OP_CONSUME: u8 = 0x2B; // 0 (pop amount, push remaining)
pub const OP_PRODUCE: u8 = 0x2C; // 0 (pop amount, push resulting quantity)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MutationOrder {
pub step: u64,
pub program: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecutionTrace {
pub final_ip: usize,
pub final_stack: Vec<u32>,
pub final_gene_commitment_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MutationError {
ProgramTooLong { len: usize },
TruncatedInstruction { opcode: u8, ip: usize },
UnknownOpcode(u8),
EmptyGene,
StackUnderflow { opcode: u8, ip: usize },
GeneFull { current_len: usize },
Base64(base64::DecodeError),
Gene(GeneError),
}
impl std::fmt::Display for MutationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ProgramTooLong { len } => write!(f, "mutation program too long: {len} bytes"),
Self::TruncatedInstruction { opcode, ip } => {
write!(f, "truncated instruction {opcode:#04x} at ip={ip}")
}
Self::UnknownOpcode(opcode) => write!(f, "unknown mutation opcode: {opcode:#04x}"),
Self::EmptyGene => write!(f, "cannot mutate an empty gene"),
Self::StackUnderflow { opcode, ip } => {
write!(f, "stack underflow in opcode {opcode:#04x} at ip={ip}")
}
Self::GeneFull { current_len } => {
write!(f, "cannot insert; gene already at max size ({current_len})")
}
Self::Base64(err) => write!(f, "invalid base64 mutation order: {err}"),
Self::Gene(err) => write!(f, "{err}"),
}
}
}
impl std::error::Error for MutationError {}
impl From<GeneError> for MutationError {
fn from(value: GeneError) -> Self {
Self::Gene(value)
}
}
/// 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)?;
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
return Err(MutationError::ProgramTooLong { len: program.len() });
}
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,
gene_size: usize,
) -> MutationOrder {
let mut program = Vec::with_capacity(128);
let mut stack_depth: i32 = 0;
let mut estimated_gene_len = gene_size.clamp(1, MAX_GENE_SIZE);
let ops = rng.gen_range(20usize..=36usize);
let mut hash_ops_needed = rng.gen_range(2..=3);
for idx in 0..ops {
let remaining = ops - idx;
let op = if hash_ops_needed > 0 && remaining <= hash_ops_needed {
OP_FINALIZE_GENE_HASH
} else if stack_depth <= 0 {
rng.gen_range(0u8..3u8)
} else {
match rng.gen_range(0u8..12u8) {
0..=1 => OP_GENE_LOAD,
2..=3 => OP_TRANSCRIBE,
4 => OP_FINALIZE_GENE_HASH,
5 => OP_GENE_STORE,
6 => OP_MUTATE_POINT,
7 => OP_INSERT,
8 => OP_DELETE,
9 => OP_APPLY_MUTAGEN,
10 => OP_CONSUME,
_ => OP_PRODUCE,
}
};
match op {
OP_GENE_LOAD => {
program.push(OP_GENE_LOAD);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth += 1;
}
OP_TRANSCRIBE => {
program.push(OP_TRANSCRIBE);
push_u16(&mut program, rng.r#gen::<u16>());
program.push(rng.gen_range(1u8..=16u8));
stack_depth += 1;
}
OP_FINALIZE_GENE_HASH => {
program.push(OP_FINALIZE_GENE_HASH);
stack_depth += 1;
hash_ops_needed = hash_ops_needed.saturating_sub(1);
}
OP_GENE_STORE => {
if stack_depth > 0 {
program.push(OP_GENE_STORE);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth -= 1;
}
}
OP_MUTATE_POINT => {
program.push(OP_MUTATE_POINT);
push_u16(&mut program, rng.r#gen::<u16>());
program.push(rng.r#gen::<u8>());
}
OP_INSERT => {
if stack_depth > 0 && estimated_gene_len < MAX_GENE_SIZE {
program.push(OP_INSERT);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth -= 1;
estimated_gene_len += 1;
}
}
OP_DELETE => {
program.push(OP_DELETE);
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth += 1;
if estimated_gene_len > 1 {
estimated_gene_len -= 1;
}
}
OP_APPLY_MUTAGEN => {
if stack_depth > 0 {
program.push(OP_APPLY_MUTAGEN);
push_u16(&mut program, rng.r#gen::<u16>());
push_u16(&mut program, rng.r#gen::<u16>());
stack_depth -= 1;
}
}
OP_CONSUME => {
if stack_depth > 0 {
program.push(OP_CONSUME);
push_u16(&mut program, rng.r#gen::<u16>());
}
}
OP_PRODUCE if stack_depth > 0 => {
program.push(OP_PRODUCE);
push_u16(&mut program, rng.r#gen::<u16>());
}
_ => {}
}
}
while hash_ops_needed > 0 && program.len() < MAX_MUTATION_PROGRAM_BYTES {
program.push(OP_FINALIZE_GENE_HASH);
hash_ops_needed -= 1;
}
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],
rounds: u8,
) -> Result<GeneState, MutationError> {
let mut next = state.clone();
execute_program_with_rounds(&mut next, program, 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],
rounds: u8,
) -> Result<(), MutationError> {
let _ = execute_program_with_rounds(state, program, 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],
rounds: u8,
) -> Result<ExecutionTrace, MutationError> {
if state.gene.is_empty() {
return Err(MutationError::EmptyGene);
}
validate_state(state)?;
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
return Err(MutationError::ProgramTooLong { len: program.len() });
}
let program_cost = estimate_program_cost(program);
let max_rounds = std::cmp::max(1, MAX_MUTATION_INSTRUCTION_BUDGET / program_cost);
let actual_rounds = std::cmp::min(rounds as usize, max_rounds) as u8;
let start = Instant::now();
let mut trace = None;
for _round in 0..actual_rounds {
trace = Some(execute_program(state, program)?);
}
let elapsed = start.elapsed();
tracing::debug!(
rounds = actual_rounds,
requested_rounds = rounds,
elapsed_ms = elapsed.as_millis(),
program_len = program.len(),
"mutation execution"
);
if actual_rounds < rounds {
tracing::debug!(
requested_rounds = rounds,
executed_rounds = actual_rounds,
"mutation soft cap reduced mutation rounds to preserve host responsiveness"
);
}
if elapsed.as_millis() > SOFT_CAP_DURATION_MS {
tracing::debug!(
elapsed_ms = elapsed.as_millis(),
"mutation execution exceeded soft cap duration"
);
}
Ok(trace.unwrap_or_else(|| ExecutionTrace {
final_ip: 0,
final_stack: Vec::new(),
final_gene_commitment_hex: crate::gene::commitment_hex(state),
}))
}
fn estimate_program_cost(program: &[u8]) -> usize {
let mut ip = 0;
let mut cost = 0;
while ip < program.len() {
let opcode = program[ip];
ip += 1;
cost += if opcode == OP_FINALIZE_GENE_HASH {
HASH_OPCODE_INSTRUCTION_COST
} else {
1
};
ip += match opcode {
OP_GENE_LOAD | OP_GENE_STORE | OP_INSERT | OP_DELETE | OP_CONSUME | OP_PRODUCE => 2,
OP_MUTATE_POINT | OP_TRANSCRIBE => 3,
OP_APPLY_MUTAGEN => 4,
OP_FINALIZE_GENE_HASH => 0,
_ => 0,
}
}
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],
) -> Result<ExecutionTrace, MutationError> {
if state.gene.is_empty() {
return Err(MutationError::EmptyGene);
}
validate_state(state)?;
if program.len() > MAX_MUTATION_PROGRAM_BYTES {
return Err(MutationError::ProgramTooLong { len: program.len() });
}
let mut ip = 0usize;
let mut stack: Vec<u32> = Vec::with_capacity(16);
while ip < program.len() {
let opcode_ip = ip;
let opcode = take_u8(program, &mut ip, 0x00)?;
match opcode {
OP_GENE_LOAD => {
let idx = take_u16(program, &mut ip, opcode)?;
let normalized = normalize_index(idx as usize, state.gene.len());
stack.push(state.gene[normalized] as u32);
}
OP_GENE_STORE => {
let idx = take_u16(program, &mut ip, opcode)?;
let value = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
let normalized = normalize_index(idx as usize, state.gene.len());
state.gene[normalized] = value;
}
OP_MUTATE_POINT => {
let idx = take_u16(program, &mut ip, opcode)?;
let delta = take_u8(program, &mut ip, opcode)? as i8;
let normalized = normalize_index(idx as usize, state.gene.len());
state.gene[normalized] = state.gene[normalized].wrapping_add(delta as u8);
}
OP_INSERT => {
let idx = take_u16(program, &mut ip, opcode)?;
let value = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
if state.gene.len() >= MAX_GENE_SIZE {
return Err(MutationError::GeneFull {
current_len: state.gene.len(),
});
}
let insert_at = (idx as usize).min(state.gene.len());
state.gene.insert(insert_at, value);
}
OP_DELETE => {
let idx = take_u16(program, &mut ip, opcode)?;
let normalized = normalize_index(idx as usize, state.gene.len());
let removed = if state.gene.len() > 1 {
state.gene.remove(normalized)
} else {
let prev = state.gene[0];
state.gene[0] = 0;
prev
};
stack.push(removed as u32);
}
OP_TRANSCRIBE => {
let start = take_u16(program, &mut ip, opcode)?;
let span = take_u8(program, &mut ip, opcode)?;
let transcription = transcribe_window(&state.gene, start as usize, span);
stack.push(transcription);
}
OP_APPLY_MUTAGEN => {
let symbol = take_u16(program, &mut ip, opcode)?;
let idx = take_u16(program, &mut ip, opcode)?;
let stack_mask = pop_stack(&mut stack, opcode, opcode_ip)? as u8;
let quantity = get_env_quantity(state, symbol);
let mix = ((quantity as u8)
^ ((quantity >> 8) as u8)
^ ((quantity >> 16) as u8)
^ ((quantity >> 24) as u8))
^ ((symbol & 0x00ff) as u8)
^ ((symbol >> 8) as u8)
^ stack_mask;
let normalized = normalize_index(idx as usize, state.gene.len());
state.gene[normalized] ^= mix;
}
OP_FINALIZE_GENE_HASH => {
let commit = crate::gene::commitment(state);
let hash32 = u32::from_le_bytes([commit[0], commit[1], commit[2], commit[3]]);
stack.push(hash32);
}
OP_CONSUME => {
let symbol = take_u16(program, &mut ip, opcode)?;
let amount = pop_stack(&mut stack, opcode, opcode_ip)?;
let left = sub_env_quantity(state, symbol, amount)?;
stack.push(left);
}
OP_PRODUCE => {
let symbol = take_u16(program, &mut ip, opcode)?;
let amount = pop_stack(&mut stack, opcode, opcode_ip)?;
let next = add_env_quantity(state, symbol, amount)?;
stack.push(next);
}
_ => return Err(MutationError::UnknownOpcode(opcode)),
}
}
Ok(ExecutionTrace {
final_ip: ip,
final_stack: stack,
final_gene_commitment_hex: crate::gene::commitment_hex(state),
})
}
fn transcribe_window(gene: &[u8], start: usize, span: u8) -> u32 {
let count = usize::from(span.max(1));
let mut acc = 2_166_136_261u32; // FNV offset basis
for i in 0..count {
let idx = (start + i) % gene.len();
acc ^= gene[idx] as u32;
acc = acc.wrapping_mul(16_777_619); // FNV prime
}
acc
}
fn push_u16(buf: &mut Vec<u8>, value: u16) {
buf.extend_from_slice(&value.to_le_bytes());
}
fn take_u8(bytes: &[u8], ip: &mut usize, opcode: u8) -> Result<u8, MutationError> {
if *ip >= bytes.len() {
return Err(MutationError::TruncatedInstruction { opcode, ip: *ip });
}
let value = bytes[*ip];
*ip += 1;
Ok(value)
}
fn take_u16(bytes: &[u8], ip: &mut usize, opcode: u8) -> Result<u16, MutationError> {
if *ip + 2 > bytes.len() {
return Err(MutationError::TruncatedInstruction { opcode, ip: *ip });
}
let value = u16::from_le_bytes([bytes[*ip], bytes[*ip + 1]]);
*ip += 2;
Ok(value)
}
fn pop_stack(stack: &mut Vec<u32>, opcode: u8, ip: usize) -> Result<u32, MutationError> {
stack
.pop()
.ok_or(MutationError::StackUnderflow { opcode, ip })
}
fn normalize_index(idx: usize, len: usize) -> usize {
idx % len
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gene::{commitment, new_state, set_env_quantity};
use rand::{Rng, SeedableRng};
use std::time::Instant;
fn u16_bytes(v: u16) -> [u8; 2] {
v.to_le_bytes()
}
#[test]
fn test_opcode_gene_load() {
let mut state = new_state(4).unwrap();
state.gene = vec![10, 20, 30, 40];
let trace = execute_program(&mut state, &[OP_GENE_LOAD, 1, 0]).unwrap();
assert_eq!(trace.final_stack, vec![20]);
}
#[test]
fn test_opcode_gene_store() {
let mut state = new_state(4).unwrap();
state.gene = vec![1, 2, 3, 4];
let program = vec![
OP_GENE_LOAD,
0,
0, // stack: [1]
OP_GENE_STORE,
2,
0, // gene[2] <- 1
];
execute_program(&mut state, &program).unwrap();
assert_eq!(state.gene, vec![1, 2, 1, 4]);
}
#[test]
fn test_opcode_mutate_point() {
let mut state = new_state(4).unwrap();
state.gene[0] = 200;
let program = vec![OP_MUTATE_POINT, 0, 0, 100u8];
execute_program(&mut state, &program).unwrap();
assert_eq!(state.gene[0], 44);
}
#[test]
fn test_opcode_insert() {
let mut state = new_state(3).unwrap();
state.gene = vec![10, 20, 30];
let program = vec![
OP_GENE_LOAD,
1,
0, // stack: [20]
OP_INSERT,
0,
0, // insert 20 at position 0
];
execute_program(&mut state, &program).unwrap();
assert_eq!(state.gene, vec![20, 10, 20, 30]);
}
#[test]
fn test_opcode_delete() {
let mut state = new_state(4).unwrap();
state.gene = vec![9, 8, 7, 6];
let trace = execute_program(&mut state, &[OP_DELETE, 2, 0]).unwrap();
assert_eq!(state.gene, vec![9, 8, 6]);
assert_eq!(trace.final_stack, vec![7]);
}
#[test]
fn test_opcode_transcribe() {
let mut state = new_state(5).unwrap();
state.gene = vec![1, 2, 3, 4, 5];
let trace = execute_program(&mut state, &[OP_TRANSCRIBE, 1, 0, 3]).unwrap();
assert_eq!(trace.final_stack.len(), 1);
assert_ne!(trace.final_stack[0], 0);
}
#[test]
fn test_opcode_apply_mutagen() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 7, 0x1234_5678).unwrap();
state.gene[1] = 0xAA;
let program = vec![
OP_GENE_LOAD,
0,
0, // stack mask source
OP_APPLY_MUTAGEN,
7,
0,
1,
0,
];
execute_program(&mut state, &program).unwrap();
assert_ne!(state.gene[1], 0xAA);
}
#[test]
fn test_opcode_finalize_gene_hash() {
let mut state = new_state(4).unwrap();
let trace = execute_program(&mut state, &[OP_FINALIZE_GENE_HASH]).unwrap();
assert_eq!(trace.final_stack.len(), 1);
}
#[test]
fn test_opcode_consume() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 3, 100).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0, // stack = [0]
OP_MUTATE_POINT,
0,
0,
15, // gene[0]=15
OP_GENE_LOAD,
0,
0, // stack=[0,15]
OP_CONSUME,
3,
0, // consume 15
];
let trace = execute_program(&mut state, &program).unwrap();
assert_eq!(get_env_quantity(&state, 3), 85);
assert_eq!(trace.final_stack.last().copied().unwrap(), 85);
}
#[test]
fn test_opcode_produce() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 9, 5).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0, // stack [0]
OP_MUTATE_POINT,
0,
0,
10, // gene[0]=10
OP_GENE_LOAD,
0,
0, // stack [0,10]
OP_PRODUCE,
9,
0, // +10
];
let trace = execute_program(&mut state, &program).unwrap();
assert_eq!(get_env_quantity(&state, 9), 15);
assert_eq!(trace.final_stack.last().copied().unwrap(), 15);
}
#[test]
fn test_zero_length_gene_is_rejected() {
let mut state = GeneState {
gene: vec![],
environment: vec![],
};
let err = execute_program(&mut state, &[OP_FINALIZE_GENE_HASH]).unwrap_err();
assert_eq!(err, MutationError::EmptyGene);
}
#[test]
fn test_insert_rejects_max_size_gene() {
let mut state = new_state(MAX_GENE_SIZE).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0, // push value
OP_INSERT,
0,
0,
];
let err = execute_program(&mut state, &program).unwrap_err();
assert!(matches!(err, MutationError::GeneFull { .. }));
}
#[test]
fn test_invalid_positions_wrap_deterministically() {
let mut state_a = new_state(5).unwrap();
let mut state_b = new_state(5).unwrap();
let max_u16 = u16::MAX;
let [a0, a1] = u16_bytes(max_u16);
let program = vec![OP_MUTATE_POINT, a0, a1, 1];
execute_program(&mut state_a, &program).unwrap();
let wrapped = (max_u16 as usize % 5) as u16;
let [w0, w1] = u16_bytes(wrapped);
let wrapped_program = vec![OP_MUTATE_POINT, w0, w1, 1];
execute_program(&mut state_b, &wrapped_program).unwrap();
assert_eq!(state_a, state_b);
}
#[test]
fn test_quantity_underflow_is_saturating() {
let mut state = new_state(4).unwrap();
set_env_quantity(&mut state, 1, 3).unwrap();
state.gene[0] = 8;
let program = vec![
OP_GENE_LOAD,
0,
0, // 8
OP_CONSUME,
1,
0, // consume 8 from qty 3 => 0
];
let trace = execute_program(&mut state, &program).unwrap();
assert_eq!(get_env_quantity(&state, 1), 0);
assert_eq!(trace.final_stack.last().copied().unwrap(), 0);
}
#[test]
fn test_rejects_unknown_opcode() {
let mut state = new_state(8).unwrap();
let err = execute_program(&mut state, &[0xFF]).unwrap_err();
assert_eq!(err, MutationError::UnknownOpcode(0xFF));
}
#[test]
fn test_rejects_truncated_instruction() {
let mut state = new_state(8).unwrap();
let err = execute_program(&mut state, &[OP_GENE_LOAD, 1]).unwrap_err();
assert!(matches!(err, MutationError::TruncatedInstruction { .. }));
}
#[test]
fn test_rejects_stack_underflow() {
let mut state = new_state(8).unwrap();
let err = execute_program(&mut state, &[OP_GENE_STORE, 0, 0]).unwrap_err();
assert!(matches!(err, MutationError::StackUnderflow { .. }));
}
#[test]
fn test_base64_order_roundtrip() {
let order = MutationOrder {
step: 17,
program: vec![OP_GENE_LOAD, 1, 0, OP_GENE_STORE, 2, 0],
};
let b64 = encode_order_b64(&order);
let decoded = decode_order_b64(order.step, &b64).unwrap();
assert_eq!(decoded, order);
}
#[test]
fn test_generate_order_is_deterministic_for_seeded_rng() {
let mut rng_a = rand::rngs::StdRng::seed_from_u64(99);
let mut rng_b = rand::rngs::StdRng::seed_from_u64(99);
let order_a = generate_order_with_rng(&mut rng_a, 5, 64);
let order_b = generate_order_with_rng(&mut rng_b, 5, 64);
assert_eq!(order_a, order_b);
}
#[test]
fn test_mutation_chain() {
let mut server_state = new_state(32).unwrap();
let mut client_state = new_state(32).unwrap();
let program = vec![
OP_GENE_LOAD,
0,
0,
OP_PRODUCE,
2,
0, // env[2]+=gene[0]
OP_GENE_LOAD,
1,
0,
OP_APPLY_MUTAGEN,
2,
0,
1,
0, // mutagen at idx1
OP_TRANSCRIBE,
0,
0,
8, // hash window
OP_GENE_STORE,
2,
0, // gene[2]=transcription_low_byte
OP_DELETE,
0,
0, // stack pushes removed
OP_INSERT,
3,
0, // insert removed at position 3
OP_FINALIZE_GENE_HASH,
];
let server_trace = execute_program(&mut server_state, &program).unwrap();
let client_trace = execute_program(&mut client_state, &program).unwrap();
assert_eq!(server_state, client_state);
assert_eq!(server_trace.final_stack, client_trace.final_stack);
assert_eq!(
server_trace.final_gene_commitment_hex,
client_trace.final_gene_commitment_hex
);
}
#[test]
fn test_server_client_parity_across_random_orders() {
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
for step in 0..128u64 {
let order = generate_order_with_rng(&mut rng, step, 128);
let mut server_state = new_state(128).unwrap();
let mut client_state = new_state(128).unwrap();
let server_result = execute_program(&mut server_state, &order.program);
let client_result = execute_program(&mut client_state, &order.program);
assert_eq!(server_result.is_ok(), client_result.is_ok());
match (server_result, client_result) {
(Ok(server_trace), Ok(client_trace)) => {
assert_eq!(server_state, client_state);
assert_eq!(server_trace.final_stack, client_trace.final_stack);
assert_eq!(
commitment(&server_state),
commitment(&client_state),
"step {step}"
);
}
(Err(a), Err(b)) => assert_eq!(a.to_string(), b.to_string()),
_ => unreachable!(),
}
}
}
#[test]
fn test_fuzz_style_random_program_bytes_do_not_diverge() {
let mut rng = rand::rngs::StdRng::seed_from_u64(2026);
for _ in 0..256 {
let len = rng.gen_range(1usize..=MAX_MUTATION_PROGRAM_BYTES);
let mut program = vec![0u8; len];
for b in &mut program {
*b = rng.r#gen::<u8>();
}
let mut a = new_state(64).unwrap();
let mut b = new_state(64).unwrap();
let ra = execute_program(&mut a, &program);
let rb = execute_program(&mut b, &program);
assert_eq!(ra.is_ok(), rb.is_ok());
if ra.is_ok() {
assert_eq!(a, b);
}
}
}
#[test]
fn test_performance_smoke_mutation_execution() {
let mut rng = rand::rngs::StdRng::seed_from_u64(11);
let mut programs = Vec::new();
for step in 0..200u64 {
programs.push(generate_order_with_rng(&mut rng, step + 1, 512).program);
}
let start = Instant::now();
let mut state = new_state(512).unwrap();
for program in &programs {
let _ = execute_program(&mut state, program);
}
let elapsed = start.elapsed();
// Wide bound for CI variability; this is a regression guard, not a strict benchmark.
assert!(
elapsed.as_secs_f64() < 2.0,
"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());
}
}
+17
View File
@@ -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);
}
}
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "chronoseal-wasm" name = "chronoseal-wasm"
version = "0.5.0" version = "1.0.1"
edition = "2021" edition = "2021"
[lib] [lib]
@@ -18,3 +18,4 @@ getrandom = { version = "0.2", features = ["js"] }
hex = "0.4" hex = "0.4"
base64 = "0.22" base64 = "0.22"
serde-wasm-bindgen = "0.6" serde-wasm-bindgen = "0.6"
tracing = "0.1"
+8 -2
View File
@@ -51,8 +51,14 @@ pub fn compute_next_hash(
) -> String { ) -> String {
let prev = hex::decode(prev_hash_hex).unwrap_or_default(); let prev = hex::decode(prev_hash_hex).unwrap_or_default();
let salt = hex::decode(salt_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 entropy = match serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json) {
let stack = serde_json::from_str::<shared::protocol::StackState>(stack_state_json).unwrap(); 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); let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
hex::encode(new) hex::encode(new)
} }
+1
View File
@@ -4,3 +4,4 @@ pub mod entropy;
pub mod fingerprint; pub mod fingerprint;
pub mod transport; pub mod transport;
pub mod vm; pub mod vm;
pub mod vm_extensions;
+9 -60
View File
@@ -4,70 +4,19 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
pub fn run_program(program_b64: &str) -> JsValue { pub fn run_program(program_b64: &str) -> JsValue {
use base64::Engine; use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD let bytes = match base64::engine::general_purpose::STANDARD.decode(program_b64) {
.decode(program_b64) Ok(v) => v,
.unwrap(); Err(_) => return JsValue::NULL,
};
let state = execute(&bytes); 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 { fn execute(program: &[u8]) -> StackState {
let mut stack: Vec<u32> = Vec::new(); shared::vm::execute(program)
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,
}
} }
#[cfg(test)] #[cfg(test)]
+226
View File
@@ -0,0 +1,226 @@
use std::cell::RefCell;
use std::time::Instant;
use wasm_bindgen::prelude::*;
thread_local! {
static GENE_STATE: RefCell<Option<shared::gene::GeneState>> = const { RefCell::new(None) };
static PREVIEW_STATE: RefCell<Option<shared::gene::GeneState>> = const { RefCell::new(None) };
}
#[wasm_bindgen]
pub fn init_gene_state(gene_size: u32) -> bool {
let Ok(state) = shared::gene::new_state(gene_size as usize) else {
return false;
};
GENE_STATE.with(|slot| *slot.borrow_mut() = Some(state));
PREVIEW_STATE.with(|slot| *slot.borrow_mut() = None);
true
}
#[wasm_bindgen]
pub fn preview_gene_commitment(
order_b64: &str,
session_id: &str,
mutation_step: u64,
rounds: u8,
) -> String {
let order = match shared::vm_extensions::decode_order_b64(mutation_step, order_b64) {
Ok(order) => order,
Err(_) => return String::new(),
};
let start = Instant::now();
let candidate = GENE_STATE.with(|slot| {
let state = slot.borrow();
let current = state.as_ref()?;
shared::vm_extensions::apply_program_clone_with_rounds(
current,
&order.program,
if rounds == 0 {
shared::constants::DEFAULT_MUTATION_ROUNDS
} else {
rounds
},
)
.ok()
});
let elapsed = start.elapsed();
tracing::debug!(session_id = %session_id, mutation_step = mutation_step, elapsed_ms = elapsed.as_millis(), "wasm mutation preview execution");
let Some(candidate) = candidate else {
return String::new();
};
let commitment =
shared::gene::commitment_hex_with_context(&candidate, session_id, mutation_step);
PREVIEW_STATE.with(|slot| *slot.borrow_mut() = Some(candidate));
commitment
}
#[wasm_bindgen]
pub fn commit_gene_preview() -> bool {
let next = PREVIEW_STATE.with(|slot| slot.borrow_mut().take());
let Some(next) = next else {
return false;
};
GENE_STATE.with(|slot| *slot.borrow_mut() = Some(next));
true
}
#[wasm_bindgen]
pub fn discard_gene_preview() {
PREVIEW_STATE.with(|slot| *slot.borrow_mut() = None);
}
#[wasm_bindgen]
pub fn current_gene_commitment(session_id: &str, mutation_step: u64) -> String {
GENE_STATE.with(|slot| {
slot.borrow()
.as_ref()
.map(|state| {
shared::gene::commitment_hex_with_context(state, session_id, mutation_step)
})
.unwrap_or_default()
})
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
fn order_b64(program: Vec<u8>) -> String {
let order = shared::vm_extensions::MutationOrder { step: 1, program };
shared::vm_extensions::encode_order_b64(&order)
}
#[test]
fn test_init_gene_state_success() {
assert!(init_gene_state(64));
let commitment = current_gene_commitment("deadbeef", 1);
assert_eq!(commitment.len(), 64);
}
#[test]
fn test_init_gene_state_rejects_zero() {
assert!(!init_gene_state(0));
}
#[test]
fn test_preview_requires_initialized_state() {
discard_gene_preview();
GENE_STATE.with(|slot| *slot.borrow_mut() = None);
let c = preview_gene_commitment(
&order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 1]),
"deadbeef",
1,
0,
);
assert!(c.is_empty());
}
#[test]
fn test_preview_rejects_invalid_order() {
init_gene_state(16);
let c = preview_gene_commitment("***bad-base64***", "deadbeef", 1, 0);
assert!(c.is_empty());
}
#[test]
fn test_commit_applies_preview() {
init_gene_state(16);
let before = current_gene_commitment("deadbeef", 1);
let order = order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 1]);
let preview = preview_gene_commitment(&order, "deadbeef", 1, 0);
assert_ne!(preview, before);
assert!(commit_gene_preview());
let after = current_gene_commitment("deadbeef", 1);
assert_eq!(preview, after);
}
#[test]
fn test_discard_preview_keeps_committed_state() {
init_gene_state(16);
let before = current_gene_commitment("deadbeef", 1);
let order = order_b64(vec![shared::vm_extensions::OP_MUTATE_POINT, 0, 0, 0xFF]);
let preview = preview_gene_commitment(&order, "deadbeef", 1, 0);
assert_ne!(preview, before);
discard_gene_preview();
let after = current_gene_commitment("deadbeef", 1);
assert_eq!(before, after);
}
#[test]
fn test_commit_without_preview_returns_false() {
init_gene_state(16);
discard_gene_preview();
assert!(!commit_gene_preview());
}
#[test]
fn test_preview_commitment_matches_shared_engine() {
init_gene_state(16);
let order = shared::vm_extensions::MutationOrder {
step: 3,
program: vec![
shared::vm_extensions::OP_GENE_LOAD,
0,
0,
shared::vm_extensions::OP_PRODUCE,
1,
0,
shared::vm_extensions::OP_GENE_LOAD,
2,
0,
shared::vm_extensions::OP_APPLY_MUTAGEN,
1,
0,
2,
0,
],
};
let b64 = shared::vm_extensions::encode_order_b64(&order);
let preview = preview_gene_commitment(&b64, "deadbeef", 3, 0);
let mut expected = shared::gene::new_state(16).unwrap();
shared::vm_extensions::apply_program_with_rounds(
&mut expected,
&order.program,
shared::constants::DEFAULT_MUTATION_ROUNDS,
)
.unwrap();
assert_eq!(
preview,
shared::gene::commitment_hex_with_context(&expected, "deadbeef", 3)
);
}
#[test]
fn test_table_driven_parity_across_many_generated_orders() {
init_gene_state(64);
let mut rng = rand::rngs::StdRng::seed_from_u64(123);
let mut expected = shared::gene::new_state(64).unwrap();
for step in 0..24u64 {
let order = shared::vm_extensions::generate_order_with_rng(&mut rng, step + 1, 64);
let b64 = shared::vm_extensions::encode_order_b64(&order);
let preview = preview_gene_commitment(&b64, "deadbeef", step + 1, 0);
shared::vm_extensions::apply_program_with_rounds(
&mut expected,
&order.program,
shared::constants::DEFAULT_MUTATION_ROUNDS,
)
.unwrap();
let expected_commitment =
shared::gene::commitment_hex_with_context(&expected, "deadbeef", step + 1);
assert_eq!(preview, expected_commitment);
assert!(commit_gene_preview());
assert_eq!(
current_gene_commitment("deadbeef", step + 1),
expected_commitment
);
}
}
}
+403
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>&copy; 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>
+300
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &lt; 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 &amp; 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>&copy; 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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+127
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+127
View File
@@ -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

+356
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>&copy; 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>
+984
View File
@@ -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;
}
}
+440
View File
@@ -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;
}
}
+39
View File
@@ -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}
}
+334
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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> &gt; <code>CHRONOSEAL_* Environment Variables</code> &gt; <code>TOML file</code> &gt; <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 &amp; 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>&copy; 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>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

+43
View File
@@ -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
+127
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -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

+21
View File
@@ -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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -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

+21
View File
@@ -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"
}
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more