//! Seed corpus generator for soliton fuzz targets (core + CAPI). //! //! Generates seeds for all core fuzz targets in `corpus/` and all CAPI fuzz //! targets in `../../soliton_capi/fuzz/corpus/`. //! //! Run: cd soliton/soliton/fuzz && cargo +nightly run --bin gen_corpus use soliton::auth::auth_challenge; use soliton::call::derive_call_keys; use soliton::identity::{generate_identity, hybrid_sign, GeneratedIdentity}; use soliton::kex::{encode_session_init, initiate_session, sign_prekey, verify_bundle, PreKeyBundle}; use soliton::primitives::{ed25519, xwing}; use soliton::ratchet::RatchetState; use soliton::storage::{encrypt_blob, StorageKey}; use soliton::verification::verification_phrase; use zeroize::Zeroizing; use std::fs; use std::path::Path; fn write(dir: &Path, name: &str, data: &[u8]) { fs::write(dir.join(name), data).unwrap(); println!(" wrote {}/{} ({} bytes)", dir.display(), name, data.len()); } fn gen_ratchet_encrypt() { let dir = Path::new("corpus/fuzz_ratchet_encrypt"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_ratchet_encrypt ==="); let fp_a = [0xAA_u8; 32]; let fp_b = [0xBB_u8; 32]; let rk: [u8; 32] = [0x11; 32]; let ck: [u8; 32] = [0x22; 32]; // Alice init — has send keys, ratchet_pending = false. let (ek_pk, ek_sk) = xwing::keygen().unwrap(); let alice = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk).unwrap(); write(dir, "alice_init", &alice.to_bytes().unwrap().0); // Bob init — no send keys, ratchet_pending = true (triggers perform_kem_ratchet_send). let bob = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk.clone()).unwrap(); write(dir, "bob_init", &bob.to_bytes().unwrap().0); // Post-exchange state — both parties have full key material. let (ek_pk2, ek_sk2) = xwing::keygen().unwrap(); let mut alice2 = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk2.clone(), ek_sk2).unwrap(); let mut bob2 = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk2).unwrap(); let enc = alice2.encrypt(b"msg1").unwrap(); bob2.decrypt(&enc.header, &enc.ciphertext).unwrap(); let enc = bob2.encrypt(b"msg2").unwrap(); alice2.decrypt(&enc.header, &enc.ciphertext).unwrap(); // State after direction change — ratchet_pending = true again. let enc = bob2.encrypt(b"msg3").unwrap(); alice2.decrypt(&enc.header, &enc.ciphertext).unwrap(); // Serialize after all mutations — to_bytes consumes self. write(dir, "alice_pending", &alice2.to_bytes().unwrap().0); write(dir, "bob_exchanged", &bob2.to_bytes().unwrap().0); // State near chain exhaustion — send_count just below u32::MAX. // The fuzzer would take ages to discover this boundary through random mutation. // from_bytes rejects u32::MAX, so use MAX-2 (allows fuzzer to mutate +1 to MAX-1). // Alice after init_alice + one encrypt: send_ratchet_sk=Some, send_ratchet_pk=Some, recv_ratchet_pk=None. // Counter offset: v(1) + epoch(8) + rk(32) + sck(32) + rck(32) + lfp(32) + rfp(32) // + sk(1+2+2432) + pk(1+2+1216) + rpk(1) + prev_rek(1) + prev_rpk(1) = 3826. // Must match the sc_offset in ratchet::tests::invariant_b_send_count_requires_send_key. let (ek_pk3, ek_sk3) = xwing::keygen().unwrap(); let mut near_exhausted = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk3, ek_sk3).unwrap(); let _ = near_exhausted.encrypt(b"x").unwrap(); let mut bytes = near_exhausted.to_bytes().unwrap().0.to_vec(); let sc_offset = 3826; // Verify the offset actually points at send_count. Alice starts at send_count=1 // (counter 0 is reserved), so after one encrypt it advances to 2. // Catches serialization format drift — if this fires, the offset calculation is stale. let actual_sc = u32::from_be_bytes(bytes[sc_offset..sc_offset + 4].try_into().unwrap()); assert_eq!(actual_sc, 2, "sc_offset {sc_offset} does not point at send_count (got {actual_sc}, expected 2)"); bytes[sc_offset..sc_offset + 4].copy_from_slice(&(u32::MAX - 2).to_be_bytes()); write(dir, "near_exhausted", &bytes); } fn gen_storage_decrypt_blob() { let dir = Path::new("corpus/fuzz_storage_decrypt_blob"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_storage_decrypt_blob ==="); // Distinct keys per version — must match the harness. let key1 = StorageKey::new(1, [0x42; 32]).unwrap(); let key2 = StorageKey::new(2, [0x43; 32]).unwrap(); let key3 = StorageKey::new(3, [0x44; 32]).unwrap(); // AAD must match the harness: "fuzz-channel" / "fuzz-segment". let ch = "fuzz-channel"; let seg = "fuzz-segment"; // Valid uncompressed blob. let blob = encrypt_blob(&key1, b"hello fuzz", ch, seg, false).unwrap(); write(dir, "valid_uncompressed", &blob); // Valid compressed blob. let blob_c = encrypt_blob(&key1, b"hello compressed fuzz data", ch, seg, true).unwrap(); write(dir, "valid_compressed", &blob_c); // Version 2 blob (distinct key). let blob2 = encrypt_blob(&key2, b"version 2", ch, seg, false).unwrap(); write(dir, "valid_version2", &blob2); // Version 3 blob (distinct key). let blob3 = encrypt_blob(&key3, b"version 3", ch, seg, false).unwrap(); write(dir, "valid_version3", &blob3); // Edge cases. write(dir, "empty", &[]); write(dir, "one_byte", &[0x01]); write(dir, "min_header", &[0x01, 0x00]); // version + flags only // Bad flags (reserved bits set). let mut bad_flags = blob.clone(); bad_flags[1] = 0xFE; write(dir, "bad_flags", &bad_flags); // Unknown version. let mut unknown_ver = blob.clone(); unknown_ver[0] = 0xFF; write(dir, "unknown_version", &unknown_ver); } fn gen_ratchet_decrypt() { let dir = Path::new("corpus/fuzz_ratchet_decrypt"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_ratchet_decrypt ==="); let fp_a = [0xAA_u8; 32]; let fp_b = [0xBB_u8; 32]; let rk: [u8; 32] = [0x11; 32]; let ck: [u8; 32] = [0x22; 32]; let (ek_pk, ek_sk) = xwing::keygen().unwrap(); // Save peer_ek bytes before ek_pk is consumed by init_bob: the harness reads // peer_ek as the first 1216 bytes and passes it to init_bob, so seeds must // carry the same key that Bob was initialized with. let peer_ek_bytes = ek_pk.as_bytes().to_vec(); let mut alice = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk).unwrap(); let _bob = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk).unwrap(); // Encrypt a message from Alice — this produces a valid (header, ciphertext) pair. let enc = alice.encrypt(b"fuzz corpus msg").unwrap(); // Serialize in the harness's expected wire format: // peer_ek (1216) + ratchet_pk (1216) + has_kem_ct (1) + [kem_ct (1120)] + n (4) + pn (4) + ciphertext let mut wire = Vec::new(); wire.extend_from_slice(&peer_ek_bytes); wire.extend_from_slice(enc.header.ratchet_pk.as_bytes()); if let Some(ref ct) = enc.header.kem_ct { wire.push(0x01); wire.extend_from_slice(ct.as_bytes()); } else { wire.push(0x00); } wire.extend_from_slice(&enc.header.n.to_be_bytes()); wire.extend_from_slice(&enc.header.pn.to_be_bytes()); wire.extend_from_slice(&enc.ciphertext); write(dir, "valid_alice_msg", &wire); // Seed without kem_ct (has_kem_ct=0) — exercises no-ratchet-step parsing path. let mut wire_no_ct = Vec::new(); wire_no_ct.extend_from_slice(&peer_ek_bytes); wire_no_ct.extend_from_slice(enc.header.ratchet_pk.as_bytes()); wire_no_ct.push(0x00); // no kem_ct wire_no_ct.extend_from_slice(&enc.header.n.to_be_bytes()); wire_no_ct.extend_from_slice(&enc.header.pn.to_be_bytes()); wire_no_ct.extend_from_slice(&enc.ciphertext); write(dir, "no_kem_ct", &wire_no_ct); // Minimal valid-length input (all zeros — will fail AEAD but exercises parsing). // Size: peer_ek(1216) + ratchet_pk(1216) + has_kem_ct(1) + n(4) + pn(4) + tag(16) = 2457. write(dir, "minimal_zeros", &vec![0u8; 2457]); // Seeds with counters near u32::MAX — exercises the counter-exhaustion guard. // Format: peer_ek(1216) + ratchet_pk(1216) + 0x00(no kem_ct) + n(4 BE) + pn(4 BE) + ct(16 zeros). let zero_1216 = vec![0u8; 1216]; let zero_ct = vec![0u8; 16]; let mut large_n = zero_1216.clone(); // peer_ek large_n.extend_from_slice(&zero_1216); // ratchet_pk large_n.push(0x00); large_n.extend_from_slice(&(u32::MAX - 1).to_be_bytes()); // n = MAX-1 large_n.extend_from_slice(&0u32.to_be_bytes()); // pn = 0 large_n.extend_from_slice(&zero_ct); write(dir, "n_near_max", &large_n); let mut large_pn = zero_1216.clone(); // peer_ek large_pn.extend_from_slice(&zero_1216); // ratchet_pk large_pn.push(0x00); large_pn.extend_from_slice(&0u32.to_be_bytes()); // n = 0 large_pn.extend_from_slice(&(u32::MAX - 1).to_be_bytes()); // pn = MAX-1 large_pn.extend_from_slice(&zero_ct); write(dir, "pn_near_max", &large_pn); // Zero-length plaintext (tag-only ciphertext = 16 bytes) — exercises the // minimum-valid ciphertext path through aead_decrypt. // Format: peer_ek(1216) + ratchet_pk(1216) + 0x00(no kem_ct) + n(4) + pn(4) + tag(16). let mut zero_pt = zero_1216.clone(); // peer_ek zero_pt.extend_from_slice(&zero_1216); // ratchet_pk zero_pt.push(0x00); // no kem_ct zero_pt.extend_from_slice(&0u32.to_be_bytes()); // n = 0 zero_pt.extend_from_slice(&0u32.to_be_bytes()); // pn = 0 zero_pt.extend_from_slice(&zero_ct); // 16-byte tag-only ciphertext write(dir, "zero_plaintext", &zero_pt); } fn gen_ratchet_decrypt_stateful() { let dir = Path::new("corpus/fuzz_ratchet_decrypt_stateful"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_ratchet_decrypt_stateful ==="); let fp_a = [0xAA_u8; 32]; let fp_b = [0xBB_u8; 32]; let rk: [u8; 32] = [0x11; 32]; let ck: [u8; 32] = [0x22; 32]; // Build a post-exchange Bob state with send_ratchet_sk present — this is // the state that makes the ratchet-step decrypt path reachable. let (ek_pk, ek_sk) = xwing::keygen().unwrap(); let mut alice = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk).unwrap(); let mut bob = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk).unwrap(); // Exchange messages so Bob has a full ratchet state (send_ratchet_sk populated). let enc1 = alice.encrypt(b"setup1").unwrap(); bob.decrypt(&enc1.header, &enc1.ciphertext).unwrap(); let enc2 = bob.encrypt(b"setup2").unwrap(); alice.decrypt(&enc2.header, &enc2.ciphertext).unwrap(); // Alice sends another message — Bob can decrypt this with his exchanged state. let enc3 = alice.encrypt(b"stateful test").unwrap(); // Helper: pack (state, header, ciphertext) into the harness wire format. // state_len (4 BE) | state | ratchet_pk (1216) | has_kem_ct (1) | [kem_ct (1120)] // | n (4) | pn (4) | ciphertext let pack = |state_bytes: &[u8], header: &soliton::ratchet::RatchetHeader, ct: &[u8]| -> Vec { let mut wire = Vec::new(); wire.extend_from_slice(&(state_bytes.len() as u32).to_be_bytes()); wire.extend_from_slice(state_bytes); wire.extend_from_slice(header.ratchet_pk.as_bytes()); if let Some(ref kem_ct) = header.kem_ct { wire.push(0x01); wire.extend_from_slice(kem_ct.as_bytes()); } else { wire.push(0x00); } wire.extend_from_slice(&header.n.to_be_bytes()); wire.extend_from_slice(&header.pn.to_be_bytes()); wire.extend_from_slice(ct); wire }; // Seed 1: valid decrypt with ratchet step (Alice's new ratchet_pk triggers ratchet in Bob). let bob_bytes = bob.to_bytes().unwrap().0; write(dir, "valid_ratchet_step", &pack(&bob_bytes, &enc3.header, &enc3.ciphertext)); // Seed 2: same-chain decrypt (Bob decrypts a second message from Alice without ratchet step). // Re-create the state since to_bytes consumed bob. let (ek_pk2, ek_sk2) = xwing::keygen().unwrap(); let mut alice2 = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk2.clone(), ek_sk2).unwrap(); let mut bob2 = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk2).unwrap(); let e1 = alice2.encrypt(b"s1").unwrap(); bob2.decrypt(&e1.header, &e1.ciphertext).unwrap(); let e2 = alice2.encrypt(b"same chain").unwrap(); // Bob has not sent yet — recv_ratchet_pk matches Alice's current pk, so no ratchet step. let bob2_bytes = bob2.to_bytes().unwrap().0; write(dir, "valid_same_chain", &pack(&bob2_bytes, &e2.header, &e2.ciphertext)); // Seed 3: state with populated recv_seen set. Alice sends messages 1, 3, 4 // in delivery order — Bob receives 1, then 3 and 4 out-of-order, populating // recv_seen with counters {0, 2, 3}. Seeding with Bob's state + message 2's // header/ciphertext exercises the counter-mode O(1) key derivation for a // late-arriving message within a populated recv_seen set. // to_bytes consumed bob2 above, so reconstruct a fresh pair. let (ek_pk3, ek_sk3) = xwing::keygen().unwrap(); let mut alice3 = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk3.clone(), ek_sk3).unwrap(); let mut bob3 = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk3).unwrap(); let msg1 = alice3.encrypt(b"msg1").unwrap(); let msg2 = alice3.encrypt(b"msg2").unwrap(); // will be delivered last let msg3 = alice3.encrypt(b"msg3").unwrap(); let msg4 = alice3.encrypt(b"msg4").unwrap(); // Deliver 1, 3, 4 out-of-order — Bob's recv_seen tracks counters {0, 2, 3}. bob3.decrypt(&msg1.header, &msg1.ciphertext).unwrap(); bob3.decrypt(&msg3.header, &msg3.ciphertext).unwrap(); bob3.decrypt(&msg4.header, &msg4.ciphertext).unwrap(); // Bob now has recv_seen populated; msg2 (counter 1) is unseen but derivable. let bob3_bytes = bob3.to_bytes().unwrap().0; write(dir, "out_of_order_late", &pack(&bob3_bytes, &msg2.header, &msg2.ciphertext)); // Seed 4: True duplicate — Bob has already decrypted this exact message, // so its counter is in recv_seen. The fuzzer cannot reach DuplicateMessage // through mutation (mutating n changes the AAD, failing AEAD before the // duplicate check), so this seed is the only way to exercise the post-AEAD // duplicate detection and rollback path. let (ek_pk4, ek_sk4) = xwing::keygen().unwrap(); let mut alice4 = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk4.clone(), ek_sk4).unwrap(); let mut bob4 = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk4).unwrap(); let enc_dup = alice4.encrypt(b"this will be a duplicate").unwrap(); bob4.decrypt(&enc_dup.header, &enc_dup.ciphertext).unwrap(); let bob4_bytes = bob4.to_bytes().unwrap().0; write(dir, "true_duplicate", &pack(&bob4_bytes, &enc_dup.header, &enc_dup.ciphertext)); // Seed 5: empty/minimal state_len (from_bytes will reject, exercises parse path). let mut minimal = Vec::new(); minimal.extend_from_slice(&0u32.to_be_bytes()); // state_len = 0 minimal.extend_from_slice(&[0u8; 1216 + 1 + 4 + 4 + 16]); // ratchet_pk + flag + n + pn + tag write(dir, "empty_state", &minimal); } fn gen_identity_from_bytes() { let dir = Path::new("corpus/fuzz_identity_from_bytes"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_identity_from_bytes ==="); let GeneratedIdentity { public_key: ik_pk, secret_key: ik_sk, .. } = generate_identity().unwrap(); write(dir, "valid_pk", ik_pk.as_bytes()); write(dir, "valid_sk", ik_sk.as_bytes()); // PK boundary: 3199 / 3200 / 3201 write(dir, "pk_minus_one", &vec![0xBB; 3199]); write(dir, "pk_exact", &vec![0xCC; 3200]); write(dir, "pk_plus_one", &vec![0xDD; 3201]); // SK boundary: 2495 / 2496 / 2497 write(dir, "sk_minus_one", &vec![0xEE; 2495]); write(dir, "sk_exact", &vec![0xEE; 2496]); write(dir, "sk_plus_one", &vec![0xEE; 2497]); // Sig boundary: 3372 / 3373 / 3374 write(dir, "sig_minus_one", &vec![0xFF; 3372]); write(dir, "sig_exact", &vec![0xFF; 3373]); write(dir, "sig_plus_one", &vec![0xFF; 3374]); // Short / empty write(dir, "empty", &[]); write(dir, "one_byte", &[0xAA]); } fn gen_ed25519_verify() { let dir = Path::new("corpus/fuzz_ed25519_verify"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_ed25519_verify ==="); // Valid (pk, sig, msg) triple using native Ed25519. let (vk, sk) = ed25519::keygen(); let msg = b"fuzz corpus msg"; let sig = ed25519::sign(&sk, msg); let mut seed = Vec::new(); seed.extend_from_slice(vk.as_bytes()); seed.extend_from_slice(&sig); seed.extend_from_slice(msg); write(dir, "valid_verify", &seed); // All zeros — exercises point decompression rejection (identity point is // not a valid Ed25519 public key in strict mode). write(dir, "zeros_96", &[0u8; 96]); // All 0xFF — exercises rejection paths. write(dir, "ones_96", &[0xFF; 96]); // Non-canonical encoding (top bit of pk set) — exercises dalek's rejection // of non-canonical encodings in VerifyingKey::from_bytes. let mut non_canonical = [0u8; 96]; non_canonical[0] = 0x01; non_canonical[31] = 0xFF; // bit 255 set write(dir, "pk_non_canonical", &non_canonical); // Valid pk + mangled sig. let mut bad_sig = seed.clone(); bad_sig[32] ^= 0x01; write(dir, "bad_sig_bit", &bad_sig); } fn gen_hybrid_verify() { let dir = Path::new("corpus/fuzz_hybrid_verify"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_hybrid_verify ==="); let GeneratedIdentity { public_key: ik_pk, secret_key: ik_sk, .. } = generate_identity().unwrap(); let msg = b"fuzz hybrid verify corpus"; // Valid (pk, sig, msg) triple. let sig = hybrid_sign(&ik_sk, msg).unwrap(); let mut seed = Vec::new(); seed.extend_from_slice(ik_pk.as_bytes()); seed.extend_from_slice(sig.as_bytes()); seed.extend_from_slice(msg); write(dir, "valid_hybrid", &seed); // Minimum-length input (pk + sig, empty message). let mut min_seed = Vec::new(); min_seed.extend_from_slice(ik_pk.as_bytes()); min_seed.extend_from_slice(sig.as_bytes()); write(dir, "valid_empty_msg", &min_seed); // All zeros at exact pk+sig length — exercises parsing rejection. write(dir, "zeros_6573", &vec![0u8; 3200 + 3373]); // Mixed-validity: valid pk + zeroed Ed25519 portion (bytes 0..64 of sig), valid ML-DSA. // Exercises the `r1.and(r2)` combinator rejecting when only one component passes. let sig_bytes = sig.as_bytes(); let mut bad_ed25519 = Vec::new(); bad_ed25519.extend_from_slice(ik_pk.as_bytes()); bad_ed25519.extend_from_slice(&[0u8; 64]); // zeroed Ed25519 sig bad_ed25519.extend_from_slice(&sig_bytes[64..]); // valid ML-DSA sig bad_ed25519.extend_from_slice(msg); write(dir, "bad_ed25519_valid_mldsa", &bad_ed25519); // Mixed-validity: valid pk + valid Ed25519, zeroed ML-DSA portion. let mut bad_mldsa = Vec::new(); bad_mldsa.extend_from_slice(ik_pk.as_bytes()); bad_mldsa.extend_from_slice(&sig_bytes[..64]); // valid Ed25519 sig bad_mldsa.extend_from_slice(&vec![0u8; 3373 - 64]); // zeroed ML-DSA sig bad_mldsa.extend_from_slice(msg); write(dir, "valid_ed25519_bad_mldsa", &bad_mldsa); } fn gen_decrypt_first_message() { let dir = Path::new("corpus/fuzz_decrypt_first_message"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_decrypt_first_message ==="); // Valid encrypted first message. let ck: [u8; 32] = [0x22; 32]; let aad = b"lo-dm-v1"; let (blob, _new_ck) = RatchetState::encrypt_first_message(Zeroizing::new(ck), b"hello first", aad).unwrap(); write(dir, "valid_first_msg", &blob); // Edge cases. write(dir, "empty", &[]); write(dir, "too_short", &[0xAA; 24]); // 16 bytes short of the 40-byte minimum (nonce=24 + tag=16) write(dir, "nonce_plus_tag", &[0xBB; 24 + 16]); // exactly nonce + Poly1305 tag, zero plaintext } // ── Tier-1 targets ────────────────────────────────────────────────────────── fn gen_kex_receive_session() { let dir = Path::new("corpus/fuzz_kex_receive_session"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_kex_receive_session ==="); // Generate Alice + Bob identities and Bob's SPK for a real session init. let GeneratedIdentity { public_key: alice_pk, secret_key: alice_sk, .. } = generate_identity().unwrap(); let GeneratedIdentity { public_key: bob_pk, secret_key: bob_sk, .. } = generate_identity().unwrap(); let (spk_pk, _spk_sk) = xwing::keygen().unwrap(); let spk_sig = sign_prekey(&bob_sk, &spk_pk).unwrap(); let bundle = PreKeyBundle { ik_pub: bob_pk.clone(), crypto_version: "lo-crypto-v1".to_string(), spk_pub: spk_pk, spk_id: 1, spk_sig, opk_pub: None, opk_id: None, }; let verified = verify_bundle(bundle, &bob_pk).unwrap(); let initiated = initiate_session(&alice_pk, &alice_sk, &verified).unwrap(); let si = &initiated.session_init; let sig = &initiated.sender_sig; // Encode as the harness wire format: // sig(3373) | sender_fp(32) | recipient_fp(32) | sender_ek(1216) // | ct_ik(1120) | ct_spk(1120) | spk_id(4 BE) | has_opk(1) // | [ct_opk(1120) | opk_id(4 BE)] ← only when has_opk & 0x01 // | crypto_version (rest) let mut wire = Vec::new(); wire.extend_from_slice(sig.as_bytes()); wire.extend_from_slice(&si.sender_ik_fingerprint); wire.extend_from_slice(&si.recipient_ik_fingerprint); wire.extend_from_slice(si.sender_ek.as_bytes()); wire.extend_from_slice(si.ct_ik.as_bytes()); wire.extend_from_slice(si.ct_spk.as_bytes()); wire.extend_from_slice(&si.spk_id.to_be_bytes()); wire.push(0x00); // no OPK wire.extend_from_slice(si.crypto_version.as_bytes()); write(dir, "valid_session_init", &wire); // All-zeros seed: correct structure, all crypto checks will reject gracefully. let mut zeros = vec![0u8; 3373 + 32 + 32 + 1216 + 1120 + 1120 + 4 + 1]; zeros.extend_from_slice(b"lo-crypto-v1"); write(dir, "zeros_no_opk", &zeros); // Seed with OPK flag set (has_opk=1, zero ct_opk + opk_id). let mut zeros_opk = vec![0u8; 3373 + 32 + 32 + 1216 + 1120 + 1120 + 4]; zeros_opk.push(0x01); // has_opk zeros_opk.extend_from_slice(&[0u8; 1120 + 4]); // ct_opk + opk_id zeros_opk.extend_from_slice(b"lo-crypto-v1"); write(dir, "zeros_with_opk", &zeros_opk); } fn gen_storage_encrypt_blob() { let dir = Path::new("corpus/fuzz_storage_encrypt_blob"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_storage_encrypt_blob ==="); // data[0] encodes the compress flag; data[1..] is plaintext. // compress = 0 (even byte), various plaintext lengths. let mut s0 = vec![0x00u8]; // compress = false write(dir, "empty_no_compress", &s0); s0.extend_from_slice(b"hello fuzz"); write(dir, "hello_no_compress", &s0); // compress = 1 (odd byte), compressible content. let mut s1 = vec![0x01u8]; // compress = true s1.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); write(dir, "compressible", &s1); // Large plaintext — exercises capacity calculation path. let mut large = vec![0x00u8]; large.extend_from_slice(&[0x42u8; 65536]); write(dir, "large_65k_no_compress", &large); let mut large_c = vec![0x01u8]; large_c.extend_from_slice(&[0x42u8; 65536]); write(dir, "large_65k_compress", &large_c); } fn gen_auth_respond() { let dir = Path::new("corpus/fuzz_auth_respond"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_auth_respond ==="); // Valid ciphertext produced by auth_challenge for a fresh identity. let GeneratedIdentity { public_key: client_pk, .. } = generate_identity().unwrap(); let (ct, _ss) = auth_challenge(&client_pk).unwrap(); write(dir, "valid_ct", ct.as_bytes()); // All-zeros ct (correct size, decaps will return some garbage ss — no panic). write(dir, "zeros_1120", &[0u8; 1120]); // All-0xFF ct. write(dir, "ones_1120", &[0xFF; 1120]); } fn gen_kex_verify_bundle() { let dir = Path::new("corpus/fuzz_kex_verify_bundle"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_kex_verify_bundle ==="); // ik_pub is always KNOWN_IK in the harness — not part of fuzz input. // Seed with a real SPK signed by a fresh identity (sig verification fails // since KNOWN_IK is different, but teaches the fuzzer valid byte layout). let GeneratedIdentity { secret_key: bob_sk, .. } = generate_identity().unwrap(); let (spk_pk, _) = xwing::keygen().unwrap(); let spk_sig = sign_prekey(&bob_sk, &spk_pk).unwrap(); // Wire format: spk(1216) | spk_sig(3373) | spk_id(4 BE) | has_opk(1) | [opk_pub(1216) | opk_id(4 BE)] | crypto_version let mut wire = Vec::new(); wire.extend_from_slice(spk_pk.as_bytes()); wire.extend_from_slice(spk_sig.as_bytes()); wire.extend_from_slice(&1u32.to_be_bytes()); wire.push(0x00); // no OPK wire.extend_from_slice(b"lo-crypto-v1"); write(dir, "valid_structure", &wire); // All-zeros seed (correct structure, all crypto checks reject gracefully). let mut zeros = vec![0u8; 1216 + 3373 + 4 + 1]; zeros.extend_from_slice(b"lo-crypto-v1"); write(dir, "zeros_no_opk", &zeros); // With OPK flag set. let mut zeros_opk = vec![0u8; 1216 + 3373 + 4]; zeros_opk.push(0x01); // has_opk zeros_opk.extend_from_slice(&[0u8; 1216 + 4]); // opk_pub + opk_id zeros_opk.extend_from_slice(b"lo-crypto-v1"); write(dir, "zeros_with_opk", &zeros_opk); } fn gen_verification_phrase() { let dir = Path::new("corpus/fuzz_verification_phrase"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_verification_phrase ==="); // Two correctly-sized PKs — exercises the full SHA-512 + word-derivation path. // (verification_phrase requires exactly 3200 bytes each; fixed split in the harness.) write(dir, "two_valid_size_zero_pks", &[0u8; 6400]); // Two real PKs — a valid call that actually produces a phrase. let GeneratedIdentity { public_key: pk_a, .. } = generate_identity().unwrap(); let GeneratedIdentity { public_key: pk_b, .. } = generate_identity().unwrap(); let mut seed = Vec::new(); seed.extend_from_slice(pk_a.as_bytes()); seed.extend_from_slice(pk_b.as_bytes()); write(dir, "two_real_pks", &seed); // Smoke-test the seed is valid (panics here would indicate a gen_corpus bug). let _ = verification_phrase(pk_a.as_bytes(), pk_b.as_bytes()).unwrap(); // Short inputs — hit InvalidLength return early. write(dir, "empty", &[]); write(dir, "too_short", &[0u8; 100]); } // ── Tier-2 targets ────────────────────────────────────────────────────────── fn gen_ratchet_roundtrip() { let dir = Path::new("corpus/fuzz_ratchet_roundtrip"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_ratchet_roundtrip ==="); // Valid ratchet state serialisations: any blob accepted by from_bytes is // also a valid seed for fuzz_ratchet_roundtrip (encode → decode must be identity). let rk: [u8; 32] = [0x11; 32]; let ck: [u8; 32] = [0x22; 32]; let (ek_pk, ek_sk) = xwing::keygen().unwrap(); let fp_a = [0xAA_u8; 32]; let fp_b = [0xBB_u8; 32]; let alice = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk).unwrap(); write(dir, "alice_init", &alice.to_bytes().unwrap().0); let bob = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk.clone()).unwrap(); write(dir, "bob_init", &bob.to_bytes().unwrap().0); let (ek_pk2, ek_sk2) = xwing::keygen().unwrap(); let mut alice2 = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk2.clone(), ek_sk2).unwrap(); let mut bob2 = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk2).unwrap(); let enc = alice2.encrypt(b"roundtrip test").unwrap(); bob2.decrypt(&enc.header, &enc.ciphertext).unwrap(); write(dir, "alice_exchanged", &alice2.to_bytes().unwrap().0); write(dir, "bob_exchanged", &bob2.to_bytes().unwrap().0); // Edge cases — fuzzer needs rejection inputs as seeds too. write(dir, "empty", &[]); write(dir, "wrong_version_0x01", &[0x01]); write(dir, "truncated_32", &[0x02; 32]); } fn gen_xwing_roundtrip() { let dir = Path::new("corpus/fuzz_xwing_roundtrip"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_xwing_roundtrip ==="); // Mode A (data[0] & 0x01 != 0): fuzz ciphertext bytes. let (pk, _sk) = xwing::keygen().unwrap(); let (ct, _ss) = xwing::encapsulate(&pk).unwrap(); let mut mode_a_valid = vec![0x01u8]; // odd → mode A mode_a_valid.extend_from_slice(ct.as_bytes()); write(dir, "mode_a_valid_ct", &mode_a_valid); let mut mode_a_zeros = vec![0x01u8]; mode_a_zeros.extend_from_slice(&[0u8; 1120]); write(dir, "mode_a_zeros_ct", &mode_a_zeros); // Mode B (data[0] & 0x01 == 0): encapsulate → decapsulate roundtrip. write(dir, "mode_b_trigger", &[0x00u8]); write(dir, "mode_b_with_trailing", &[0x02u8, 0xFF, 0x42]); } fn gen_identity_sign_verify() { let dir = Path::new("corpus/fuzz_identity_sign_verify"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_identity_sign_verify ==="); // The fuzz input is the message to sign. Any non-empty byte sequence works. write(dir, "hello", b"hello"); write(dir, "one_byte", &[0x42]); write(dir, "binary_msg", &[0x00, 0xFF, 0x42, 0x00, 0x01]); write(dir, "long_msg", &[0xABu8; 1024]); // Edge case: single byte (data[0] ^= 0x01 in the harness changes it). write(dir, "single_zero", &[0x00]); write(dir, "single_ff", &[0xFF]); } fn gen_session_init_roundtrip() { let dir = Path::new("corpus/fuzz_session_init_roundtrip"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_session_init_roundtrip ==="); // Seed 1: valid encoded SessionInit without OPK (real keygen, correct wire format). let GeneratedIdentity { public_key: alice_pk, secret_key: alice_sk, .. } = generate_identity().unwrap(); let GeneratedIdentity { public_key: bob_pk, secret_key: bob_sk, .. } = generate_identity().unwrap(); let (spk_pk, _) = xwing::keygen().unwrap(); let spk_sig = sign_prekey(&bob_sk, &spk_pk).unwrap(); let bundle = PreKeyBundle { ik_pub: bob_pk.clone(), crypto_version: "lo-crypto-v1".to_string(), spk_pub: spk_pk, spk_id: 1, spk_sig, opk_pub: None, opk_id: None, }; let verified = verify_bundle(bundle, &bob_pk).unwrap(); let initiated = initiate_session(&alice_pk, &alice_sk, &verified).unwrap(); let encoded_no_opk = encode_session_init(&initiated.session_init).unwrap(); write(dir, "valid_no_opk", &encoded_no_opk); // Seed 2: valid encoded SessionInit with OPK. let (opk_pk, _) = xwing::keygen().unwrap(); let (spk_pk2, _) = xwing::keygen().unwrap(); let spk_sig2 = sign_prekey(&bob_sk, &spk_pk2).unwrap(); let bundle_opk = PreKeyBundle { ik_pub: bob_pk.clone(), crypto_version: "lo-crypto-v1".to_string(), spk_pub: spk_pk2, spk_id: 2, spk_sig: spk_sig2, opk_pub: Some(opk_pk), opk_id: Some(7), }; let verified_opk = verify_bundle(bundle_opk, &bob_pk).unwrap(); let initiated_opk = initiate_session(&alice_pk, &alice_sk, &verified_opk).unwrap(); let encoded_with_opk = encode_session_init(&initiated_opk.session_init).unwrap(); write(dir, "valid_with_opk", &encoded_with_opk); // Seed 3: truncated by 1 byte — decoder returns InvalidLength, harness skips. let truncated = &encoded_no_opk[..encoded_no_opk.len() - 1]; write(dir, "truncated_1", truncated); } fn gen_call_derive() { let dir = Path::new("corpus/fuzz_call_derive"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_call_derive ==="); // Fixed root key matching the harness constant. let root_key = [0x11_u8; 32]; // Valid seed: kem_ss non-zero and fp_a != fp_b — derives keys and exercises advance(). // Wire layout: kem_ss(32) | call_id(16) | fp_a(32) | fp_b(32). let mut valid = Vec::new(); valid.extend_from_slice(&[0x01_u8; 32]); // kem_ss (non-zero) valid.extend_from_slice(&[0xCC_u8; 16]); // call_id valid.extend_from_slice(&[0xAA_u8; 32]); // fp_a valid.extend_from_slice(&[0xBB_u8; 32]); // fp_b // Verify the seed hits the success path before writing it. derive_call_keys(&root_key, valid[..32].try_into().unwrap(), valid[32..48].try_into().unwrap(), valid[48..80].try_into().unwrap(), valid[80..112].try_into().unwrap()).unwrap(); write(dir, "valid_derive_and_advance", &valid); // Guard path: kem_ss all-zeros — exercises the zero-kem_ss InvalidData check. let mut zero_ss = valid.clone(); zero_ss[..32].fill(0x00); write(dir, "zero_kem_ss", &zero_ss); // Guard path: fp_a == fp_b — exercises the equal-fingerprints InvalidData check. let mut equal_fp = valid.clone(); equal_fp[48..80].fill(0xAA); equal_fp[80..112].fill(0xAA); write(dir, "equal_fingerprints", &equal_fp); // Valid seed with fp_a > fp_b — exercises the lower_role=false branch of advance(), // where send_key = key_b and recv_key = key_a (role assignment is fp_a < fp_b). let mut valid_lower_false = Vec::new(); valid_lower_false.extend_from_slice(&[0x01_u8; 32]); // kem_ss (non-zero) valid_lower_false.extend_from_slice(&[0xCC_u8; 16]); // call_id valid_lower_false.extend_from_slice(&[0xBB_u8; 32]); // fp_a (BB > AA) valid_lower_false.extend_from_slice(&[0xAA_u8; 32]); // fp_b (AA < BB) derive_call_keys(&root_key, valid_lower_false[..32].try_into().unwrap(), valid_lower_false[32..48].try_into().unwrap(), valid_lower_false[48..80].try_into().unwrap(), valid_lower_false[80..112].try_into().unwrap()).unwrap(); write(dir, "valid_lower_role_false", &valid_lower_false); // Below-minimum input — harness returns immediately without calling derive_call_keys. write(dir, "too_short", &valid[..111]); } fn gen_auth_verify() { let dir = Path::new("corpus/fuzz_auth_verify"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_auth_verify ==="); // Matching pair — auth_verify returns true. let token = [0x42_u8; 32]; let mut seed = Vec::new(); seed.extend_from_slice(&token); seed.extend_from_slice(&token); write(dir, "matching_pair", &seed); // Mismatched pair — auth_verify returns false. let mut mismatch = Vec::new(); mismatch.extend_from_slice(&[0xAA; 32]); mismatch.extend_from_slice(&[0xBB; 32]); write(dir, "mismatched_pair", &mismatch); // All zeros. write(dir, "zeros_64", &[0u8; 64]); // All 0xFF. write(dir, "ones_64", &[0xFF; 64]); // Short input (below 64-byte minimum — harness returns early). write(dir, "too_short", &[0x42; 63]); } fn gen_ratchet_from_bytes_epoch() { let dir = Path::new("corpus/fuzz_ratchet_from_bytes_epoch"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_ratchet_from_bytes_epoch ==="); let fp_a = [0xAA_u8; 32]; let fp_b = [0xBB_u8; 32]; let rk: [u8; 32] = [0x11; 32]; let ck: [u8; 32] = [0x22; 32]; // Valid ratchet state with min_epoch = 0 (always passes). let (ek_pk, ek_sk) = xwing::keygen().unwrap(); let alice = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk).unwrap(); let state_bytes = alice.to_bytes().unwrap().0; let mut seed = Vec::new(); seed.extend_from_slice(&0u64.to_be_bytes()); // min_epoch = 0 seed.extend_from_slice(&state_bytes); write(dir, "valid_epoch_0", &seed); // Same state with min_epoch = 1 — state has epoch = 0, so this rejects. let (ek_pk2, ek_sk2) = xwing::keygen().unwrap(); let alice2 = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk2, ek_sk2).unwrap(); let state_bytes2 = alice2.to_bytes().unwrap().0; let mut seed_reject = Vec::new(); seed_reject.extend_from_slice(&1u64.to_be_bytes()); // min_epoch = 1 seed_reject.extend_from_slice(&state_bytes2); write(dir, "epoch_too_low", &seed_reject); // min_epoch = u64::MAX — always rejects. let (ek_pk3, ek_sk3) = xwing::keygen().unwrap(); let alice3 = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk3, ek_sk3).unwrap(); let state_bytes3 = alice3.to_bytes().unwrap().0; let mut seed_max = Vec::new(); seed_max.extend_from_slice(&u64::MAX.to_be_bytes()); seed_max.extend_from_slice(&state_bytes3); write(dir, "max_epoch", &seed_max); // Short input (below 8-byte minimum — harness returns early). write(dir, "too_short", &[0x00; 7]); // 8 bytes only (empty state — from_bytes rejects). write(dir, "epoch_only", &0u64.to_be_bytes()); } fn gen_kex_decode_receive() { let dir = Path::new("corpus/fuzz_kex_decode_receive"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_kex_decode_receive ==="); // Valid encoded session init with a real signature prefix. let GeneratedIdentity { public_key: alice_pk, secret_key: alice_sk, .. } = generate_identity().unwrap(); let GeneratedIdentity { public_key: bob_pk, secret_key: bob_sk, .. } = generate_identity().unwrap(); let (spk_pk, _) = xwing::keygen().unwrap(); let spk_sig = sign_prekey(&bob_sk, &spk_pk).unwrap(); let bundle = PreKeyBundle { ik_pub: bob_pk.clone(), crypto_version: "lo-crypto-v1".to_string(), spk_pub: spk_pk, spk_id: 1, spk_sig, opk_pub: None, opk_id: None, }; let verified = verify_bundle(bundle, &bob_pk).unwrap(); let initiated = initiate_session(&alice_pk, &alice_sk, &verified).unwrap(); let encoded = encode_session_init(&initiated.session_init).unwrap(); // Wire layout: sig(3373) | encoded_session_init(rest) let mut wire = Vec::new(); wire.extend_from_slice(initiated.sender_sig.as_bytes()); wire.extend_from_slice(&encoded); write(dir, "valid_no_opk", &wire); // Valid with OPK. let (opk_pk, _) = xwing::keygen().unwrap(); let (spk_pk2, _) = xwing::keygen().unwrap(); let spk_sig2 = sign_prekey(&bob_sk, &spk_pk2).unwrap(); let bundle_opk = PreKeyBundle { ik_pub: bob_pk.clone(), crypto_version: "lo-crypto-v1".to_string(), spk_pub: spk_pk2, spk_id: 2, spk_sig: spk_sig2, opk_pub: Some(opk_pk), opk_id: Some(7), }; let verified_opk = verify_bundle(bundle_opk, &bob_pk).unwrap(); let initiated_opk = initiate_session(&alice_pk, &alice_sk, &verified_opk).unwrap(); let encoded_opk = encode_session_init(&initiated_opk.session_init).unwrap(); let mut wire_opk = Vec::new(); wire_opk.extend_from_slice(initiated_opk.sender_sig.as_bytes()); wire_opk.extend_from_slice(&encoded_opk); write(dir, "valid_with_opk", &wire_opk); // All-zeros at minimum size (sig + some encoded bytes). let zeros = vec![0u8; 3373 + 100]; write(dir, "zeros_min", &zeros); // Too short for signature (harness returns early). write(dir, "too_short", &[0u8; 3372]); } // ── CAPI targets ─────────────────────────────────────────────────────────── // // CAPI fuzz targets consume the same wire formats as core targets (ratchet // state bytes, storage blobs, encoded session inits). Seeds are generated // here and written to ../../soliton_capi/fuzz/corpus/ so the CAPI fuzz // crate doesn't need a core-crate dependency (which has version-resolution // issues in a separate workspace). fn gen_capi_ratchet_from_bytes() { let dir = Path::new("../../soliton_capi/fuzz/corpus/fuzz_capi_ratchet_from_bytes"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_capi_ratchet_from_bytes ==="); let fp_a = [0xAA_u8; 32]; let fp_b = [0xBB_u8; 32]; let rk: [u8; 32] = [0x11; 32]; let ck: [u8; 32] = [0x22; 32]; // Valid Alice ratchet state (has send_ratchet_sk). let (ek_pk, ek_sk) = xwing::keygen().unwrap(); let alice = RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk).unwrap(); write(dir, "alice_init", &alice.to_bytes().unwrap().0); // Valid Bob ratchet state (no send_ratchet_sk). let bob = RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk).unwrap(); write(dir, "bob_init", &bob.to_bytes().unwrap().0); // Edge cases. write(dir, "empty", &[]); write(dir, "one_byte", &[0x01]); write(dir, "wrong_version", &[0xFF; 100]); } fn gen_capi_storage_decrypt() { let dir = Path::new("../../soliton_capi/fuzz/corpus/fuzz_capi_storage_decrypt"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_capi_storage_decrypt ==="); // Key must match the CAPI harness constant: [0x42; 32], version 1. let key = StorageKey::new(1, [0x42; 32]).unwrap(); // Valid uncompressed blob. let blob = encrypt_blob(&key, b"hello capi fuzz", "fuzz-ch", "fuzz-seg", false).unwrap(); write(dir, "valid_uncompressed", &blob); // Valid compressed blob. let blob_c = encrypt_blob(&key, b"compressible capi data", "fuzz-ch", "fuzz-seg", true).unwrap(); write(dir, "valid_compressed", &blob_c); // Edge cases. write(dir, "empty", &[]); write(dir, "one_byte", &[0x01]); write(dir, "min_header", &[0x01, 0x00]); // Bad flags. let mut bad_flags = blob.clone(); bad_flags[1] = 0xFE; write(dir, "bad_flags", &bad_flags); // Unknown version. let mut unknown_ver = blob.clone(); unknown_ver[0] = 0xFF; write(dir, "unknown_version", &unknown_ver); } fn gen_capi_decode_session_init() { let dir = Path::new("../../soliton_capi/fuzz/corpus/fuzz_capi_decode_session_init"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_capi_decode_session_init ==="); // Valid encoded session init (CAPI harness passes raw bytes to the decoder). let GeneratedIdentity { public_key: alice_pk, secret_key: alice_sk, .. } = generate_identity().unwrap(); let GeneratedIdentity { public_key: bob_pk, secret_key: bob_sk, .. } = generate_identity().unwrap(); let (spk_pk, _) = xwing::keygen().unwrap(); let spk_sig = sign_prekey(&bob_sk, &spk_pk).unwrap(); let bundle = PreKeyBundle { ik_pub: bob_pk.clone(), crypto_version: "lo-crypto-v1".to_string(), spk_pub: spk_pk, spk_id: 1, spk_sig, opk_pub: None, opk_id: None, }; let verified = verify_bundle(bundle, &bob_pk).unwrap(); let initiated = initiate_session(&alice_pk, &alice_sk, &verified).unwrap(); let encoded = encode_session_init(&initiated.session_init).unwrap(); write(dir, "valid_no_opk", &encoded); // With OPK. let (opk_pk, _) = xwing::keygen().unwrap(); let (spk_pk2, _) = xwing::keygen().unwrap(); let spk_sig2 = sign_prekey(&bob_sk, &spk_pk2).unwrap(); let bundle_opk = PreKeyBundle { ik_pub: bob_pk.clone(), crypto_version: "lo-crypto-v1".to_string(), spk_pub: spk_pk2, spk_id: 2, spk_sig: spk_sig2, opk_pub: Some(opk_pk), opk_id: Some(7), }; let verified_opk = verify_bundle(bundle_opk, &bob_pk).unwrap(); let initiated_opk = initiate_session(&alice_pk, &alice_sk, &verified_opk).unwrap(); let encoded_opk = encode_session_init(&initiated_opk.session_init).unwrap(); write(dir, "valid_with_opk", &encoded_opk); // Edge cases. write(dir, "empty", &[]); write(dir, "too_short", &[0x01; 10]); write(dir, "zeros_100", &[0u8; 100]); } fn gen_dm_queue_roundtrip() { let dir = Path::new("corpus/fuzz_dm_queue_roundtrip"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_dm_queue_roundtrip ==="); // Wire layout: [compress_flag (1)] [batch_id_len (1)] [plaintext (..)] // Valid seed: compress=true, batch_id_len=0 (uses "fuzz-batch"), short plaintext. let mut seed = vec![0x01u8, 0x00]; // compress=true, batch_id_len=0 seed.extend_from_slice(b"hello dm queue"); write(dir, "valid_compressed", &seed); // Uncompressed variant. let mut seed2 = vec![0x00u8, 0x01]; // compress=false, batch_id_len=1 (uses "fuzz-batch-alt") seed2.extend_from_slice(b"uncompressed payload"); write(dir, "valid_uncompressed", &seed2); // Empty plaintext. write(dir, "empty_plaintext", &[0x00, 0x00]); // Minimal (just the 2-byte header). write(dir, "minimal", &[0x01, 0xFF]); // Larger payload to exercise compression. let mut large = vec![0x01u8, 0x00]; large.extend_from_slice(&[0x41u8; 8192]); write(dir, "large_payload", &large); } fn gen_dm_queue_decrypt_blob() { let dir = Path::new("corpus/fuzz_dm_queue_decrypt_blob"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_dm_queue_decrypt_blob ==="); // Keys must match the harness (versions 1-3). let key1 = StorageKey::new(1, [0x42; 32]).unwrap(); let recipient_fp: [u8; 32] = [0xAA; 32]; // Valid encrypted blob — compressed. let blob = soliton::storage::encrypt_dm_queue_blob( &key1, b"seed plaintext", &recipient_fp, "fuzz-batch", true, ).unwrap(); write(dir, "valid_compressed", &blob); // Valid encrypted blob — uncompressed. let blob_uc = soliton::storage::encrypt_dm_queue_blob( &key1, b"uncompressed seed", &recipient_fp, "fuzz-batch", false, ).unwrap(); write(dir, "valid_uncompressed", &blob_uc); // Truncated blob (first 16 bytes of valid blob). write(dir, "truncated", &blob[..16.min(blob.len())]); // Empty input. write(dir, "empty", &[]); // All-zero blob (exercises version routing / parsing failures). write(dir, "all_zero", &[0u8; 64]); } fn gen_argon2_params() { let dir = Path::new("corpus/fuzz_argon2_params"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_argon2_params ==="); // Wire layout: [m_cost (4 LE)] [t_cost (4 LE)] [p_cost (4 LE)] [out_len (2 LE)] // [salt_len (1)] [salt (..)] [password (..)] // Valid OWASP_MIN params: m_cost=19456, t_cost=2, p_cost=1, out_len=32, salt=16 bytes. let mut seed = Vec::new(); seed.extend_from_slice(&19456u32.to_le_bytes()); // m_cost (capped to 1024 by fuzzer) seed.extend_from_slice(&2u32.to_le_bytes()); // t_cost seed.extend_from_slice(&1u32.to_le_bytes()); // p_cost seed.extend_from_slice(&32u16.to_le_bytes()); // out_len seed.push(16); // salt_len seed.extend_from_slice(&[0xAA; 16]); // salt seed.extend_from_slice(b"password123"); // password write(dir, "owasp_min", &seed); // Edge: zero-length password. let mut seed2 = Vec::new(); seed2.extend_from_slice(&1024u32.to_le_bytes()); seed2.extend_from_slice(&1u32.to_le_bytes()); seed2.extend_from_slice(&1u32.to_le_bytes()); seed2.extend_from_slice(&32u16.to_le_bytes()); seed2.push(8); seed2.extend_from_slice(&[0xBB; 8]); write(dir, "empty_password", &seed2); // Edge: minimum salt (8 bytes, argon2 minimum). let mut seed3 = Vec::new(); seed3.extend_from_slice(&512u32.to_le_bytes()); seed3.extend_from_slice(&1u32.to_le_bytes()); seed3.extend_from_slice(&1u32.to_le_bytes()); seed3.extend_from_slice(&64u16.to_le_bytes()); seed3.push(8); seed3.extend_from_slice(&[0xCC; 8]); seed3.extend_from_slice(b"test"); write(dir, "min_salt", &seed3); // Edge: out_len=1 (minimum valid output). let mut seed4 = Vec::new(); seed4.extend_from_slice(&256u32.to_le_bytes()); seed4.extend_from_slice(&1u32.to_le_bytes()); seed4.extend_from_slice(&1u32.to_le_bytes()); seed4.extend_from_slice(&1u16.to_le_bytes()); seed4.push(16); seed4.extend_from_slice(&[0xDD; 16]); seed4.extend_from_slice(b"x"); write(dir, "min_output", &seed4); // Edge: invalid params (m_cost too high, exercises rejection path). let mut seed5 = Vec::new(); seed5.extend_from_slice(&0xFFFFFFFFu32.to_le_bytes()); // huge m_cost seed5.extend_from_slice(&0u32.to_le_bytes()); // t_cost=0 (invalid) seed5.extend_from_slice(&0u32.to_le_bytes()); // p_cost=0 (invalid) seed5.extend_from_slice(&0u16.to_le_bytes()); // out_len=0 (skipped by fuzzer) seed5.push(0); // salt_len=0 write(dir, "invalid_params", &seed5); } fn gen_stream_decrypt() { use soliton::streaming::stream_encrypt_init; let dir = Path::new("corpus/fuzz_stream_decrypt"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_stream_decrypt ==="); let key = [0x42u8; 32]; // Single-chunk uncompressed stream (variable-length framing: 2-byte BE len + chunk). let mut enc = stream_encrypt_init(&key, b"fuzz-aad", false).unwrap(); let header = enc.header(); let chunk = enc.encrypt_chunk(b"hello-fuzz", true).unwrap(); let mut seed = Vec::new(); seed.extend_from_slice(&header); seed.extend_from_slice(&(chunk.len() as u16).to_be_bytes()); seed.extend_from_slice(&chunk); write(dir, "single_uncompressed", &seed); // Single-chunk compressed stream. let mut enc_c = stream_encrypt_init(&key, b"fuzz-aad", true).unwrap(); let header_c = enc_c.header(); let chunk_c = enc_c.encrypt_chunk(b"aaaaaaaaaa", true).unwrap(); let mut seed_c = Vec::new(); seed_c.extend_from_slice(&header_c); seed_c.extend_from_slice(&(chunk_c.len() as u16).to_be_bytes()); seed_c.extend_from_slice(&chunk_c); write(dir, "single_compressed", &seed_c); } fn gen_stream_encrypt_decrypt() { let dir = Path::new("corpus/fuzz_stream_encrypt_decrypt"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_stream_encrypt_decrypt ==="); // Format: flags(1) + plaintext. Bit 0 = compress. write(dir, "small_uncompressed", &[0x00, 0x41, 0x42, 0x43]); write(dir, "small_compressed", &[0x01, 0x41, 0x41, 0x41, 0x41, 0x41]); write(dir, "empty_uncompressed", &[0x00]); write(dir, "empty_compressed", &[0x01]); } fn gen_stream_encrypt_at() { let dir = Path::new("corpus/fuzz_stream_encrypt_at"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_stream_encrypt_at ==="); // Format: flags(1) + plaintext. Bit 0 = compress, bit 1 = reversed order. write(dir, "small_forward", &[0x00, 0x41, 0x42, 0x43]); write(dir, "small_reversed", &[0x02, 0x41, 0x42, 0x43]); write(dir, "compressed_forward", &[0x01, 0x41, 0x41, 0x41, 0x41]); write(dir, "compressed_reversed", &[0x03, 0x41, 0x41, 0x41, 0x41]); } fn gen_stream_decrypt_at() { use soliton::streaming::stream_encrypt_init; let dir = Path::new("corpus/fuzz_stream_decrypt_at"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_stream_decrypt_at ==="); let key = [0x42u8; 32]; // Format: header(26) + (index(8 BE) + chunk)* pairs. let mut enc = stream_encrypt_init(&key, b"fuzz-at-aad", false).unwrap(); let header = enc.header(); let chunk = enc.encrypt_chunk(b"decrypt-at-seed", true).unwrap(); let mut seed = Vec::new(); seed.extend_from_slice(&header); seed.extend_from_slice(&0u64.to_be_bytes()); seed.extend_from_slice(&chunk); write(dir, "single_chunk_idx0", &seed); } fn gen_capi_stream_decrypt() { use soliton::streaming::stream_encrypt_init; let dir = Path::new("../../soliton_capi/fuzz/corpus/fuzz_capi_stream_decrypt"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_capi_stream_decrypt (CAPI) ==="); let key = [0x42u8; 32]; // CAPI fuzz target uses fixed 2048-byte slices, so provide a valid // single-chunk stream that fits within one slice. let mut enc = stream_encrypt_init(&key, b"", false).unwrap(); let header = enc.header(); let chunk = enc.encrypt_chunk(b"capi-fuzz-seed", true).unwrap(); let mut seed = Vec::new(); seed.extend_from_slice(&header); seed.extend_from_slice(&chunk); write(dir, "single_uncompressed", &seed); } fn gen_capi_dm_queue_decrypt() { let dir = Path::new("../../soliton_capi/fuzz/corpus/fuzz_capi_dm_queue_decrypt"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_capi_dm_queue_decrypt (CAPI) ==="); let key = StorageKey::new(1, [0x42; 32]).unwrap(); let recipient_fp: [u8; 32] = [0xAA; 32]; // Input format: fp_len (1 byte) || blob (rest). // fp_len=32 exercises the valid path. let blob = soliton::storage::encrypt_dm_queue_blob( &key, b"capi-dm-seed", &recipient_fp, "fuzz-batch", true, ).unwrap(); let mut seed = vec![32u8]; // fp_len = 32 (valid) seed.extend_from_slice(&blob); write(dir, "valid_compressed", &seed); // fp_len=0 exercises the check_len! guard. let mut seed_bad_fp = vec![0u8]; seed_bad_fp.extend_from_slice(&blob); write(dir, "invalid_fp_len", &seed_bad_fp); } fn gen_capi_stream_decrypt_at() { use soliton::streaming::stream_encrypt_init; let dir = Path::new("../../soliton_capi/fuzz/corpus/fuzz_capi_stream_decrypt_at"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_capi_stream_decrypt_at (CAPI) ==="); let key = [0x42u8; 32]; // Input format: header (26 bytes) || index (8 bytes LE) || chunk data. let enc = stream_encrypt_init(&key, b"", false).unwrap(); let header = enc.header(); let chunk = enc.encrypt_chunk_at(0, true, b"capi-at-seed").unwrap(); let mut seed = Vec::new(); seed.extend_from_slice(&header); seed.extend_from_slice(&0u64.to_le_bytes()); seed.extend_from_slice(&chunk); write(dir, "index_0_final", &seed); // Large index to exercise index arithmetic. let enc2 = stream_encrypt_init(&key, b"", false).unwrap(); let header2 = enc2.header(); let chunk2 = enc2.encrypt_chunk_at(u64::MAX - 1, true, b"max-idx").unwrap(); let mut seed2 = Vec::new(); seed2.extend_from_slice(&header2); seed2.extend_from_slice(&(u64::MAX - 1).to_le_bytes()); seed2.extend_from_slice(&chunk2); write(dir, "index_max_minus_1", &seed2); } fn gen_capi_stream_encrypt_at() { let dir = Path::new("../../soliton_capi/fuzz/corpus/fuzz_capi_stream_encrypt_at"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_capi_stream_encrypt_at (CAPI) ==="); // Input format: index (8 bytes LE) || is_last (1 byte) || plaintext. // Index 0, non-final, small plaintext. let mut seed = Vec::new(); seed.extend_from_slice(&0u64.to_le_bytes()); seed.push(0x00); // is_last = false seed.extend_from_slice(b"capi-encrypt-at-seed"); write(dir, "index_0_nonfinal", &seed); // Index 0, final, empty plaintext. let mut seed2 = Vec::new(); seed2.extend_from_slice(&0u64.to_le_bytes()); seed2.push(0x01); // is_last = true write(dir, "index_0_final_empty", &seed2); // Large index. let mut seed3 = Vec::new(); seed3.extend_from_slice(&(u64::MAX / 2).to_le_bytes()); seed3.push(0x01); seed3.extend_from_slice(b"midpoint"); write(dir, "index_midpoint_final", &seed3); } fn gen_ratchet_state_machine() { let dir = Path::new("corpus/fuzz_ratchet_state_machine"); fs::create_dir_all(dir).unwrap(); println!("=== fuzz_ratchet_state_machine ==="); // Each byte is an action opcode: // 0x00..0x3F Alice sends // 0x40..0x7F Bob sends // 0x80..0xBF Deliver Alice's outbox[idx] to Bob // 0xC0..0xFF Deliver Bob's outbox[idx] to Alice // Seed 1: Simple A→B→A exchange (3 direction changes, 3 KEM ratchet steps). // Alice sends (0x00), deliver to Bob (0x80=outbox[0]), Bob sends (0x40), // deliver to Alice (0xC0=outbox[0]), Alice sends (0x01), deliver to Bob (0x81=outbox[1]). write(dir, "simple_exchange", &[0x00, 0x80, 0x40, 0xC0, 0x01, 0x81]); // Seed 2: Out-of-order delivery — Alice sends 3, deliver in reverse order. write(dir, "out_of_order", &[0x00, 0x01, 0x02, 0x82, 0x81, 0x80]); // Seed 3: Duplicate delivery — Alice sends 1, deliver twice (second triggers DuplicateMessage). write(dir, "duplicate", &[0x00, 0x80, 0x80]); // Seed 4: Many messages same direction (tests recv_seen growth without ratchet step). let mut many_sends: Vec = (0x00..0x10).collect(); // Alice sends 16 msgs many_sends.extend((0x80..0x90).collect::>()); // Deliver all 16 to Bob write(dir, "many_same_direction", &many_sends); // Seed 5: Rapid direction changes (each send triggers a KEM ratchet step). write(dir, "rapid_ratchet", &[ 0x00, 0x80, // A sends, deliver to B 0x40, 0xC0, // B sends, deliver to A 0x01, 0x81, // A sends, deliver to B 0x41, 0xC1, // B sends, deliver to A 0x02, 0x82, // A sends, deliver to B 0x42, 0xC2, // B sends, deliver to A ]); // Seed 6: Previous-epoch late delivery — A sends 2, deliver only first, // B replies (ratchet), then deliver A's second (previous epoch). write(dir, "prev_epoch_late", &[ 0x00, 0x01, // A sends 2 msgs 0x80, // Deliver first to B 0x40, 0xC0, // B replies, A receives (ratchet step) 0x02, 0x82, // A sends new epoch msg, deliver to B (second ratchet) 0x81, // NOW deliver A's old msg — prev_recv_epoch_key path ]); // Seed 7: Empty — exercises init-only path (no actions). write(dir, "empty", &[]); // Seed 8: Long sequence — 64 interleaved sends and deliveries. let mut long_seq = Vec::new(); for i in 0u8..32 { long_seq.push(i % 2 * 0x40 + (i / 2)); // alternating A/B sends long_seq.push(0x80 + (i / 2) * (1 - i % 2) + 0x40 * (i % 2)); // deliver } write(dir, "long_interleaved", &long_seq); } fn main() { gen_ratchet_encrypt(); gen_storage_decrypt_blob(); gen_ratchet_decrypt(); gen_ratchet_decrypt_stateful(); gen_identity_from_bytes(); gen_ed25519_verify(); gen_hybrid_verify(); gen_decrypt_first_message(); gen_kex_receive_session(); gen_storage_encrypt_blob(); gen_auth_respond(); gen_kex_verify_bundle(); gen_verification_phrase(); gen_ratchet_roundtrip(); gen_xwing_roundtrip(); gen_identity_sign_verify(); gen_session_init_roundtrip(); gen_call_derive(); gen_auth_verify(); gen_ratchet_from_bytes_epoch(); gen_kex_decode_receive(); gen_dm_queue_roundtrip(); gen_dm_queue_decrypt_blob(); gen_argon2_params(); gen_stream_decrypt(); gen_stream_encrypt_decrypt(); gen_stream_encrypt_at(); gen_stream_decrypt_at(); gen_capi_stream_decrypt(); gen_ratchet_state_machine(); gen_capi_ratchet_from_bytes(); gen_capi_storage_decrypt(); gen_capi_decode_session_init(); gen_capi_dm_queue_decrypt(); gen_capi_stream_decrypt_at(); gen_capi_stream_encrypt_at(); println!("\nDone. Seed corpus generated."); }