//! Robustness/fuzz tests for identity deserializers (§8.2). //! //! Mirror of `levcs-core/tests/fuzz.rs`, scoped to authority parsing //! (binary and TOML) and key string parsers. Same panic-catching //! discipline: nothing in this crate may panic on hostile bytes. use std::panic::{catch_unwind, AssertUnwindSafe}; use levcs_identity::authority::{parse_toml_authority, AuthorityBody}; use levcs_identity::keys::{PublicKey, SecretKey}; const ITERS: u32 = 5_000; fn lcg(state: &mut u64) -> u64 { *state = state .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); *state } fn rand_bytes(seed: &mut u64, n: usize) -> Vec { (0..n).map(|_| (lcg(seed) >> 33) as u8).collect() } fn rand_size(seed: &mut u64) -> usize { match lcg(seed) % 5 { 0 => 0, 1 => (lcg(seed) % 16) as usize, 2 => (lcg(seed) % 256) as usize, 3 => 256 + (lcg(seed) % 4096) as usize, _ => 4096 + (lcg(seed) % 16384) as usize, } } fn assert_no_panic(label: &str, seed: u64, input: &[u8], parser: F) where F: FnOnce(&[u8]) -> T, { let r = catch_unwind(AssertUnwindSafe(|| parser(input))); if r.is_err() { panic!( "{label} panicked on seed {seed:#x}, input len {}: {:02x?}", input.len(), &input[..input.len().min(64)] ); } } #[test] fn authority_body_parse_does_not_panic_on_random_bytes() { let mut seed = 0x1111_2222_3333_4444u64; for _ in 0..ITERS { let n = rand_size(&mut seed); let bytes = rand_bytes(&mut seed, n); assert_no_panic("AuthorityBody::parse", seed, &bytes, |b| { let _ = AuthorityBody::parse(b); }); } } /// Authority bodies have many length-prefixed fields (member count, /// handle length, policy count, key length, value length). Each is a /// classic place for a parser to fail to bounds-check. Build inputs with /// a *valid* prefix (so the parser advances past schema version checks) /// and then random nonsense — that exercises the inner offset arithmetic. #[test] fn authority_body_parse_survives_crafted_length_prefixes() { let mut seed = 0xfeed_face_8888_1234u64; for _ in 0..ITERS { let mut buf = Vec::new(); // schema_version = 1 (so we get past the version check). buf.extend_from_slice(&1u16.to_le_bytes()); // repo_id (32 bytes), previous_authority (32 bytes). buf.extend(rand_bytes(&mut seed, 32)); buf.extend(rand_bytes(&mut seed, 32)); // version (u32), created_micros (i64). buf.extend(rand_bytes(&mut seed, 4)); buf.extend(rand_bytes(&mut seed, 8)); // Crafted member_count: pick a value that may or may not match // the bytes we'll append. let mc = (lcg(&mut seed) & 0xff) as u16; buf.extend_from_slice(&mc.to_le_bytes()); // Append random bytes (probably not enough to satisfy mc members). let pad = (lcg(&mut seed) % 4096) as usize; buf.extend(rand_bytes(&mut seed, pad)); assert_no_panic("AuthorityBody::parse(crafted)", seed, &buf, |b| { let _ = AuthorityBody::parse(b); }); } } #[test] fn authority_body_round_trip_when_parse_succeeds() { let mut seed = 0xbeef_dead_4321_8765u64; for _ in 0..ITERS { let n = rand_size(&mut seed); let bytes = rand_bytes(&mut seed, n); if let Ok(parsed) = AuthorityBody::parse(&bytes) { // Encode then re-parse — must yield the same body. let re = parsed.encode().expect("re-encode"); let re_parsed = AuthorityBody::parse(&re).expect("re-parse"); assert_eq!(parsed.repo_id, re_parsed.repo_id); assert_eq!(parsed.version, re_parsed.version); assert_eq!(parsed.members.len(), re_parsed.members.len()); assert_eq!(parsed.policy.len(), re_parsed.policy.len()); } } } #[test] fn parse_toml_authority_does_not_panic() { let mut seed = 0xfacefade_1357_2468u64; for _ in 0..ITERS { let n = rand_size(&mut seed); let bytes = rand_bytes(&mut seed, n); // Coerce to UTF-8 best-effort — non-UTF-8 input must still not // panic, just return an error to the caller. let text = String::from_utf8_lossy(&bytes); assert_no_panic("parse_toml_authority", seed, text.as_bytes(), |b| { let _ = parse_toml_authority(std::str::from_utf8(b).unwrap_or("")); }); } } #[test] fn key_parsers_do_not_panic_on_random_input() { let mut seed = 0xa1b2_c3d4_e5f6_0789u64; for _ in 0..ITERS { let n = (lcg(&mut seed) % 256) as usize; let bytes = rand_bytes(&mut seed, n); let text = String::from_utf8_lossy(&bytes).into_owned(); assert_no_panic("PublicKey::parse_levcs", seed, text.as_bytes(), |b| { let _ = PublicKey::parse_levcs(std::str::from_utf8(b).unwrap_or("")); }); assert_no_panic("SecretKey::parse_levcs", seed, text.as_bytes(), |b| { let _ = SecretKey::parse_levcs(std::str::from_utf8(b).unwrap_or("")); }); } } /// Build inputs that *look* like ed25519: strings but with various /// hex-content corruptions. These are realistic adversarial shapes — a /// peer might produce strings in the right syntactic form but with bad /// length, odd nibble counts, non-hex characters in the middle, etc. #[test] fn key_parsers_handle_almost_valid_inputs() { let mut seed = 0x0011_2233_4455_6677u64; for _ in 0..ITERS { let len = (lcg(&mut seed) % 80) as usize; let mut hex_part = String::new(); for _ in 0..len { // Mix valid hex chars with garbage. let pick = lcg(&mut seed) % 16; let c = match pick { 0 => '!', 1 => 'g', 2 => 'Z', 3 => ' ', _ => "0123456789abcdef" .chars() .nth((lcg(&mut seed) % 16) as usize) .unwrap(), }; hex_part.push(c); } let s = format!("ed25519:{hex_part}"); assert_no_panic("PublicKey::parse_levcs(almost)", seed, s.as_bytes(), |b| { let _ = PublicKey::parse_levcs(std::str::from_utf8(b).unwrap_or("")); }); } }