Harden validation, improve runtime security and refactor core services
Rust / build (push) Canceled after 0s
Rust / build (push) Canceled after 0s
This commit is contained in:
1 parent
aebef4c623
commit
8d119ac00e
39 files changed
+525
-155
No files matched your search
Generated
+15
@@ -296,6 +296,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
"dashmap",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"hex",
|
"hex",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
@@ -509,6 +510,20 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "6.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
|
|||||||
@@ -24,4 +24,9 @@ ENV CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
|
|||||||
ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend
|
ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend
|
||||||
ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid
|
ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid
|
||||||
|
|
||||||
|
RUN useradd -r -s /bin/false chronoseal
|
||||||
|
USER chronoseal
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s CMD chronoseal health || exit 1
|
||||||
|
|
||||||
CMD ["chronoseal", "run"]
|
CMD ["chronoseal", "run"]
|
||||||
+132
-45
@@ -117,7 +117,11 @@ fn test_entropy() -> EntropyData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_handshake(client: &reqwest::blocking::Client, base_url: &str, sk: &SigningKey) -> Result<InitResponse> {
|
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 pk_hex = hex::encode(sk.verifying_key().to_bytes());
|
||||||
let init_req = InitRequest { public_key: pk_hex };
|
let init_req = InitRequest { public_key: pk_hex };
|
||||||
let resp = client
|
let resp = client
|
||||||
@@ -126,7 +130,10 @@ fn do_handshake(client: &reqwest::blocking::Client, base_url: &str, sk: &Signing
|
|||||||
.send()?;
|
.send()?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
return Err(anyhow!("Handshake failed with HTTP status: {}", resp.status()));
|
return Err(anyhow!(
|
||||||
|
"Handshake failed with HTTP status: {}",
|
||||||
|
resp.status()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let init_resp: InitResponse = resp.json()?;
|
let init_resp: InitResponse = resp.json()?;
|
||||||
@@ -137,11 +144,17 @@ fn run_built_in_scenarios(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
let mut failures = 0;
|
let mut failures = 0;
|
||||||
|
|
||||||
let scenarios = [
|
let scenarios = [
|
||||||
("valid_progression", run_valid_progression as fn(&reqwest::blocking::Client, &str) -> Result<()>),
|
(
|
||||||
|
"valid_progression",
|
||||||
|
run_valid_progression as fn(&reqwest::blocking::Client, &str) -> Result<()>,
|
||||||
|
),
|
||||||
("stale_replay", run_stale_replay),
|
("stale_replay", run_stale_replay),
|
||||||
("invalid_signature", run_invalid_signature),
|
("invalid_signature", run_invalid_signature),
|
||||||
("invalid_vm_stack", run_invalid_vm_stack),
|
("invalid_vm_stack", run_invalid_vm_stack),
|
||||||
("invalid_mutation_commitment", run_invalid_mutation_commitment),
|
(
|
||||||
|
"invalid_mutation_commitment",
|
||||||
|
run_invalid_mutation_commitment,
|
||||||
|
),
|
||||||
("drifted_timestamp", run_drifted_timestamp),
|
("drifted_timestamp", run_drifted_timestamp),
|
||||||
("concurrent_heartbeat", run_concurrent_heartbeat),
|
("concurrent_heartbeat", run_concurrent_heartbeat),
|
||||||
("rate_limit_trigger", run_rate_limit_trigger),
|
("rate_limit_trigger", run_rate_limit_trigger),
|
||||||
@@ -197,7 +210,8 @@ fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
init.mutation_rounds,
|
init.mutation_rounds,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, mutation_step);
|
let commitment =
|
||||||
|
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, mutation_step);
|
||||||
let timestamp = current_time_ms();
|
let timestamp = current_time_ms();
|
||||||
let entropy = test_entropy();
|
let entropy = test_entropy();
|
||||||
|
|
||||||
@@ -215,13 +229,14 @@ fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
|
|
||||||
sign_request(&sk, &mut req)?;
|
sign_request(&sk, &mut req)?;
|
||||||
|
|
||||||
let resp = client
|
let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||||
.post(format!("{}/hb", base_url))
|
|
||||||
.json(&req)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
return Err(anyhow!("Step {} /hb returned HTTP error: {}", step, resp.status()));
|
return Err(anyhow!(
|
||||||
|
"Step {} /hb returned HTTP error: {}",
|
||||||
|
step,
|
||||||
|
resp.status()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hb_resp: HeartbeatResponse = resp.json()?;
|
let hb_resp: HeartbeatResponse = resp.json()?;
|
||||||
@@ -230,9 +245,15 @@ fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify it was a successful validation (not a silent rejection)
|
// 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_salt = hb_resp
|
||||||
let next_step = hb_resp.next_mutation_step.ok_or_else(|| anyhow!("Step {} missing next mutation step", step))?;
|
.next_salt
|
||||||
let next_order = hb_resp.next_mutation_order_b64.ok_or_else(|| anyhow!("Step {} missing next mutation order", step))?;
|
.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);
|
println!("Step {} successful. Salt rotated: {}", step, next_salt);
|
||||||
|
|
||||||
@@ -265,12 +286,21 @@ fn run_stale_replay(client: &reqwest::blocking::Client, base_url: &str) -> Resul
|
|||||||
let sk = SigningKey::generate(&mut csprng);
|
let sk = SigningKey::generate(&mut csprng);
|
||||||
let init = do_handshake(client, base_url, &sk)?;
|
let init = do_handshake(client, base_url, &sk)?;
|
||||||
|
|
||||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
let opcodes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&init.opcodes_b64,
|
||||||
|
)?;
|
||||||
let stack_state = shared::vm::execute(&opcodes);
|
let stack_state = shared::vm::execute(&opcodes);
|
||||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
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 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 candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
&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 {
|
let mut req = HeartbeatRequest {
|
||||||
session_id: init.session_id.clone(),
|
session_id: init.session_id.clone(),
|
||||||
@@ -296,7 +326,9 @@ fn run_stale_replay(client: &reqwest::blocking::Client, base_url: &str) -> Resul
|
|||||||
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||||
let hb2: HeartbeatResponse = resp2.json()?;
|
let hb2: HeartbeatResponse = resp2.json()?;
|
||||||
if hb2.next_salt.is_some() {
|
if hb2.next_salt.is_some() {
|
||||||
return Err(anyhow!("Replayed heartbeat was successfully accepted (broken replay protection)"));
|
return Err(anyhow!(
|
||||||
|
"Replayed heartbeat was successfully accepted (broken replay protection)"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Stale replay correctly rejected.");
|
println!("Stale replay correctly rejected.");
|
||||||
@@ -308,12 +340,21 @@ fn run_invalid_signature(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
let sk = SigningKey::generate(&mut csprng);
|
let sk = SigningKey::generate(&mut csprng);
|
||||||
let init = do_handshake(client, base_url, &sk)?;
|
let init = do_handshake(client, base_url, &sk)?;
|
||||||
|
|
||||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
let opcodes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&init.opcodes_b64,
|
||||||
|
)?;
|
||||||
let stack_state = shared::vm::execute(&opcodes);
|
let stack_state = shared::vm::execute(&opcodes);
|
||||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
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 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 candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
&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 {
|
let mut req = HeartbeatRequest {
|
||||||
session_id: init.session_id.clone(),
|
session_id: init.session_id.clone(),
|
||||||
@@ -344,10 +385,16 @@ fn run_invalid_vm_stack(client: &reqwest::blocking::Client, base_url: &str) -> R
|
|||||||
let sk = SigningKey::generate(&mut csprng);
|
let sk = SigningKey::generate(&mut csprng);
|
||||||
let init = do_handshake(client, base_url, &sk)?;
|
let init = do_handshake(client, base_url, &sk)?;
|
||||||
|
|
||||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
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 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 candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
&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 {
|
let mut req = HeartbeatRequest {
|
||||||
session_id: init.session_id.clone(),
|
session_id: init.session_id.clone(),
|
||||||
@@ -375,12 +422,18 @@ fn run_invalid_vm_stack(client: &reqwest::blocking::Client, base_url: &str) -> R
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_invalid_mutation_commitment(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> {
|
fn run_invalid_mutation_commitment(
|
||||||
|
client: &reqwest::blocking::Client,
|
||||||
|
base_url: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let mut csprng = OsRng;
|
let mut csprng = OsRng;
|
||||||
let sk = SigningKey::generate(&mut csprng);
|
let sk = SigningKey::generate(&mut csprng);
|
||||||
let init = do_handshake(client, base_url, &sk)?;
|
let init = do_handshake(client, base_url, &sk)?;
|
||||||
|
|
||||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
let opcodes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&init.opcodes_b64,
|
||||||
|
)?;
|
||||||
let stack_state = shared::vm::execute(&opcodes);
|
let stack_state = shared::vm::execute(&opcodes);
|
||||||
|
|
||||||
let mut req = HeartbeatRequest {
|
let mut req = HeartbeatRequest {
|
||||||
@@ -411,12 +464,21 @@ fn run_drifted_timestamp(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
let sk = SigningKey::generate(&mut csprng);
|
let sk = SigningKey::generate(&mut csprng);
|
||||||
let init = do_handshake(client, base_url, &sk)?;
|
let init = do_handshake(client, base_url, &sk)?;
|
||||||
|
|
||||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
let opcodes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&init.opcodes_b64,
|
||||||
|
)?;
|
||||||
let stack_state = shared::vm::execute(&opcodes);
|
let stack_state = shared::vm::execute(&opcodes);
|
||||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
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 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 candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
&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 {
|
let mut req = HeartbeatRequest {
|
||||||
session_id: init.session_id.clone(),
|
session_id: init.session_id.clone(),
|
||||||
@@ -446,12 +508,21 @@ fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str)
|
|||||||
let sk = SigningKey::generate(&mut csprng);
|
let sk = SigningKey::generate(&mut csprng);
|
||||||
let init = do_handshake(client, base_url, &sk)?;
|
let init = do_handshake(client, base_url, &sk)?;
|
||||||
|
|
||||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
let opcodes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&init.opcodes_b64,
|
||||||
|
)?;
|
||||||
let stack_state = shared::vm::execute(&opcodes);
|
let stack_state = shared::vm::execute(&opcodes);
|
||||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
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 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 candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
&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 {
|
let mut req = HeartbeatRequest {
|
||||||
session_id: init.session_id.clone(),
|
session_id: init.session_id.clone(),
|
||||||
@@ -471,9 +542,7 @@ fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str)
|
|||||||
let req_clone = req.clone();
|
let req_clone = req.clone();
|
||||||
let url_clone = format!("{}/hb", base_url);
|
let url_clone = format!("{}/hb", base_url);
|
||||||
|
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || client_clone.post(&url_clone).json(&req_clone).send());
|
||||||
client_clone.post(&url_clone).json(&req_clone).send()
|
|
||||||
});
|
|
||||||
|
|
||||||
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
|
||||||
let resp1_res = handle.join().map_err(|_| anyhow!("Thread panicked"))?;
|
let resp1_res = handle.join().map_err(|_| anyhow!("Thread panicked"))?;
|
||||||
@@ -485,7 +554,10 @@ fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str)
|
|||||||
// One must succeed and one must fail (silent rejection) because of CAS check
|
// 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);
|
let successes = (hb1.next_salt.is_some() as usize) + (hb2.next_salt.is_some() as usize);
|
||||||
if successes != 1 {
|
if successes != 1 {
|
||||||
return Err(anyhow!("Expected exactly one concurrent heartbeat to succeed. Got: {}", successes));
|
return Err(anyhow!(
|
||||||
|
"Expected exactly one concurrent heartbeat to succeed. Got: {}",
|
||||||
|
successes
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Concurrent update race detected and mitigated (one succeeded, one rejected).");
|
println!("Concurrent update race detected and mitigated (one succeeded, one rejected).");
|
||||||
@@ -497,12 +569,21 @@ fn run_rate_limit_trigger(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
let sk = SigningKey::generate(&mut csprng);
|
let sk = SigningKey::generate(&mut csprng);
|
||||||
let init = do_handshake(client, base_url, &sk)?;
|
let init = do_handshake(client, base_url, &sk)?;
|
||||||
|
|
||||||
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?;
|
let opcodes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&init.opcodes_b64,
|
||||||
|
)?;
|
||||||
let stack_state = shared::vm::execute(&opcodes);
|
let stack_state = shared::vm::execute(&opcodes);
|
||||||
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
|
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 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 candidate = shared::vm_extensions::apply_program_clone_with_rounds(
|
||||||
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
|
&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 {
|
let mut req = HeartbeatRequest {
|
||||||
session_id: init.session_id.clone(),
|
session_id: init.session_id.clone(),
|
||||||
@@ -534,14 +615,20 @@ fn run_rate_limit_trigger(client: &reqwest::blocking::Client, base_url: &str) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !rate_limited {
|
if !rate_limited {
|
||||||
return Err(anyhow!("Rate limiter was not triggered after 35 rapid requests"));
|
return Err(anyhow!(
|
||||||
|
"Rate limiter was not triggered after 35 rapid requests"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Rate limiter correctly triggered.");
|
println!("Rate limiter correctly triggered.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_file_scenario(_client: &reqwest::blocking::Client, _base_url: &str, file_path: &str) -> Result<()> {
|
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_content = std::fs::read_to_string(file_path)?;
|
||||||
let scenario: serde_json::Value = serde_json::from_str(&scenario_content)?;
|
let scenario: serde_json::Value = serde_json::from_str(&scenario_content)?;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ RestrictSUIDSGID=yes
|
|||||||
LockPersonality=yes
|
LockPersonality=yes
|
||||||
SystemCallArchitectures=native
|
SystemCallArchitectures=native
|
||||||
ReadWritePaths=/run/chronoseal.pid
|
ReadWritePaths=/run/chronoseal.pid
|
||||||
|
ReadWritePaths=/var/lib/chronoseal
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ Content-Type: application/json
|
|||||||
| `stack_state.ip` | number | yes | VM instruction pointer as an unsigned 16-bit value |
|
| `stack_state.ip` | number | yes | VM instruction pointer as an unsigned 16-bit value |
|
||||||
| `fingerprint.aspectRatio` | string | yes | Screen aspect ratio; server accepts numeric strings in range `0.5..=3.0` |
|
| `fingerprint.aspectRatio` | string | yes | Screen aspect ratio; server accepts numeric strings in range `0.5..=3.0` |
|
||||||
| `fingerprint.devicePixelRatio` | string | yes | Device pixel ratio; server accepts numeric strings in range `(0, 5]` |
|
| `fingerprint.devicePixelRatio` | string | yes | Device pixel ratio; server accepts numeric strings in range `(0, 5]` |
|
||||||
| `fingerprint.hardwareConcurrency` | number | yes | Positive hardware concurrency value |
|
| `fingerprint.hardwareConcurrency` | number | yes | Hardware concurrency value; server accepts integers in range `1..=256` |
|
||||||
| `mutation_step` | number | yes | Mutation step currently expected by the server |
|
| `mutation_step` | number | yes | Mutation step currently expected by the server |
|
||||||
| `gene_commitment` | string | yes | Context-bound commitment produced by the WASM mutation preview |
|
| `gene_commitment` | string | yes | Context-bound commitment produced by the WASM mutation preview |
|
||||||
| `signature` | string | yes | Ed25519 signature over the canonical payload |
|
| `signature` | string | yes | Ed25519 signature over the canonical payload |
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 594 B |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" width="296.99997mm" viewBox="0 0 1122.5196 793.7008" enable-background="new 0 0 1254 1254" xml:space="preserve" height="210mm" sodipodi:docname="logo1.svg" inkscape:export-filename="logo1.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="1.1654266" inkscape:cx="561.16791" inkscape:cy="396.85039" inkscape:window-width="2048" inkscape:window-height="1205" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="Layer_1"></sodipodi:namedview><defs id="defs44"></defs><path fill="none" opacity="1" stroke="none" d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path2"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path45" style="fill:#e1e1e4;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path46" style="fill:#b0b2b8;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20Line truncated
|
||||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "MyWebSite",
|
||||||
|
"short_name": "MySite",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -77,7 +77,6 @@ async function sendHeartbeat() {
|
|||||||
const sig = sign_message(msg);
|
const sig = sign_message(msg);
|
||||||
if (!sig) {
|
if (!sig) {
|
||||||
discard_gene_preview();
|
discard_gene_preview();
|
||||||
console.error('Keypair not initialised — skipping heartbeat');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resp = await sendRequest('/hb', 'POST', {
|
const resp = await sendRequest('/hb', 'POST', {
|
||||||
@@ -107,11 +106,9 @@ async function sendHeartbeat() {
|
|||||||
pendingMutationOrderB64 = resp.next_mutation_order_b64;
|
pendingMutationOrderB64 = resp.next_mutation_order_b64;
|
||||||
} else {
|
} else {
|
||||||
discard_gene_preview();
|
discard_gene_preview();
|
||||||
console.warn('Heartbeat rejected');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
discard_gene_preview();
|
discard_gene_preview();
|
||||||
console.error(e);
|
|
||||||
} finally {
|
} finally {
|
||||||
scheduleNext();
|
scheduleNext();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; style-src 'self' 'unsafe-inline'">
|
||||||
<title>Anti-Scraper Demo</title>
|
<title>Anti-Scraper Demo</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
import { initHeartbeat } from './heartbeat.js';
|
import { initHeartbeat } from './heartbeat.js';
|
||||||
|
|
||||||
(async () => {
|
initHeartbeat().catch(() => {});
|
||||||
await initHeartbeat();
|
|
||||||
})();
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
echo "Starting server with static frontend serving..."
|
echo "Starting server with static frontend serving..."
|
||||||
cd ../server
|
cd ../server
|
||||||
cargo run --release
|
cargo run --release
|
||||||
@@ -31,4 +31,5 @@ base64 = "0.22"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
ed25519-dalek = "2"
|
ed25519-dalek = "2"
|
||||||
redis = { version = "0.29", features = ["r2d2"] }
|
redis = { version = "0.29", features = ["r2d2"] }
|
||||||
|
dashmap = "6"
|
||||||
|
|
||||||
@@ -20,12 +20,10 @@ pub async fn cleanup_loop(state: Arc<AppState>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evict stale rate-limiter entries to prevent unbounded HashMap growth.
|
// Evict stale rate-limiter entries to prevent unbounded map growth.
|
||||||
{
|
{
|
||||||
let window_secs = state.get_config().rate_limit_window_secs;
|
let window_secs = state.get_config().rate_limit_window_secs;
|
||||||
let mut rl = state.rate_limiter.lock().await;
|
state.rate_limiter.evict_stale(window_secs);
|
||||||
rl.evict_stale(window_secs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +137,9 @@ impl Config {
|
|||||||
size: self.gene_size,
|
size: self.gene_size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if !(1..=shared::constants::MAX_MUTATION_ROUNDS).contains(&self.mutation_rounds) {
|
if !(shared::constants::MIN_MUTATION_ROUNDS..=shared::constants::MAX_MUTATION_ROUNDS)
|
||||||
|
.contains(&self.mutation_rounds)
|
||||||
|
{
|
||||||
return Err(ConfigError::InvalidMutationRounds {
|
return Err(ConfigError::InvalidMutationRounds {
|
||||||
rounds: self.mutation_rounds,
|
rounds: self.mutation_rounds,
|
||||||
});
|
});
|
||||||
@@ -274,7 +276,8 @@ impl std::fmt::Display for ConfigError {
|
|||||||
Self::InvalidMutationRounds { rounds } => {
|
Self::InvalidMutationRounds { rounds } => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"invalid mutation rounds {rounds}; expected 1..={}",
|
"invalid mutation rounds {rounds}; expected {}..={}",
|
||||||
|
shared::constants::MIN_MUTATION_ROUNDS,
|
||||||
shared::constants::MAX_MUTATION_ROUNDS
|
shared::constants::MAX_MUTATION_ROUNDS
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,3 @@ pub fn verify_signature(
|
|||||||
pk.verify_strict(message.as_bytes(), &sig)?;
|
pk.verify_strict(message.as_bytes(), &sig)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,12 +25,19 @@ pub enum SessionError {
|
|||||||
|
|
||||||
#[error("Invalid gene configuration: {0}")]
|
#[error("Invalid gene configuration: {0}")]
|
||||||
InvalidGeneConfiguration(String),
|
InvalidGeneConfiguration(String),
|
||||||
|
|
||||||
|
#[error("Rate limited")]
|
||||||
|
RateLimited,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for SessionError {
|
impl IntoResponse for SessionError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let (status, error_message) = match self {
|
let (status, error_message) = match self {
|
||||||
SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()),
|
SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||||
|
SessionError::RateLimited => (
|
||||||
|
StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
"Too many requests".to_string(),
|
||||||
|
),
|
||||||
_ => (
|
_ => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Internal server error".to_string(),
|
"Internal server error".to_string(),
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
use shared::protocol::Fingerprint;
|
use shared::protocol::Fingerprint;
|
||||||
|
|
||||||
|
const MIN_ASPECT_RATIO: f64 = 0.5;
|
||||||
|
const MAX_ASPECT_RATIO: f64 = 3.0;
|
||||||
|
const MAX_DEVICE_PIXEL_RATIO: f64 = 5.0;
|
||||||
|
const MAX_HARDWARE_CONCURRENCY: u32 = 256;
|
||||||
|
|
||||||
/// Validates the browser fingerprint fields submitted by the client.
|
/// Validates the browser fingerprint fields submitted by the client.
|
||||||
///
|
///
|
||||||
/// Checks basic screen aspect ratio thresholds, device pixel ratio limits,
|
/// Checks basic screen aspect ratio thresholds, device pixel ratio limits,
|
||||||
@@ -9,16 +14,66 @@ use shared::protocol::Fingerprint;
|
|||||||
/// * `fp` - The client's hardware and screen layout fingerprint.
|
/// * `fp` - The client's hardware and screen layout fingerprint.
|
||||||
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
|
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
|
||||||
if !(0.5..=3.0).contains(&ar) {
|
if !ar.is_finite() || !(MIN_ASPECT_RATIO..=MAX_ASPECT_RATIO).contains(&ar) {
|
||||||
return Err("aspect ratio".into());
|
return Err("aspect ratio".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
|
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
|
||||||
if dpr <= 0.0 || dpr > 5.0 {
|
if !dpr.is_finite() || dpr <= 0.0 || dpr > MAX_DEVICE_PIXEL_RATIO {
|
||||||
return Err("dpr".into());
|
return Err("dpr".into());
|
||||||
}
|
}
|
||||||
if fp.hardware_concurrency == 0 {
|
|
||||||
|
if fp.hardware_concurrency == 0 || fp.hardware_concurrency > MAX_HARDWARE_CONCURRENCY {
|
||||||
return Err("hw".into());
|
return Err("hw".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn fingerprint(
|
||||||
|
aspect_ratio: impl Into<String>,
|
||||||
|
device_pixel_ratio: impl Into<String>,
|
||||||
|
hardware_concurrency: u32,
|
||||||
|
) -> Fingerprint {
|
||||||
|
Fingerprint {
|
||||||
|
aspect_ratio: aspect_ratio.into(),
|
||||||
|
device_pixel_ratio: device_pixel_ratio.into(),
|
||||||
|
hardware_concurrency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_valid_fingerprint() {
|
||||||
|
assert!(validate(&fingerprint("1.7777777778", "2", 8)).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_boundary_values() {
|
||||||
|
assert!(validate(&fingerprint("0.5", "1", 1)).is_ok());
|
||||||
|
assert!(validate(&fingerprint("3.0", "5.0", MAX_HARDWARE_CONCURRENCY)).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_aspect_ratios() {
|
||||||
|
for aspect_ratio in ["not-a-number", "NaN", "inf", "0.49", "3.01"] {
|
||||||
|
assert!(validate(&fingerprint(aspect_ratio, "2", 8)).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_device_pixel_ratios() {
|
||||||
|
for device_pixel_ratio in ["not-a-number", "NaN", "inf", "0", "-1", "5.01"] {
|
||||||
|
assert!(validate(&fingerprint("1.77", device_pixel_ratio, 8)).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_hardware_concurrency() {
|
||||||
|
assert!(validate(&fingerprint("1.77", "2", 0)).is_err());
|
||||||
|
assert!(validate(&fingerprint("1.77", "2", MAX_HARDWARE_CONCURRENCY + 1)).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,9 +30,6 @@ async fn main() {
|
|||||||
|
|
||||||
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
if let Some(config_path) = cli.globals.config.as_deref() {
|
|
||||||
std::env::set_var("CHRONOSEAL_CONFIG", config_path);
|
|
||||||
}
|
|
||||||
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
|
let log_filter = cli.globals.log.as_deref().unwrap_or("info");
|
||||||
let log_file = log_file_for_command(&cli);
|
let log_file = log_file_for_command(&cli);
|
||||||
let _log_guard = init_logging(log_filter, log_file)?;
|
let _log_guard = init_logging(log_filter, log_file)?;
|
||||||
|
|||||||
@@ -9,3 +9,25 @@ pub async fn log_request(req: Request, next: Next) -> Response {
|
|||||||
tracing::info!("{} {} -> {}", method, uri, response.status());
|
tracing::info!("{} {} -> {}", method, uri, response.status());
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Injects defensive HTTP response headers on every response.
|
||||||
|
///
|
||||||
|
/// These headers mitigate several classes of attacks:
|
||||||
|
/// - `X-Content-Type-Options: nosniff` — prevents MIME-type sniffing.
|
||||||
|
/// - `X-Frame-Options: DENY` — blocks clickjacking via framing.
|
||||||
|
/// - `Referrer-Policy: no-referrer` — suppresses referrer leakage.
|
||||||
|
/// - `X-XSS-Protection: 0` — disables legacy XSS auditors (can introduce bugs).
|
||||||
|
/// - `Permissions-Policy` — restricts powerful browser features.
|
||||||
|
pub async fn security_headers(req: Request, next: Next) -> Response {
|
||||||
|
let mut response = next.run(req).await;
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
headers.insert("x-content-type-options", "nosniff".parse().unwrap());
|
||||||
|
headers.insert("x-frame-options", "DENY".parse().unwrap());
|
||||||
|
headers.insert("referrer-policy", "no-referrer".parse().unwrap());
|
||||||
|
headers.insert("x-xss-protection", "0".parse().unwrap());
|
||||||
|
headers.insert(
|
||||||
|
"permissions-policy",
|
||||||
|
"camera=(), microphone=(), geolocation=()".parse().unwrap(),
|
||||||
|
);
|
||||||
|
response
|
||||||
|
}
|
||||||
+20
-15
@@ -1,17 +1,20 @@
|
|||||||
use std::collections::HashMap;
|
use dashmap::DashMap;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
/// A simple, in-memory sliding-window rate limiter for tracking client heartbeat frequency.
|
/// A lock-free, concurrent sliding-window rate limiter backed by `DashMap`.
|
||||||
|
///
|
||||||
|
/// All public methods take `&self` (no `&mut self`), so the limiter can live in
|
||||||
|
/// an `Arc<AppState>` without a `Mutex` wrapper.
|
||||||
pub struct RateLimiter {
|
pub struct RateLimiter {
|
||||||
/// Maps session identifiers to request counts and window start timestamps.
|
/// Maps rate-limit keys to request counts and window start timestamps.
|
||||||
buckets: HashMap<String, (u32, Instant)>,
|
buckets: DashMap<String, (u32, Instant)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RateLimiter {
|
impl RateLimiter {
|
||||||
/// Creates a new, empty `RateLimiter`.
|
/// Creates a new, empty `RateLimiter`.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buckets: HashMap::new(),
|
buckets: DashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,19 +23,21 @@ impl RateLimiter {
|
|||||||
/// Returns `true` if allowed, or `false` if the rate limit is exceeded.
|
/// Returns `true` if allowed, or `false` if the rate limit is exceeded.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `key` - The unique identifier to rate-limit (e.g., session ID).
|
/// * `key` - The unique identifier to rate-limit (e.g., client IP address).
|
||||||
/// * `limit` - The maximum number of allowed requests per window.
|
/// * `limit` - The maximum number of allowed requests per window.
|
||||||
/// * `window_secs` - The length of the sliding-window in seconds.
|
/// * `window_secs` - The length of the sliding-window in seconds.
|
||||||
pub fn check(&mut self, key: &str, limit: u32, window_secs: u64) -> bool {
|
pub fn check(&self, key: &str, limit: u32, window_secs: u64) -> bool {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let entry = self.buckets.entry(key.to_string()).or_insert((0, now));
|
let mut entry = self.buckets.entry(key.to_string()).or_insert((0, now));
|
||||||
if now.duration_since(entry.1).as_secs() >= window_secs {
|
let (count, ts) = entry.value_mut();
|
||||||
*entry = (1, now);
|
if now.duration_since(*ts).as_secs() >= window_secs {
|
||||||
|
*count = 1;
|
||||||
|
*ts = now;
|
||||||
true
|
true
|
||||||
} else if entry.0 >= limit {
|
} else if *count >= limit {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
entry.0 += 1;
|
*count += 1;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +48,7 @@ impl RateLimiter {
|
|||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `window_secs` - The active rate-limiting window duration in seconds.
|
/// * `window_secs` - The active rate-limiting window duration in seconds.
|
||||||
pub fn evict_stale(&mut self, window_secs: u64) {
|
pub fn evict_stale(&self, window_secs: u64) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
self.buckets
|
self.buckets
|
||||||
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs);
|
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs);
|
||||||
@@ -58,7 +63,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rate_limiter() {
|
fn test_rate_limiter() {
|
||||||
let mut rl = RateLimiter::new();
|
let rl = RateLimiter::new();
|
||||||
// Limit of 2 requests per 1 second window
|
// Limit of 2 requests per 1 second window
|
||||||
assert!(rl.check("user1", 2, 1));
|
assert!(rl.check("user1", 2, 1));
|
||||||
assert!(rl.check("user1", 2, 1));
|
assert!(rl.check("user1", 2, 1));
|
||||||
@@ -72,7 +77,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rate_limiter_eviction() {
|
fn test_rate_limiter_eviction() {
|
||||||
let mut rl = RateLimiter::new();
|
let rl = RateLimiter::new();
|
||||||
assert!(rl.check("user1", 1, 1));
|
assert!(rl.check("user1", 1, 1));
|
||||||
assert_eq!(rl.buckets.len(), 1);
|
assert_eq!(rl.buckets.len(), 1);
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,51 @@ pub async fn handler(
|
|||||||
Json(payload): Json<HeartbeatRequest>,
|
Json(payload): Json<HeartbeatRequest>,
|
||||||
) -> (StatusCode, Json<HeartbeatResponse>) {
|
) -> (StatusCode, Json<HeartbeatResponse>) {
|
||||||
let start_http = std::time::Instant::now();
|
let start_http = std::time::Instant::now();
|
||||||
state.heartbeats_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
|
.heartbeats_total
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
// Rate limiting
|
// Cap entropy events to prevent oversized payloads from exhausting memory.
|
||||||
|
if payload.entropy_data.events.len() > 1000 {
|
||||||
|
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||||
|
state
|
||||||
|
.http_latency_ns
|
||||||
|
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
state
|
||||||
|
.http_ops_count
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
return (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(HeartbeatResponse {
|
||||||
|
status: "ok".into(),
|
||||||
|
next_salt: None,
|
||||||
|
next_mutation_step: None,
|
||||||
|
next_mutation_order_b64: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting (lock-free via DashMap)
|
||||||
{
|
{
|
||||||
let (limit, window_secs) = {
|
let (limit, window_secs) = {
|
||||||
let cfg = state.get_config();
|
let cfg = state.get_config();
|
||||||
(cfg.rate_limit_count, cfg.rate_limit_window_secs)
|
(cfg.rate_limit_count, cfg.rate_limit_window_secs)
|
||||||
};
|
};
|
||||||
let mut rl = state.rate_limiter.lock().await;
|
if !state
|
||||||
if !rl.check(&payload.session_id, limit, window_secs) {
|
.rate_limiter
|
||||||
|
.check(&payload.session_id, limit, window_secs)
|
||||||
|
{
|
||||||
tracing::debug!("Rate limit hit: {}", payload.session_id);
|
tracing::debug!("Rate limit hit: {}", payload.session_id);
|
||||||
state.verification_failures_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
|
.verification_failures_total
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
let http_dur = start_http.elapsed().as_nanos() as u64;
|
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||||
state.http_latency_ns.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
state.http_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
.http_latency_ns
|
||||||
|
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
state
|
||||||
|
.http_ops_count
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
return (
|
return (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(HeartbeatResponse {
|
Json(HeartbeatResponse {
|
||||||
@@ -39,8 +69,12 @@ pub async fn handler(
|
|||||||
let start_db = std::time::Instant::now();
|
let start_db = std::time::Instant::now();
|
||||||
let db_res = crate::session::verify_heartbeat(&state.db_pool, &config, &payload);
|
let db_res = crate::session::verify_heartbeat(&state.db_pool, &config, &payload);
|
||||||
let db_dur = start_db.elapsed().as_nanos() as u64;
|
let db_dur = start_db.elapsed().as_nanos() as u64;
|
||||||
state.storage_latency_ns.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
state.storage_ops_count.fetch_add(2, std::sync::atomic::Ordering::Relaxed); // read + write
|
.storage_latency_ns
|
||||||
|
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
state
|
||||||
|
.storage_ops_count
|
||||||
|
.fetch_add(2, std::sync::atomic::Ordering::Relaxed); // read + write
|
||||||
|
|
||||||
let outcome = match db_res {
|
let outcome = match db_res {
|
||||||
Ok(result) => (
|
Ok(result) => (
|
||||||
@@ -54,15 +88,21 @@ pub async fn handler(
|
|||||||
),
|
),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
|
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
|
||||||
state.verification_failures_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
|
.verification_failures_total
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
match &e {
|
match &e {
|
||||||
crate::errors::VerificationError::ChainBroken => {
|
crate::errors::VerificationError::ChainBroken => {
|
||||||
state.replay_attempts_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
|
.replay_attempts_total
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
crate::errors::VerificationError::MutationCommitmentMismatch
|
crate::errors::VerificationError::MutationCommitmentMismatch
|
||||||
| crate::errors::VerificationError::MutationProgram(_)
|
| crate::errors::VerificationError::MutationProgram(_)
|
||||||
| crate::errors::VerificationError::GeneState(_) => {
|
| crate::errors::VerificationError::GeneState(_) => {
|
||||||
state.mutation_failures_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
|
.mutation_failures_total
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -79,8 +119,12 @@ pub async fn handler(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let http_dur = start_http.elapsed().as_nanos() as u64;
|
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||||
state.http_latency_ns.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
state.http_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
.http_latency_ns
|
||||||
|
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
state
|
||||||
|
.http_ops_count
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
outcome
|
outcome
|
||||||
}
|
}
|
||||||
@@ -138,7 +182,11 @@ mod tests {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
let program_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64).unwrap();
|
let program_bytes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&init.opcodes_b64,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let stack_state = shared::vm::execute(&program_bytes);
|
let stack_state = shared::vm::execute(&program_bytes);
|
||||||
|
|
||||||
let order =
|
let order =
|
||||||
@@ -176,7 +224,7 @@ mod tests {
|
|||||||
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
|
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
db_pool: pool.clone(),
|
db_pool: pool.clone(),
|
||||||
rate_limiter: tokio::sync::Mutex::new(crate::ratelimit::RateLimiter::new()),
|
rate_limiter: crate::ratelimit::RateLimiter::new(),
|
||||||
config: std::sync::RwLock::new(config.clone()),
|
config: std::sync::RwLock::new(config.clone()),
|
||||||
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
|
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
|
||||||
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
|
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
|
||||||
|
|||||||
@@ -11,17 +11,34 @@ pub async fn handler(
|
|||||||
let start_http = std::time::Instant::now();
|
let start_http = std::time::Instant::now();
|
||||||
let config = state.get_config();
|
let config = state.get_config();
|
||||||
|
|
||||||
|
// Rate limit session creation by public key to prevent storage exhaustion.
|
||||||
|
if !state.rate_limiter.check(
|
||||||
|
&payload.public_key,
|
||||||
|
config.rate_limit_count,
|
||||||
|
config.rate_limit_window_secs,
|
||||||
|
) {
|
||||||
|
return Err(SessionError::RateLimited);
|
||||||
|
}
|
||||||
|
|
||||||
let start_db = std::time::Instant::now();
|
let start_db = std::time::Instant::now();
|
||||||
let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key);
|
let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key);
|
||||||
let db_dur = start_db.elapsed().as_nanos() as u64;
|
let db_dur = start_db.elapsed().as_nanos() as u64;
|
||||||
state.storage_latency_ns.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
state.storage_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
.storage_latency_ns
|
||||||
|
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
state
|
||||||
|
.storage_ops_count
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
let resp = resp?;
|
let resp = resp?;
|
||||||
|
|
||||||
let http_dur = start_http.elapsed().as_nanos() as u64;
|
let http_dur = start_http.elapsed().as_nanos() as u64;
|
||||||
state.http_latency_ns.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
state
|
||||||
state.http_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
.http_latency_ns
|
||||||
|
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
state
|
||||||
|
.http_ops_count
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
Ok(Json(resp))
|
Ok(Json(resp))
|
||||||
}
|
}
|
||||||
+55
-15
@@ -5,17 +5,19 @@ use crate::{
|
|||||||
routes, session,
|
routes, session,
|
||||||
storage::{self, StoreStats},
|
storage::{self, StoreStats},
|
||||||
};
|
};
|
||||||
use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
use axum::{
|
||||||
|
extract::ConnectInfo, http::StatusCode, response::IntoResponse, routing::get, Json, Router,
|
||||||
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
net::{SocketAddr, TcpStream},
|
net::{IpAddr, SocketAddr, TcpStream},
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::sync::{Mutex, Notify};
|
use tokio::sync::Notify;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -144,7 +146,7 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
|
|||||||
let db_pool = init_db_pool(&config)?;
|
let db_pool = init_db_pool(&config)?;
|
||||||
let state = Arc::new(session::AppState {
|
let state = Arc::new(session::AppState {
|
||||||
db_pool,
|
db_pool,
|
||||||
rate_limiter: Mutex::new(RateLimiter::new()),
|
rate_limiter: RateLimiter::new(),
|
||||||
config: std::sync::RwLock::new(config.clone()),
|
config: std::sync::RwLock::new(config.clone()),
|
||||||
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
|
heartbeats_total: std::sync::atomic::AtomicU64::new(0),
|
||||||
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
|
verification_failures_total: std::sync::atomic::AtomicU64::new(0),
|
||||||
@@ -170,7 +172,11 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
|
|||||||
tower_http::services::ServeDir::new(&config.frontend_dir),
|
tower_http::services::ServeDir::new(&config.frontend_dir),
|
||||||
)
|
)
|
||||||
.layer(tower_http::cors::CorsLayer::permissive())
|
.layer(tower_http::cors::CorsLayer::permissive())
|
||||||
|
.layer(axum::middleware::from_fn(
|
||||||
|
crate::middleware::security_headers,
|
||||||
|
))
|
||||||
.layer(axum::middleware::from_fn(crate::middleware::log_request))
|
.layer(axum::middleware::from_fn(crate::middleware::log_request))
|
||||||
|
.layer(axum::extract::DefaultBodyLimit::max(64 * 1024)) // 64 KiB
|
||||||
.with_state(state.clone());
|
.with_state(state.clone());
|
||||||
|
|
||||||
let addr: SocketAddr = config.bind.parse()?;
|
let addr: SocketAddr = config.bind.parse()?;
|
||||||
@@ -178,9 +184,12 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
|
|||||||
info!(bind = %config.bind, "chronoseal daemon started");
|
info!(bind = %config.bind, "chronoseal daemon started");
|
||||||
|
|
||||||
let shutdown = signal_task(state.clone());
|
let shutdown = signal_task(state.clone());
|
||||||
let result = axum::serve(listener, app)
|
let result = axum::serve(
|
||||||
.with_graceful_shutdown(shutdown)
|
listener,
|
||||||
.await;
|
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.with_graceful_shutdown(shutdown)
|
||||||
|
.await;
|
||||||
|
|
||||||
remove_pid_file(&config.pid_file);
|
remove_pid_file(&config.pid_file);
|
||||||
result?;
|
result?;
|
||||||
@@ -277,8 +286,12 @@ async fn health_handler() -> impl IntoResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn stats_handler(
|
async fn stats_handler(
|
||||||
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||||
) -> Result<Json<StoreStats>, (StatusCode, String)> {
|
) -> Result<Json<StoreStats>, (StatusCode, String)> {
|
||||||
|
if !is_loopback(addr.ip()) {
|
||||||
|
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||||
|
}
|
||||||
state
|
state
|
||||||
.db_pool
|
.db_pool
|
||||||
.stats()
|
.stats()
|
||||||
@@ -287,25 +300,45 @@ async fn stats_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn metrics_handler(
|
async fn metrics_handler(
|
||||||
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
|
||||||
) -> Result<String, (StatusCode, String)> {
|
) -> Result<String, (StatusCode, String)> {
|
||||||
|
if !is_loopback(addr.ip()) {
|
||||||
|
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||||
|
}
|
||||||
let stats = state
|
let stats = state
|
||||||
.db_pool
|
.db_pool
|
||||||
.stats()
|
.stats()
|
||||||
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
||||||
|
|
||||||
let heartbeats = state.heartbeats_total.load(std::sync::atomic::Ordering::Relaxed);
|
let heartbeats = state
|
||||||
let ver_failures = state.verification_failures_total.load(std::sync::atomic::Ordering::Relaxed);
|
.heartbeats_total
|
||||||
let mut_failures = state.mutation_failures_total.load(std::sync::atomic::Ordering::Relaxed);
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
let replays = state.replay_attempts_total.load(std::sync::atomic::Ordering::Relaxed);
|
let ver_failures = state
|
||||||
|
.verification_failures_total
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let mut_failures = state
|
||||||
|
.mutation_failures_total
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let replays = state
|
||||||
|
.replay_attempts_total
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
let store_ns = state.storage_latency_ns.load(std::sync::atomic::Ordering::Relaxed) as f64;
|
let store_ns = state
|
||||||
|
.storage_latency_ns
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed) as f64;
|
||||||
let store_sum = store_ns / 1_000_000_000.0;
|
let store_sum = store_ns / 1_000_000_000.0;
|
||||||
let store_count = state.storage_ops_count.load(std::sync::atomic::Ordering::Relaxed);
|
let store_count = state
|
||||||
|
.storage_ops_count
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
let http_ns = state.http_latency_ns.load(std::sync::atomic::Ordering::Relaxed) as f64;
|
let http_ns = state
|
||||||
|
.http_latency_ns
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed) as f64;
|
||||||
let http_sum = http_ns / 1_000_000_000.0;
|
let http_sum = http_ns / 1_000_000_000.0;
|
||||||
let http_count = state.http_ops_count.load(std::sync::atomic::Ordering::Relaxed);
|
let http_count = state
|
||||||
|
.http_ops_count
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"# HELP chronoseal_active_sessions Active ChronoSeal sessions\n\
|
"# HELP chronoseal_active_sessions Active ChronoSeal sessions\n\
|
||||||
@@ -355,6 +388,13 @@ async fn metrics_handler(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_loopback(ip: IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(v4) => v4.is_loopback(),
|
||||||
|
IpAddr::V6(v6) => v6.is_loopback(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn signal_task(state: Arc<session::AppState>) {
|
async fn signal_task(state: Arc<session::AppState>) {
|
||||||
let shutdown = Arc::new(Notify::new());
|
let shutdown = Arc::new(Notify::new());
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db_pool: crate::storage::DbPool,
|
pub db_pool: crate::storage::DbPool,
|
||||||
pub rate_limiter: tokio::sync::Mutex<crate::ratelimit::RateLimiter>,
|
pub rate_limiter: crate::ratelimit::RateLimiter,
|
||||||
pub config: std::sync::RwLock<crate::config::Config>,
|
pub config: std::sync::RwLock<crate::config::Config>,
|
||||||
pub heartbeats_total: std::sync::atomic::AtomicU64,
|
pub heartbeats_total: std::sync::atomic::AtomicU64,
|
||||||
pub verification_failures_total: std::sync::atomic::AtomicU64,
|
pub verification_failures_total: std::sync::atomic::AtomicU64,
|
||||||
@@ -271,7 +271,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn test_fingerprint() -> Fingerprint {
|
fn test_fingerprint() -> Fingerprint {
|
||||||
Fingerprint {
|
Fingerprint {
|
||||||
aspect_ratio: "1.77".to_string(),
|
aspect_ratio: "1.77".to_string(),
|
||||||
@@ -324,7 +323,11 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let entropy = test_entropy();
|
let entropy = test_entropy();
|
||||||
|
|
||||||
let program_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &client.opcodes_b64).unwrap();
|
let program_bytes = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::STANDARD,
|
||||||
|
&client.opcodes_b64,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let stack = shared::vm::execute(&program_bytes);
|
let stack = shared::vm::execute(&program_bytes);
|
||||||
|
|
||||||
let mut req = HeartbeatRequest {
|
let mut req = HeartbeatRequest {
|
||||||
|
|||||||
+54
-24
@@ -1,8 +1,8 @@
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use redis::Commands;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use redis::Commands;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StoreStats {
|
pub struct StoreStats {
|
||||||
@@ -54,11 +54,12 @@ impl DbPool {
|
|||||||
crate::config::DbType::Valkey => {
|
crate::config::DbType::Valkey => {
|
||||||
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
|
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
|
||||||
.unwrap_or_else(|_| "127.0.0.1:6666".to_string());
|
.unwrap_or_else(|_| "127.0.0.1:6666".to_string());
|
||||||
let connection_string = if addr.starts_with("redis://") || addr.starts_with("rediss://") {
|
let connection_string =
|
||||||
addr.clone()
|
if addr.starts_with("redis://") || addr.starts_with("rediss://") {
|
||||||
} else {
|
addr.clone()
|
||||||
format!("redis://{}", addr)
|
} else {
|
||||||
};
|
format!("redis://{}", addr)
|
||||||
|
};
|
||||||
let client = redis::Client::open(connection_string)?;
|
let client = redis::Client::open(connection_string)?;
|
||||||
let pool = r2d2::Pool::builder().build(client)?;
|
let pool = r2d2::Pool::builder().build(client)?;
|
||||||
Ok(DbPool::Valkey(ValkeyStore {
|
Ok(DbPool::Valkey(ValkeyStore {
|
||||||
@@ -354,9 +355,19 @@ impl ValkeyStore {
|
|||||||
|
|
||||||
redis::pipe()
|
redis::pipe()
|
||||||
.atomic()
|
.atomic()
|
||||||
.cmd("SET").arg(&key).arg(&value).arg("EX").arg(ttl_seconds)
|
.cmd("SET")
|
||||||
.cmd("ZADD").arg(&self.index_key).arg(record.expires_at).arg(&record.session_id)
|
.arg(&key)
|
||||||
.cmd("ZADD").arg("sessions:chain_lengths").arg(record.chain_length).arg(&record.session_id)
|
.arg(&value)
|
||||||
|
.arg("EX")
|
||||||
|
.arg(ttl_seconds)
|
||||||
|
.cmd("ZADD")
|
||||||
|
.arg(&self.index_key)
|
||||||
|
.arg(record.expires_at)
|
||||||
|
.arg(&record.session_id)
|
||||||
|
.cmd("ZADD")
|
||||||
|
.arg("sessions:chain_lengths")
|
||||||
|
.arg(record.chain_length)
|
||||||
|
.arg(&record.session_id)
|
||||||
.query::<()>(&mut *conn)?;
|
.query::<()>(&mut *conn)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -400,9 +411,19 @@ impl ValkeyStore {
|
|||||||
|
|
||||||
let response: Option<()> = redis::pipe()
|
let response: Option<()> = redis::pipe()
|
||||||
.atomic()
|
.atomic()
|
||||||
.cmd("SET").arg(&key).arg(&value).arg("EX").arg(ttl_seconds)
|
.cmd("SET")
|
||||||
.cmd("ZADD").arg(&self.index_key).arg(record.expires_at).arg(&record.session_id)
|
.arg(&key)
|
||||||
.cmd("ZADD").arg("sessions:chain_lengths").arg(record.chain_length).arg(&record.session_id)
|
.arg(&value)
|
||||||
|
.arg("EX")
|
||||||
|
.arg(ttl_seconds)
|
||||||
|
.cmd("ZADD")
|
||||||
|
.arg(&self.index_key)
|
||||||
|
.arg(record.expires_at)
|
||||||
|
.arg(&record.session_id)
|
||||||
|
.cmd("ZADD")
|
||||||
|
.arg("sessions:chain_lengths")
|
||||||
|
.arg(record.chain_length)
|
||||||
|
.arg(&record.session_id)
|
||||||
.query(&mut *conn)?;
|
.query(&mut *conn)?;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -422,8 +443,12 @@ impl ValkeyStore {
|
|||||||
if !expired_ids.is_empty() {
|
if !expired_ids.is_empty() {
|
||||||
redis::pipe()
|
redis::pipe()
|
||||||
.atomic()
|
.atomic()
|
||||||
.cmd("ZREM").arg(&self.index_key).arg(&expired_ids)
|
.cmd("ZREM")
|
||||||
.cmd("ZREM").arg("sessions:chain_lengths").arg(&expired_ids)
|
.arg(&self.index_key)
|
||||||
|
.arg(&expired_ids)
|
||||||
|
.cmd("ZREM")
|
||||||
|
.arg("sessions:chain_lengths")
|
||||||
|
.arg(&expired_ids)
|
||||||
.query::<()>(&mut *conn)?;
|
.query::<()>(&mut *conn)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -435,8 +460,12 @@ impl ValkeyStore {
|
|||||||
let sessions: u64 = conn.zcard(&self.index_key)?;
|
let sessions: u64 = conn.zcard(&self.index_key)?;
|
||||||
let expired_sessions: u64 = conn.zcount(&self.index_key, 0, now)?;
|
let expired_sessions: u64 = conn.zcount(&self.index_key, 0, now)?;
|
||||||
|
|
||||||
let max_chain_length_res: Vec<(String, u64)> = conn.zrevrange_withscores("sessions:chain_lengths", 0, 0)?;
|
let max_chain_length_res: Vec<(String, u64)> =
|
||||||
let max_chain_length = max_chain_length_res.first().map(|(_, score)| *score).unwrap_or(0);
|
conn.zrevrange_withscores("sessions:chain_lengths", 0, 0)?;
|
||||||
|
let max_chain_length = max_chain_length_res
|
||||||
|
.first()
|
||||||
|
.map(|(_, score)| *score)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
Ok(StoreStats {
|
Ok(StoreStats {
|
||||||
sessions,
|
sessions,
|
||||||
@@ -449,7 +478,7 @@ impl ValkeyStore {
|
|||||||
pub fn current_time_ms() -> u64 {
|
pub fn current_time_ms() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.expect("system clock is before UNIX epoch; check system time")
|
||||||
.as_millis() as u64
|
.as_millis() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +488,8 @@ mod valkey_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valkey_store_operations() {
|
fn test_valkey_store_operations() {
|
||||||
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR").unwrap_or_else(|_| "127.0.0.1:6379".to_string());
|
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
|
||||||
let connection_string = format!("redis://{}", addr);
|
let connection_string = format!("redis://{}", addr);
|
||||||
let client = match redis::Client::open(connection_string) {
|
let client = match redis::Client::open(connection_string) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -522,10 +552,11 @@ mod valkey_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valkey_pool_concurrency() {
|
fn test_valkey_pool_concurrency() {
|
||||||
use std::thread;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR").unwrap_or_else(|_| "127.0.0.1:6379".to_string());
|
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
|
||||||
let connection_string = format!("redis://{}", addr);
|
let connection_string = format!("redis://{}", addr);
|
||||||
let client = match redis::Client::open(connection_string) {
|
let client = match redis::Client::open(connection_string) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -589,7 +620,8 @@ mod valkey_tests {
|
|||||||
// Cleanup
|
// Cleanup
|
||||||
let mut conn = store_arc.pool.get().unwrap();
|
let mut conn = store_arc.pool.get().unwrap();
|
||||||
for t in 0..10 {
|
for t in 0..10 {
|
||||||
let _: Result<(), _> = conn.del(store_arc.session_key(&format!("valkey_concurrent_{}", t)));
|
let _: Result<(), _> =
|
||||||
|
conn.del(store_arc.session_key(&format!("valkey_concurrent_{}", t)));
|
||||||
}
|
}
|
||||||
let _: Result<(), _> = conn.del(&store_arc.index_key);
|
let _: Result<(), _> = conn.del(&store_arc.index_key);
|
||||||
}
|
}
|
||||||
@@ -598,8 +630,8 @@ mod valkey_tests {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod sqlite_tests {
|
mod sqlite_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::thread;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sqlite_pool_concurrency() {
|
fn test_sqlite_pool_concurrency() {
|
||||||
@@ -655,5 +687,3 @@ mod sqlite_tests {
|
|||||||
let _ = std::fs::remove_file(db_path);
|
let _ = std::fs::remove_file(db_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -49,7 +49,6 @@ pub fn validate_mouse(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ pub fn execute_mutation_order(
|
|||||||
vm_extensions::execute_program(state, &order.program)
|
vm_extensions::execute_program(state, &order.program)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ pub const MAX_MUTATION_ROUNDS: u8 = 10;
|
|||||||
pub const MAX_MUTATION_INSTRUCTION_BUDGET: usize = 2048;
|
pub const MAX_MUTATION_INSTRUCTION_BUDGET: usize = 2048;
|
||||||
pub const HASH_OPCODE_INSTRUCTION_COST: usize = 16;
|
pub const HASH_OPCODE_INSTRUCTION_COST: usize = 16;
|
||||||
pub const SOFT_CAP_DURATION_MS: u128 = 50;
|
pub const SOFT_CAP_DURATION_MS: u128 = 50;
|
||||||
|
pub const MAX_STACK_DEPTH: usize = 64;
|
||||||
@@ -37,8 +37,8 @@ pub fn next_chain_hash(
|
|||||||
stack: &StackState,
|
stack: &StackState,
|
||||||
salt: &[u8],
|
salt: &[u8],
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
let entropy_bytes = serde_json::to_vec(entropy).unwrap();
|
let entropy_bytes = serde_json::to_vec(entropy).unwrap_or_default();
|
||||||
let stack_bytes = serde_json::to_vec(stack).unwrap();
|
let stack_bytes = serde_json::to_vec(stack).unwrap_or_default();
|
||||||
|
|
||||||
let entropy_hash = blake3::hash(&entropy_bytes);
|
let entropy_hash = blake3::hash(&entropy_bytes);
|
||||||
let stack_hash = blake3::hash(&stack_bytes);
|
let stack_hash = blake3::hash(&stack_bytes);
|
||||||
@@ -63,4 +63,3 @@ pub fn hash_stack(stack: &[u32]) -> u32 {
|
|||||||
let hash = blake3::hash(&data);
|
let hash = blake3::hash(&data);
|
||||||
u32::from_le_bytes(hash.as_bytes()[..4].try_into().unwrap())
|
u32::from_le_bytes(hash.as_bytes()[..4].try_into().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,4 +116,3 @@ pub struct StackState {
|
|||||||
/// The final instruction pointer location at program completion or termination.
|
/// The final instruction pointer location at program completion or termination.
|
||||||
pub ip: u16,
|
pub ip: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +25,9 @@ pub fn execute(program: &[u8]) -> StackState {
|
|||||||
program[ip + 3],
|
program[ip + 3],
|
||||||
]);
|
]);
|
||||||
ip += 4;
|
ip += 4;
|
||||||
|
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||||
|
break;
|
||||||
|
}
|
||||||
stack.push(val);
|
stack.push(val);
|
||||||
}
|
}
|
||||||
0x01..=0x07 => {
|
0x01..=0x07 => {
|
||||||
@@ -43,6 +46,9 @@ pub fn execute(program: &[u8]) -> StackState {
|
|||||||
0x07 => a.rotate_left(b % 32),
|
0x07 => a.rotate_left(b % 32),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||||
|
break;
|
||||||
|
}
|
||||||
stack.push(r);
|
stack.push(r);
|
||||||
}
|
}
|
||||||
0x08 => {
|
0x08 => {
|
||||||
@@ -50,11 +56,17 @@ pub fn execute(program: &[u8]) -> StackState {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let a = stack.pop().unwrap();
|
let a = stack.pop().unwrap();
|
||||||
|
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||||
|
break;
|
||||||
|
}
|
||||||
stack.push(!a);
|
stack.push(!a);
|
||||||
}
|
}
|
||||||
0x09 => {
|
0x09 => {
|
||||||
let r = crate::hashing::hash_stack(&stack);
|
let r = crate::hashing::hash_stack(&stack);
|
||||||
stack.clear();
|
stack.clear();
|
||||||
|
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
|
||||||
|
break;
|
||||||
|
}
|
||||||
stack.push(r);
|
stack.push(r);
|
||||||
}
|
}
|
||||||
_ => break,
|
_ => break,
|
||||||
|
|||||||
@@ -269,7 +269,6 @@ pub fn apply_program(state: &mut GeneState, program: &[u8]) -> Result<(), Mutati
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Executes the mutation program on the mutable `GeneState` reference for multiple rounds.
|
/// Executes the mutation program on the mutable `GeneState` reference for multiple rounds.
|
||||||
///
|
///
|
||||||
/// Implements a soft instruction cost-budget cap check to prevent hostile/inefficient
|
/// Implements a soft instruction cost-budget cap check to prevent hostile/inefficient
|
||||||
|
|||||||
+8
-2
@@ -51,8 +51,14 @@ pub fn compute_next_hash(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let prev = hex::decode(prev_hash_hex).unwrap_or_default();
|
let prev = hex::decode(prev_hash_hex).unwrap_or_default();
|
||||||
let salt = hex::decode(salt_hex).unwrap_or_default();
|
let salt = hex::decode(salt_hex).unwrap_or_default();
|
||||||
let entropy = serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json).unwrap();
|
let entropy = match serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json) {
|
||||||
let stack = serde_json::from_str::<shared::protocol::StackState>(stack_state_json).unwrap();
|
Ok(v) => v,
|
||||||
|
Err(_) => return String::new(),
|
||||||
|
};
|
||||||
|
let stack = match serde_json::from_str::<shared::protocol::StackState>(stack_state_json) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return String::new(),
|
||||||
|
};
|
||||||
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
|
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
|
||||||
hex::encode(new)
|
hex::encode(new)
|
||||||
}
|
}
|
||||||
+8
-4
@@ -4,11 +4,15 @@ use wasm_bindgen::prelude::*;
|
|||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn run_program(program_b64: &str) -> JsValue {
|
pub fn run_program(program_b64: &str) -> JsValue {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
let bytes = base64::engine::general_purpose::STANDARD
|
let bytes = match base64::engine::general_purpose::STANDARD.decode(program_b64) {
|
||||||
.decode(program_b64)
|
Ok(v) => v,
|
||||||
.unwrap();
|
Err(_) => return JsValue::NULL,
|
||||||
|
};
|
||||||
let state = execute(&bytes);
|
let state = execute(&bytes);
|
||||||
serde_wasm_bindgen::to_value(&state).unwrap()
|
match serde_wasm_bindgen::to_value(&state) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => JsValue::NULL,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(program: &[u8]) -> StackState {
|
fn execute(program: &[u8]) -> StackState {
|
||||||
|
|||||||
Reference in new issue
Block a user