use soliton::identity::{GeneratedIdentity, generate_identity};
use soliton::primitives::{mlkem, xwing};
use soliton::storage::{StorageKey, StorageKeyRing};
use std::mem::ManuallyDrop;
#[cfg(target_os = "linux")]
fn read_process_bytes(ptr: *const u8, len: usize) -> Vec<u8> {
use std::io::{Read, Seek, SeekFrom};
let mut f = std::fs::File::open("/proc/self/mem").unwrap();
f.seek(SeekFrom::Start(ptr as u64)).unwrap();
let mut buf = vec![0u8; len];
f.read_exact(&mut buf).unwrap();
buf
}
#[cfg(target_os = "linux")]
struct ProcMemReader {
file: std::fs::File,
buf: Vec<u8>,
}
#[cfg(target_os = "linux")]
impl ProcMemReader {
fn new(len: usize) -> Self {
Self {
file: std::fs::File::open("/proc/self/mem").unwrap(),
buf: vec![0u8; len],
}
}
fn read_at(&mut self, ptr: *const u8) -> &[u8] {
use std::io::{Read, Seek, SeekFrom};
self.file.seek(SeekFrom::Start(ptr as u64)).unwrap();
self.file.read_exact(&mut self.buf).unwrap();
&self.buf
}
}
#[cfg(target_os = "linux")]
fn assert_heap_zeroed_with(reader: &mut ProcMemReader, ptr: *const u8, len: usize, label: &str) {
let after = reader.read_at(ptr);
let skip = 64.min(len);
let check = &after[skip..];
assert!(
check.iter().all(|&b| b == 0),
"{label}: found non-zero bytes after skip={skip} in {len}-byte allocation.\n\
First non-zero at offset {}: 0x{:02x}",
skip + check.iter().position(|&b| b != 0).unwrap_or(0),
check.iter().find(|&&b| b != 0).unwrap_or(&0),
);
}
#[cfg(target_os = "linux")]
fn assert_heap_zeroed(ptr: *const u8, len: usize, label: &str) {
let after = read_process_bytes(ptr, len);
let skip = 64.min(len);
let check = &after[skip..];
assert!(
check.iter().all(|&b| b == 0),
"{label}: found non-zero bytes after skip={skip} in {len}-byte allocation.\n\
First non-zero at offset {}: 0x{:02x}",
skip + check.iter().position(|&b| b != 0).unwrap_or(0),
check.iter().find(|&&b| b != 0).unwrap_or(&0),
);
}
#[test]
#[cfg(all(target_os = "linux", not(miri)))]
fn xwing_sk_zeroized_on_drop() {
let (_, sk) = xwing::keygen().unwrap();
let mut md = ManuallyDrop::new(sk);
let ptr = md.as_bytes().as_ptr();
let len = md.as_bytes().len();
assert_eq!(len, 2432, "unexpected xwing SK size");
let mut reader = ProcMemReader::new(len);
let before = read_process_bytes(ptr, len);
assert!(
before.iter().any(|&b| b != 0),
"xwing SK was all zeros before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
assert_heap_zeroed_with(&mut reader, ptr, len, "xwing::SecretKey");
}
#[test]
#[cfg(all(target_os = "linux", not(miri)))]
fn identity_sk_zeroized_on_drop() {
let GeneratedIdentity { secret_key: sk, .. } = generate_identity().unwrap();
let mut md = ManuallyDrop::new(sk);
let ptr = md.as_bytes().as_ptr();
let len = md.as_bytes().len();
assert_eq!(len, 2496, "unexpected identity SK size");
let mut reader = ProcMemReader::new(len);
let before = read_process_bytes(ptr, len);
assert!(
before.iter().any(|&b| b != 0),
"identity SK was all zeros before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
assert_heap_zeroed_with(&mut reader, ptr, len, "IdentitySecretKey");
}
#[test]
#[cfg(not(miri))]
fn xwing_shared_secret_zeroized_on_drop() {
let (pk, _) = xwing::keygen().unwrap();
let (_, ss) = xwing::encapsulate(&pk).unwrap();
let mut md = ManuallyDrop::new(ss);
let ptr = md.as_bytes().as_ptr();
let before = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert!(
before.iter().any(|&b| b != 0),
"xwing SS was all zeros before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
let after = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert_eq!(
after, [0u8; 32],
"xwing::SharedSecret not fully zeroized after drop"
);
}
#[test]
#[cfg(not(miri))]
fn mlkem_shared_secret_zeroized_on_drop() {
let (pk, _) = mlkem::keygen().unwrap();
let (_, ss) = mlkem::encapsulate(&pk).unwrap();
let mut md = ManuallyDrop::new(ss);
let ptr = md.as_bytes().as_ptr();
let before = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert!(
before.iter().any(|&b| b != 0),
"mlkem SS was all zeros before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
let after = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert_eq!(
after, [0u8; 32],
"mlkem::SharedSecret not fully zeroized after drop"
);
}
#[test]
#[cfg(all(target_os = "linux", not(miri)))]
fn storage_keyring_zeroized_on_drop() {
use soliton::primitives::random;
let key1 = StorageKey::new(1, random::random_array()).unwrap();
let key2 = StorageKey::new(2, random::random_array()).unwrap();
let mut ring = StorageKeyRing::new(key1).unwrap();
ring.add_key(key2, true).unwrap();
let k1_ptr = ring.get_key(1).unwrap().key().as_ptr();
let k2_ptr = ring.get_key(2).unwrap().key().as_ptr();
let before1 = read_process_bytes(k1_ptr, 32);
let before2 = read_process_bytes(k2_ptr, 32);
assert!(
before1.iter().any(|&b| b != 0),
"key1 was all zeros before drop"
);
assert!(
before2.iter().any(|&b| b != 0),
"key2 was all zeros before drop"
);
let mut md = ManuallyDrop::new(ring);
unsafe { ManuallyDrop::drop(&mut md) };
let after1 = read_process_bytes(k1_ptr, 32);
let after2 = read_process_bytes(k2_ptr, 32);
let k1_zero = after1.iter().all(|&b| b == 0);
let k2_zero = after2.iter().all(|&b| b == 0);
assert!(
k1_zero || k2_zero,
"StorageKeyRing: neither key was fully zeroized after drop.\n\
key1 residue: {:?}\nkey2 residue: {:?}",
&after1[..8],
&after2[..8],
);
}
#[test]
#[cfg(not(miri))]
fn call_keys_drop_zeroizes() {
use soliton::call::derive_call_keys;
let rk = [0x01u8; 32];
let ss = [0x02u8; 32];
let call_id = [0x03u8; 16];
let fp_lo = [0x00u8; 32];
let fp_hi = [0xFFu8; 32];
let keys = derive_call_keys(&rk, &ss, &call_id, &fp_lo, &fp_hi).unwrap();
let mut md = ManuallyDrop::new(keys);
let send_ptr = md.send_key().as_ptr();
let recv_ptr = md.recv_key().as_ptr();
let send_before = unsafe { std::ptr::read_volatile(send_ptr as *const [u8; 32]) };
let recv_before = unsafe { std::ptr::read_volatile(recv_ptr as *const [u8; 32]) };
assert_ne!(send_before, [0u8; 32], "send_key was zero before drop");
assert_ne!(recv_before, [0u8; 32], "recv_key was zero before drop");
unsafe { ManuallyDrop::drop(&mut md) };
let send_after = unsafe { std::ptr::read_volatile(send_ptr as *const [u8; 32]) };
let recv_after = unsafe { std::ptr::read_volatile(recv_ptr as *const [u8; 32]) };
assert_eq!(
send_after, [0u8; 32],
"CallKeys::send_key not zeroized after drop"
);
assert_eq!(
recv_after, [0u8; 32],
"CallKeys::recv_key not zeroized after drop"
);
}
#[test]
#[cfg(not(miri))]
fn call_keys_drop_after_advance_zeroizes() {
use soliton::call::derive_call_keys;
let rk = [0x01u8; 32];
let ss = [0x02u8; 32];
let call_id = [0x03u8; 16];
let fp_lo = [0x00u8; 32];
let fp_hi = [0xFFu8; 32];
let mut keys = derive_call_keys(&rk, &ss, &call_id, &fp_lo, &fp_hi).unwrap();
let old_send = *keys.send_key();
let old_recv = *keys.recv_key();
keys.advance().unwrap();
assert_ne!(keys.send_key(), &old_send, "advance didn't change send_key");
assert_ne!(keys.recv_key(), &old_recv, "advance didn't change recv_key");
let mut md = ManuallyDrop::new(keys);
let send_ptr = md.send_key().as_ptr();
let recv_ptr = md.recv_key().as_ptr();
unsafe { ManuallyDrop::drop(&mut md) };
let send_after = unsafe { std::ptr::read_volatile(send_ptr as *const [u8; 32]) };
let recv_after = unsafe { std::ptr::read_volatile(recv_ptr as *const [u8; 32]) };
assert_eq!(
send_after, [0u8; 32],
"CallKeys::send_key not zeroized after advance+drop"
);
assert_eq!(
recv_after, [0u8; 32],
"CallKeys::recv_key not zeroized after advance+drop"
);
}
#[test]
#[cfg(not(miri))]
fn stream_encryptor_drop_zeroizes() {
use soliton::streaming::stream_encrypt_init;
let key = [0x42u8; 32];
let enc = stream_encrypt_init(&key, b"", false).unwrap();
let mut md = ManuallyDrop::new(enc);
#[allow(deprecated)]
let key_ptr = md.key_ptr();
let before = unsafe { std::ptr::read_volatile(key_ptr as *const [u8; 32]) };
assert_eq!(before, [0x42u8; 32], "encryptor key wrong before drop");
unsafe { ManuallyDrop::drop(&mut md) };
let after = unsafe { std::ptr::read_volatile(key_ptr as *const [u8; 32]) };
assert_eq!(
after, [0u8; 32],
"StreamEncryptor::key not zeroized after drop"
);
}
#[test]
#[cfg(not(miri))]
fn stream_decryptor_drop_zeroizes() {
use soliton::streaming::{stream_decrypt_init, stream_encrypt_init};
let key = [0x42u8; 32];
let enc = stream_encrypt_init(&key, b"", false).unwrap();
let header = enc.header();
let dec = stream_decrypt_init(&key, &header, b"").unwrap();
let mut md = ManuallyDrop::new(dec);
#[allow(deprecated)]
let key_ptr = md.key_ptr();
let before = unsafe { std::ptr::read_volatile(key_ptr as *const [u8; 32]) };
assert_eq!(before, [0x42u8; 32], "decryptor key wrong before drop");
unsafe { ManuallyDrop::drop(&mut md) };
let after = unsafe { std::ptr::read_volatile(key_ptr as *const [u8; 32]) };
assert_eq!(
after, [0u8; 32],
"StreamDecryptor::key not zeroized after drop"
);
}
#[test]
#[cfg(not(miri))]
fn initiated_session_drop_zeroizes() {
use soliton::constants;
use soliton::kex::{PreKeyBundle, initiate_session, sign_prekey, verify_bundle};
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: constants::CRYPTO_VERSION.to_string(),
spk_pub: spk_pk,
spk_id: 1,
spk_sig,
opk_pub: None,
opk_id: None,
};
let vb = verify_bundle(bundle, &bob_pk).unwrap();
let initiated = initiate_session(&alice_pk, &alice_sk, &vb).unwrap();
let mut md = ManuallyDrop::new(initiated);
#[allow(deprecated)]
let rk_ptr = md.root_key_ptr();
#[allow(deprecated)]
let ck_ptr = md.initial_chain_key_ptr();
let rk_before = unsafe { std::ptr::read_volatile(rk_ptr as *const [u8; 32]) };
assert!(
rk_before.iter().any(|&b| b != 0),
"root_key was zero before drop"
);
let ck_before = unsafe { std::ptr::read_volatile(ck_ptr as *const [u8; 32]) };
assert!(
ck_before.iter().any(|&b| b != 0),
"initial_chain_key was zero before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
let rk_after = unsafe { std::ptr::read_volatile(rk_ptr as *const [u8; 32]) };
let ck_after = unsafe { std::ptr::read_volatile(ck_ptr as *const [u8; 32]) };
assert_eq!(
rk_after, [0u8; 32],
"InitiatedSession::root_key not zeroized after drop"
);
assert_eq!(
ck_after, [0u8; 32],
"InitiatedSession::initial_chain_key not zeroized after drop"
);
}
#[test]
#[cfg(not(miri))]
fn received_session_drop_zeroizes() {
use soliton::constants;
use soliton::kex::{
PreKeyBundle, initiate_session, receive_session, sign_prekey, verify_bundle,
};
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: constants::CRYPTO_VERSION.to_string(),
spk_pub: spk_pk,
spk_id: 1,
spk_sig,
opk_pub: None,
opk_id: None,
};
let vb = verify_bundle(bundle, &bob_pk).unwrap();
let initiated = initiate_session(&alice_pk, &alice_sk, &vb).unwrap();
let received = receive_session(
&bob_pk,
&bob_sk,
&alice_pk,
&initiated.session_init,
&initiated.sender_sig,
&spk_sk,
None,
)
.unwrap();
let mut md = ManuallyDrop::new(received);
#[allow(deprecated)]
let rk_ptr = md.root_key_ptr();
#[allow(deprecated)]
let ck_ptr = md.initial_chain_key_ptr();
let rk_before = unsafe { std::ptr::read_volatile(rk_ptr as *const [u8; 32]) };
assert!(
rk_before.iter().any(|&b| b != 0),
"root_key was zero before drop"
);
let ck_before = unsafe { std::ptr::read_volatile(ck_ptr as *const [u8; 32]) };
assert!(
ck_before.iter().any(|&b| b != 0),
"initial_chain_key was zero before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
let rk_after = unsafe { std::ptr::read_volatile(rk_ptr as *const [u8; 32]) };
let ck_after = unsafe { std::ptr::read_volatile(ck_ptr as *const [u8; 32]) };
assert_eq!(
rk_after, [0u8; 32],
"ReceivedSession::root_key not zeroized after drop"
);
assert_eq!(
ck_after, [0u8; 32],
"ReceivedSession::initial_chain_key not zeroized after drop"
);
}
#[test]
#[cfg(not(miri))]
fn ratchet_state_drop_zeroizes_key_material() {
let (ek_pk, ek_sk) = xwing::keygen().unwrap();
let rk = [0x11u8; 32];
let ck = [0x22u8; 32];
let fp_a = [0xAAu8; 32];
let fp_b = [0xBBu8; 32];
let alice =
soliton::ratchet::RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk, ek_sk).unwrap();
let mut md = ManuallyDrop::new(alice);
#[allow(deprecated)]
let rk_ptr = md.root_key_ptr();
#[allow(deprecated)]
let sek_ptr = md.send_epoch_key_ptr();
#[allow(deprecated)]
let rek_ptr = md.recv_epoch_key_ptr();
let rk_before = unsafe { std::ptr::read_volatile(rk_ptr as *const [u8; 32]) };
let sek_before = unsafe { std::ptr::read_volatile(sek_ptr as *const [u8; 32]) };
assert!(
rk_before.iter().any(|&b| b != 0),
"root_key was zero before drop"
);
assert!(
sek_before.iter().any(|&b| b != 0),
"send_epoch_key was zero before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
let rk_after = unsafe { std::ptr::read_volatile(rk_ptr as *const [u8; 32]) };
let sek_after = unsafe { std::ptr::read_volatile(sek_ptr as *const [u8; 32]) };
let rek_after = unsafe { std::ptr::read_volatile(rek_ptr as *const [u8; 32]) };
assert_eq!(
rk_after, [0u8; 32],
"RatchetState::root_key not zeroized after drop"
);
assert_eq!(
sek_after, [0u8; 32],
"RatchetState::send_epoch_key not zeroized after drop"
);
assert_eq!(
rek_after, [0u8; 32],
"RatchetState::recv_epoch_key not zeroized after drop"
);
}
fn make_ratchet_pair() -> (
soliton::ratchet::RatchetState,
soliton::ratchet::RatchetState,
[u8; 32],
[u8; 32],
) {
use soliton::primitives::random;
let (ek_pk, ek_sk) = xwing::keygen().unwrap();
let rk: [u8; 32] = random::random_array();
let ck: [u8; 32] = random::random_array();
let fp_a = [0xAAu8; 32];
let fp_b = [0xBBu8; 32];
let alice =
soliton::ratchet::RatchetState::init_alice(rk, ck, fp_a, fp_b, ek_pk.clone(), ek_sk)
.unwrap();
let bob = soliton::ratchet::RatchetState::init_bob(rk, ck, fp_b, fp_a, ek_pk).unwrap();
(alice, bob, fp_a, fp_b)
}
#[test]
fn ratchet_reset_then_encrypt_fails() {
let (mut alice, _, _fp_a, _fp_b) = make_ratchet_pair();
assert!(alice.encrypt(b"test").is_ok());
alice.reset();
assert!(alice.encrypt(b"test").is_err());
}
#[test]
fn ratchet_recv_seen_cleared_on_reset() {
let (mut alice, mut bob, _fp_a, _fp_b) = make_ratchet_pair();
let _enc0 = alice.encrypt(b"msg0").unwrap();
let _enc1 = alice.encrypt(b"msg1").unwrap();
let enc2 = alice.encrypt(b"msg2").unwrap();
bob.decrypt(&enc2.header, &enc2.ciphertext).unwrap();
bob.reset();
assert!(bob.encrypt(b"test").is_err());
}
#[test]
fn ratchet_aead_failure_no_state_leak() {
let (mut alice, mut bob, _fp_a, _fp_b) = make_ratchet_pair();
let enc = alice.encrypt(b"good message").unwrap();
let mut bad_ct = enc.ciphertext.clone();
bad_ct[0] ^= 0xFF;
assert!(bob.decrypt(&enc.header, &bad_ct).is_err());
let pt = bob.decrypt(&enc.header, &enc.ciphertext).unwrap();
assert_eq!(&*pt, b"good message");
}
#[test]
#[cfg(not(miri))]
fn zeroizing_array_drop_zeros() {
use zeroize::Zeroizing;
let secret = Zeroizing::new([0xAAu8; 32]);
let mut md = ManuallyDrop::new(secret);
let ptr = md.as_ptr();
let before = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert_eq!(before, [0xAAu8; 32]);
unsafe { ManuallyDrop::drop(&mut md) };
let after = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert_eq!(
after, [0u8; 32],
"Zeroizing<[u8; 32]> not zeroed after drop"
);
}
#[test]
#[cfg(all(target_os = "linux", not(miri)))]
fn zeroizing_vec_drop_zeros() {
use zeroize::Zeroizing;
let secret = Zeroizing::new(vec![0xBBu8; 256]);
let mut md = ManuallyDrop::new(secret);
let ptr = md.as_ptr();
let len = md.len();
let before = read_process_bytes(ptr, len);
assert!(before.iter().all(|&b| b == 0xBB));
unsafe { ManuallyDrop::drop(&mut md) };
assert_heap_zeroed(ptr, len, "Zeroizing<Vec<u8>>");
}
#[test]
#[cfg(not(miri))]
fn storage_key_zeroized_on_drop() {
use soliton::primitives::random;
let key = StorageKey::new(1, random::random_array()).unwrap();
let mut md = ManuallyDrop::new(key);
let ptr = md.key().as_ptr();
let before = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert!(
before.iter().any(|&b| b != 0),
"StorageKey was zero before drop"
);
unsafe { ManuallyDrop::drop(&mut md) };
let after = unsafe { std::ptr::read_volatile(ptr as *const [u8; 32]) };
assert_eq!(after, [0u8; 32], "StorageKey::key not zeroized after drop");
}
#[test]
#[cfg(not(miri))]
fn copy_gap_pattern_validated() {
use zeroize::Zeroize;
let mut original = [0xCCu8; 32];
let original_ptr = original.as_ptr();
let wrapper = zeroize::Zeroizing::new(original);
assert_eq!(
unsafe { std::ptr::read_volatile(original_ptr as *const [u8; 32]) },
[0xCCu8; 32],
"original should still hold secret after copy into Zeroizing",
);
original.zeroize();
assert_eq!(
unsafe { std::ptr::read_volatile(original_ptr as *const [u8; 32]) },
[0u8; 32],
"original not zeroized after explicit .zeroize()",
);
assert_eq!(*wrapper, [0xCCu8; 32]);
let mut md = ManuallyDrop::new(wrapper);
let wrapper_ptr = md.as_ptr();
unsafe { ManuallyDrop::drop(&mut md) };
let after = unsafe { std::ptr::read_volatile(wrapper_ptr as *const [u8; 32]) };
assert_eq!(after, [0u8; 32], "Zeroizing wrapper not zeroed after drop");
}