From 3c9fdc691a1a234d05610ba1e21ac40e8e51291f Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Thu, 25 Sep 2025 13:57:44 +0300 Subject: [PATCH] feat: add seed --- Cargo.lock | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba5218a..8831acc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,7 @@ dependencies = [ "claims", "color-eyre", "glob", + "rand", "serde", "serde_json", "tempfile", @@ -246,6 +247,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[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.101" @@ -270,6 +280,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + [[package]] name = "regex-automata" version = "0.4.10" @@ -595,3 +634,23 @@ name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 024c946..033ffa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ categories = ["cryptography::randomness", "encoding"] [dependencies] color-eyre = "0.6" glob = "0.3" +rand = "0.9" serde = { version = "1", features = ["derive"] } serde_json = "1" tracing = "0.1" diff --git a/src/lib.rs b/src/lib.rs index f716a9c..13a723b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use color_eyre::eyre::Result; use glob::Pattern; +use rand::{Rng, SeedableRng, rngs::StdRng}; use serde::{Deserialize, Serialize}; use std::{ fs::{self, read_to_string}, @@ -7,7 +8,10 @@ use std::{ }; use tracing::{Level, info}; -#[derive(Debug, Serialize, Deserialize)] +/// Prime for seq hashing to derive per-event RNG state +const PRIME_MULTIPLIER: u64 = 314_159; + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct FsCapability { pub read: Option>, // Glob patter for read pub write: Option>, // Stub for now @@ -19,12 +23,12 @@ pub enum Capability { // TODO: add Net, Cpu, etc } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Capabilities { pub fs: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct CapabilityManifest { pub plugin: String, pub version: String, @@ -39,21 +43,24 @@ pub struct TraceEvent { pub event_type: String, pub input: String, pub outcome: bool, + pub ts_seed: u64, } #[derive(Debug)] pub struct HostState { pub manifest: CapabilityManifest, pub trace: Vec, + pub seed: u64, } impl HostState { #[inline] #[must_use] - pub const fn new(manifest: CapabilityManifest) -> Self { + pub const fn new(manifest: CapabilityManifest, seed: u64) -> Self { Self { manifest, trace: Vec::new(), + seed, } } @@ -61,8 +68,6 @@ impl HostState { /// Returns `true` if allowed, `false` if denied. #[must_use] pub fn execute_plugin>(&mut self, path: P) -> bool { - let seq = u64::try_from(self.trace.len()).map_or_else(|_| 1, |len| len + 1); - let Some(fs_cap) = &self.manifest.capabilities.fs else { return false; }; @@ -71,8 +76,12 @@ impl HostState { return false; }; + let seq = u64::try_from(self.trace.len()).map_or(1, |len| len + 1); let path_str = path.as_ref().to_string_lossy(); + let mut rng = StdRng::seed_from_u64(self.seed.wrapping_mul(PRIME_MULTIPLIER + seq)); + let ts_seed = rng.random(); + let is_allowed = read_patterns.iter().any(|pattern| { Pattern::new(pattern) .map(|p| p.matches(&path_str)) @@ -81,6 +90,7 @@ impl HostState { info!( seq = seq, + ts_seed = ts_seed, event_type = "cap.call", input = %path_str, outcome = is_allowed, @@ -92,6 +102,7 @@ impl HostState { event_type: "cap.call".into(), input: path_str.into(), outcome: is_allowed, + ts_seed, }); is_allowed @@ -152,7 +163,7 @@ mod tests { #[test] fn manifest_loader() { - let manifest = load_manifest("examples/manifest.json").expect("Load failed"); + let manifest = assert_ok!(load_manifest("examples/manifest.json"), "Load failed"); assert_eq!(manifest.plugin, "formatter-v1"); assert_eq!(manifest.version, "0.1"); assert_eq!(manifest.issued_by, "dev-team"); @@ -169,8 +180,9 @@ mod tests { fn host_enforcement_with_trace() { init_tracing(); - let manifest = load_manifest("examples/manifest.json").expect("Load failed"); - let mut host = HostState::new(manifest); + let manifest = assert_ok!(load_manifest("examples/manifest.json"), "Load failed"); + let fixed_seed = 12345; + let mut host = HostState::new(manifest, fixed_seed); // allowed - expect a tracing event let out1 = host.execute_plugin("./workspace/config.toml"); @@ -186,18 +198,22 @@ mod tests { assert_eq!(trace1.event_type, "cap.call"); assert_eq!(trace1.input, "./workspace/config.toml"); assert!(trace1.outcome); + assert_eq!(trace1.ts_seed, 8_166_419_713_379_829_776); + assert_ne!(trace1.ts_seed, 0); let trace2 = assert_some!(host.trace.get(1)); assert_eq!(trace2.seq, 2); assert_eq!(trace2.input, "/etc/passwd"); assert!(!trace2.outcome); + assert_eq!(trace2.ts_seed, 10_553_447_931_939_622_718); + assert_ne!(trace1.ts_seed, trace2.ts_seed); let tmp_dir = tempdir().expect("Temp dir failed"); let tmp_path = tmp_dir.as_ref().join("trace.json"); - assert_ok!(host.save_trace(&tmp_path)); + assert_ok!(host.save_trace(&tmp_path), "Save failed"); - let loaded_trace = assert_ok!(HostState::load_trace(tmp_path)); + let loaded_trace = assert_ok!(HostState::load_trace(tmp_path), "Load failed"); assert_eq!(loaded_trace.len(), 2); let loaded_trace1 = assert_some!(loaded_trace.first()); @@ -206,4 +222,26 @@ mod tests { let loaded_trace2 = assert_some!(loaded_trace.get(1)); assert_eq!(trace2, loaded_trace2); } + + #[test] + fn trace_reproducibility() { + init_tracing(); + + let manifest = assert_ok!(load_manifest("examples/manifest.json"), "Load failed"); + + let fixed_seed = 12345; + + let mut host1 = HostState::new(manifest.clone(), fixed_seed); + assert!(!host1.execute_plugin("./allowed.txt")); + let trace1 = host1.finalize_trace(); + + let mut host2 = HostState::new(manifest, fixed_seed); + assert!(!host2.execute_plugin("./allowed.txt")); + let trace2 = host2.finalize_trace(); + + let parsed1 = assert_ok!(serde_json::from_str::>(&trace1)); + let parsed2 = assert_ok!(serde_json::from_str::>(&trace2)); + assert_eq!(parsed1, parsed2); + assert_eq!(parsed1[0].ts_seed, parsed2[0].ts_seed); + } }