#![no_main] #![allow(deprecated)] use libfuzzer_sys::fuzz_target; use soliton::ratchet::{RatchetHeader, RatchetState}; use soliton::primitives::xwing; fuzz_target!(|data: &[u8]| { // Stateful decrypt: deserialize Bob from the first chunk, then parse a // header + ciphertext from the remainder and attempt decryption. // // Unlike fuzz_ratchet_decrypt (which constructs a fresh Bob via init_bob // and can never reach a successful decrypt-with-ratchet-step), this harness // can start from any serialized state — including post-exchange states where // send_ratchet_sk is Some, making the full ratchet-step decrypt path // reachable. Corpus seeds include states with populated recv_seen sets, // exercising the duplicate-detection and out-of-order paths that would take // the fuzzer a long time to discover through mutation alone. // // Input layout: // state_len (4 BE) | state (state_len) | ratchet_pk (1216) // | has_kem_ct (1) | [kem_ct (1120)] | n (4) | pn (4) | ciphertext // // The practical minimum for reaching decrypt() is ~5000+ bytes (valid // serialized state + header + ciphertext). The early returns below are // fast-path rejections for obviously-too-short input, not the true minimum // for interesting coverage. // // Because state_len is fuzzer-controlled, the fuzzer naturally explores // degenerate cases: state_len=0 (from_bytes rejects), state_len=data.len()-4 // (rest is empty, minimum-size check rejects), and state_len > data.len()-4 // (second guard rejects). All three are handled by the guards below. if data.len() < 4 { return; } let state_len = u32::from_be_bytes(data[..4].try_into().unwrap()) as usize; if data.len() < 4 + state_len { return; } let Ok(mut bob) = RatchetState::from_bytes(&data[4..4 + state_len]) else { return; }; let rest = &data[4 + state_len..]; // ratchet_pk (1216) + has_kem_ct (1) + n (4) + pn (4) + tag (16) = 1241 minimum if rest.len() < 1216 + 1 + 4 + 4 + 16 { return; } let Ok(ratchet_pk) = xwing::PublicKey::from_bytes(rest[..1216].to_vec()) else { return; }; let has_kem_ct = rest[1216] & 0x01 != 0; let (kem_ct, counter_start) = if has_kem_ct { if rest.len() < 1216 + 1 + 1120 + 8 { return; } match xwing::Ciphertext::from_bytes(rest[1217..2337].to_vec()) { Ok(ct) => (Some(ct), 2337), Err(_) => return, } } else { (None, 1217) }; if rest.len() < counter_start + 8 { return; } let n = u32::from_be_bytes(rest[counter_start..counter_start + 4].try_into().unwrap()); let pn = u32::from_be_bytes(rest[counter_start + 4..counter_start + 8].try_into().unwrap()); let ciphertext = &rest[counter_start + 8..]; let header = RatchetHeader { ratchet_pk, kem_ct, n, pn }; // decrypt must never panic. This harness covers the full state machine: // same-chain path (ratchet_pk matches recv_ratchet_pk), ratchet-step path // with real decapsulation (send_ratchet_sk present in deserialized state), // counter-mode key derivation, recv_seen duplicate detection, previous-epoch // grace period, and rollback on any failure. let _ = bob.decrypt(&header, ciphertext); });