Release v1.0.0: protocol freeze, replay testing, fuzzing and audit readiness
Rust / build (push) Canceled after 0s

This commit is contained in:
thakares committed 2026-05-30 20:09:24 +05:30
1 parent c7873b429d
commit 1a58ef9796
15 files changed
+1652

No files matched your search

+15
View File
@@ -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"] }
+551
View File
@@ -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(&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(())
}
+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.
+88
View File
@@ -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.
+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.
+33
View File
@@ -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.
+56
View File
@@ -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.
+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.
+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);
});
+67
View File
@@ -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,
}
}
+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);
}
}