libsoliton/soliton/fuzz/gen_corpus.rs
Kamal Tufekcic 1d99048c95
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
initial commit
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
2026-04-02 23:48:10 +03:00

1399 lines
58 KiB
Rust

//! 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<u8> {
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<u8> = (0x00..0x10).collect(); // Alice sends 16 msgs
many_sends.extend((0x80..0x90).collect::<Vec<u8>>()); // 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.");
}