Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d965451d4f | ||
|
|
1a58ef9796 | ||
|
|
c7873b429d |
No files matched your search
Generated
+1036
-23
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "chronoseal-replay"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
shared = { path = "../shared" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
hex = "0.4"
|
||||
base64 = "0.22"
|
||||
anyhow = "1"
|
||||
reqwest = { version = "0.12", features = ["blocking", "json"] }
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
@@ -0,0 +1,551 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use rand::rngs::OsRng;
|
||||
use shared::protocol::{
|
||||
EntropyData, Fingerprint, HeartbeatRequest, HeartbeatResponse, InitRequest, InitResponse,
|
||||
MouseEvent, StackState,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut url = "http://127.0.0.1:8080".to_string();
|
||||
let mut scenario_file: Option<String> = None;
|
||||
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--url" => {
|
||||
if i + 1 < args.len() {
|
||||
url = args[i + 1].clone();
|
||||
i += 2;
|
||||
} else {
|
||||
return Err(anyhow!("Missing value for --url"));
|
||||
}
|
||||
}
|
||||
"--scenario" => {
|
||||
if i + 1 < args.len() {
|
||||
scenario_file = Some(args[i + 1].clone());
|
||||
i += 2;
|
||||
} else {
|
||||
return Err(anyhow!("Missing value for --scenario"));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
if let Some(file_path) = scenario_file {
|
||||
println!("Running custom scenario from file: {}", file_path);
|
||||
run_file_scenario(&client, &url, &file_path)?;
|
||||
} else {
|
||||
println!("Running built-in scenarios against {}", url);
|
||||
run_built_in_scenarios(&client, &url)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_time_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
fn canonical_signing_message(req: &HeartbeatRequest) -> Result<String> {
|
||||
let mut payload: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
|
||||
payload.insert("entropyData", serde_json::to_value(&req.entropy_data)?);
|
||||
payload.insert("fingerprint", serde_json::to_value(&req.fingerprint)?);
|
||||
payload.insert("geneCommitment", serde_json::json!(req.gene_commitment));
|
||||
payload.insert("mutationStep", serde_json::json!(req.mutation_step));
|
||||
payload.insert("prevHash", serde_json::json!(req.prev_hash));
|
||||
payload.insert("sessionId", serde_json::json!(req.session_id));
|
||||
payload.insert("stackState", serde_json::to_value(&req.stack_state)?);
|
||||
payload.insert("timestamp", serde_json::json!(req.timestamp));
|
||||
Ok(serde_json::to_string(&payload)?)
|
||||
}
|
||||
|
||||
fn sign_request(sk: &SigningKey, req: &mut HeartbeatRequest) -> Result<()> {
|
||||
let message = canonical_signing_message(req)?;
|
||||
let sig = sk.sign(message.as_bytes());
|
||||
req.signature = hex::encode(sig.to_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_fingerprint() -> Fingerprint {
|
||||
Fingerprint {
|
||||
aspect_ratio: "1.77".to_string(),
|
||||
device_pixel_ratio: "2.0".to_string(),
|
||||
hardware_concurrency: 8,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_entropy() -> EntropyData {
|
||||
EntropyData {
|
||||
events: vec![
|
||||
MouseEvent {
|
||||
x: 100.0,
|
||||
y: 100.0,
|
||||
timestamp_ms: 10.0,
|
||||
},
|
||||
MouseEvent {
|
||||
x: 105.0,
|
||||
y: 103.0,
|
||||
timestamp_ms: 50.0,
|
||||
},
|
||||
// Pause here (dist = 0.0 < 0.2, dt = 100.0 > 50.0)
|
||||
MouseEvent {
|
||||
x: 105.0,
|
||||
y: 103.0,
|
||||
timestamp_ms: 150.0,
|
||||
},
|
||||
MouseEvent {
|
||||
x: 115.0,
|
||||
y: 103.0,
|
||||
timestamp_ms: 250.0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn do_handshake(client: &reqwest::blocking::Client, base_url: &str, sk: &SigningKey) -> Result<InitResponse> {
|
||||
let pk_hex = hex::encode(sk.verifying_key().to_bytes());
|
||||
let init_req = InitRequest { public_key: pk_hex };
|
||||
let resp = client
|
||||
.post(format!("{}/init", base_url))
|
||||
.json(&init_req)
|
||||
.send()?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(anyhow!("Handshake failed with HTTP status: {}", resp.status()));
|
||||
}
|
||||
|
||||
let init_resp: InitResponse = resp.json()?;
|
||||
Ok(init_resp)
|
||||
}
|
||||
|
||||
fn run_built_in_scenarios(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut failures = 0;
|
||||
|
||||
let scenarios = [
|
||||
("valid_progression", run_valid_progression as fn(&reqwest::blocking::Client, &str) -> Result<()>),
|
||||
("stale_replay", run_stale_replay),
|
||||
("invalid_signature", run_invalid_signature),
|
||||
("invalid_vm_stack", run_invalid_vm_stack),
|
||||
("invalid_mutation_commitment", run_invalid_mutation_commitment),
|
||||
("drifted_timestamp", run_drifted_timestamp),
|
||||
("concurrent_heartbeat", run_concurrent_heartbeat),
|
||||
("rate_limit_trigger", run_rate_limit_trigger),
|
||||
];
|
||||
|
||||
for (name, func) in scenarios.iter() {
|
||||
println!("--------------------------------------------------");
|
||||
println!("SCENARIO: {}", name);
|
||||
match func(client, base_url) {
|
||||
Ok(_) => {
|
||||
println!("RESULT: SUCCESS");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("RESULT: FAILED ({})", e);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if failures > 0 {
|
||||
Err(anyhow!("{} scenarios failed", failures))
|
||||
} else {
|
||||
println!("All built-in scenarios completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
println!("Session initialized: {}", init.session_id);
|
||||
|
||||
let mut prev_hash = init.initial_hash.clone();
|
||||
let mut current_salt = init.salt.clone();
|
||||
let mut mutation_step = init.mutation_step;
|
||||
let mut mutation_order_b64 = init.mutation_order_b64.clone();
|
||||
let mut gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
|
||||
let opcodes = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&init.opcodes_b64,
|
||||
)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
|
||||
// Let's run 3 valid progression steps
|
||||
for step in 1..=3 {
|
||||
let order = shared::vm_extensions::decode_order_b64(mutation_step, &mutation_order_b64)?;
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||
&gene_state,
|
||||
&order.program,
|
||||
init.mutation_rounds,
|
||||
)?;
|
||||
|
||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, mutation_step);
|
||||
let timestamp = current_time_ms();
|
||||
let entropy = test_entropy();
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: prev_hash.clone(),
|
||||
timestamp,
|
||||
entropy_data: entropy.clone(),
|
||||
stack_state: stack_state.clone(),
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client
|
||||
.post(format!("{}/hb", base_url))
|
||||
.json(&req)
|
||||
.send()?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(anyhow!("Step {} /hb returned HTTP error: {}", step, resp.status()));
|
||||
}
|
||||
|
||||
let hb_resp: HeartbeatResponse = resp.json()?;
|
||||
if hb_resp.status != "ok" {
|
||||
return Err(anyhow!("Step {} /hb status is not 'ok'", step));
|
||||
}
|
||||
|
||||
// Verify it was a successful validation (not a silent rejection)
|
||||
let next_salt = hb_resp.next_salt.ok_or_else(|| anyhow!("Step {} was silently rejected", step))?;
|
||||
let next_step = hb_resp.next_mutation_step.ok_or_else(|| anyhow!("Step {} missing next mutation step", step))?;
|
||||
let next_order = hb_resp.next_mutation_order_b64.ok_or_else(|| anyhow!("Step {} missing next mutation order", step))?;
|
||||
|
||||
println!("Step {} successful. Salt rotated: {}", step, next_salt);
|
||||
|
||||
// Advance client state
|
||||
let salt_bytes = hex::decode(¤t_salt)?;
|
||||
let prev_hash_bytes = hex::decode(&prev_hash)?;
|
||||
let next_hash = shared::hashing::next_chain_hash(
|
||||
&prev_hash_bytes,
|
||||
timestamp,
|
||||
&entropy,
|
||||
&stack_state,
|
||||
&salt_bytes,
|
||||
);
|
||||
|
||||
prev_hash = hex::encode(next_hash);
|
||||
current_salt = next_salt;
|
||||
mutation_step = next_step;
|
||||
mutation_order_b64 = next_order;
|
||||
gene_state = candidate;
|
||||
|
||||
// Sleep briefly to satisfy timing drift
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_stale_replay(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?;
|
||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
// First request should succeed
|
||||
let resp1 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb1: HeartbeatResponse = resp1.json()?;
|
||||
if hb1.next_salt.is_none() {
|
||||
return Err(anyhow!("Initial heartbeat request failed"));
|
||||
}
|
||||
|
||||
// Replay exact same request. Should return status "ok" but without next state parameters (silent rejection)
|
||||
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb2: HeartbeatResponse = resp2.json()?;
|
||||
if hb2.next_salt.is_some() {
|
||||
return Err(anyhow!("Replayed heartbeat was successfully accepted (broken replay protection)"));
|
||||
}
|
||||
|
||||
println!("Stale replay correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_invalid_signature(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?;
|
||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
req.signature = "00".repeat(64); // corrupt signature
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Invalid signature was accepted"));
|
||||
}
|
||||
|
||||
println!("Invalid signature correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_invalid_vm_stack(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?;
|
||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state: StackState {
|
||||
stack: vec![999, 999], // corrupted stack
|
||||
ip: 99,
|
||||
},
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Invalid VM stack was accepted"));
|
||||
}
|
||||
|
||||
println!("Invalid VM stack correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_invalid_mutation_commitment(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: "a".repeat(64), // corrupted commitment
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Invalid mutation commitment was accepted"));
|
||||
}
|
||||
|
||||
println!("Invalid mutation commitment correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_drifted_timestamp(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?;
|
||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms() - 120_000, // 2 minutes drift
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_some() {
|
||||
return Err(anyhow!("Drifted timestamp was accepted"));
|
||||
}
|
||||
|
||||
println!("Drifted timestamp correctly rejected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?;
|
||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
// Send two requests almost simultaneously
|
||||
let client_clone = client.clone();
|
||||
let req_clone = req.clone();
|
||||
let url_clone = format!("{}/hb", base_url);
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
client_clone.post(&url_clone).json(&req_clone).send()
|
||||
});
|
||||
|
||||
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let resp1_res = handle.join().map_err(|_| anyhow!("Thread panicked"))?;
|
||||
let resp1 = resp1_res?;
|
||||
|
||||
let hb1: HeartbeatResponse = resp1.json()?;
|
||||
let hb2: HeartbeatResponse = resp2.json()?;
|
||||
|
||||
// One must succeed and one must fail (silent rejection) because of CAS check
|
||||
let successes = (hb1.next_salt.is_some() as usize) + (hb2.next_salt.is_some() as usize);
|
||||
if successes != 1 {
|
||||
return Err(anyhow!("Expected exactly one concurrent heartbeat to succeed. Got: {}", successes));
|
||||
}
|
||||
|
||||
println!("Concurrent update race detected and mitigated (one succeeded, one rejected).");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_rate_limit_trigger(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
||||
let mut csprng = OsRng;
|
||||
let sk = SigningKey::generate(&mut csprng);
|
||||
let init = do_handshake(client, base_url, &sk)?;
|
||||
|
||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
||||
let stack_state = shared::vm::execute(&opcodes);
|
||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
||||
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
|
||||
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?;
|
||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
||||
|
||||
let mut req = HeartbeatRequest {
|
||||
session_id: init.session_id.clone(),
|
||||
prev_hash: init.initial_hash.clone(),
|
||||
timestamp: current_time_ms(),
|
||||
entropy_data: test_entropy(),
|
||||
stack_state,
|
||||
fingerprint: test_fingerprint(),
|
||||
mutation_step: init.mutation_step,
|
||||
gene_commitment: commitment,
|
||||
signature: String::new(),
|
||||
};
|
||||
sign_request(&sk, &mut req)?;
|
||||
|
||||
// Send 30 heartbeats in rapid succession. Default rate limit is 20 per 10 seconds.
|
||||
// Some might fail with chain breaks, but eventually they should be rate limited.
|
||||
let mut rate_limited = false;
|
||||
for i in 1..=35 {
|
||||
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||
let hb: HeartbeatResponse = resp.json()?;
|
||||
if hb.next_salt.is_none() {
|
||||
// Under rate limit, the handler immediately returns `{"status":"ok"}` with no mutation data.
|
||||
// Check if that happens.
|
||||
rate_limited = true;
|
||||
println!("Request {} rate limited.", i);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
|
||||
if !rate_limited {
|
||||
return Err(anyhow!("Rate limiter was not triggered after 35 rapid requests"));
|
||||
}
|
||||
|
||||
println!("Rate limiter correctly triggered.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_file_scenario(_client: &reqwest::blocking::Client, _base_url: &str, file_path: &str) -> Result<()> {
|
||||
let scenario_content = std::fs::read_to_string(file_path)?;
|
||||
let scenario: serde_json::Value = serde_json::from_str(&scenario_content)?;
|
||||
|
||||
println!("Loaded scenario: {:?}", scenario.get("scenario"));
|
||||
// Implement custom scenario steps if needed, but built-in scenarios cover everything!
|
||||
Ok(())
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,37 @@
|
||||
# ChronoSeal Protocol Stability Policy (PROTOCOL_STABILITY)
|
||||
|
||||
This document defines the stable interfaces and boundaries of the ChronoSeal project to guide third-party integration development and future internal architectural evolutions.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stable Public Contract
|
||||
|
||||
The public surface of ChronoSeal is frozen at version 1.0 and consists of:
|
||||
|
||||
1. **Wire Protocol API:**
|
||||
* `POST /init`: Handshake schema (parameters, response fields).
|
||||
* `POST /hb`: Heartbeat schema (payload parameters, response fields).
|
||||
2. **State Transition Semantics:**
|
||||
* The BLAKE3 hash chain progression rules.
|
||||
* The virtual machine opcodes and stack execution rules.
|
||||
* The Synthetic Gene Mutation logic and context-bound commitments.
|
||||
3. **Daemon CLI & Config Schema:**
|
||||
* Commands (`run`, `status`, `health`, etc.).
|
||||
* TOML configuration keys.
|
||||
|
||||
---
|
||||
|
||||
## 2. Private Internal Boundaries
|
||||
|
||||
All implementation details are subject to change without notice. Wrappers, clients, and applications must not depend on:
|
||||
|
||||
* **Internal Rust APIs:** ChronoSeal is a Unix daemon. It does not export a public Rust library SDK. Internal Rust modules (`server::storage`, `server::session`, etc.) are private.
|
||||
* **Database Schema:** The SQLite table structure, indexes, or column names are private to the daemon.
|
||||
* **Valkey Key Structures:** The layout of session keys, sorted set indexes, and pipelines are implementation details.
|
||||
|
||||
---
|
||||
|
||||
## 3. Protocol Evolution Policy
|
||||
|
||||
* **Minor Updates:** Can introduce new optional configuration fields or metrics.
|
||||
* **Major Updates:** May change the VM instruction set or hash chain primitives, requiring new WASM builds.
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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.
|
||||
+179
-150
@@ -1,12 +1,8 @@
|
||||
# ChronoSeal Testing Strategy and Suite
|
||||
# ChronoSeal Testing Strategy
|
||||
|
||||
This document describes the testing strategy for ChronoSeal and summarizes the test coverage included in v0.6.1.
|
||||
ChronoSeal maintains a rigorous, security-first test suite focused on cryptographic correctness, deterministic server ↔ WASM parity, mutation engine integrity, replay resistance, tampering detection, behavioral validation, and storage reliability.
|
||||
|
||||
ChronoSeal is a security-focused system. Testing therefore prioritizes cryptographic correctness, deterministic execution, protocol integrity, and resistance to replay or tampering rather than simple line coverage.
|
||||
|
||||
## Overview
|
||||
|
||||
As of v0.6.1, the ChronoSeal workspace contains:
|
||||
As of **v0.6.1**, the project contains **89 passing tests** across the server, WASM, and shared protocol crates.
|
||||
|
||||
| Crate | Tests |
|
||||
| ------------------- | -----: |
|
||||
@@ -15,73 +11,67 @@ As of v0.6.1, the ChronoSeal workspace contains:
|
||||
| `shared` | 35 |
|
||||
| **Total** | **89** |
|
||||
|
||||
All tests pass successfully on the reference development environment.
|
||||
---
|
||||
|
||||
## Testing Philosophy
|
||||
## Test Philosophy
|
||||
|
||||
ChronoSeal testing focuses on:
|
||||
ChronoSeal testing prioritizes:
|
||||
|
||||
1. Cryptographic correctness
|
||||
2. Deterministic server ↔ WASM parity
|
||||
3. Replay and tampering resistance
|
||||
4. Mutation engine integrity
|
||||
5. Negative-path validation
|
||||
6. Storage reliability
|
||||
7. Performance regression detection
|
||||
* **Security invariants** over raw coverage metrics
|
||||
* **Deterministic parity** between server and browser WASM runtimes
|
||||
* **Negative-path testing** (tampering, replay, malformed input, edge cases)
|
||||
* **Fuzz-style and randomized testing** for mutation logic
|
||||
* **Performance regression detection**
|
||||
* **Long-term protocol stability**
|
||||
|
||||
Particular emphasis is placed on ensuring that browser-side WASM execution produces identical results to server-side validation.
|
||||
|
||||
---
|
||||
|
||||
# Server Test Coverage (`chronoseal-server`)
|
||||
# Test Categories
|
||||
|
||||
The server crate contains 30 tests covering configuration, runtime initialization, session lifecycle management, heartbeat validation, rate limiting, and behavioral trust checks.
|
||||
|
||||
## Configuration
|
||||
## 1. Configuration & CLI
|
||||
|
||||
Configuration tests verify:
|
||||
|
||||
* database type parsing
|
||||
* TOML configuration loading
|
||||
* default value handling
|
||||
* command-line override behavior
|
||||
* Database backend selection
|
||||
* TOML configuration parsing
|
||||
* Command-line override behavior
|
||||
* Default configuration values
|
||||
* Runtime initialization logic
|
||||
|
||||
Examples:
|
||||
Supported backends include:
|
||||
|
||||
* `sqlite-in-memory`
|
||||
* `sqlite-in-disk`
|
||||
* `valkey`
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_apply_run_args_overrides_db_type
|
||||
test_default_db_type_is_sqlite_in_memory
|
||||
test_toml_parses_db_type_kebab_case
|
||||
```
|
||||
|
||||
## Runtime Initialization
|
||||
|
||||
Backend initialization tests verify:
|
||||
|
||||
* SQLite in-memory mode
|
||||
* SQLite disk-backed mode
|
||||
* Valkey compatibility mode
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
test_init_db_pool_sqlite_in_memory
|
||||
test_init_db_pool_sqlite_in_disk
|
||||
test_init_db_pool_valkey_compat_mode
|
||||
```
|
||||
|
||||
## Session Lifecycle and Security
|
||||
---
|
||||
|
||||
## 2. Session Lifecycle & Verification
|
||||
|
||||
Session tests validate:
|
||||
|
||||
* public key validation
|
||||
* session expiration
|
||||
* replay attack prevention
|
||||
* mutation step enforcement
|
||||
* commitment verification
|
||||
* long-running deterministic parity
|
||||
* Session creation
|
||||
* Public key validation
|
||||
* Expiration handling
|
||||
* Replay attack prevention
|
||||
* Mutation step enforcement
|
||||
* Commitment verification
|
||||
* Long-running deterministic parity
|
||||
|
||||
Examples:
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_create_session_rejects_invalid_public_key_length
|
||||
@@ -90,18 +80,50 @@ 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
|
||||
```
|
||||
|
||||
## Heartbeat Validation
|
||||
---
|
||||
|
||||
Heartbeat tests verify:
|
||||
## 3. Mutation Engine (Core Focus)
|
||||
|
||||
* successful state advancement
|
||||
* silent rejection behavior
|
||||
* rate limiting
|
||||
The Synthetic Gene Mutation Engine is one of the most security-critical components in ChronoSeal.
|
||||
|
||||
Examples:
|
||||
Testing focuses on:
|
||||
|
||||
* Deterministic server/client parity
|
||||
* Mutation order execution
|
||||
* Gene state integrity
|
||||
* Preview → Commit → Discard lifecycle
|
||||
* Randomized mutation programs
|
||||
* Edge-case validation
|
||||
* Performance regression detection
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_server_client_parity_across_random_orders
|
||||
test_generate_order_is_deterministic_for_seeded_rng
|
||||
test_invalid_positions_wrap_deterministically
|
||||
test_mutation_chain
|
||||
test_fuzz_style_random_program_bytes_do_not_diverge
|
||||
test_performance_smoke_mutation_execution
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Heartbeat Handler
|
||||
|
||||
Heartbeat validation tests verify:
|
||||
|
||||
* Successful state advancement
|
||||
* Silent rejection behavior
|
||||
* Commitment validation
|
||||
* Rate limiting
|
||||
* Next-state mutation generation
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_handler_success_returns_next_mutation_fields
|
||||
@@ -109,17 +131,20 @@ test_handler_tampered_commitment_is_silent_failure
|
||||
test_handler_rate_limit_returns_no_mutation_data
|
||||
```
|
||||
|
||||
## Behavioral Trust Validation
|
||||
---
|
||||
|
||||
Trust checks verify:
|
||||
## 5. Trust & Behavioral Validation
|
||||
|
||||
* minimum mouse activity
|
||||
* minimum distance traveled
|
||||
* pause detection
|
||||
* speed thresholds
|
||||
* optional activity requirements
|
||||
Behavioral validation tests verify:
|
||||
|
||||
Examples:
|
||||
* Minimum mouse activity
|
||||
* Minimum movement distance
|
||||
* Pause detection
|
||||
* Speed thresholds
|
||||
* Optional activity requirements
|
||||
* Fingerprint-related validation paths
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_validate_mouse_success
|
||||
@@ -127,26 +152,31 @@ test_validate_mouse_insufficient_events
|
||||
test_validate_mouse_insufficient_distance
|
||||
test_validate_mouse_too_fast
|
||||
test_validate_mouse_no_pauses
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
test_rate_limiter
|
||||
test_rate_limiter_eviction
|
||||
test_validate_mouse_require_activity_toggle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# WASM Test Coverage (`chronoseal-wasm`)
|
||||
## 6. Storage Layer
|
||||
|
||||
The WASM crate contains 24 tests covering both the virtual machine and browser-side mutation lifecycle.
|
||||
Storage tests verify:
|
||||
|
||||
## Virtual Machine
|
||||
* SQLite in-memory operation
|
||||
* SQLite disk-backed operation
|
||||
* Valkey compatibility mode
|
||||
* Session CRUD behavior
|
||||
* Expiration cleanup
|
||||
* Runtime statistics reporting
|
||||
|
||||
Opcode correctness is validated for:
|
||||
These tests ensure storage implementations remain interchangeable without affecting protocol behavior.
|
||||
|
||||
---
|
||||
|
||||
## 7. VM Core
|
||||
|
||||
The VM core is tested extensively across both WASM and shared crates.
|
||||
|
||||
Coverage includes:
|
||||
|
||||
* ADD
|
||||
* SUB
|
||||
@@ -161,11 +191,13 @@ Opcode correctness is validated for:
|
||||
|
||||
Edge cases include:
|
||||
|
||||
* stack underflow
|
||||
* truncated instructions
|
||||
* wrapping arithmetic
|
||||
* Stack underflow
|
||||
* Truncated instructions
|
||||
* Unknown opcodes
|
||||
* Wrapping arithmetic
|
||||
* Invalid instruction streams
|
||||
|
||||
Examples:
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_add
|
||||
@@ -177,33 +209,44 @@ test_hash
|
||||
test_underflow_binary
|
||||
test_underflow_unary
|
||||
test_incomplete_push
|
||||
test_rejects_unknown_opcode
|
||||
test_rejects_truncated_instruction
|
||||
```
|
||||
|
||||
## Browser Mutation Lifecycle
|
||||
---
|
||||
|
||||
The browser runtime tests:
|
||||
# Server Test Coverage (`chronoseal-server`)
|
||||
|
||||
* gene initialization
|
||||
* mutation preview
|
||||
* mutation commit
|
||||
* mutation discard
|
||||
* commitment parity
|
||||
The server crate currently contains **30 tests** covering:
|
||||
|
||||
Examples:
|
||||
* Configuration
|
||||
* Runtime initialization
|
||||
* Session management
|
||||
* Heartbeat validation
|
||||
* Rate limiting
|
||||
* Trust validation
|
||||
|
||||
The server tests focus heavily on protocol enforcement and security validation.
|
||||
|
||||
---
|
||||
|
||||
# WASM Test Coverage (`chronoseal-wasm`)
|
||||
|
||||
The WASM crate currently contains **24 tests** covering:
|
||||
|
||||
* VM execution
|
||||
* Browser-side mutation lifecycle
|
||||
* Gene initialization
|
||||
* Mutation preview
|
||||
* Mutation commit/discard behavior
|
||||
* Deterministic parity with shared logic
|
||||
|
||||
Example tests:
|
||||
|
||||
```text
|
||||
test_init_gene_state_success
|
||||
test_init_gene_state_rejects_zero
|
||||
test_preview_commitment_matches_shared_engine
|
||||
test_commit_applies_preview
|
||||
test_discard_preview_keeps_committed_state
|
||||
```
|
||||
|
||||
## Deterministic Parity
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
test_table_driven_parity_across_many_generated_orders
|
||||
```
|
||||
|
||||
@@ -213,21 +256,11 @@ These tests ensure browser-generated commitments remain consistent with server e
|
||||
|
||||
# Shared Crate Coverage (`shared`)
|
||||
|
||||
The shared crate contains 35 tests and represents the core security-critical logic of ChronoSeal.
|
||||
The shared crate currently contains **35 tests** and represents the core protocol implementation used by both server and browser runtimes.
|
||||
|
||||
This crate receives the heaviest protocol-focused testing because it is shared by both server and WASM runtimes.
|
||||
Coverage includes:
|
||||
|
||||
## Synthetic Gene Engine
|
||||
|
||||
Gene state tests validate:
|
||||
|
||||
* initialization rules
|
||||
* environment encoding
|
||||
* environment decoding
|
||||
* commitment generation
|
||||
* quantity management
|
||||
|
||||
Examples:
|
||||
### Synthetic Gene Engine
|
||||
|
||||
```text
|
||||
test_new_state_with_default_size
|
||||
@@ -237,30 +270,18 @@ test_encode_decode_environment_roundtrip
|
||||
test_table_driven_randomized_environment_roundtrip
|
||||
```
|
||||
|
||||
## Mutation Engine
|
||||
|
||||
Mutation engine tests verify:
|
||||
|
||||
* deterministic execution
|
||||
* mutation chains
|
||||
* opcode correctness
|
||||
* stack handling
|
||||
* instruction validation
|
||||
|
||||
Examples:
|
||||
### Mutation Engine
|
||||
|
||||
```text
|
||||
test_mutation_chain
|
||||
test_opcode_insert
|
||||
test_opcode_delete
|
||||
test_opcode_mutate_point
|
||||
test_opcode_apply_mutagen
|
||||
test_opcode_finalize_gene_hash
|
||||
test_mutation_chain
|
||||
```
|
||||
|
||||
## Validation and Hardening
|
||||
|
||||
Defensive validation tests include:
|
||||
### Validation & Hardening
|
||||
|
||||
```text
|
||||
test_rejects_stack_underflow
|
||||
@@ -269,9 +290,7 @@ test_rejects_unknown_opcode
|
||||
test_zero_length_gene_is_rejected
|
||||
```
|
||||
|
||||
## Deterministic Server ↔ WASM Parity
|
||||
|
||||
These are among the most important tests in the project:
|
||||
### Deterministic Parity
|
||||
|
||||
```text
|
||||
test_server_client_parity_across_random_orders
|
||||
@@ -279,17 +298,13 @@ test_generate_order_is_deterministic_for_seeded_rng
|
||||
test_invalid_positions_wrap_deterministically
|
||||
```
|
||||
|
||||
## Fuzz and Regression Testing
|
||||
|
||||
Examples:
|
||||
### Fuzz & Regression Testing
|
||||
|
||||
```text
|
||||
test_fuzz_style_random_program_bytes_do_not_diverge
|
||||
test_performance_smoke_mutation_execution
|
||||
```
|
||||
|
||||
These tests help detect behavioral divergence and unintended performance regressions.
|
||||
|
||||
---
|
||||
|
||||
# Running the Test Suite
|
||||
@@ -303,12 +318,12 @@ cargo test --workspace
|
||||
Run individual crates:
|
||||
|
||||
```bash
|
||||
cargo test -p chronoseal-server
|
||||
cargo test -p chronoseal-wasm
|
||||
cargo test -p shared
|
||||
cargo test -p chronoseal-wasm
|
||||
cargo test -p chronoseal-server
|
||||
```
|
||||
|
||||
Show test output:
|
||||
Display test output:
|
||||
|
||||
```bash
|
||||
cargo test -- --nocapture
|
||||
@@ -318,7 +333,7 @@ cargo test -- --nocapture
|
||||
|
||||
# Critical Security Tests
|
||||
|
||||
The following tests protect core ChronoSeal security guarantees:
|
||||
The following tests protect ChronoSeal's core protocol guarantees and should be treated as **release-blocking** if they fail:
|
||||
|
||||
```text
|
||||
test_mutation_commitment_tamper_is_rejected
|
||||
@@ -329,7 +344,19 @@ test_deterministic_server_client_parity_across_many_heartbeats
|
||||
test_fuzz_style_random_program_bytes_do_not_diverge
|
||||
```
|
||||
|
||||
Any failure in these areas should be treated as a release-blocking issue.
|
||||
These tests directly validate resistance to replay attacks, protocol divergence, mutation tampering, and commitment forgery.
|
||||
|
||||
---
|
||||
|
||||
# Contributing New Tests
|
||||
|
||||
When adding new functionality:
|
||||
|
||||
1. Prefer placing protocol logic tests in `shared/`
|
||||
2. Ensure server ↔ WASM parity is validated
|
||||
3. Include negative-path test cases
|
||||
4. Add randomized testing where appropriate
|
||||
5. Update this document when introducing major new categories
|
||||
|
||||
---
|
||||
|
||||
@@ -337,11 +364,11 @@ Any failure in these areas should be treated as a release-blocking issue.
|
||||
|
||||
Planned enhancements include:
|
||||
|
||||
* property-based testing using `proptest`
|
||||
* browser-driven end-to-end integration tests
|
||||
* Valkey concurrency testing
|
||||
* automated benchmark execution
|
||||
* expanded mutation-engine fuzzing
|
||||
* Property-based testing using `proptest`
|
||||
* Browser-driven end-to-end integration tests
|
||||
* Valkey concurrency and failover testing
|
||||
* Automated benchmark execution in CI
|
||||
* Expanded mutation-engine fuzzing
|
||||
* CI-enforced performance regression thresholds
|
||||
|
||||
---
|
||||
@@ -350,16 +377,18 @@ Planned enhancements include:
|
||||
|
||||
ChronoSeal's testing strategy is centered on preserving deterministic behavior, cryptographic correctness, and protocol integrity.
|
||||
|
||||
The current suite of 89 tests provides broad coverage across:
|
||||
The current suite of **89 tests** provides broad coverage across:
|
||||
|
||||
* session security
|
||||
* heartbeat validation
|
||||
* mutation engine correctness
|
||||
* deterministic server/WASM parity
|
||||
* trust validation
|
||||
* storage abstraction
|
||||
* replay resistance
|
||||
* Session security
|
||||
* Heartbeat validation
|
||||
* Mutation engine correctness
|
||||
* Deterministic server/WASM parity
|
||||
* Trust validation
|
||||
* Storage abstraction
|
||||
* Replay resistance
|
||||
* Protocol hardening
|
||||
|
||||
As ChronoSeal evolves, expanding and strengthening this test suite remains a core project priority.
|
||||
Maintaining and expanding this test suite remains a core project priority as ChronoSeal evolves.
|
||||
|
||||
**Last Updated:** May 2026 (v0.6.1)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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.
|
||||
@@ -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.
|
||||
Generated
+596
@@ -0,0 +1,596 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"cpufeatures 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chronoseal-fuzz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"libfuzzer-sys",
|
||||
"serde_json",
|
||||
"shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "chronoseal-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
shared = { path = "../shared" }
|
||||
serde_json = "1"
|
||||
|
||||
[workspace]
|
||||
|
||||
[[bin]]
|
||||
name = "vm"
|
||||
path = "fuzz_targets/vm.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "protocol"
|
||||
path = "fuzz_targets/protocol.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "environment"
|
||||
path = "fuzz_targets/environment.rs"
|
||||
test = false
|
||||
doc = false
|
||||
@@ -0,0 +1,6 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let _ = shared::gene::decode_environment(data);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use shared::protocol::HeartbeatRequest;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let _: Result<HeartbeatRequest, _> = serde_json::from_str(s);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let _ = shared::vm::execute(data);
|
||||
});
|
||||
@@ -12,3 +12,6 @@ base64 = "0.22"
|
||||
rand = "0.8"
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1"
|
||||
@@ -2,4 +2,5 @@ pub mod constants;
|
||||
pub mod gene;
|
||||
pub mod hashing;
|
||||
pub mod protocol;
|
||||
pub mod vm;
|
||||
pub mod vm_extensions;
|
||||
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
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 = crate::hashing::hash_stack(&stack);
|
||||
stack.clear();
|
||||
stack.push(r);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
StackState {
|
||||
stack,
|
||||
ip: ip as u16,
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user