//! CAPI integration tests. //! //! Test categories: //! //! - **Error paths** — null pointer guards, zero-length guards, co-presence //! guards, and output-zeroing verification. MIRI-safe for all 51 CAPI //! functions; these are the primary target of this suite. //! //! - **PQ-free happy paths** — round-trips using zeroed/minimal key material. //! Relies on `xwing::from_bytes` and `IdentityPublicKey::from_bytes` being //! length-only (no structural validation), so zeroed bytes are accepted. //! Alice's first ratchet encrypt sends `kem_ct = None` (no PQ step). //! Bob's first decrypt matches `recv_ratchet_pk`, so no KEM decapsulation. //! All MIRI-safe. //! //! - **`mod pq`** — PQ-dependent happy paths (keygen, encap, decap, KEX). //! Excluded from the MIRI nextest profile via //! `- test(/^capi_tests::pq::/)`. use std::ffi::CString; use std::mem::{MaybeUninit, size_of}; use std::ptr; use soliton_capi::*; // ── Error code shorthands ──────────────────────────────────────────────────── const OK: i32 = SolitonError::Ok as i32; const E_NULL: i32 = SolitonError::NullPointer as i32; const E_LEN: i32 = SolitonError::InvalidLength as i32; const E_DATA: i32 = SolitonError::InvalidData as i32; const E_AEAD: i32 = SolitonError::AeadFailed as i32; const E_VER: i32 = SolitonError::VerificationFailed as i32; const E_BUNDLE: i32 = SolitonError::BundleVerificationFailed as i32; const E_UNSUP_VER: i32 = SolitonError::UnsupportedVersion as i32; const E_CONCURRENT: i32 = SolitonError::ConcurrentAccess as i32; // ── Key / buffer size shorthands ───────────────────────────────────────────── const PK: usize = SOLITON_PUBLIC_KEY_SIZE; const SK: usize = SOLITON_SECRET_KEY_SIZE; const XPKZ: usize = SOLITON_XWING_PK_SIZE; const XSKZ: usize = SOLITON_XWING_SK_SIZE; const XCTZ: usize = SOLITON_XWING_CT_SIZE; const FP: usize = SOLITON_FINGERPRINT_SIZE; const HSIG: usize = SOLITON_HYBRID_SIG_SIZE; const NONCE: usize = SOLITON_AEAD_NONCE_SIZE; // ── Test helpers ───────────────────────────────────────────────────────────── /// Returns true iff every byte of the slice is zero. fn is_zeroed(buf: &[u8]) -> bool { buf.iter().all(|&b| b == 0) } /// Fill a stack buffer with a known non-zero sentinel before passing to a /// failing CAPI call, so we can assert the output was actually zeroed. fn fill_nonzero() -> [u8; N] { [0xAB; N] } /// Construct a valid (zeroed-bytes) Alice ratchet state for tests. /// /// Uses `[0u8; XPKZ]` / `[0u8; XSKZ]` as the ephemeral X-Wing keys. /// `from_bytes` is length-only, so no PQ keygen is needed. /// Fingerprints: local=0x01, remote=0x02 (must differ to pass init guard). unsafe fn make_alice() -> *mut SolitonRatchet { let rk = [0x11u8; 32]; let ck = [0x22u8; 32]; let local_fp = [0x01u8; 32]; let remote_fp = [0x02u8; 32]; let ek_pk = [0u8; XPKZ]; // First 32 bytes are the X25519 secret key — must be non-zero to pass // the from_bytes zero-check on the send_ratchet_sk X25519 portion. let mut ek_sk = [0u8; XSKZ]; ek_sk[..32].fill(0x33); let mut out: *mut SolitonRatchet = ptr::null_mut(); let rc = unsafe { soliton_ratchet_init_alice( rk.as_ptr(), 32, ck.as_ptr(), 32, local_fp.as_ptr(), 32, remote_fp.as_ptr(), 32, ek_pk.as_ptr(), XPKZ, ek_sk.as_ptr(), XSKZ, &mut out, ) }; assert_eq!(rc, OK); assert!(!out.is_null()); out } /// Construct a valid (zeroed-bytes) Bob ratchet state for tests. /// /// `peer_ek` is the same zeroed 1216-byte blob used by Alice. /// Fingerprints: local=0x02, remote=0x01 (mirrored from Alice). unsafe fn make_bob() -> *mut SolitonRatchet { let rk = [0x11u8; 32]; let ck = [0x22u8; 32]; let local_fp = [0x02u8; 32]; let remote_fp = [0x01u8; 32]; let peer_ek = [0u8; XPKZ]; let mut out: *mut SolitonRatchet = ptr::null_mut(); let rc = unsafe { soliton_ratchet_init_bob( rk.as_ptr(), 32, ck.as_ptr(), 32, local_fp.as_ptr(), 32, remote_fp.as_ptr(), 32, peer_ek.as_ptr(), XPKZ, &mut out, ) }; assert_eq!(rc, OK); assert!(!out.is_null()); out } /// Construct a valid storage keyring (version 1, non-zero 32-byte key). unsafe fn make_keyring() -> *mut SolitonKeyRing { let key = [0x33u8; 32]; let mut out: *mut SolitonKeyRing = ptr::null_mut(); let rc = unsafe { soliton_keyring_new(key.as_ptr(), 32, 1, &mut out) }; assert_eq!(rc, OK); assert!(!out.is_null()); out } // ═══════════════════════════════════════════════════════════════════════════ // Version & buf_free // ═══════════════════════════════════════════════════════════════════════════ #[test] fn version_returns_nonnull_cstr() { let ptr = soliton_version(); assert!(!ptr.is_null()); // Must be a valid null-terminated string. let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_str().unwrap() }; assert!(!s.is_empty()); } #[test] fn buf_free_null_ptr_is_noop() { // Must not crash. unsafe { soliton_buf_free(ptr::null_mut()); } } #[test] fn buf_free_null_inner_ptr_is_noop() { // A SolitonBuf with ptr=null is safe to free. let mut buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; unsafe { soliton_buf_free(&mut buf); } } #[test] fn buf_free_double_free_is_safe() { // After the first free, ptr is set to null — second call is a no-op. unsafe { let mut buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let data = b"test"; // Construct a SolitonBuf wrapping an AEAD ciphertext for double-free test. let key = [0u8; 32]; let nonce = [0u8; NONCE]; let rc = soliton_aead_encrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, data.as_ptr(), data.len(), ptr::null(), 0, &mut buf, ); assert_eq!(rc, OK); assert!(!buf.ptr.is_null()); soliton_buf_free(&mut buf); assert!(buf.ptr.is_null()); soliton_buf_free(&mut buf); // second free must not crash } } #[test] fn buf_free_corrupted_len_uses_internal_header() { // RT-70/RT-191: soliton_buf_free uses the internal allocation header // (stored at ptr-8, inaccessible to callers) for deallocation — not the // caller-visible `len`. Corrupting `len` must not cause heap corruption. unsafe { let key = [0u8; 32]; let nonce = [0u8; NONCE]; let data = b"test payload for internal header check"; let mut buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_aead_encrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, data.as_ptr(), data.len(), ptr::null(), 0, &mut buf, ); assert_eq!(rc, OK); assert!(!buf.ptr.is_null()); let original_len = buf.len; assert!(original_len > 0); // Corrupt the caller-visible len to a different value. buf.len = 1; // Free must use internal header for dealloc — no heap corruption. soliton_buf_free(&mut buf); assert!(buf.ptr.is_null(), "ptr must be nulled after free"); assert_eq!(buf.len, 0, "len must be zeroed after free"); } } // ═══════════════════════════════════════════════════════════════════════════ // random_bytes // ═══════════════════════════════════════════════════════════════════════════ #[test] fn random_bytes_null_buf_returns_null_pointer() { let rc = unsafe { soliton_random_bytes(ptr::null_mut(), 16) }; assert_eq!(rc, E_NULL); } #[test] fn random_bytes_zero_len_is_ok() { let mut buf = [0u8; 0]; let rc = unsafe { soliton_random_bytes(buf.as_mut_ptr(), 0) }; assert_eq!(rc, OK); } #[test] fn random_bytes_fills_buffer() { let mut buf = [0u8; 32]; let rc = unsafe { soliton_random_bytes(buf.as_mut_ptr(), 32) }; assert_eq!(rc, OK); // With overwhelming probability, 32 random bytes are not all zero. // (Probability of failure: 1 in 2^256.) assert_ne!(buf, [0u8; 32]); } // ═══════════════════════════════════════════════════════════════════════════ // zeroize // ═══════════════════════════════════════════════════════════════════════════ #[test] fn zeroize_null_ptr_is_noop() { // Must not crash. unsafe { soliton_zeroize(ptr::null_mut(), 64) }; } #[test] fn zeroize_zero_len_is_noop() { let mut buf = [0xFFu8; 8]; unsafe { soliton_zeroize(buf.as_mut_ptr(), 0) }; assert_eq!(buf, [0xFF; 8]); } #[test] fn zeroize_clears_buffer() { let mut buf = [0xABu8; 64]; unsafe { soliton_zeroize(buf.as_mut_ptr(), buf.len()) }; assert!(is_zeroed(&buf)); } // ═══════════════════════════════════════════════════════════════════════════ // sha3_256 // ═══════════════════════════════════════════════════════════════════════════ #[test] fn sha3_256_null_out_returns_null_pointer() { let data = b"hello"; let rc = unsafe { soliton_sha3_256(data.as_ptr(), data.len(), ptr::null_mut(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn sha3_256_null_data_nonzero_len_returns_null_pointer() { // Output is zeroed before the data-null check fires. let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_sha3_256(ptr::null(), 5, out.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn sha3_256_null_data_zero_len_hashes_empty() { // Null data with len=0 is allowed — hashes the empty string. let mut out = [0u8; 32]; let rc = unsafe { soliton_sha3_256(ptr::null(), 0, out.as_mut_ptr(), 32) }; assert_eq!(rc, OK); // SHA3-256("") = a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a let expected: [u8; 32] = [ 0xa7, 0xff, 0xc6, 0xf8, 0xbf, 0x1e, 0xd7, 0x66, 0x51, 0xc1, 0x47, 0x56, 0xa0, 0x61, 0xd6, 0x62, 0xf5, 0x80, 0xff, 0x4d, 0xe4, 0x3b, 0x49, 0xfa, 0x82, 0xd8, 0x0a, 0x4b, 0x80, 0xf8, 0x43, 0x4a, ]; assert_eq!(out, expected); } #[test] fn sha3_256_known_hash() { let data = b"abc"; let mut out = [0u8; 32]; let rc = unsafe { soliton_sha3_256(data.as_ptr(), data.len(), out.as_mut_ptr(), 32) }; assert_eq!(rc, OK); // SHA3-256("abc") from FIPS 202. let expected: [u8; 32] = [ 0x3a, 0x98, 0x5d, 0xa7, 0x4f, 0xe2, 0x25, 0xb2, 0x04, 0x5c, 0x17, 0x2d, 0x6b, 0xd3, 0x90, 0xbd, 0x85, 0x5f, 0x08, 0x6e, 0x3e, 0x9d, 0x52, 0x5b, 0x46, 0xbf, 0xe2, 0x45, 0x11, 0x43, 0x15, 0x32, ]; assert_eq!(out, expected); } // ═══════════════════════════════════════════════════════════════════════════ // hmac_sha3_256 // ═══════════════════════════════════════════════════════════════════════════ #[test] fn hmac_sha3_256_null_key_returns_null_pointer() { let mut out = [0u8; 32]; let rc = unsafe { soliton_hmac_sha3_256(ptr::null(), 4, ptr::null(), 0, out.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn hmac_sha3_256_null_out_returns_null_pointer() { let key = b"key"; let rc = unsafe { soliton_hmac_sha3_256(key.as_ptr(), key.len(), ptr::null(), 0, ptr::null_mut(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn hmac_sha3_256_null_data_nonzero_len_returns_null_pointer() { let key = b"key"; let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_hmac_sha3_256( key.as_ptr(), key.len(), ptr::null(), 5, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn hmac_sha3_256_empty_key_is_valid() { // RFC 2104 allows zero-length keys (unusual but not prohibited). let key = [0u8; 0]; let data = b"message"; let mut out = [0u8; 32]; let rc = unsafe { soliton_hmac_sha3_256( key.as_ptr(), 0, data.as_ptr(), data.len(), out.as_mut_ptr(), 32, ) }; assert_eq!(rc, OK); assert!(!is_zeroed(&out)); } #[test] fn hmac_sha3_256_known_mac() { // HMAC-SHA3-256 test vector (RFC 4231 inputs, SHA3-256 hash): // Key = 0x0b * 20 // Data = "Hi There" // HMAC = ba85192310dffa96e2a3a40e69774351140bb7185e1202cdcc917589f95e16bb let key = [0x0bu8; 20]; let data = b"Hi There"; let mut out = [0u8; 32]; let rc = unsafe { soliton_hmac_sha3_256( key.as_ptr(), key.len(), data.as_ptr(), data.len(), out.as_mut_ptr(), 32, ) }; assert_eq!(rc, OK); let expected: [u8; 32] = [ 0xba, 0x85, 0x19, 0x23, 0x10, 0xdf, 0xfa, 0x96, 0xe2, 0xa3, 0xa4, 0x0e, 0x69, 0x77, 0x43, 0x51, 0x14, 0x0b, 0xb7, 0x18, 0x5e, 0x12, 0x02, 0xcd, 0xcc, 0x91, 0x75, 0x89, 0xf9, 0x5e, 0x16, 0xbb, ]; assert_eq!(out, expected); } // ═══════════════════════════════════════════════════════════════════════════ // hmac_sha3_256_verify // ═══════════════════════════════════════════════════════════════════════════ #[test] fn hmac_sha3_256_verify_null_tag_a_returns_null_pointer() { let tag = [0x42u8; 32]; let rc = unsafe { soliton_hmac_sha3_256_verify(ptr::null(), 32, tag.as_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn hmac_sha3_256_verify_null_tag_b_returns_null_pointer() { let tag = [0x42u8; 32]; let rc = unsafe { soliton_hmac_sha3_256_verify(tag.as_ptr(), 32, ptr::null(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn hmac_sha3_256_verify_matching_tags_returns_ok() { let key = [0x0bu8; 20]; let data = b"Hi There"; let mut tag = [0u8; 32]; let rc = unsafe { soliton_hmac_sha3_256( key.as_ptr(), key.len(), data.as_ptr(), data.len(), tag.as_mut_ptr(), 32, ) }; assert_eq!(rc, OK); let rc = unsafe { soliton_hmac_sha3_256_verify(tag.as_ptr(), 32, tag.as_ptr(), 32) }; assert_eq!(rc, OK); } #[test] fn hmac_sha3_256_verify_different_tags_returns_verification_failed() { let tag_a = [0x11u8; 32]; let tag_b = [0x22u8; 32]; let rc = unsafe { soliton_hmac_sha3_256_verify(tag_a.as_ptr(), 32, tag_b.as_ptr(), 32) }; assert_eq!(rc, E_VER); } #[test] fn hmac_sha3_256_verify_single_bit_difference_returns_verification_failed() { let tag_a = [0x00u8; 32]; let mut tag_b = [0x00u8; 32]; tag_b[31] = 0x01; let rc = unsafe { soliton_hmac_sha3_256_verify(tag_a.as_ptr(), 32, tag_b.as_ptr(), 32) }; assert_eq!(rc, E_VER); } #[test] fn hmac_sha3_256_verify_wrong_tag_a_len_returns_invalid_length() { let tag_a = [0x42u8; 32]; let tag_b = [0x42u8; 32]; let rc = unsafe { soliton_hmac_sha3_256_verify(tag_a.as_ptr(), 31, tag_b.as_ptr(), 32) }; assert_eq!(rc, E_LEN); } #[test] fn hmac_sha3_256_verify_wrong_tag_b_len_returns_invalid_length() { let tag_a = [0x42u8; 32]; let tag_b = [0x42u8; 32]; let rc = unsafe { soliton_hmac_sha3_256_verify(tag_a.as_ptr(), 32, tag_b.as_ptr(), 0) }; assert_eq!(rc, E_LEN); } // ═══════════════════════════════════════════════════════════════════════════ // hkdf_sha3_256 // ═══════════════════════════════════════════════════════════════════════════ #[test] fn hkdf_sha3_256_null_out_returns_null_pointer() { let rc = unsafe { soliton_hkdf_sha3_256( ptr::null(), 0, ptr::null(), 0, ptr::null(), 0, ptr::null_mut(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn hkdf_sha3_256_zero_out_len_returns_invalid_length() { let mut out = [0u8; 32]; let rc = unsafe { soliton_hkdf_sha3_256( ptr::null(), 0, ptr::null(), 0, ptr::null(), 0, out.as_mut_ptr(), 0, ) }; assert_eq!(rc, E_LEN); } #[test] fn hkdf_sha3_256_out_len_too_large_returns_invalid_length() { let mut out = vec![0u8; 8161]; let rc = unsafe { soliton_hkdf_sha3_256( ptr::null(), 0, ptr::null(), 0, ptr::null(), 0, out.as_mut_ptr(), 8161, ) }; assert_eq!(rc, E_LEN); } #[test] fn hkdf_sha3_256_null_salt_nonzero_len_returns_null_pointer() { let mut out = fill_nonzero::<32>(); let ikm = b"ikm"; let rc = unsafe { soliton_hkdf_sha3_256( ptr::null(), 5, ikm.as_ptr(), ikm.len(), ptr::null(), 0, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn hkdf_sha3_256_null_ikm_nonzero_len_returns_null_pointer() { // Co-presence guard: null ikm with nonzero ikm_len is invalid. let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_hkdf_sha3_256( ptr::null(), 0, ptr::null(), 1, ptr::null(), 0, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn hkdf_sha3_256_null_info_nonzero_len_returns_null_pointer() { // Co-presence guard: null info with nonzero info_len is invalid. let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_hkdf_sha3_256( ptr::null(), 0, ptr::null(), 0, ptr::null(), 1, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn hkdf_sha3_256_empty_inputs_ok() { // All-null inputs with len=0 are valid (empty salt, empty IKM, empty info). let mut out = [0u8; 32]; let rc = unsafe { soliton_hkdf_sha3_256( ptr::null(), 0, ptr::null(), 0, ptr::null(), 0, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, OK); assert!(!is_zeroed(&out)); } #[test] fn hkdf_sha3_256_roundtrip_deterministic() { let salt = b"salt"; let ikm = b"ikm"; let info = b"info"; let mut out1 = [0u8; 32]; let mut out2 = [0u8; 32]; unsafe { soliton_hkdf_sha3_256( salt.as_ptr(), salt.len(), ikm.as_ptr(), ikm.len(), info.as_ptr(), info.len(), out1.as_mut_ptr(), 32, ); soliton_hkdf_sha3_256( salt.as_ptr(), salt.len(), ikm.as_ptr(), ikm.len(), info.as_ptr(), info.len(), out2.as_mut_ptr(), 32, ); } assert_eq!(out1, out2); assert!(!is_zeroed(&out1)); } // ═══════════════════════════════════════════════════════════════════════════ // aead_encrypt / aead_decrypt // ═══════════════════════════════════════════════════════════════════════════ #[test] fn aead_encrypt_null_key_returns_null_pointer() { let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_encrypt( ptr::null(), 32, [0u8; NONCE].as_ptr(), NONCE, ptr::null(), 0, ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn aead_encrypt_null_nonce_returns_null_pointer() { let key = [0u8; 32]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_encrypt( key.as_ptr(), 32, ptr::null(), NONCE, ptr::null(), 0, ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn aead_encrypt_null_plaintext_nonzero_len_returns_null_pointer() { let key = [0u8; 32]; let nonce = [0u8; NONCE]; // out is zeroed upfront; then null plaintext check fires. let mut nonzero = 0xABu8; let mut out = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let rc = unsafe { soliton_aead_encrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, ptr::null(), 5, ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_NULL); assert!(out.ptr.is_null()); assert_eq!(out.len, 0); } #[test] fn aead_encrypt_null_aad_nonzero_len_returns_null_pointer() { // aad=null with aad_len>0 is a co-presence violation — caller logic bug. let key = [0u8; 32]; let nonce = [0u8; NONCE]; let plaintext = b"hello"; let mut nonzero = 0xABu8; let mut out = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let rc = unsafe { soliton_aead_encrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, plaintext.as_ptr(), plaintext.len(), ptr::null(), 5, &mut out, ) }; assert_eq!(rc, E_NULL); assert!(out.ptr.is_null()); } #[test] fn aead_decrypt_null_aad_nonzero_len_returns_null_pointer() { // aad=null with aad_len>0 is a co-presence violation — caller logic bug. let key = [0u8; 32]; let nonce = [0u8; NONCE]; let ct = [0u8; 32]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_decrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, ct.as_ptr(), ct.len(), ptr::null(), 5, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn aead_decrypt_null_key_returns_null_pointer() { let ct = [0u8; 32]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_decrypt( ptr::null(), 32, [0u8; NONCE].as_ptr(), NONCE, ct.as_ptr(), ct.len(), ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn aead_decrypt_zero_ciphertext_len_returns_invalid_length() { let key = [0u8; 32]; let nonce = [0u8; NONCE]; let ct = [0u8; 1]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_decrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, ct.as_ptr(), 0, ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_LEN); } #[test] fn aead_encrypt_wrong_key_len_returns_invalid_length() { let key = [0u8; 32]; let nonce = [0u8; NONCE]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_encrypt( key.as_ptr(), 16, nonce.as_ptr(), NONCE, ptr::null(), 0, ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_LEN); } #[test] fn aead_encrypt_wrong_nonce_len_returns_invalid_length() { let key = [0u8; 32]; let nonce = [0u8; NONCE]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_encrypt( key.as_ptr(), 32, nonce.as_ptr(), 12, ptr::null(), 0, ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_LEN); } #[test] fn aead_decrypt_wrong_key_len_returns_invalid_length() { let key = [0u8; 32]; let nonce = [0u8; NONCE]; let ct = [0u8; 32]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_decrypt( key.as_ptr(), 33, nonce.as_ptr(), NONCE, ct.as_ptr(), ct.len(), ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_LEN); } #[test] fn aead_decrypt_wrong_nonce_len_returns_invalid_length() { let key = [0u8; 32]; let nonce = [0u8; NONCE]; let ct = [0u8; 32]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_aead_decrypt( key.as_ptr(), 32, nonce.as_ptr(), 0, ct.as_ptr(), ct.len(), ptr::null(), 0, &mut out, ) }; assert_eq!(rc, E_LEN); } #[test] fn aead_encrypt_decrypt_round_trip() { let key = [0x42u8; 32]; let nonce = [0x01u8; NONCE]; let plaintext = b"hello world"; let aad = b"associated"; unsafe { let mut ct_buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_aead_encrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, plaintext.as_ptr(), plaintext.len(), aad.as_ptr(), aad.len(), &mut ct_buf, ); assert_eq!(rc, OK); assert!(!ct_buf.ptr.is_null()); assert_eq!(ct_buf.len, plaintext.len() + 16); // plaintext + tag let mut pt_buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_aead_decrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, ct_buf.ptr, ct_buf.len, aad.as_ptr(), aad.len(), &mut pt_buf, ); assert_eq!(rc, OK); assert!(!pt_buf.ptr.is_null()); let pt = std::slice::from_raw_parts(pt_buf.ptr, pt_buf.len); assert_eq!(pt, plaintext); soliton_buf_free(&mut pt_buf); soliton_buf_free(&mut ct_buf); } } #[test] fn aead_decrypt_tampered_ciphertext_returns_aead_failed() { let key = [0x42u8; 32]; let nonce = [0x01u8; NONCE]; let plaintext = b"hello"; unsafe { let mut ct_buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_aead_encrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, plaintext.as_ptr(), plaintext.len(), ptr::null(), 0, &mut ct_buf, ); assert_eq!(rc, OK); // Flip one byte in the ciphertext. *ct_buf.ptr ^= 0xFF; let mut pt_buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_aead_decrypt( key.as_ptr(), 32, nonce.as_ptr(), NONCE, ct_buf.ptr, ct_buf.len, ptr::null(), 0, &mut pt_buf, ); assert_eq!(rc, E_AEAD); // Output must be zeroed on failure. assert!(pt_buf.ptr.is_null()); assert_eq!(pt_buf.len, 0); soliton_buf_free(&mut ct_buf); } } // ═══════════════════════════════════════════════════════════════════════════ // argon2id // ═══════════════════════════════════════════════════════════════════════════ #[test] fn argon2id_null_out_returns_null_pointer() { let rc = unsafe { soliton_argon2id( ptr::null(), 0, [0u8; 8].as_ptr(), 8, 8, 1, 1, ptr::null_mut(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn argon2id_zero_out_len_returns_invalid_length() { let mut out = [0u8; 32]; let rc = unsafe { soliton_argon2id( ptr::null(), 0, [0u8; 8].as_ptr(), 8, 8, 1, 1, out.as_mut_ptr(), 0, ) }; assert_eq!(rc, E_LEN); } #[test] fn argon2id_null_password_nonzero_len_returns_null_pointer() { let mut out = fill_nonzero::<32>(); let salt = [0u8; 8]; let rc = unsafe { soliton_argon2id( ptr::null(), 5, salt.as_ptr(), salt.len(), 8, 1, 1, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn argon2id_null_salt_nonzero_len_returns_null_pointer() { let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_argon2id( ptr::null(), 0, ptr::null(), 5, 8, 1, 1, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn argon2id_minimal_params_produces_output() { // m=8 KiB, t=1, p=1 — absolute minimum. Salt must be >= 8 bytes. let password = b"password"; let salt = b"saltbytes"; let mut out = [0u8; 32]; let rc = unsafe { soliton_argon2id( password.as_ptr(), password.len(), salt.as_ptr(), salt.len(), 8, 1, 1, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, OK); assert!(!is_zeroed(&out)); } #[test] fn argon2id_deterministic() { let password = b"pw"; let salt = b"saltsalt"; let mut out1 = [0u8; 32]; let mut out2 = [0u8; 32]; unsafe { soliton_argon2id( password.as_ptr(), password.len(), salt.as_ptr(), salt.len(), 8, 1, 1, out1.as_mut_ptr(), 32, ); soliton_argon2id( password.as_ptr(), password.len(), salt.as_ptr(), salt.len(), 8, 1, 1, out2.as_mut_ptr(), 32, ); } assert_eq!(out1, out2); } #[test] fn argon2id_excessive_m_cost_returns_invalid_data() { let mut out = fill_nonzero::<32>(); let salt = b"saltsalt"; let rc = unsafe { soliton_argon2id( ptr::null(), 0, salt.as_ptr(), salt.len(), 4_194_305, 1, 1, out.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_DATA, "m_cost above 4 GiB cap must be rejected"); assert!(is_zeroed(&out), "output must be zeroed on error"); } // ═══════════════════════════════════════════════════════════════════════════ // auth_verify (PQ-free: just HMAC comparison) // ═══════════════════════════════════════════════════════════════════════════ #[test] fn auth_verify_null_token_returns_null_pointer() { let proof = [0u8; 32]; let rc = unsafe { soliton_auth_verify(ptr::null(), 32, proof.as_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn auth_verify_null_proof_returns_null_pointer() { let token = [0u8; 32]; let rc = unsafe { soliton_auth_verify(token.as_ptr(), 32, ptr::null(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn auth_verify_matching_tokens() { let token = [0x42u8; 32]; let rc = unsafe { soliton_auth_verify(token.as_ptr(), 32, token.as_ptr(), 32) }; assert_eq!(rc, OK); } #[test] fn auth_verify_mismatched_tokens() { let token = [0x42u8; 32]; let proof = [0x43u8; 32]; let rc = unsafe { soliton_auth_verify(token.as_ptr(), 32, proof.as_ptr(), 32) }; assert_eq!(rc, E_VER); } #[test] fn auth_verify_wrong_token_len_returns_invalid_length() { let token = [0x42u8; 32]; let proof = [0x42u8; 32]; let rc = unsafe { soliton_auth_verify(token.as_ptr(), 31, proof.as_ptr(), 32) }; assert_eq!(rc, E_LEN); } #[test] fn auth_verify_wrong_proof_len_returns_invalid_length() { let token = [0x42u8; 32]; let proof = [0x42u8; 32]; let rc = unsafe { soliton_auth_verify(token.as_ptr(), 32, proof.as_ptr(), 33) }; assert_eq!(rc, E_LEN); } // ═══════════════════════════════════════════════════════════════════════════ // identity_generate (null-guard tests — no keygen, MIRI-safe) // ═══════════════════════════════════════════════════════════════════════════ #[test] fn identity_generate_null_pk_out_returns_null_pointer() { let mut sk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut fp_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_identity_generate(ptr::null_mut(), &mut sk_out, &mut fp_out) }; assert_eq!(rc, E_NULL); } #[test] fn identity_generate_null_sk_out_returns_null_pointer() { let mut pk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut fp_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_identity_generate(&mut pk_out, ptr::null_mut(), &mut fp_out) }; assert_eq!(rc, E_NULL); } #[test] fn identity_generate_null_fp_out_returns_null_pointer() { let mut pk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut sk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_identity_generate(&mut pk_out, &mut sk_out, ptr::null_mut()) }; assert_eq!(rc, E_NULL); } // ═══════════════════════════════════════════════════════════════════════════ // identity_fingerprint (PQ-free: just SHA3-256 of key bytes) // ═══════════════════════════════════════════════════════════════════════════ #[test] fn identity_fingerprint_null_pk_returns_null_pointer() { let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_identity_fingerprint(ptr::null(), PK, out.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); assert!(is_zeroed(&out), "out must be zeroed when pk is null"); } #[test] fn identity_fingerprint_null_out_returns_null_pointer() { let pk = [0u8; PK]; let rc = unsafe { soliton_identity_fingerprint(pk.as_ptr(), PK, ptr::null_mut(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn identity_fingerprint_zero_pk_len_returns_invalid_length() { let pk = [0u8; PK]; let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_identity_fingerprint(pk.as_ptr(), 0, out.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn identity_fingerprint_wrong_pk_len_returns_invalid_length() { let pk = [0u8; PK - 1]; let mut out = fill_nonzero::<32>(); let rc = unsafe { soliton_identity_fingerprint(pk.as_ptr(), PK - 1, out.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(is_zeroed(&out), "output must be zeroed on error"); } #[test] fn identity_fingerprint_zeroed_pk_produces_output() { // from_bytes is length-only — no keygen needed. let pk = [0u8; PK]; let mut out = [0u8; 32]; let rc = unsafe { soliton_identity_fingerprint(pk.as_ptr(), PK, out.as_mut_ptr(), 32) }; assert_eq!(rc, OK); assert!(!is_zeroed(&out)); } #[test] fn identity_fingerprint_deterministic() { let pk = [0u8; PK]; let mut fp1 = [0u8; FP]; let mut fp2 = [0u8; FP]; unsafe { soliton_identity_fingerprint(pk.as_ptr(), PK, fp1.as_mut_ptr(), 32); soliton_identity_fingerprint(pk.as_ptr(), PK, fp2.as_mut_ptr(), 32); } assert_eq!(fp1, fp2); } // ═══════════════════════════════════════════════════════════════════════════ // identity_sign / verify / encapsulate / decapsulate — error paths only // ═══════════════════════════════════════════════════════════════════════════ #[test] fn identity_sign_null_sk_returns_null_pointer() { let msg = b"message"; let mut sig_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_identity_sign(ptr::null(), SK, msg.as_ptr(), msg.len(), &mut sig_out) }; assert_eq!(rc, E_NULL); } #[test] fn identity_sign_zero_sk_len_returns_invalid_length() { let sk = [0u8; SK]; let msg = b"message"; let mut nonzero = 0xABu8; let mut sig = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let rc = unsafe { soliton_identity_sign(sk.as_ptr(), 0, msg.as_ptr(), msg.len(), &mut sig) }; assert_eq!(rc, E_LEN); assert!(sig.ptr.is_null()); } #[test] fn identity_sign_null_message_nonzero_len_returns_null_pointer() { // Co-presence guard: null message with nonzero message_len is invalid. // Use a non-null sentinel so that the zeroing step (which happens between // the null-pointer guard and the co-presence check) is observable. let sk = [0u8; SK]; let mut sig_out = SolitonBuf { ptr: std::ptr::dangling_mut::(), len: 99, }; let rc = unsafe { soliton_identity_sign(sk.as_ptr(), SK, ptr::null(), 1, &mut sig_out) }; assert_eq!(rc, E_NULL); assert!(sig_out.ptr.is_null(), "sig_out.ptr must be zeroed on error"); assert_eq!(sig_out.len, 0, "sig_out.len must be zeroed on error"); } #[test] fn identity_sign_null_sig_out_returns_null_pointer() { // sig_out is checked alongside sk in the combined null guard; passing null // sig_out returns E_NULL regardless of the other parameters. let sk = [0u8; SK]; let rc = unsafe { soliton_identity_sign(sk.as_ptr(), SK, ptr::null(), 0, ptr::null_mut()) }; assert_eq!(rc, E_NULL); } #[test] fn identity_verify_null_pk_returns_null_pointer() { let msg = b"message"; let sig = [0u8; HSIG]; let rc = unsafe { soliton_identity_verify(ptr::null(), PK, msg.as_ptr(), msg.len(), sig.as_ptr(), HSIG) }; assert_eq!(rc, E_NULL); } #[test] fn identity_verify_zero_pk_len_returns_invalid_length() { let pk = [0u8; PK]; let msg = b"message"; let sig = [0u8; HSIG]; let rc = unsafe { soliton_identity_verify(pk.as_ptr(), 0, msg.as_ptr(), msg.len(), sig.as_ptr(), HSIG) }; assert_eq!(rc, E_LEN); } #[test] fn identity_verify_null_message_nonzero_len_returns_null_pointer() { // Co-presence guard: null message with nonzero message_len is invalid. let pk = [0u8; PK]; let sig = [0u8; HSIG]; let rc = unsafe { soliton_identity_verify(pk.as_ptr(), PK, ptr::null(), 1, sig.as_ptr(), HSIG) }; assert_eq!(rc, E_NULL); } #[test] fn identity_verify_null_sig_nonzero_len_returns_null_pointer() { let pk = [0u8; PK]; let msg = b"message"; let rc = unsafe { soliton_identity_verify(pk.as_ptr(), PK, msg.as_ptr(), msg.len(), ptr::null(), HSIG) }; assert_eq!(rc, E_NULL); } #[test] fn identity_encapsulate_null_pk_returns_null_pointer() { let mut ct_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut ss = [0u8; 32]; let rc = unsafe { soliton_identity_encapsulate(ptr::null(), PK, &mut ct_out, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn identity_encapsulate_zero_pk_len_zeroes_outputs() { let pk = [0u8; PK]; let mut nonzero = 0xABu8; let mut ct = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let mut ss = fill_nonzero::<32>(); let rc = unsafe { soliton_identity_encapsulate(pk.as_ptr(), 0, &mut ct, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(ct.ptr.is_null()); assert!(is_zeroed(&ss), "ss must be zeroed on error"); } #[test] fn identity_decapsulate_null_sk_returns_null_pointer() { let ct = [0u8; XCTZ]; let mut ss = [0u8; 32]; let rc = unsafe { soliton_identity_decapsulate(ptr::null(), SK, ct.as_ptr(), XCTZ, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn identity_decapsulate_zero_sk_len_zeroes_ss() { let sk = [0u8; SK]; let ct = [0u8; XCTZ]; let mut ss = fill_nonzero::<32>(); let rc = unsafe { soliton_identity_decapsulate(sk.as_ptr(), 0, ct.as_ptr(), XCTZ, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(is_zeroed(&ss), "ss must be zeroed on error"); } // ═══════════════════════════════════════════════════════════════════════════ // auth_challenge / auth_respond — error paths only // ═══════════════════════════════════════════════════════════════════════════ #[test] fn auth_challenge_null_pk_returns_null_pointer() { let mut ct_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut token = [0u8; 32]; let rc = unsafe { soliton_auth_challenge(ptr::null(), PK, &mut ct_out, token.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn auth_challenge_zero_pk_len_zeroes_outputs() { let pk = [0u8; PK]; let mut nonzero = 0xABu8; let mut ct = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let mut token = fill_nonzero::<32>(); let rc = unsafe { soliton_auth_challenge(pk.as_ptr(), 0, &mut ct, token.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(ct.ptr.is_null()); assert!(is_zeroed(&token), "token must be zeroed on error"); } #[test] fn auth_respond_null_sk_returns_null_pointer() { let ct = [0u8; XCTZ]; let mut proof = [0u8; 32]; let rc = unsafe { soliton_auth_respond(ptr::null(), SK, ct.as_ptr(), XCTZ, proof.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn auth_respond_zero_sk_len_zeroes_proof() { let sk = [0u8; SK]; let ct = [0u8; XCTZ]; let mut proof = fill_nonzero::<32>(); let rc = unsafe { soliton_auth_respond(sk.as_ptr(), 0, ct.as_ptr(), XCTZ, proof.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(is_zeroed(&proof), "proof must be zeroed on error"); } // ═══════════════════════════════════════════════════════════════════════════ // ratchet_init_alice / init_bob // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_init_alice_null_root_key_returns_null_pointer() { let ck = [0u8; 32]; let fp = [0x01u8; 32]; let fp2 = [0x02u8; 32]; let ek_pk = [0u8; XPKZ]; let ek_sk = [0u8; XSKZ]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_init_alice( ptr::null(), 32, ck.as_ptr(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, ek_pk.as_ptr(), XPKZ, ek_sk.as_ptr(), XSKZ, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_init_alice_zero_ek_pk_len_sets_out_null() { let rk = [0u8; 32]; let ck = [0u8; 32]; let fp = [0x01u8; 32]; let fp2 = [0x02u8; 32]; let ek_pk = [0u8; XPKZ]; let ek_sk = [0u8; XSKZ]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_init_alice( rk.as_ptr(), 32, ck.as_ptr(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, ek_pk.as_ptr(), 0, ek_sk.as_ptr(), XSKZ, &mut out, ) }; assert_eq!(rc, E_LEN); assert!(out.is_null(), "*out must be null on error"); } #[test] fn ratchet_init_alice_wrong_ek_pk_size_returns_invalid_length() { let rk = [0u8; 32]; let ck = [0u8; 32]; let fp = [0x01u8; 32]; let fp2 = [0x02u8; 32]; let ek_pk = [0u8; XPKZ - 1]; let ek_sk = [0u8; XSKZ]; let mut out: *mut SolitonRatchet = ptr::null_mut(); let rc = unsafe { soliton_ratchet_init_alice( rk.as_ptr(), 32, ck.as_ptr(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, ek_pk.as_ptr(), XPKZ - 1, ek_sk.as_ptr(), XSKZ, &mut out, ) }; assert_eq!(rc, E_LEN); assert!(out.is_null()); } #[test] fn ratchet_init_bob_null_root_key_returns_null_pointer() { let ck = [0u8; 32]; let fp = [0x02u8; 32]; let fp2 = [0x01u8; 32]; let peer_ek = [0u8; XPKZ]; let mut out: *mut SolitonRatchet = ptr::null_mut(); let rc = unsafe { soliton_ratchet_init_bob( ptr::null(), 32, ck.as_ptr(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, peer_ek.as_ptr(), XPKZ, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_init_bob_zero_peer_ek_len_sets_out_null() { let rk = [0u8; 32]; let ck = [0u8; 32]; let fp = [0x02u8; 32]; let fp2 = [0x01u8; 32]; let peer_ek = [0u8; XPKZ]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_init_bob( rk.as_ptr(), 32, ck.as_ptr(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, peer_ek.as_ptr(), 0, &mut out, ) }; assert_eq!(rc, E_LEN); assert!(out.is_null(), "*out must be null on error"); } #[test] fn ratchet_init_alice_null_chain_key_returns_null_pointer() { let rk = [0u8; 32]; let fp = [0x01u8; 32]; let fp2 = [0x02u8; 32]; let ek_pk = [0u8; XPKZ]; let ek_sk = [0u8; XSKZ]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_init_alice( rk.as_ptr(), 32, ptr::null(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, ek_pk.as_ptr(), XPKZ, ek_sk.as_ptr(), XSKZ, &mut out, ) }; assert_eq!(rc, E_NULL); assert!(out.is_null(), "*out must be null on error"); } #[test] fn ratchet_init_bob_null_chain_key_returns_null_pointer() { let rk = [0u8; 32]; let fp = [0x02u8; 32]; let fp2 = [0x01u8; 32]; let peer_ek = [0u8; XPKZ]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_init_bob( rk.as_ptr(), 32, ptr::null(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, peer_ek.as_ptr(), XPKZ, &mut out, ) }; assert_eq!(rc, E_NULL); assert!(out.is_null(), "*out must be null on error"); } #[test] fn ratchet_init_alice_wrong_root_key_len_returns_invalid_length() { let rk = [0u8; 32]; let ck = [0u8; 32]; let fp = [0x01u8; 32]; let fp2 = [0x02u8; 32]; let ek_pk = [0u8; XPKZ]; let ek_sk = [0u8; XSKZ]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_init_alice( rk.as_ptr(), 31, ck.as_ptr(), 32, fp.as_ptr(), 32, fp2.as_ptr(), 32, ek_pk.as_ptr(), XPKZ, ek_sk.as_ptr(), XSKZ, &mut out, ) }; assert_eq!(rc, E_LEN); assert!(out.is_null()); } #[test] fn ratchet_init_bob_wrong_fp_len_returns_invalid_length() { let rk = [0u8; 32]; let ck = [0u8; 32]; let fp = [0x02u8; 32]; let fp2 = [0x01u8; 32]; let peer_ek = [0u8; XPKZ]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_init_bob( rk.as_ptr(), 32, ck.as_ptr(), 32, fp.as_ptr(), 33, fp2.as_ptr(), 32, peer_ek.as_ptr(), XPKZ, &mut out, ) }; assert_eq!(rc, E_LEN); assert!(out.is_null()); } #[test] fn ratchet_free_null_is_noop() { unsafe { soliton_ratchet_free(ptr::null_mut()); } } #[test] fn ratchet_free_double_is_noop() { unsafe { let mut alice = make_alice(); soliton_ratchet_free(&mut alice); assert!(alice.is_null(), "pointer must be nulled after free"); soliton_ratchet_free(&mut alice); } } #[test] fn encrypted_message_free_null_is_noop() { // Must not crash — consistent with ratchet_free_null_is_noop. unsafe { soliton_encrypted_message_free(ptr::null_mut()); } } #[test] fn encrypted_message_free_double_is_noop() { // Inner SolitonBuf fields are nulled by soliton_buf_free on first call; // second call sees null ptrs and no-ops. unsafe { let mut alice = make_alice(); let plaintext = b"double-free test"; let mut msg = MaybeUninit::::zeroed(); let rc = soliton_ratchet_encrypt(alice, plaintext.as_ptr(), plaintext.len(), msg.as_mut_ptr()); assert_eq!(rc, OK); let mut msg = msg.assume_init(); soliton_encrypted_message_free(&mut msg); soliton_encrypted_message_free(&mut msg); // second free must not crash soliton_ratchet_free(&mut alice); } } #[test] fn initiated_session_free_double_is_noop() { // Inner SolitonBuf fields are nulled on first free; second is a no-op. unsafe { let mut session = std::mem::zeroed::(); // Zero-init is valid for this type (all ptrs null, all bufs empty). soliton_kex_initiated_session_free(&mut session); soliton_kex_initiated_session_free(&mut session); // must not crash } } #[test] fn received_session_free_double_is_noop() { unsafe { let mut session = SolitonReceivedSession { root_key: [0u8; 32], chain_key: [0u8; 32], peer_ek: SolitonBuf { ptr: ptr::null_mut(), len: 0, }, }; soliton_kex_received_session_free(&mut session); soliton_kex_received_session_free(&mut session); // must not crash } } #[test] fn decoded_session_init_free_double_is_noop() { // Decode a valid session init, then free twice. let encoded = make_encoded_session_init(false); let mut out: SolitonDecodedSessionInit = unsafe { MaybeUninit::zeroed().assume_init() }; let rc = unsafe { soliton_kex_decode_session_init(encoded.as_ptr(), encoded.len(), &mut out) }; assert_eq!(rc, OK); unsafe { soliton_decoded_session_init_free(&mut out); soliton_decoded_session_init_free(&mut out); // must not crash } } // ═══════════════════════════════════════════════════════════════════════════ // ratchet_encrypt // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_encrypt_null_ratchet_returns_null_pointer() { // Use a sentinel-filled output buffer to verify that *out is zeroed before // the ratchet-null check fires. The out-null and ratchet-null guards are // separate so that zeroing always occurs when out is non-null. unsafe { let mut out = MaybeUninit::::uninit(); std::ptr::write_bytes(out.as_mut_ptr(), 0xAB, 1); let rc = soliton_ratchet_encrypt(ptr::null_mut(), ptr::null(), 0, out.as_mut_ptr()); assert_eq!(rc, E_NULL); let out_bytes = std::slice::from_raw_parts( out.as_ptr() as *const u8, size_of::(), ); assert!( is_zeroed(out_bytes), "out must be zeroed when ratchet is null" ); } } #[test] fn ratchet_encrypt_null_plaintext_zero_len_succeeds() { // null plaintext with zero len is valid — encrypts an empty message. unsafe { let mut alice = make_alice(); let mut out = MaybeUninit::::zeroed(); let rc = soliton_ratchet_encrypt(alice, ptr::null(), 0, out.as_mut_ptr()); assert_eq!(rc, OK); let mut msg = out.assume_init(); soliton_encrypted_message_free(&mut msg); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_encrypt_null_plaintext_nonzero_len_returns_null_pointer() { // Co-presence guard: null plaintext with nonzero plaintext_len is invalid. unsafe { let mut alice = make_alice(); let mut out = MaybeUninit::::zeroed(); let rc = soliton_ratchet_encrypt(alice, ptr::null(), 1, out.as_mut_ptr()); assert_eq!(rc, E_NULL); soliton_ratchet_free(&mut alice); } } // ═══════════════════════════════════════════════════════════════════════════ // ratchet_decrypt — including co-presence guards // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_decrypt_null_ratchet_returns_null_pointer() { let rpk = [0u8; XPKZ]; let ct = [0u8; 1]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_ratchet_decrypt( ptr::null_mut(), rpk.as_ptr(), XPKZ, ptr::null(), 0, 0, 0, ct.as_ptr(), ct.len(), &mut pt_out, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_decrypt_zero_ciphertext_len_returns_invalid_length() { unsafe { let mut bob = make_bob(); let rpk = [0u8; XPKZ]; let ct = [0u8; 1]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( bob, rpk.as_ptr(), XPKZ, ptr::null(), 0, 0, 0, ct.as_ptr(), 0, &mut pt_out, ); assert_eq!(rc, E_LEN); soliton_ratchet_free(&mut bob); } } #[test] fn ratchet_decrypt_copresence_null_ptr_nonzero_len() { // kem_ct=null but kem_ct_len=5 → NullPointer (co-presence guard). unsafe { let mut bob = make_bob(); let rpk = [0u8; XPKZ]; let ct = [0u8; 32]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( bob, rpk.as_ptr(), XPKZ, ptr::null(), 5, 0, 0, ct.as_ptr(), ct.len(), &mut pt_out, ); assert_eq!(rc, E_NULL); soliton_ratchet_free(&mut bob); } } #[test] fn ratchet_decrypt_copresence_nonnull_ptr_zero_len() { // kem_ct=non-null but kem_ct_len=0 → NullPointer (co-presence guard). unsafe { let mut bob = make_bob(); let rpk = [0u8; XPKZ]; let kem_ct = [0u8; XCTZ]; let ct = [0u8; 32]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( bob, rpk.as_ptr(), XPKZ, kem_ct.as_ptr(), 0, 0, 0, ct.as_ptr(), ct.len(), &mut pt_out, ); assert_eq!(rc, E_NULL); soliton_ratchet_free(&mut bob); } } // ═══════════════════════════════════════════════════════════════════════════ // ratchet_encrypt_first / decrypt_first // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_encrypt_first_null_chain_key_returns_null_pointer() { let mut payload = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_encrypt_first( ptr::null(), 0, ptr::null(), 0, ptr::null(), 0, &mut payload, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_encrypt_first_null_plaintext_nonzero_len_returns_null_pointer() { // Co-presence guard: null plaintext with nonzero plaintext_len is invalid. // Outputs are zeroed after the null guard and before co-presence checks. let chain_key = [0u8; 32]; let mut payload = SolitonBuf { ptr: ptr::null_mut(), len: 99, }; let mut next_ck = fill_nonzero::<32>(); let rc = unsafe { soliton_ratchet_encrypt_first( chain_key.as_ptr(), 32, ptr::null(), 1, ptr::null(), 0, &mut payload, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert_eq!( payload.len, 0, "payload.len must be zeroed after co-presence error" ); assert_eq!( next_ck, [0u8; 32], "next_ck must be zeroed after co-presence error" ); } #[test] fn ratchet_encrypt_first_null_aad_nonzero_len_returns_null_pointer() { // Co-presence guard: null aad with nonzero aad_len is invalid. // Outputs are zeroed after the null guard and before co-presence checks. let chain_key = [0u8; 32]; let plaintext = b"msg"; let mut payload = SolitonBuf { ptr: ptr::null_mut(), len: 99, }; let mut next_ck = fill_nonzero::<32>(); let rc = unsafe { soliton_ratchet_encrypt_first( chain_key.as_ptr(), 32, plaintext.as_ptr(), plaintext.len(), ptr::null(), 1, &mut payload, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert_eq!( payload.len, 0, "payload.len must be zeroed after co-presence error" ); assert_eq!( next_ck, [0u8; 32], "next_ck must be zeroed after co-presence error" ); } #[test] fn ratchet_encrypt_first_null_payload_out_returns_null_pointer() { let chain_key = [0u8; 32]; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_encrypt_first( chain_key.as_ptr(), 32, ptr::null(), 0, ptr::null(), 0, ptr::null_mut(), next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_encrypt_first_null_ratchet_init_key_out_returns_null_pointer() { let chain_key = [0u8; 32]; let mut payload = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_ratchet_encrypt_first( chain_key.as_ptr(), 32, ptr::null(), 0, ptr::null(), 0, &mut payload, ptr::null_mut(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_decrypt_first_null_chain_key_returns_null_pointer() { let payload = [0u8; 1]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_decrypt_first( ptr::null(), 0, payload.as_ptr(), payload.len(), ptr::null(), 0, &mut pt_out, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_decrypt_first_null_encrypted_payload_returns_null_pointer() { let chain_key = [0u8; 32]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_decrypt_first( chain_key.as_ptr(), 32, ptr::null(), 1, ptr::null(), 0, &mut pt_out, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_decrypt_first_null_plaintext_out_returns_null_pointer() { let chain_key = [0u8; 32]; let payload = [0u8; 1]; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_decrypt_first( chain_key.as_ptr(), 32, payload.as_ptr(), payload.len(), ptr::null(), 0, ptr::null_mut(), next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_decrypt_first_null_ratchet_init_key_out_returns_null_pointer() { let chain_key = [0u8; 32]; let payload = [0u8; 1]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_ratchet_decrypt_first( chain_key.as_ptr(), 32, payload.as_ptr(), payload.len(), ptr::null(), 0, &mut pt_out, ptr::null_mut(), 32, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_decrypt_first_null_aad_nonzero_len_returns_null_pointer() { // Co-presence guard: null aad with nonzero aad_len is invalid. // Outputs are zeroed after the null guard and before co-presence checks. let chain_key = [0u8; 32]; let payload = [0u8; 1]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 99, }; let mut next_ck = fill_nonzero::<32>(); let rc = unsafe { soliton_ratchet_decrypt_first( chain_key.as_ptr(), 32, payload.as_ptr(), payload.len(), ptr::null(), 1, &mut pt_out, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_NULL); assert_eq!( pt_out.len, 0, "pt_out.len must be zeroed after co-presence error" ); assert_eq!( next_ck, [0u8; 32], "next_ck must be zeroed after co-presence error" ); } #[test] fn ratchet_decrypt_first_zero_payload_len_returns_invalid_length() { // encrypted_payload_len == 0 is rejected after the null and co-presence // guards: a zero-length encrypted payload cannot contain a valid GCM tag. // Pass aad = null, aad_len = 0 to ensure the aad co-presence guard is not // the first error (null aad with aad_len == 0 is a valid empty AAD). let chain_key = [0u8; 32]; let dummy_payload = [0u8; 1]; // non-null pointer, but encrypted_payload_len = 0 let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_decrypt_first( chain_key.as_ptr(), 32, dummy_payload.as_ptr(), 0, ptr::null(), 0, &mut pt_out, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_LEN); } #[test] fn ratchet_encrypt_first_decrypt_first_round_trip() { let chain_key = [0x11u8; 32]; let plaintext = b"session init payload"; let aad = b"session aad"; unsafe { let mut payload_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck_enc = [0u8; 32]; let rc = soliton_ratchet_encrypt_first( chain_key.as_ptr(), 32, plaintext.as_ptr(), plaintext.len(), aad.as_ptr(), aad.len(), &mut payload_out, next_ck_enc.as_mut_ptr(), 32, ); assert_eq!(rc, OK); assert!(!payload_out.ptr.is_null()); assert!(!is_zeroed(&next_ck_enc)); let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck_dec = [0u8; 32]; let rc = soliton_ratchet_decrypt_first( chain_key.as_ptr(), 32, payload_out.ptr, payload_out.len, aad.as_ptr(), aad.len(), &mut pt_out, next_ck_dec.as_mut_ptr(), 32, ); assert_eq!(rc, OK); assert!(!pt_out.ptr.is_null()); let pt = std::slice::from_raw_parts(pt_out.ptr, pt_out.len); assert_eq!(pt, plaintext); // Both sides derive the same ratchet-init key. assert_eq!(next_ck_enc, next_ck_dec); soliton_buf_free(&mut pt_out); soliton_buf_free(&mut payload_out); } } #[test] fn ratchet_decrypt_first_tampered_returns_aead_failed() { let chain_key = [0x22u8; 32]; let plaintext = b"payload"; unsafe { let mut payload_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck = [0u8; 32]; let rc = soliton_ratchet_encrypt_first( chain_key.as_ptr(), 32, plaintext.as_ptr(), plaintext.len(), ptr::null(), 0, &mut payload_out, next_ck.as_mut_ptr(), 32, ); assert_eq!(rc, OK); // Flip one byte. *payload_out.ptr ^= 0xFF; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck_dec = [0u8; 32]; let rc = soliton_ratchet_decrypt_first( chain_key.as_ptr(), 32, payload_out.ptr, payload_out.len, ptr::null(), 0, &mut pt_out, next_ck_dec.as_mut_ptr(), 32, ); assert_eq!(rc, E_AEAD); soliton_buf_free(&mut payload_out); } } #[test] fn ratchet_encrypt_first_wrong_chain_key_len_returns_invalid_length() { let chain_key = [0u8; 32]; let mut payload = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_encrypt_first( chain_key.as_ptr(), 31, ptr::null(), 0, ptr::null(), 0, &mut payload, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_LEN); } #[test] fn ratchet_decrypt_first_wrong_chain_key_len_returns_invalid_length() { let chain_key = [0u8; 32]; let payload = [0u8; 41]; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck = [0u8; 32]; let rc = unsafe { soliton_ratchet_decrypt_first( chain_key.as_ptr(), 33, payload.as_ptr(), payload.len(), ptr::null(), 0, &mut pt_out, next_ck.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_LEN); } // ═══════════════════════════════════════════════════════════════════════════ // ratchet_reset, ratchet_to_bytes, ratchet_from_bytes // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_reset_null_returns_null_pointer() { let rc = unsafe { soliton_ratchet_reset(ptr::null_mut()) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_reset_zeroes_state() { unsafe { let mut alice = make_alice(); let rc = soliton_ratchet_reset(alice); assert_eq!(rc, OK); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_to_bytes_null_ratchet_returns_null_pointer() { let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_ratchet_to_bytes(ptr::null_mut(), &mut out, ptr::null_mut()) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_to_bytes_null_inner_ratchet_returns_null_pointer() { let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut ratchet: *mut SolitonRatchet = ptr::null_mut(); let rc = unsafe { soliton_ratchet_to_bytes(&mut ratchet, &mut out, ptr::null_mut()) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_to_bytes_null_out_returns_null_pointer() { unsafe { let mut alice = make_alice(); // data_out null → early return, ratchet NOT consumed. let rc = soliton_ratchet_to_bytes(&mut alice, ptr::null_mut(), ptr::null_mut()); assert_eq!(rc, E_NULL); assert!( !alice.is_null(), "ratchet must not be consumed on early error" ); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_from_bytes_null_data_returns_null_pointer() { let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_from_bytes(ptr::null(), 1, &mut out) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_from_bytes_null_out_returns_null_pointer() { let data = [0u8; 1]; let rc = unsafe { soliton_ratchet_from_bytes(data.as_ptr(), data.len(), ptr::null_mut()) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_from_bytes_zero_len_returns_invalid_length() { let data = [0u8; 1]; let mut out: *mut SolitonRatchet = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_from_bytes(data.as_ptr(), 0, &mut out) }; assert_eq!(rc, E_LEN); assert!(out.is_null()); } #[test] fn ratchet_serialize_deserialize_round_trip() { unsafe { let mut alice = make_alice(); let mut bytes_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut epoch1_out: u64 = 0; // to_bytes consumes alice — *alice is set to null on success. let rc = soliton_ratchet_to_bytes(&mut alice, &mut bytes_out, &mut epoch1_out); assert_eq!(rc, OK); assert!(!bytes_out.ptr.is_null()); assert!(alice.is_null(), "ratchet must be consumed after to_bytes"); assert!(epoch1_out > 0, "epoch_out must be populated"); let mut alice2: *mut SolitonRatchet = ptr::null_mut(); let rc = soliton_ratchet_from_bytes(bytes_out.ptr, bytes_out.len, &mut alice2); assert_eq!(rc, OK); assert!(!alice2.is_null()); // Serialize alice2 and compare: both serializations should match // (excluding the epoch field at bytes 1..9, which advances on each to_bytes call). let mut bytes_out2 = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut epoch2_out: u64 = 0; let rc = soliton_ratchet_to_bytes(&mut alice2, &mut bytes_out2, &mut epoch2_out); assert_eq!(rc, OK); assert!(alice2.is_null(), "ratchet must be consumed after to_bytes"); let b1 = std::slice::from_raw_parts(bytes_out.ptr, bytes_out.len); let b2 = std::slice::from_raw_parts(bytes_out2.ptr, bytes_out2.len); assert_eq!(b1[0], b2[0], "version must match"); assert_eq!(b1[9..], b2[9..], "all fields after epoch must match"); // Epoch must advance by exactly 1. let epoch1 = u64::from_be_bytes(b1[1..9].try_into().unwrap()); let epoch2 = u64::from_be_bytes(b2[1..9].try_into().unwrap()); assert_eq!(epoch2, epoch1 + 1); // epoch_out must match the epoch embedded in the blob. assert_eq!(epoch1_out, epoch1, "epoch_out must match blob epoch"); assert_eq!(epoch2_out, epoch2, "epoch_out must match blob epoch"); soliton_buf_free(&mut bytes_out); soliton_buf_free(&mut bytes_out2); // alice and alice2 already consumed by to_bytes — no ratchet_free needed. } } // ═══════════════════════════════════════════════════════════════════════════ // Ratchet round-trip (PQ-free: Alice's first encrypt has no KEM step) // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_pq_free_round_trip_alice_to_bob() { // Alice and Bob both initialized with zeroed keys on the same root_key/chain_key. // Alice's first encrypt: kem_ct=None (send_ratchet_pk=Some, ratchet_pending=false). // Bob's first decrypt: recv_ratchet_pk=[0;1216] == header.ratchet_pk=[0;1216] → no KEM. unsafe { let mut alice = make_alice(); let mut bob = make_bob(); let plaintext = b"hello ratchet"; // Alice encrypts. let mut msg: MaybeUninit = MaybeUninit::zeroed(); let rc = soliton_ratchet_encrypt(alice, plaintext.as_ptr(), plaintext.len(), msg.as_mut_ptr()); assert_eq!(rc, OK); let msg = msg.assume_init(); // Verify kem_ct is absent (no PQ ratchet step on first message). assert!( msg.header.kem_ct.ptr.is_null() || msg.header.kem_ct.len == 0, "first message must not contain kem_ct" ); // Bob decrypts. let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( bob, msg.header.ratchet_pk.ptr, msg.header.ratchet_pk.len, ptr::null(), 0, // kem_ct absent msg.header.n, msg.header.pn, msg.ciphertext.ptr, msg.ciphertext.len, &mut pt_out, ); assert_eq!(rc, OK, "Bob should decrypt Alice's first message"); assert!(!pt_out.ptr.is_null()); let pt = std::slice::from_raw_parts(pt_out.ptr, pt_out.len); assert_eq!(pt, plaintext); soliton_buf_free(&mut pt_out); // Free SolitonEncryptedMessage buffers. let mut msg_owned = msg; soliton_encrypted_message_free(&mut msg_owned); soliton_ratchet_free(&mut alice); soliton_ratchet_free(&mut bob); } } #[test] fn ratchet_pq_free_decrypt_tampered_returns_aead_failed() { unsafe { let mut alice = make_alice(); let mut bob = make_bob(); let mut msg: MaybeUninit = MaybeUninit::zeroed(); let rc = soliton_ratchet_encrypt(alice, b"secret".as_ptr(), 6, msg.as_mut_ptr()); assert_eq!(rc, OK); let msg = msg.assume_init(); // Tamper with the ciphertext. let ct_copy = std::slice::from_raw_parts(msg.ciphertext.ptr, msg.ciphertext.len).to_vec(); let mut bad_ct = ct_copy.clone(); bad_ct[0] ^= 0xFF; let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( bob, msg.header.ratchet_pk.ptr, msg.header.ratchet_pk.len, ptr::null(), 0, msg.header.n, msg.header.pn, bad_ct.as_ptr(), bad_ct.len(), &mut pt_out, ); assert_eq!(rc, E_AEAD); soliton_buf_free(&mut pt_out); let mut msg_owned = msg; soliton_encrypted_message_free(&mut msg_owned); soliton_ratchet_free(&mut alice); soliton_ratchet_free(&mut bob); } } // ═══════════════════════════════════════════════════════════════════════════ // ratchet_derive_call_keys + call_keys_* // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_derive_call_keys_null_ratchet_returns_null_pointer() { let kem_ss = [0u8; 32]; let call_id = [0u8; 16]; let mut out: *mut SolitonCallKeys = 0xDEAD as *mut _; let rc = unsafe { soliton_ratchet_derive_call_keys( ptr::null(), kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn ratchet_derive_call_keys_null_kem_ss_returns_null_pointer() { unsafe { let mut alice = make_alice(); let call_id = [0u8; 16]; let mut out: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, ptr::null(), 32, call_id.as_ptr(), 16, &mut out, ); assert_eq!(rc, E_NULL); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_derive_call_keys_null_call_id_returns_null_pointer() { unsafe { let mut alice = make_alice(); let kem_ss = [0u8; 32]; let mut out: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys(alice, kem_ss.as_ptr(), 32, ptr::null(), 16, &mut out); assert_eq!(rc, E_NULL); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_derive_call_keys_null_out_returns_null_pointer() { unsafe { let mut alice = make_alice(); let kem_ss = [0u8; 32]; let call_id = [0u8; 16]; let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, ptr::null_mut(), ); assert_eq!(rc, E_NULL); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_derive_call_keys_wrong_kem_ss_len_returns_invalid_length() { unsafe { let mut alice = make_alice(); let kem_ss = [0x55u8; 32]; let call_id = [0xAAu8; 16]; let mut ck_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 16, call_id.as_ptr(), 16, &mut ck_ptr, ); assert_eq!(rc, E_LEN); assert!(ck_ptr.is_null()); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_derive_call_keys_wrong_call_id_len_returns_invalid_length() { unsafe { let mut alice = make_alice(); let kem_ss = [0x55u8; 32]; let call_id = [0xAAu8; 16]; let mut ck_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 32, &mut ck_ptr, ); assert_eq!(rc, E_LEN); assert!(ck_ptr.is_null()); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_derive_call_keys_zero_kem_ss_returns_invalid_data() { // All-zero kem_ss indicates a KEM failure or uninitialized buffer; the // call would derive keys from only root_key + call_id, losing ephemeral FS. unsafe { let mut alice = make_alice(); let kem_ss = [0u8; 32]; let call_id = [0xAAu8; 16]; let mut ck_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut ck_ptr, ); assert_eq!(rc, E_DATA); assert!(ck_ptr.is_null()); soliton_ratchet_free(&mut alice); } } #[test] fn ratchet_derive_call_keys_equal_fingerprints_returns_invalid_data() { // Equal fingerprints collapse send/recv role assignment: both parties // would compute the same key assignment, defeating the role split. // This is caught at init_alice/init_bob time. With the test helpers // using distinct fps (0x01 vs 0x02), derive_call_keys should succeed. // To test this guard, we'd need to construct a ratchet with equal fps, // which init_alice/init_bob now rejects. This test verifies that // derive_call_keys propagates the error if the state somehow has equal fps. unsafe { // make_alice uses 0x01/0x02 fps, so derive_call_keys will succeed. // The equal-fingerprint guard is now in init_alice/init_bob instead. let mut alice = make_alice(); let kem_ss = [0x55u8; 32]; let call_id = [0xAAu8; 16]; let mut ck_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut ck_ptr, ); assert_eq!(rc, OK); soliton_call_keys_free(&mut ck_ptr); soliton_ratchet_free(&mut alice); } } #[test] fn call_keys_null_keys_returns_null_pointer() { let mut out = [0u8; 32]; assert_eq!( unsafe { soliton_call_keys_send_key(ptr::null(), out.as_mut_ptr(), 32) }, E_NULL ); assert_eq!( unsafe { soliton_call_keys_recv_key(ptr::null(), out.as_mut_ptr(), 32) }, E_NULL ); assert_eq!( unsafe { soliton_call_keys_advance(ptr::null_mut()) }, E_NULL ); } #[test] fn call_keys_send_key_null_out_returns_null_pointer() { unsafe { let mut alice = make_alice(); let kem_ss = [0x01u8; 32]; let call_id = [0xAAu8; 16]; let mut keys_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut keys_ptr, ); assert_eq!(rc, OK); assert_eq!( soliton_call_keys_send_key(keys_ptr, ptr::null_mut(), 32), E_NULL ); soliton_call_keys_free(&mut keys_ptr); soliton_ratchet_free(&mut alice); } } #[test] fn call_keys_recv_key_null_out_returns_null_pointer() { unsafe { let mut alice = make_alice(); let kem_ss = [0x01u8; 32]; let call_id = [0xAAu8; 16]; let mut keys_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut keys_ptr, ); assert_eq!(rc, OK); assert_eq!( soliton_call_keys_recv_key(keys_ptr, ptr::null_mut(), 32), E_NULL ); soliton_call_keys_free(&mut keys_ptr); soliton_ratchet_free(&mut alice); } } #[test] fn call_keys_send_key_wrong_out_len_returns_invalid_length() { unsafe { let mut alice = make_alice(); let kem_ss = [0x01u8; 32]; let call_id = [0xAAu8; 16]; let mut keys_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut keys_ptr, ); assert_eq!(rc, OK); let mut out = [0u8; 32]; assert_eq!( soliton_call_keys_send_key(keys_ptr, out.as_mut_ptr(), 16), E_LEN ); soliton_call_keys_free(&mut keys_ptr); soliton_ratchet_free(&mut alice); } } #[test] fn call_keys_recv_key_wrong_out_len_returns_invalid_length() { unsafe { let mut alice = make_alice(); let kem_ss = [0x01u8; 32]; let call_id = [0xAAu8; 16]; let mut keys_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut keys_ptr, ); assert_eq!(rc, OK); let mut out = [0u8; 32]; assert_eq!( soliton_call_keys_recv_key(keys_ptr, out.as_mut_ptr(), 16), E_LEN ); soliton_call_keys_free(&mut keys_ptr); soliton_ratchet_free(&mut alice); } } #[test] fn call_keys_free_null_is_noop() { unsafe { soliton_call_keys_free(ptr::null_mut()); } } #[test] fn call_keys_free_double_is_noop() { unsafe { let mut alice = make_alice(); let kem_ss = [0xCCu8; 32]; let call_id = [0xDDu8; 32]; let mut keys_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut keys_ptr, ); assert_eq!(rc, 0); soliton_call_keys_free(&mut keys_ptr); assert!(keys_ptr.is_null(), "pointer must be nulled after free"); soliton_call_keys_free(&mut keys_ptr); soliton_ratchet_free(&mut alice); } } #[test] fn call_keys_derive_and_advance() { unsafe { let mut alice = make_alice(); let kem_ss = [0x55u8; 32]; let call_id = [0xAAu8; 16]; let mut ck_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut ck_ptr, ); assert_eq!(rc, OK); assert!(!ck_ptr.is_null()); let mut send_key1 = [0u8; 32]; let mut recv_key1 = [0u8; 32]; soliton_call_keys_send_key(ck_ptr, send_key1.as_mut_ptr(), 32); soliton_call_keys_recv_key(ck_ptr, recv_key1.as_mut_ptr(), 32); assert!(!is_zeroed(&send_key1)); assert!(!is_zeroed(&recv_key1)); assert_ne!(send_key1, recv_key1, "send and recv keys must differ"); // Advance ratchets keys. soliton_call_keys_advance(ck_ptr); let mut send_key2 = [0u8; 32]; soliton_call_keys_send_key(ck_ptr, send_key2.as_mut_ptr(), 32); assert_ne!(send_key1, send_key2, "keys must change after advance"); soliton_call_keys_free(&mut ck_ptr); soliton_ratchet_free(&mut alice); } } // ═══════════════════════════════════════════════════════════════════════════ // keyring_new / add_key / remove_key // ═══════════════════════════════════════════════════════════════════════════ #[test] fn keyring_new_null_key_returns_null_pointer() { let mut out: *mut SolitonKeyRing = ptr::null_mut(); let rc = unsafe { soliton_keyring_new(ptr::null(), 32, 1, &mut out) }; assert_eq!(rc, E_NULL); } #[test] fn keyring_new_version_zero_rejected() { let key = [0u8; 32]; let mut out: *mut SolitonKeyRing = 0xDEAD as *mut _; let rc = unsafe { soliton_keyring_new(key.as_ptr(), 32, 0, &mut out) }; assert_eq!(rc, E_UNSUP_VER, "version 0 must return UnsupportedVersion"); assert!(out.is_null()); } #[test] fn keyring_new_wrong_key_len_returns_invalid_length() { let key = [0u8; 32]; let mut out: *mut SolitonKeyRing = ptr::null_mut(); let rc = unsafe { soliton_keyring_new(key.as_ptr(), 16, 1, &mut out) }; assert_eq!(rc, E_LEN); assert!(out.is_null()); } #[test] fn keyring_add_key_wrong_key_len_returns_invalid_length() { unsafe { let mut kr = make_keyring(); let key = [0u8; 32]; let rc = soliton_keyring_add_key(kr, key.as_ptr(), 0, 2, 0); assert_eq!(rc, E_LEN); soliton_keyring_free(&mut kr); } } #[test] fn keyring_add_key_null_keyring_returns_null_pointer() { let key = [0u8; 32]; let rc = unsafe { soliton_keyring_add_key(ptr::null_mut(), key.as_ptr(), 32, 2, 0) }; assert_eq!(rc, E_NULL); } #[test] fn keyring_add_key_version_zero_rejected() { unsafe { let mut kr = make_keyring(); let key = [0u8; 32]; let rc = soliton_keyring_add_key(kr, key.as_ptr(), 32, 0, 0); assert_eq!(rc, E_UNSUP_VER, "version 0 must return UnsupportedVersion"); soliton_keyring_free(&mut kr); } } #[test] fn keyring_remove_key_null_keyring_returns_null_pointer() { let rc = unsafe { soliton_keyring_remove_key(ptr::null_mut(), 1) }; assert_eq!(rc, E_NULL); } #[test] fn keyring_remove_key_version_zero_rejected() { unsafe { let mut kr = make_keyring(); let rc = soliton_keyring_remove_key(kr, 0); assert_eq!(rc, E_UNSUP_VER, "version 0 must return UnsupportedVersion"); soliton_keyring_free(&mut kr); } } #[test] fn keyring_remove_active_key_rejected() { // Removing the active key (version 1) must fail. unsafe { let mut kr = make_keyring(); let rc = soliton_keyring_remove_key(kr, 1); assert_eq!( rc, E_DATA, "removing the active key must return InvalidData" ); soliton_keyring_free(&mut kr); } } #[test] fn keyring_free_null_is_noop() { unsafe { soliton_keyring_free(ptr::null_mut()); } } #[test] fn keyring_free_double_is_noop() { unsafe { let key = [0x42u8; 32]; let mut kr: *mut SolitonKeyRing = ptr::null_mut(); soliton_keyring_new(key.as_ptr(), 32, 1, &mut kr); assert!(!kr.is_null()); soliton_keyring_free(&mut kr); assert!(kr.is_null(), "pointer must be nulled after free"); soliton_keyring_free(&mut kr); } } #[test] fn keyring_add_rotate_remove() { unsafe { let mut kr = make_keyring(); let key2 = [0x22u8; 32]; // Add key 2, make it active. let rc = soliton_keyring_add_key(kr, key2.as_ptr(), 32, 2, 1); assert_eq!(rc, OK); // Now key 1 is no longer active — it can be removed. let rc = soliton_keyring_remove_key(kr, 1); assert_eq!(rc, OK); soliton_keyring_free(&mut kr); } } // ═══════════════════════════════════════════════════════════════════════════ // storage_encrypt / storage_decrypt // ═══════════════════════════════════════════════════════════════════════════ #[test] fn storage_encrypt_null_keyring_returns_null_pointer() { let ch = CString::new("ch").unwrap(); let seg = CString::new("seg").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_storage_encrypt( ptr::null(), ptr::null(), 0, ch.as_ptr(), seg.as_ptr(), 0, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn storage_encrypt_null_channel_id_returns_null_pointer() { unsafe { let mut kr = make_keyring(); let seg = CString::new("seg").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_encrypt(kr, ptr::null(), 0, ptr::null(), seg.as_ptr(), 0, &mut out); assert_eq!(rc, E_NULL); soliton_keyring_free(&mut kr); } } #[test] fn storage_encrypt_null_plaintext_nonzero_len_returns_null_pointer() { // Co-presence guard: null plaintext with nonzero plaintext_len is invalid. unsafe { let mut kr = make_keyring(); let ch = CString::new("ch").unwrap(); let seg = CString::new("seg").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_encrypt(kr, ptr::null(), 1, ch.as_ptr(), seg.as_ptr(), 0, &mut out); assert_eq!(rc, E_NULL); soliton_keyring_free(&mut kr); } } #[test] fn storage_decrypt_null_keyring_returns_null_pointer() { let blob = [0u8; 64]; let ch = CString::new("ch").unwrap(); let seg = CString::new("seg").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_storage_decrypt( ptr::null(), blob.as_ptr(), blob.len(), ch.as_ptr(), seg.as_ptr(), &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn storage_decrypt_zero_blob_len_returns_invalid_length() { unsafe { let mut kr = make_keyring(); let blob = [0u8; 1]; let ch = CString::new("ch").unwrap(); let seg = CString::new("seg").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_decrypt(kr, blob.as_ptr(), 0, ch.as_ptr(), seg.as_ptr(), &mut out); assert_eq!(rc, E_LEN); soliton_keyring_free(&mut kr); } } #[test] fn storage_encrypt_decrypt_round_trip() { unsafe { let mut kr = make_keyring(); let plaintext = b"sensitive channel message"; let ch = CString::new("channel-1").unwrap(); let seg = CString::new("segment-0").unwrap(); let mut blob_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_encrypt( kr, plaintext.as_ptr(), plaintext.len(), ch.as_ptr(), seg.as_ptr(), 0, &mut blob_out, ); assert_eq!(rc, OK); assert!(!blob_out.ptr.is_null()); let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_decrypt( kr, blob_out.ptr, blob_out.len, ch.as_ptr(), seg.as_ptr(), &mut pt_out, ); assert_eq!(rc, OK); let pt = std::slice::from_raw_parts(pt_out.ptr, pt_out.len); assert_eq!(pt, plaintext); soliton_buf_free(&mut pt_out); soliton_buf_free(&mut blob_out); soliton_keyring_free(&mut kr); } } #[test] fn storage_decrypt_wrong_aad_returns_aead_failed() { // Decrypting with wrong channel_id must fail authentication. unsafe { let mut kr = make_keyring(); let plaintext = b"data"; let ch = CString::new("channel-right").unwrap(); let ch_wrong = CString::new("channel-wrong").unwrap(); let seg = CString::new("seg").unwrap(); let mut blob_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_encrypt( kr, plaintext.as_ptr(), plaintext.len(), ch.as_ptr(), seg.as_ptr(), 0, &mut blob_out, ); assert_eq!(rc, OK); let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_decrypt( kr, blob_out.ptr, blob_out.len, ch_wrong.as_ptr(), seg.as_ptr(), &mut pt_out, ); assert_eq!(rc, E_AEAD); soliton_buf_free(&mut blob_out); soliton_keyring_free(&mut kr); } } #[test] fn storage_encrypt_empty_plaintext_round_trip() { unsafe { let mut kr = make_keyring(); let ch = CString::new("channel-1").unwrap(); let seg = CString::new("segment-0").unwrap(); let mut blob_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_encrypt( kr, ptr::null(), 0, ch.as_ptr(), seg.as_ptr(), 0, &mut blob_out, ); assert_eq!(rc, OK); assert!(!blob_out.ptr.is_null()); let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_decrypt( kr, blob_out.ptr, blob_out.len, ch.as_ptr(), seg.as_ptr(), &mut pt_out, ); assert_eq!(rc, OK); assert_eq!(pt_out.len, 0); soliton_buf_free(&mut pt_out); soliton_buf_free(&mut blob_out); soliton_keyring_free(&mut kr); } } // ═══════════════════════════════════════════════════════════════════════════ // dm_queue_encrypt / dm_queue_decrypt // ═══════════════════════════════════════════════════════════════════════════ #[test] fn dm_queue_encrypt_null_keyring_returns_null_pointer() { let fp = [0xABu8; 32]; let bid = CString::new("batch-0").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_dm_queue_encrypt( ptr::null(), ptr::null(), 0, fp.as_ptr(), 32, bid.as_ptr(), 0, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn dm_queue_encrypt_null_recipient_fp_returns_null_pointer() { unsafe { let mut kr = make_keyring(); let bid = CString::new("batch-0").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_encrypt( kr, ptr::null(), 0, ptr::null(), 0, bid.as_ptr(), 0, &mut out, ); assert_eq!(rc, E_NULL); soliton_keyring_free(&mut kr); } } #[test] fn dm_queue_encrypt_null_batch_id_returns_null_pointer() { unsafe { let mut kr = make_keyring(); let fp = [0xABu8; 32]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_encrypt( kr, ptr::null(), 0, fp.as_ptr(), 32, ptr::null(), 0, &mut out, ); assert_eq!(rc, E_NULL); soliton_keyring_free(&mut kr); } } #[test] fn dm_queue_encrypt_wrong_fp_len_returns_invalid_length() { unsafe { let mut kr = make_keyring(); let fp = [0xABu8; 32]; let bid = CString::new("batch-0").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_encrypt( kr, ptr::null(), 0, fp.as_ptr(), 16, bid.as_ptr(), 0, &mut out, ); assert_eq!(rc, E_LEN); soliton_keyring_free(&mut kr); } } #[test] fn dm_queue_decrypt_wrong_fp_len_returns_invalid_length() { unsafe { let mut kr = make_keyring(); let fp = [0xABu8; 32]; let bid = CString::new("batch-0").unwrap(); let blob = [0u8; 64]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_decrypt( kr, blob.as_ptr(), blob.len(), fp.as_ptr(), 16, bid.as_ptr(), &mut out, ); assert_eq!(rc, E_LEN); soliton_keyring_free(&mut kr); } } #[test] fn dm_queue_decrypt_null_keyring_returns_null_pointer() { let fp = [0xABu8; 32]; let bid = CString::new("batch-0").unwrap(); let blob = [0u8; 64]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_dm_queue_decrypt( ptr::null(), blob.as_ptr(), blob.len(), fp.as_ptr(), 32, bid.as_ptr(), &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn dm_queue_decrypt_zero_blob_len_returns_invalid_length() { unsafe { let mut kr = make_keyring(); let fp = [0xABu8; 32]; let bid = CString::new("batch-0").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_decrypt( kr, [0u8; 1].as_ptr(), 0, fp.as_ptr(), 32, bid.as_ptr(), &mut out, ); assert_eq!(rc, E_LEN); soliton_keyring_free(&mut kr); } } #[test] fn dm_queue_encrypt_decrypt_round_trip() { unsafe { let mut kr = make_keyring(); let plaintext = b"dm queue payload"; let fp = [0xABu8; 32]; let bid = CString::new("batch-42").unwrap(); let mut blob_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_encrypt( kr, plaintext.as_ptr(), plaintext.len(), fp.as_ptr(), 32, bid.as_ptr(), 0, &mut blob_out, ); assert_eq!(rc, OK); assert!(!blob_out.ptr.is_null()); let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_decrypt( kr, blob_out.ptr, blob_out.len, fp.as_ptr(), 32, bid.as_ptr(), &mut pt_out, ); assert_eq!(rc, OK); let pt = std::slice::from_raw_parts(pt_out.ptr, pt_out.len); assert_eq!(pt, plaintext); soliton_buf_free(&mut pt_out); soliton_buf_free(&mut blob_out); soliton_keyring_free(&mut kr); } } #[test] fn dm_queue_decrypt_wrong_fp_returns_aead_failed() { unsafe { let mut kr = make_keyring(); let plaintext = b"data"; let fp = [0xABu8; 32]; let fp_wrong = [0xCDu8; 32]; let bid = CString::new("batch-0").unwrap(); let mut blob_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_encrypt( kr, plaintext.as_ptr(), plaintext.len(), fp.as_ptr(), 32, bid.as_ptr(), 0, &mut blob_out, ); assert_eq!(rc, OK); let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_decrypt( kr, blob_out.ptr, blob_out.len, fp_wrong.as_ptr(), 32, bid.as_ptr(), &mut pt_out, ); assert_eq!(rc, E_AEAD); soliton_buf_free(&mut blob_out); soliton_keyring_free(&mut kr); } } #[test] fn dm_queue_encrypt_empty_plaintext_round_trip() { unsafe { let mut kr = make_keyring(); let fp = [0xABu8; 32]; let bid = CString::new("batch-0").unwrap(); let mut blob_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_encrypt( kr, ptr::null(), 0, fp.as_ptr(), 32, bid.as_ptr(), 0, &mut blob_out, ); assert_eq!(rc, OK); assert!(!blob_out.ptr.is_null()); let mut pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_dm_queue_decrypt( kr, blob_out.ptr, blob_out.len, fp.as_ptr(), 32, bid.as_ptr(), &mut pt_out, ); assert_eq!(rc, OK); assert_eq!(pt_out.len, 0); soliton_buf_free(&mut pt_out); soliton_buf_free(&mut blob_out); soliton_keyring_free(&mut kr); } } // ═══════════════════════════════════════════════════════════════════════════ // kex_sign_prekey / kex_verify_bundle / kex_initiate / kex_receive — error paths // ═══════════════════════════════════════════════════════════════════════════ #[test] fn kex_sign_prekey_null_sk_returns_null_pointer() { let spk = [0u8; XPKZ]; let mut sig_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_sign_prekey(ptr::null(), SK, spk.as_ptr(), XPKZ, &mut sig_out) }; assert_eq!(rc, E_NULL); } #[test] fn kex_sign_prekey_zero_sk_len_returns_invalid_length() { let sk = [0u8; SK]; let spk = [0u8; XPKZ]; let mut nonzero = 0xABu8; let mut sig = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let rc = unsafe { soliton_kex_sign_prekey(sk.as_ptr(), 0, spk.as_ptr(), XPKZ, &mut sig) }; assert_eq!(rc, E_LEN); assert!(sig.ptr.is_null()); } #[test] fn kex_sign_prekey_null_spk_pub_nonzero_len_returns_null_pointer() { // spk_pub is always null-checked (not co-presence); nonzero len still triggers E_NULL. let sk = [0u8; SK]; let mut sig_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_sign_prekey(sk.as_ptr(), SK, ptr::null(), XPKZ, &mut sig_out) }; assert_eq!(rc, E_NULL); } #[test] fn kex_verify_bundle_null_pk_returns_null_pointer() { let pk = [0u8; PK]; let spk = [0u8; XPKZ]; let sig = [0u8; HSIG]; let cv = CString::new("lo-crypto-v1").unwrap(); let rc = unsafe { soliton_kex_verify_bundle( ptr::null(), PK, pk.as_ptr(), PK, spk.as_ptr(), XPKZ, sig.as_ptr(), HSIG, cv.as_ptr(), ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_verify_bundle_zero_len_returns_invalid_length() { let pk = [0u8; PK]; let spk = [0u8; XPKZ]; let sig = [0u8; HSIG]; let cv = CString::new("lo-crypto-v1").unwrap(); let rc = unsafe { soliton_kex_verify_bundle( pk.as_ptr(), 0, pk.as_ptr(), PK, spk.as_ptr(), XPKZ, sig.as_ptr(), HSIG, cv.as_ptr(), ) }; assert_eq!(rc, E_LEN); } #[test] fn kex_verify_bundle_null_known_ik_pk_returns_null_pointer() { let pk = [0u8; PK]; let spk = [0u8; XPKZ]; let sig = [0u8; HSIG]; let cv = CString::new("lo-crypto-v1").unwrap(); let rc = unsafe { soliton_kex_verify_bundle( pk.as_ptr(), PK, ptr::null(), PK, spk.as_ptr(), XPKZ, sig.as_ptr(), HSIG, cv.as_ptr(), ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_initiate_null_alice_pk_returns_null_pointer() { let pk = [0u8; PK]; let sk = [0u8; SK]; let spk = [0u8; XPKZ]; let sig = [0u8; HSIG]; let cv = CString::new("lo-crypto-v1").unwrap(); let mut out_storage = MaybeUninit::::uninit(); let out = out_storage.as_mut_ptr(); let rc = unsafe { soliton_kex_initiate( ptr::null(), PK, sk.as_ptr(), SK, pk.as_ptr(), PK, spk.as_ptr(), XPKZ, 1, sig.as_ptr(), HSIG, ptr::null(), 0, 0, cv.as_ptr(), out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_receive_null_bob_pk_returns_null_pointer() { let pk = [0u8; PK]; let _sk = [0u8; SK]; // declared for symmetry; null check fires before it's needed let spk_sk = [0u8; XSKZ]; let sig = [0u8; HSIG]; let ek = [0u8; XPKZ]; let ct = [0u8; XCTZ]; let fp = [0u8; 32]; let cv = CString::new("lo-crypto-v1").unwrap(); let wire = SolitonSessionInitWire { sender_sig: sig.as_ptr(), sender_sig_len: HSIG, sender_ik_fingerprint: fp.as_ptr(), sender_ik_fingerprint_len: 32, recipient_ik_fingerprint: fp.as_ptr(), recipient_ik_fingerprint_len: 32, sender_ek: ek.as_ptr(), sender_ek_len: XPKZ, ct_ik: ct.as_ptr(), ct_ik_len: XCTZ, ct_spk: ct.as_ptr(), ct_spk_len: XCTZ, spk_id: 1, ct_opk: ptr::null(), ct_opk_len: 0, opk_id: 0, crypto_version: cv.as_ptr(), }; let decap_keys = SolitonSessionDecapKeys { spk_sk: spk_sk.as_ptr(), spk_sk_len: XSKZ, opk_sk: ptr::null(), opk_sk_len: 0, }; let mut out = SolitonReceivedSession { root_key: [0u8; 32], chain_key: [0u8; 32], peer_ek: SolitonBuf { ptr: ptr::null_mut(), len: 0, }, }; let rc = unsafe { soliton_kex_receive( ptr::null(), PK, pk.as_ptr(), SK, pk.as_ptr(), PK, &wire, &decap_keys, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_received_session_free_null_is_noop() { unsafe { soliton_kex_received_session_free(ptr::null_mut()); } } #[test] fn kex_initiated_session_free_null_is_noop() { unsafe { soliton_kex_initiated_session_free(ptr::null_mut()); } } #[test] fn kex_receive_null_sender_sig_returns_null_pointer() { let pk = [0u8; PK]; let spk_sk = [0u8; XSKZ]; let ek = [0u8; XPKZ]; let ct = [0u8; XCTZ]; let fp = [0u8; 32]; let cv = CString::new("lo-crypto-v1").unwrap(); let wire = SolitonSessionInitWire { sender_sig: ptr::null(), // null — triggers NullPointer sender_sig_len: HSIG, sender_ik_fingerprint: fp.as_ptr(), sender_ik_fingerprint_len: 32, recipient_ik_fingerprint: fp.as_ptr(), recipient_ik_fingerprint_len: 32, sender_ek: ek.as_ptr(), sender_ek_len: XPKZ, ct_ik: ct.as_ptr(), ct_ik_len: XCTZ, ct_spk: ct.as_ptr(), ct_spk_len: XCTZ, spk_id: 1, ct_opk: ptr::null(), ct_opk_len: 0, opk_id: 0, crypto_version: cv.as_ptr(), }; let decap_keys = SolitonSessionDecapKeys { spk_sk: spk_sk.as_ptr(), spk_sk_len: XSKZ, opk_sk: ptr::null(), opk_sk_len: 0, }; let mut out = SolitonReceivedSession { root_key: [0u8; 32], chain_key: [0u8; 32], peer_ek: SolitonBuf { ptr: ptr::null_mut(), len: 0, }, }; let rc = unsafe { soliton_kex_receive( pk.as_ptr(), PK, pk.as_ptr(), PK, pk.as_ptr(), PK, &wire, &decap_keys, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_receive_null_crypto_version_returns_null_pointer() { let pk = [0u8; PK]; let spk_sk = [0u8; XSKZ]; let sig = [0u8; HSIG]; let ek = [0u8; XPKZ]; let ct = [0u8; XCTZ]; let fp = [0u8; 32]; let wire = SolitonSessionInitWire { sender_sig: sig.as_ptr(), sender_sig_len: HSIG, sender_ik_fingerprint: fp.as_ptr(), sender_ik_fingerprint_len: 32, recipient_ik_fingerprint: fp.as_ptr(), recipient_ik_fingerprint_len: 32, sender_ek: ek.as_ptr(), sender_ek_len: XPKZ, ct_ik: ct.as_ptr(), ct_ik_len: XCTZ, ct_spk: ct.as_ptr(), ct_spk_len: XCTZ, spk_id: 1, ct_opk: ptr::null(), ct_opk_len: 0, opk_id: 0, crypto_version: ptr::null(), // null — triggers NullPointer }; let decap_keys = SolitonSessionDecapKeys { spk_sk: spk_sk.as_ptr(), spk_sk_len: XSKZ, opk_sk: ptr::null(), opk_sk_len: 0, }; let mut out = SolitonReceivedSession { root_key: [0u8; 32], chain_key: [0u8; 32], peer_ek: SolitonBuf { ptr: ptr::null_mut(), len: 0, }, }; let rc = unsafe { soliton_kex_receive( pk.as_ptr(), PK, pk.as_ptr(), PK, pk.as_ptr(), PK, &wire, &decap_keys, &mut out, ) }; assert_eq!(rc, E_NULL); } // ═══════════════════════════════════════════════════════════════════════════ // kex_build_first_message_aad / kex_encode_session_init // ═══════════════════════════════════════════════════════════════════════════ #[test] fn kex_build_aad_null_sender_fp_returns_null_pointer() { let fp = [0u8; 32]; let encoded = b"data"; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_build_first_message_aad( ptr::null(), 32, fp.as_ptr(), 32, encoded.as_ptr(), encoded.len(), &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_build_aad_null_recipient_fp_returns_null_pointer() { let fp = [0u8; 32]; let encoded = b"data"; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_build_first_message_aad( fp.as_ptr(), 32, ptr::null(), 32, encoded.as_ptr(), encoded.len(), &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_build_aad_null_session_init_encoded_returns_null_pointer() { let fp = [0u8; 32]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_build_first_message_aad( fp.as_ptr(), 32, fp.as_ptr(), 32, ptr::null(), 1, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_build_aad_null_aad_out_returns_null_pointer() { let fp = [0u8; 32]; let encoded = b"data"; let rc = unsafe { soliton_kex_build_first_message_aad( fp.as_ptr(), 32, fp.as_ptr(), 32, encoded.as_ptr(), encoded.len(), ptr::null_mut(), ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_build_aad_zero_encoded_len_returns_invalid_length() { let fp = [0u8; 32]; let encoded = b"data"; let mut nonzero = 0xABu8; let mut out = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let rc = unsafe { soliton_kex_build_first_message_aad( fp.as_ptr(), 32, fp.as_ptr(), 32, encoded.as_ptr(), 0, &mut out, ) }; assert_eq!(rc, E_LEN); assert!(out.ptr.is_null()); } #[test] fn kex_build_aad_oversized_encoded_returns_invalid_length() { // encoded_len > 8192 → InvalidLength. let fp = [0u8; 32]; let encoded = vec![0u8; 8193]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_build_first_message_aad( fp.as_ptr(), 32, fp.as_ptr(), 32, encoded.as_ptr(), encoded.len(), &mut out, ) }; assert_eq!(rc, E_LEN); } #[test] fn kex_build_aad_valid_inputs_returns_aad() { let fp_a = [0x01u8; 32]; let fp_b = [0x02u8; 32]; let encoded = b"minimal_encoded_session_init"; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_build_first_message_aad( fp_a.as_ptr(), 32, fp_b.as_ptr(), 32, encoded.as_ptr(), encoded.len(), &mut out, ) }; assert_eq!(rc, OK); assert!(!out.ptr.is_null()); unsafe { soliton_buf_free(&mut out) }; } #[test] fn kex_encode_session_init_null_crypto_version_returns_null_pointer() { let fp = [0u8; 32]; let ek = [0u8; XPKZ]; let ct = [0u8; XCTZ]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_encode_session_init( ptr::null(), fp.as_ptr(), 32, fp.as_ptr(), 32, ek.as_ptr(), XPKZ, ct.as_ptr(), XCTZ, ct.as_ptr(), XCTZ, 1, ptr::null(), 0, 0, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_encode_session_init_copresence_ct_opk_null_nonzero_len() { // ct_opk=null but ct_opk_len=5 → NullPointer. let fp = [0u8; 32]; let ek = [0u8; XPKZ]; let ct = [0u8; XCTZ]; let cv = CString::new("lo-crypto-v1").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_encode_session_init( cv.as_ptr(), fp.as_ptr(), 32, fp.as_ptr(), 32, ek.as_ptr(), XPKZ, ct.as_ptr(), XCTZ, ct.as_ptr(), XCTZ, 1, ptr::null(), 5, 0, &mut out, ) }; assert_eq!(rc, E_NULL); } #[test] fn kex_encode_session_init_nonzero_opk_id_without_ct_opk_returns_invalid_data() { // ct_opk=null but opk_id=5 → InvalidData. let fp = [0u8; 32]; let ek = [0u8; XPKZ]; let ct = [0u8; XCTZ]; let cv = CString::new("lo-crypto-v1").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_encode_session_init( cv.as_ptr(), fp.as_ptr(), 32, fp.as_ptr(), 32, ek.as_ptr(), XPKZ, ct.as_ptr(), XCTZ, ct.as_ptr(), XCTZ, 1, ptr::null(), 0, 5, &mut out, ) }; assert_eq!(rc, E_DATA); } #[test] fn kex_encode_session_init_zeroed_keys_succeeds() { // With zeroed X-Wing keys (length-only validation), encoding must succeed. let fp = [0u8; 32]; let ek = [0u8; XPKZ]; let ct = [0u8; XCTZ]; let cv = CString::new("lo-crypto-v1").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_kex_encode_session_init( cv.as_ptr(), fp.as_ptr(), 32, fp.as_ptr(), 32, ek.as_ptr(), XPKZ, ct.as_ptr(), XCTZ, ct.as_ptr(), XCTZ, 1, ptr::null(), 0, 0, &mut out, ) }; assert_eq!(rc, OK); assert!(!out.ptr.is_null()); unsafe { soliton_buf_free(&mut out) }; } // ── kex_decode_session_init ────────────────────────────────────────────────── /// Produce valid encoded SessionInit bytes via encode, for use in decode tests. /// /// Uses zeroed key bytes — length-only validation, no keygen, MIRI-safe. fn make_encoded_session_init(with_opk: bool) -> Vec { let fp = [0x01u8; 32]; let rfp = [0x02u8; 32]; let ek = [0x03u8; XPKZ]; let ct = [0x04u8; XCTZ]; let ct2 = [0x05u8; XCTZ]; let ct_opk = [0x06u8; XCTZ]; let cv = CString::new("lo-crypto-v1").unwrap(); let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { if with_opk { soliton_kex_encode_session_init( cv.as_ptr(), fp.as_ptr(), 32, rfp.as_ptr(), 32, ek.as_ptr(), XPKZ, ct.as_ptr(), XCTZ, ct2.as_ptr(), XCTZ, 42, ct_opk.as_ptr(), XCTZ, 99, &mut out, ) } else { soliton_kex_encode_session_init( cv.as_ptr(), fp.as_ptr(), 32, rfp.as_ptr(), 32, ek.as_ptr(), XPKZ, ct.as_ptr(), XCTZ, ct2.as_ptr(), XCTZ, 42, ptr::null(), 0, 0, &mut out, ) } }; assert_eq!(rc, OK, "encode must succeed for decode tests"); let bytes = unsafe { std::slice::from_raw_parts(out.ptr, out.len) }.to_vec(); unsafe { soliton_buf_free(&mut out) }; bytes } #[test] fn kex_decode_session_init_null_encoded_returns_null_pointer() { let mut out: SolitonDecodedSessionInit = unsafe { MaybeUninit::zeroed().assume_init() }; let rc = unsafe { soliton_kex_decode_session_init(ptr::null(), 10, &mut out) }; assert_eq!(rc, E_NULL); } #[test] fn kex_decode_session_init_null_out_returns_null_pointer() { let encoded = [0u8; 10]; let rc = unsafe { soliton_kex_decode_session_init(encoded.as_ptr(), encoded.len(), ptr::null_mut()) }; assert_eq!(rc, E_NULL); } #[test] fn kex_decode_session_init_zero_len_returns_invalid_length() { let encoded = [0u8; 10]; let mut out: SolitonDecodedSessionInit = unsafe { MaybeUninit::zeroed().assume_init() }; let rc = unsafe { soliton_kex_decode_session_init(encoded.as_ptr(), 0, &mut out) }; assert_eq!(rc, E_LEN); } #[test] fn kex_decode_session_init_truncated_returns_invalid_data() { let encoded = make_encoded_session_init(false); // Truncate by 1 byte — parser can't reach the end. let truncated = &encoded[..encoded.len() - 1]; let mut out: SolitonDecodedSessionInit = unsafe { MaybeUninit::zeroed().assume_init() }; let rc = unsafe { soliton_kex_decode_session_init(truncated.as_ptr(), truncated.len(), &mut out) }; assert_eq!(rc, E_DATA); // Output must be zeroed on error. assert!(out.crypto_version.ptr.is_null()); } #[test] fn kex_decode_session_init_trailing_byte_returns_invalid_data() { let mut encoded = make_encoded_session_init(false); encoded.push(0x00); let mut out: SolitonDecodedSessionInit = unsafe { MaybeUninit::zeroed().assume_init() }; let rc = unsafe { soliton_kex_decode_session_init(encoded.as_ptr(), encoded.len(), &mut out) }; assert_eq!(rc, E_DATA); assert!(out.crypto_version.ptr.is_null()); } #[test] fn kex_decode_session_init_roundtrip_without_opk() { let encoded = make_encoded_session_init(false); let mut out: SolitonDecodedSessionInit = unsafe { MaybeUninit::zeroed().assume_init() }; let rc = unsafe { soliton_kex_decode_session_init(encoded.as_ptr(), encoded.len(), &mut out) }; assert_eq!(rc, OK); // crypto_version must be "lo-crypto-v1" with null terminator assert!(!out.crypto_version.ptr.is_null()); let cv_bytes = unsafe { std::slice::from_raw_parts(out.crypto_version.ptr, out.crypto_version.len) }; assert_eq!(cv_bytes, b"lo-crypto-v1\0"); // Fixed fields assert_eq!(out.sender_fp, [0x01u8; 32]); assert_eq!(out.recipient_fp, [0x02u8; 32]); assert_eq!(out.sender_ek, [0x03u8; XPKZ]); assert_eq!(out.ct_ik, [0x04u8; XCTZ]); assert_eq!(out.ct_spk, [0x05u8; XCTZ]); assert_eq!(out.spk_id, 42); assert_eq!(out.has_opk, 0x00); // ct_opk / opk_id not valid — no assertion on their values unsafe { soliton_decoded_session_init_free(&mut out) }; } #[test] fn kex_decode_session_init_roundtrip_with_opk() { let encoded = make_encoded_session_init(true); let mut out: SolitonDecodedSessionInit = unsafe { MaybeUninit::zeroed().assume_init() }; let rc = unsafe { soliton_kex_decode_session_init(encoded.as_ptr(), encoded.len(), &mut out) }; assert_eq!(rc, OK); // crypto_version must be "lo-crypto-v1" with null terminator assert!(!out.crypto_version.ptr.is_null()); let cv_bytes = unsafe { std::slice::from_raw_parts(out.crypto_version.ptr, out.crypto_version.len) }; assert_eq!(cv_bytes, b"lo-crypto-v1\0"); assert_eq!(out.sender_fp, [0x01u8; 32]); assert_eq!(out.recipient_fp, [0x02u8; 32]); assert_eq!(out.sender_ek, [0x03u8; XPKZ]); assert_eq!(out.ct_ik, [0x04u8; XCTZ]); assert_eq!(out.ct_spk, [0x05u8; XCTZ]); assert_eq!(out.spk_id, 42); assert_eq!(out.has_opk, 0x01); assert_eq!(out.ct_opk, [0x06u8; XCTZ]); assert_eq!(out.opk_id, 99); unsafe { soliton_decoded_session_init_free(&mut out) }; } // ═══════════════════════════════════════════════════════════════════════════ // verification_phrase // ═══════════════════════════════════════════════════════════════════════════ #[test] fn verification_phrase_null_pk_a_returns_null_pointer() { let pk = [0u8; PK]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_verification_phrase(ptr::null(), PK, pk.as_ptr(), PK, &mut out) }; assert_eq!(rc, E_NULL); } #[test] fn verification_phrase_null_pk_b_returns_null_pointer() { let pk = [0u8; PK]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_verification_phrase(pk.as_ptr(), PK, ptr::null(), PK, &mut out) }; assert_eq!(rc, E_NULL); } #[test] fn verification_phrase_wrong_pk_len_returns_invalid_length() { let pk = [0u8; PK]; let mut nonzero = 0xABu8; let mut out = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let rc = unsafe { soliton_verification_phrase(pk.as_ptr(), PK - 1, pk.as_ptr(), PK, &mut out) }; assert_eq!(rc, E_LEN); assert!(out.ptr.is_null()); } #[test] fn verification_phrase_zeroed_pks_produces_phrase() { // from_bytes is length-only — no keygen needed. let pk_a = [0x11u8; PK]; let pk_b = [0x22u8; PK]; let mut out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_verification_phrase(pk_a.as_ptr(), PK, pk_b.as_ptr(), PK, &mut out) }; assert_eq!(rc, OK); assert!(!out.ptr.is_null()); assert!(out.len > 0); // Phrase is null-terminated; the null byte is at index len-1. let phrase_bytes = unsafe { std::slice::from_raw_parts(out.ptr, out.len) }; assert_eq!( *phrase_bytes.last().unwrap(), 0, "phrase must be null-terminated" ); unsafe { soliton_buf_free(&mut out) }; } #[test] fn verification_phrase_deterministic() { let pk_a = [0x11u8; PK]; let pk_b = [0x22u8; PK]; unsafe { let mut out1 = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut out2 = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_verification_phrase(pk_a.as_ptr(), PK, pk_b.as_ptr(), PK, &mut out1); assert_eq!(rc, OK); let rc = soliton_verification_phrase(pk_a.as_ptr(), PK, pk_b.as_ptr(), PK, &mut out2); assert_eq!(rc, OK); let p1 = std::slice::from_raw_parts(out1.ptr, out1.len); let p2 = std::slice::from_raw_parts(out2.ptr, out2.len); assert_eq!(p1, p2); soliton_buf_free(&mut out1); soliton_buf_free(&mut out2); } } #[test] fn verification_phrase_commutative() { // verification_phrase sorts keys before hashing, so phrase(A, B) == phrase(B, A). let pk_a = [0x11u8; PK]; let pk_b = [0x22u8; PK]; unsafe { let mut out_ab = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut out_ba = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_verification_phrase(pk_a.as_ptr(), PK, pk_b.as_ptr(), PK, &mut out_ab); assert_eq!(rc, OK); let rc = soliton_verification_phrase(pk_b.as_ptr(), PK, pk_a.as_ptr(), PK, &mut out_ba); assert_eq!(rc, OK); let p_ab = std::slice::from_raw_parts(out_ab.ptr, out_ab.len); let p_ba = std::slice::from_raw_parts(out_ba.ptr, out_ba.len); assert_eq!(p_ab, p_ba, "verification_phrase must be commutative"); soliton_buf_free(&mut out_ab); soliton_buf_free(&mut out_ba); } } // ═══════════════════════════════════════════════════════════════════════════ // xwing_keygen / encapsulate / decapsulate — error paths only // ═══════════════════════════════════════════════════════════════════════════ #[test] fn xwing_keygen_null_pk_out_returns_null_pointer() { let mut sk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_xwing_keygen(ptr::null_mut(), &mut sk_out) }; assert_eq!(rc, E_NULL); } #[test] fn xwing_keygen_null_sk_out_zeroes_pk_out() { let mut pk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_xwing_keygen(&mut pk_out, ptr::null_mut()) }; assert_eq!(rc, E_NULL); // Null check fires before zeroing for xwing_keygen — only the return code matters here. } #[test] fn xwing_encapsulate_null_pk_returns_null_pointer() { let mut ct_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut ss = [0u8; 32]; let rc = unsafe { soliton_xwing_encapsulate(ptr::null(), XPKZ, &mut ct_out, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn xwing_encapsulate_zero_pk_len_zeroes_outputs() { let pk = [0u8; XPKZ]; let mut nonzero = 0xABu8; let mut ct = SolitonBuf { ptr: &mut nonzero as *mut u8, len: 42, }; let mut ss = fill_nonzero::<32>(); let rc = unsafe { soliton_xwing_encapsulate(pk.as_ptr(), 0, &mut ct, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(ct.ptr.is_null()); assert!(is_zeroed(&ss), "ss must be zeroed on error"); } #[test] fn xwing_decapsulate_null_sk_returns_null_pointer() { let ct = [0u8; XCTZ]; let mut ss = [0u8; 32]; let rc = unsafe { soliton_xwing_decapsulate(ptr::null(), XSKZ, ct.as_ptr(), XCTZ, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_NULL); } #[test] fn xwing_decapsulate_zero_sk_len_zeroes_ss() { let sk = [0u8; XSKZ]; let ct = [0u8; XCTZ]; let mut ss = fill_nonzero::<32>(); let rc = unsafe { soliton_xwing_decapsulate(sk.as_ptr(), 0, ct.as_ptr(), XCTZ, ss.as_mut_ptr(), 32) }; assert_eq!(rc, E_LEN); assert!(is_zeroed(&ss), "ss must be zeroed on error"); } #[test] fn xwing_decapsulate_wrong_ct_len_zeroes_ss() { let sk = [0u8; XSKZ]; let ct = [0u8; XCTZ]; let mut ss = fill_nonzero::<32>(); // ct_len != SOLITON_XWING_CT_SIZE → InvalidLength; ss must be zeroed. let rc = unsafe { soliton_xwing_decapsulate( sk.as_ptr(), XSKZ, ct.as_ptr(), XCTZ - 1, ss.as_mut_ptr(), 32, ) }; assert_eq!(rc, E_LEN); assert!(is_zeroed(&ss), "ss must be zeroed on wrong-length ct"); } // ═══════════════════════════════════════════════════════════════════════════ // Concurrent access detection — excluded from MIRI (multi-threaded) // ═══════════════════════════════════════════════════════════════════════════ #[test] fn ratchet_concurrent_access_detected() { use std::sync::{Arc, Barrier}; use std::thread; unsafe { let ratchet = make_alice(); let ptr_val = ratchet as usize; let barrier = Arc::new(Barrier::new(2)); let plaintext = [0u8; 64]; let b = barrier.clone(); let t = thread::spawn(move || { b.wait(); let ptr = ptr_val as *mut SolitonRatchet; let mut concurrent_seen = false; // Use encrypt (holds the guard for HMAC + AEAD, ~microseconds) // instead of reset (nanoseconds) to widen the collision window. for _ in 0..1_000 { let mut msg = std::mem::zeroed::(); let rc = soliton_ratchet_encrypt(ptr, plaintext.as_ptr(), plaintext.len(), &mut msg); if rc == E_CONCURRENT { concurrent_seen = true; } if rc == OK { soliton_encrypted_message_free(&mut msg); } } concurrent_seen }); barrier.wait(); let mut concurrent_seen = false; for _ in 0..1_000 { let mut msg = std::mem::zeroed::(); let rc = soliton_ratchet_encrypt(ratchet, plaintext.as_ptr(), plaintext.len(), &mut msg); if rc == E_CONCURRENT { concurrent_seen = true; } if rc == OK { soliton_encrypted_message_free(&mut msg); } } let thread_saw = t.join().unwrap(); assert!( concurrent_seen || thread_saw, "expected at least one ConcurrentAccess across 2000 encrypt calls" ); let mut ratchet = ratchet; soliton_ratchet_free(&mut ratchet); } } #[test] fn keyring_concurrent_access_detected() { use std::sync::{Arc, Barrier}; use std::thread; unsafe { let key = [0x42u8; 32]; let mut kr: *mut SolitonKeyRing = ptr::null_mut(); soliton_keyring_new(key.as_ptr(), 32, 1, &mut kr); assert!(!kr.is_null()); let ptr_val = kr as usize; let barrier = Arc::new(Barrier::new(2)); let plaintext = [0xAAu8; 256]; let channel = c"concurrent-ch"; let segment = c"concurrent-seg"; let b = barrier.clone(); let t = thread::spawn(move || { b.wait(); let ptr = ptr_val as *const SolitonKeyRing; let mut concurrent_seen = false; // Use storage_encrypt (holds the guard for AEAD, ~microseconds) // instead of remove_key (nanoseconds) to widen the collision window. for _ in 0..1_000 { let mut blob = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_encrypt( ptr, plaintext.as_ptr(), plaintext.len(), channel.as_ptr(), segment.as_ptr(), 0, &mut blob, ); if rc == E_CONCURRENT { concurrent_seen = true; } if rc == OK && !blob.ptr.is_null() { soliton_buf_free(&mut blob); } } concurrent_seen }); barrier.wait(); let mut concurrent_seen = false; for _ in 0..1_000 { let mut blob = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_storage_encrypt( kr, plaintext.as_ptr(), plaintext.len(), channel.as_ptr(), segment.as_ptr(), 0, &mut blob, ); if rc == E_CONCURRENT { concurrent_seen = true; } if rc == OK && !blob.ptr.is_null() { soliton_buf_free(&mut blob); } } let thread_saw = t.join().unwrap(); assert!( concurrent_seen || thread_saw, "expected at least one ConcurrentAccess across 2000 encrypt calls" ); soliton_keyring_free(&mut kr); } } #[test] fn call_keys_concurrent_access_detected() { use std::sync::{Arc, Barrier}; use std::thread; unsafe { let mut alice = make_alice(); let kem_ss = [0xCCu8; 32]; let call_id = [0xDDu8; 32]; let mut keys_ptr: *mut SolitonCallKeys = ptr::null_mut(); let rc = soliton_ratchet_derive_call_keys( alice, kem_ss.as_ptr(), 32, call_id.as_ptr(), 16, &mut keys_ptr, ); assert_eq!(rc, OK); let ptr_val = keys_ptr as usize; let barrier = Arc::new(Barrier::new(2)); let b = barrier.clone(); let t = thread::spawn(move || { b.wait(); let ptr = ptr_val as *mut SolitonCallKeys; let mut concurrent_seen = false; for _ in 0..10_000 { let rc = soliton_call_keys_advance(ptr); assert!( rc == OK || rc == E_CONCURRENT, "expected OK or ConcurrentAccess, got {rc}" ); if rc == E_CONCURRENT { concurrent_seen = true; } thread::yield_now(); } concurrent_seen }); barrier.wait(); let mut concurrent_seen = false; for _ in 0..10_000 { let rc = soliton_call_keys_advance(keys_ptr); assert!( rc == OK || rc == E_CONCURRENT, "expected OK, ConcurrentAccess, or ChainExhausted, got {rc}" ); if rc == E_CONCURRENT { concurrent_seen = true; } thread::yield_now(); } let thread_saw = t.join().unwrap(); assert!( concurrent_seen || thread_saw, "expected at least one ConcurrentAccess across 20000 calls" ); soliton_call_keys_free(&mut keys_ptr); soliton_ratchet_free(&mut alice); } } // ═══════════════════════════════════════════════════════════════════════════ // PQ round-trips — excluded from MIRI via nextest profile filter // ═══════════════════════════════════════════════════════════════════════════ pub mod pq { use super::*; #[test] fn xwing_keygen_encap_decap_round_trip() { unsafe { let mut pk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut sk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_xwing_keygen(&mut pk_out, &mut sk_out); assert_eq!(rc, OK); assert_eq!(pk_out.len, XPKZ); assert_eq!(sk_out.len, XSKZ); let mut ct_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut ss_enc = [0u8; 32]; let rc = soliton_xwing_encapsulate( pk_out.ptr, pk_out.len, &mut ct_out, ss_enc.as_mut_ptr(), 32, ); assert_eq!(rc, OK); assert_eq!(ct_out.len, XCTZ); let mut ss_dec = [0u8; 32]; let rc = soliton_xwing_decapsulate( sk_out.ptr, sk_out.len, ct_out.ptr, ct_out.len, ss_dec.as_mut_ptr(), 32, ); assert_eq!(rc, OK); assert_eq!(ss_enc, ss_dec, "shared secrets must match"); assert!(!is_zeroed(&ss_enc)); soliton_buf_free(&mut ct_out); soliton_buf_free(&mut sk_out); soliton_buf_free(&mut pk_out); } } #[test] fn identity_generate_sign_verify_round_trip() { unsafe { let mut pk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut sk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut fp_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_identity_generate(&mut pk_out, &mut sk_out, &mut fp_out); assert_eq!(rc, OK); assert_eq!(pk_out.len, PK); assert_eq!(sk_out.len, SK); assert!(fp_out.len > 0); // hex string + null terminator let msg = b"test message"; let mut sig_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_identity_sign( sk_out.ptr, sk_out.len, msg.as_ptr(), msg.len(), &mut sig_out, ); assert_eq!(rc, OK); assert_eq!(sig_out.len, HSIG); let rc = soliton_identity_verify( pk_out.ptr, pk_out.len, msg.as_ptr(), msg.len(), sig_out.ptr, sig_out.len, ); assert_eq!(rc, OK); // Wrong message must fail. let bad_msg = b"wrong message"; let rc = soliton_identity_verify( pk_out.ptr, pk_out.len, bad_msg.as_ptr(), bad_msg.len(), sig_out.ptr, sig_out.len, ); assert_eq!(rc, E_VER); soliton_buf_free(&mut sig_out); soliton_buf_free(&mut fp_out); soliton_buf_free(&mut sk_out); soliton_buf_free(&mut pk_out); } } #[test] fn identity_sign_verify_empty_message() { unsafe { let mut pk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut sk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut fp_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_identity_generate(&mut pk_out, &mut sk_out, &mut fp_out); let mut sig_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; // Sign empty message (null + len=0 is allowed). let rc = soliton_identity_sign(sk_out.ptr, sk_out.len, ptr::null(), 0, &mut sig_out); assert_eq!(rc, OK); let rc = soliton_identity_verify( pk_out.ptr, pk_out.len, ptr::null(), 0, sig_out.ptr, sig_out.len, ); assert_eq!(rc, OK); soliton_buf_free(&mut sig_out); soliton_buf_free(&mut fp_out); soliton_buf_free(&mut sk_out); soliton_buf_free(&mut pk_out); } } #[test] fn identity_encapsulate_decapsulate_round_trip() { unsafe { let mut pk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut sk_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut fp_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_identity_generate(&mut pk_out, &mut sk_out, &mut fp_out); let mut ct_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut ss_enc = [0u8; 32]; let rc = soliton_identity_encapsulate( pk_out.ptr, pk_out.len, &mut ct_out, ss_enc.as_mut_ptr(), 32, ); assert_eq!(rc, OK); let mut ss_dec = [0u8; 32]; let rc = soliton_identity_decapsulate( sk_out.ptr, sk_out.len, ct_out.ptr, ct_out.len, ss_dec.as_mut_ptr(), 32, ); assert_eq!(rc, OK); assert_eq!(ss_enc, ss_dec); assert!(!is_zeroed(&ss_enc)); soliton_buf_free(&mut ct_out); soliton_buf_free(&mut fp_out); soliton_buf_free(&mut sk_out); soliton_buf_free(&mut pk_out); } } #[test] fn auth_challenge_respond_verify_round_trip() { unsafe { // Generate client keypair. let mut client_pk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut client_sk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut fp = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_identity_generate(&mut client_pk, &mut client_sk, &mut fp); // Server: challenge. let mut ct_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut token = [0u8; 32]; let rc = soliton_auth_challenge( client_pk.ptr, client_pk.len, &mut ct_out, token.as_mut_ptr(), 32, ); assert_eq!(rc, OK); // Client: respond. let mut proof = [0u8; 32]; let rc = soliton_auth_respond( client_sk.ptr, client_sk.len, ct_out.ptr, ct_out.len, proof.as_mut_ptr(), 32, ); assert_eq!(rc, OK); // Server: verify. let rc = soliton_auth_verify(token.as_ptr(), 32, proof.as_ptr(), 32); assert_eq!(rc, OK); // Wrong proof must fail. let bad_proof = [0u8; 32]; let rc = soliton_auth_verify(token.as_ptr(), 32, bad_proof.as_ptr(), 32); assert_eq!(rc, E_VER); soliton_buf_free(&mut ct_out); soliton_buf_free(&mut fp); soliton_buf_free(&mut client_sk); soliton_buf_free(&mut client_pk); } } #[test] fn kex_sign_prekey_and_verify_bundle() { unsafe { // Generate Bob's identity keypair. let mut bob_pk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut bob_sk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut bob_fp = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_identity_generate(&mut bob_pk, &mut bob_sk, &mut bob_fp); // Generate Bob's SPK (X-Wing keypair). let mut spk_pub = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut spk_sk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_xwing_keygen(&mut spk_pub, &mut spk_sk); // Bob signs the SPK. let mut sig_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_kex_sign_prekey( bob_sk.ptr, bob_sk.len, spk_pub.ptr, spk_pub.len, &mut sig_out, ); assert_eq!(rc, OK); // Verify bundle. let cv = CString::new("lo-crypto-v1").unwrap(); let rc = soliton_kex_verify_bundle( bob_pk.ptr, bob_pk.len, bob_pk.ptr, bob_pk.len, spk_pub.ptr, spk_pub.len, sig_out.ptr, sig_out.len, cv.as_ptr(), ); assert_eq!(rc, OK); // Bundle with tampered IK must fail. let mut diff_pk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut diff_sk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut diff_fp = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_identity_generate(&mut diff_pk, &mut diff_sk, &mut diff_fp); let rc = soliton_kex_verify_bundle( bob_pk.ptr, bob_pk.len, diff_pk.ptr, diff_pk.len, // known_ik_pk differs from bundle_ik_pk spk_pub.ptr, spk_pub.len, sig_out.ptr, sig_out.len, cv.as_ptr(), ); assert_eq!( rc, E_BUNDLE, "mismatched IK must return BundleVerificationFailed" ); soliton_buf_free(&mut diff_fp); soliton_buf_free(&mut diff_sk); soliton_buf_free(&mut diff_pk); soliton_buf_free(&mut sig_out); soliton_buf_free(&mut spk_sk); soliton_buf_free(&mut spk_pub); soliton_buf_free(&mut bob_fp); soliton_buf_free(&mut bob_sk); soliton_buf_free(&mut bob_pk); } } #[test] fn kex_full_initiate_receive_round_trip() { unsafe { // Alice and Bob keypairs. let (alice_pk, alice_sk) = generate_identity_keypair(); let (bob_pk, bob_sk) = generate_identity_keypair(); // Bob's signed pre-key. let mut spk_pub = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut spk_sk_buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_xwing_keygen(&mut spk_pub, &mut spk_sk_buf); let mut spk_sig = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_kex_sign_prekey( bob_sk.ptr, bob_sk.len, spk_pub.ptr, spk_pub.len, &mut spk_sig, ); let cv = CString::new("lo-crypto-v1").unwrap(); // Alice initiates. let mut session_out = std::mem::zeroed::(); let rc = soliton_kex_initiate( alice_pk.ptr, alice_pk.len, alice_sk.ptr, alice_sk.len, bob_pk.ptr, bob_pk.len, spk_pub.ptr, spk_pub.len, 1, spk_sig.ptr, spk_sig.len, ptr::null(), 0, 0, // no OPK cv.as_ptr(), &mut session_out, ); assert_eq!(rc, OK, "kex_initiate must succeed"); assert!( !session_out.root_key.iter().all(|&b| b == 0), "root key must be non-zero" ); // Bob receives. // Reconstruct SessionInit wire fields from Alice's session_out. let wire = SolitonSessionInitWire { sender_sig: session_out.sender_sig.ptr, sender_sig_len: session_out.sender_sig.len, sender_ik_fingerprint: session_out.sender_ik_fingerprint.as_ptr(), sender_ik_fingerprint_len: 32, recipient_ik_fingerprint: session_out.recipient_ik_fingerprint.as_ptr(), recipient_ik_fingerprint_len: 32, sender_ek: session_out.ek_pk.ptr, sender_ek_len: session_out.ek_pk.len, ct_ik: session_out.ct_ik.ptr, ct_ik_len: session_out.ct_ik.len, ct_spk: session_out.ct_spk.ptr, ct_spk_len: session_out.ct_spk.len, spk_id: session_out.spk_id, ct_opk: ptr::null(), ct_opk_len: 0, opk_id: 0, crypto_version: cv.as_ptr(), }; let decap_keys = SolitonSessionDecapKeys { spk_sk: spk_sk_buf.ptr, spk_sk_len: spk_sk_buf.len, opk_sk: ptr::null(), opk_sk_len: 0, }; let mut received_out = SolitonReceivedSession { root_key: [0u8; 32], chain_key: [0u8; 32], peer_ek: SolitonBuf { ptr: ptr::null_mut(), len: 0, }, }; let rc = soliton_kex_receive( bob_pk.ptr, bob_pk.len, bob_sk.ptr, bob_sk.len, alice_pk.ptr, alice_pk.len, &wire, &decap_keys, &mut received_out, ); assert_eq!(rc, OK, "kex_receive must succeed"); // Both sides must derive the same root key and the same initial chain key. assert_eq!( session_out.root_key, received_out.root_key, "root keys must match" ); assert_ne!( received_out.root_key, [0u8; 32], "root key must be non-zero" ); // initial_chain_key (Alice's side) feeds ratchet_encrypt_first; // chain_key (Bob's side) feeds ratchet_decrypt_first. They must match // for the first application-message AEAD to succeed. assert_eq!( session_out.initial_chain_key, received_out.chain_key, "initial chain keys must match between Alice and Bob" ); assert_ne!( received_out.chain_key, [0u8; 32], "chain key must be non-zero" ); soliton_kex_received_session_free(&mut received_out); soliton_kex_initiated_session_free(&mut session_out); soliton_buf_free(&mut spk_sig); soliton_buf_free(&mut spk_sk_buf); soliton_buf_free(&mut spk_pub); free_identity_keypair(bob_pk, bob_sk); free_identity_keypair(alice_pk, alice_sk); } } /// Full PQ ratchet round-trip with direction change (RT-383). /// /// Unlike the PQ-free round-trip, this test uses real X-Wing keygen so that /// when Bob replies to Alice, the ratchet step triggers an actual KEM /// encapsulation/decapsulation through the CAPI. #[test] fn ratchet_pq_round_trip_with_direction_change() { unsafe { // Generate real X-Wing keypairs for Alice's ephemeral key. let mut ek_pk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut ek_sk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_xwing_keygen(&mut ek_pk, &mut ek_sk); // Shared root key and chain key (synthetic — in real use these come from KEX). let rk = [0x41u8; 32]; let ck = [0x42u8; 32]; let alice_fp = [0x01u8; 32]; let bob_fp = [0x02u8; 32]; // Initialize Alice (has EK secret key for KEM decapsulation on receive). let mut alice: *mut SolitonRatchet = ptr::null_mut(); let rc = soliton_ratchet_init_alice( rk.as_ptr(), 32, ck.as_ptr(), 32, alice_fp.as_ptr(), 32, bob_fp.as_ptr(), 32, ek_pk.ptr, ek_pk.len, ek_sk.ptr, ek_sk.len, &mut alice, ); assert_eq!(rc, OK, "init_alice must succeed"); // Initialize Bob (has Alice's EK public key for KEM encapsulation on send). let mut bob: *mut SolitonRatchet = ptr::null_mut(); let rc = soliton_ratchet_init_bob( rk.as_ptr(), 32, ck.as_ptr(), 32, bob_fp.as_ptr(), 32, alice_fp.as_ptr(), 32, ek_pk.ptr, ek_pk.len, &mut bob, ); assert_eq!(rc, OK, "init_bob must succeed"); // Alice → Bob (first message: no KEM step, same as PQ-free). let msg1_pt = b"alice to bob"; let mut msg1: MaybeUninit = MaybeUninit::zeroed(); let rc = soliton_ratchet_encrypt(alice, msg1_pt.as_ptr(), msg1_pt.len(), msg1.as_mut_ptr()); assert_eq!(rc, OK); let msg1 = msg1.assume_init(); // First message has no KEM ciphertext. assert!(msg1.header.kem_ct.ptr.is_null() || msg1.header.kem_ct.len == 0); let mut pt1_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( bob, msg1.header.ratchet_pk.ptr, msg1.header.ratchet_pk.len, ptr::null(), 0, msg1.header.n, msg1.header.pn, msg1.ciphertext.ptr, msg1.ciphertext.len, &mut pt1_out, ); assert_eq!(rc, OK, "Bob must decrypt Alice's first message"); assert_eq!( std::slice::from_raw_parts(pt1_out.ptr, pt1_out.len), msg1_pt ); // Bob → Alice (direction change: triggers KEM encap with a new ratchet key). let msg2_pt = b"bob to alice"; let mut msg2: MaybeUninit = MaybeUninit::zeroed(); let rc = soliton_ratchet_encrypt(bob, msg2_pt.as_ptr(), msg2_pt.len(), msg2.as_mut_ptr()); assert_eq!(rc, OK); let msg2 = msg2.assume_init(); // Direction change produces a KEM ciphertext. assert!( !msg2.header.kem_ct.ptr.is_null() && msg2.header.kem_ct.len > 0, "direction change must produce kem_ct" ); let mut pt2_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( alice, msg2.header.ratchet_pk.ptr, msg2.header.ratchet_pk.len, msg2.header.kem_ct.ptr, msg2.header.kem_ct.len, msg2.header.n, msg2.header.pn, msg2.ciphertext.ptr, msg2.ciphertext.len, &mut pt2_out, ); assert_eq!(rc, OK, "Alice must decrypt Bob's reply (KEM decap path)"); assert_eq!( std::slice::from_raw_parts(pt2_out.ptr, pt2_out.len), msg2_pt ); // Cleanup. soliton_buf_free(&mut pt1_out); soliton_buf_free(&mut pt2_out); let mut msg1 = msg1; let mut msg2 = msg2; soliton_encrypted_message_free(&mut msg1); soliton_encrypted_message_free(&mut msg2); soliton_ratchet_free(&mut alice); soliton_ratchet_free(&mut bob); soliton_buf_free(&mut ek_sk); soliton_buf_free(&mut ek_pk); } } /// Full KEX → first-message → ratchet integration test (RT-385). /// /// Exercises the complete handshake lifecycle through the CAPI: /// 1. KEX initiate (Alice) + receive (Bob) → shared root_key + chain_key /// 2. encrypt_first / decrypt_first → first application message + next_ck /// 3. ratchet_init_alice / ratchet_init_bob → steady-state ratchet /// 4. ratchet_encrypt / ratchet_decrypt → bidirectional messaging #[test] fn kex_to_ratchet_full_integration() { unsafe { // ── Step 0: Generate identity keypairs ── let (alice_pk, alice_sk) = generate_identity_keypair(); let (bob_pk, bob_sk) = generate_identity_keypair(); // Bob's signed pre-key. let mut spk_pub = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut spk_sk_buf = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_xwing_keygen(&mut spk_pub, &mut spk_sk_buf); let mut spk_sig = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; soliton_kex_sign_prekey( bob_sk.ptr, bob_sk.len, spk_pub.ptr, spk_pub.len, &mut spk_sig, ); let cv = CString::new("lo-crypto-v1").unwrap(); // ── Step 1: KEX ── let mut session_out = std::mem::zeroed::(); let rc = soliton_kex_initiate( alice_pk.ptr, alice_pk.len, alice_sk.ptr, alice_sk.len, bob_pk.ptr, bob_pk.len, spk_pub.ptr, spk_pub.len, 1, spk_sig.ptr, spk_sig.len, ptr::null(), 0, 0, cv.as_ptr(), &mut session_out, ); assert_eq!(rc, OK, "kex_initiate must succeed"); let wire = SolitonSessionInitWire { sender_sig: session_out.sender_sig.ptr, sender_sig_len: session_out.sender_sig.len, sender_ik_fingerprint: session_out.sender_ik_fingerprint.as_ptr(), sender_ik_fingerprint_len: 32, recipient_ik_fingerprint: session_out.recipient_ik_fingerprint.as_ptr(), recipient_ik_fingerprint_len: 32, sender_ek: session_out.ek_pk.ptr, sender_ek_len: session_out.ek_pk.len, ct_ik: session_out.ct_ik.ptr, ct_ik_len: session_out.ct_ik.len, ct_spk: session_out.ct_spk.ptr, ct_spk_len: session_out.ct_spk.len, spk_id: session_out.spk_id, ct_opk: ptr::null(), ct_opk_len: 0, opk_id: 0, crypto_version: cv.as_ptr(), }; let decap_keys = SolitonSessionDecapKeys { spk_sk: spk_sk_buf.ptr, spk_sk_len: spk_sk_buf.len, opk_sk: ptr::null(), opk_sk_len: 0, }; let mut received_out = SolitonReceivedSession { root_key: [0u8; 32], chain_key: [0u8; 32], peer_ek: SolitonBuf { ptr: ptr::null_mut(), len: 0, }, }; let rc = soliton_kex_receive( bob_pk.ptr, bob_pk.len, bob_sk.ptr, bob_sk.len, alice_pk.ptr, alice_pk.len, &wire, &decap_keys, &mut received_out, ); assert_eq!(rc, OK, "kex_receive must succeed"); assert_eq!(session_out.root_key, received_out.root_key); assert_eq!(session_out.initial_chain_key, received_out.chain_key); // ── Step 2: First message (encrypt_first / decrypt_first) ── let first_msg = b"hello from alice"; let first_aad = b"session-aad"; let mut first_payload = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck_alice = [0u8; 32]; let rc = soliton_ratchet_encrypt_first( session_out.initial_chain_key.as_ptr(), 32, first_msg.as_ptr(), first_msg.len(), first_aad.as_ptr(), first_aad.len(), &mut first_payload, next_ck_alice.as_mut_ptr(), 32, ); assert_eq!(rc, OK, "encrypt_first must succeed"); let mut first_pt_out = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut next_ck_bob = [0u8; 32]; let rc = soliton_ratchet_decrypt_first( received_out.chain_key.as_ptr(), 32, first_payload.ptr, first_payload.len, first_aad.as_ptr(), first_aad.len(), &mut first_pt_out, next_ck_bob.as_mut_ptr(), 32, ); assert_eq!(rc, OK, "decrypt_first must succeed"); assert_eq!( std::slice::from_raw_parts(first_pt_out.ptr, first_pt_out.len), first_msg ); assert_eq!(next_ck_alice, next_ck_bob, "next chain keys must match"); // ── Step 3: Initialize ratchets ── // Compute fingerprints for ratchet init. let mut alice_fp = [0u8; 32]; let mut bob_fp = [0u8; 32]; let rc = soliton_identity_fingerprint(alice_pk.ptr, alice_pk.len, alice_fp.as_mut_ptr(), 32); assert_eq!(rc, OK); let rc = soliton_identity_fingerprint(bob_pk.ptr, bob_pk.len, bob_fp.as_mut_ptr(), 32); assert_eq!(rc, OK); let mut alice_ratchet: *mut SolitonRatchet = ptr::null_mut(); let rc = soliton_ratchet_init_alice( session_out.root_key.as_ptr(), 32, next_ck_alice.as_ptr(), 32, alice_fp.as_ptr(), 32, bob_fp.as_ptr(), 32, session_out.ek_pk.ptr, session_out.ek_pk.len, session_out.ek_sk.ptr, session_out.ek_sk.len, &mut alice_ratchet, ); assert_eq!(rc, OK, "ratchet_init_alice must succeed"); let mut bob_ratchet: *mut SolitonRatchet = ptr::null_mut(); let rc = soliton_ratchet_init_bob( received_out.root_key.as_ptr(), 32, next_ck_bob.as_ptr(), 32, bob_fp.as_ptr(), 32, alice_fp.as_ptr(), 32, received_out.peer_ek.ptr, received_out.peer_ek.len, &mut bob_ratchet, ); assert_eq!(rc, OK, "ratchet_init_bob must succeed"); // ── Step 4: Bidirectional ratchet messaging ── // Alice → Bob let msg_a2b = b"steady-state alice to bob"; let mut enc_a2b: MaybeUninit = MaybeUninit::zeroed(); let rc = soliton_ratchet_encrypt( alice_ratchet, msg_a2b.as_ptr(), msg_a2b.len(), enc_a2b.as_mut_ptr(), ); assert_eq!(rc, OK); let enc_a2b = enc_a2b.assume_init(); let mut pt_a2b = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( bob_ratchet, enc_a2b.header.ratchet_pk.ptr, enc_a2b.header.ratchet_pk.len, if enc_a2b.header.kem_ct.ptr.is_null() { ptr::null() } else { enc_a2b.header.kem_ct.ptr }, enc_a2b.header.kem_ct.len, enc_a2b.header.n, enc_a2b.header.pn, enc_a2b.ciphertext.ptr, enc_a2b.ciphertext.len, &mut pt_a2b, ); assert_eq!(rc, OK, "Bob must decrypt ratchet message from Alice"); assert_eq!(std::slice::from_raw_parts(pt_a2b.ptr, pt_a2b.len), msg_a2b); // Bob → Alice (direction change, triggers KEM) let msg_b2a = b"steady-state bob to alice"; let mut enc_b2a: MaybeUninit = MaybeUninit::zeroed(); let rc = soliton_ratchet_encrypt( bob_ratchet, msg_b2a.as_ptr(), msg_b2a.len(), enc_b2a.as_mut_ptr(), ); assert_eq!(rc, OK); let enc_b2a = enc_b2a.assume_init(); assert!( !enc_b2a.header.kem_ct.ptr.is_null() && enc_b2a.header.kem_ct.len > 0, "direction change must produce kem_ct" ); let mut pt_b2a = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = soliton_ratchet_decrypt( alice_ratchet, enc_b2a.header.ratchet_pk.ptr, enc_b2a.header.ratchet_pk.len, enc_b2a.header.kem_ct.ptr, enc_b2a.header.kem_ct.len, enc_b2a.header.n, enc_b2a.header.pn, enc_b2a.ciphertext.ptr, enc_b2a.ciphertext.len, &mut pt_b2a, ); assert_eq!( rc, OK, "Alice must decrypt ratchet message from Bob (KEM path)" ); assert_eq!(std::slice::from_raw_parts(pt_b2a.ptr, pt_b2a.len), msg_b2a); // ── Cleanup ── soliton_buf_free(&mut pt_a2b); soliton_buf_free(&mut pt_b2a); let mut enc_a2b = enc_a2b; let mut enc_b2a = enc_b2a; soliton_encrypted_message_free(&mut enc_a2b); soliton_encrypted_message_free(&mut enc_b2a); soliton_buf_free(&mut first_pt_out); soliton_buf_free(&mut first_payload); soliton_ratchet_free(&mut alice_ratchet); soliton_ratchet_free(&mut bob_ratchet); soliton_kex_received_session_free(&mut received_out); soliton_kex_initiated_session_free(&mut session_out); soliton_buf_free(&mut spk_sig); soliton_buf_free(&mut spk_sk_buf); soliton_buf_free(&mut spk_pub); free_identity_keypair(bob_pk, bob_sk); free_identity_keypair(alice_pk, alice_sk); } } // Helpers for the KEX test. unsafe fn generate_identity_keypair() -> (SolitonBuf, SolitonBuf) { let mut pk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut sk = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let mut fp = SolitonBuf { ptr: ptr::null_mut(), len: 0, }; let rc = unsafe { soliton_identity_generate(&mut pk, &mut sk, &mut fp) }; assert_eq!(rc, OK); unsafe { soliton_buf_free(&mut fp) }; (pk, sk) } unsafe fn free_identity_keypair(mut pk: SolitonBuf, mut sk: SolitonBuf) { unsafe { soliton_buf_free(&mut pk); soliton_buf_free(&mut sk); } } } // ═══════════════════════════════════════════════════════════════════════════ // Streaming AEAD // ═══════════════════════════════════════════════════════════════════════════ const CHUNK_SZ: usize = soliton::constants::STREAM_CHUNK_SIZE; const HDR_SZ: usize = soliton::constants::STREAM_HEADER_SIZE; const ENC_MAX: usize = soliton::constants::STREAM_ENCRYPT_MAX; /// Helper: create an encryptor, return (handle, header). unsafe fn stream_enc_setup( key: &[u8; 32], aad: &[u8], compress: bool, ) -> (*mut SolitonStreamEncryptor, [u8; HDR_SZ]) { let mut enc: *mut SolitonStreamEncryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_encrypt_init( key.as_ptr(), 32, if aad.is_empty() { ptr::null() } else { aad.as_ptr() }, aad.len(), compress, &mut enc, ) }; assert_eq!(rc, OK); assert!(!enc.is_null()); let mut hdr = [0u8; HDR_SZ]; let rc = unsafe { soliton_stream_encrypt_header(enc, hdr.as_mut_ptr(), HDR_SZ) }; assert_eq!(rc, OK); (enc, hdr) } /// Helper: create a decryptor from key + header. unsafe fn stream_dec_setup( key: &[u8; 32], header: &[u8; HDR_SZ], aad: &[u8], ) -> *mut SolitonStreamDecryptor { let mut dec: *mut SolitonStreamDecryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_decrypt_init( key.as_ptr(), 32, header.as_ptr(), HDR_SZ, if aad.is_empty() { ptr::null() } else { aad.as_ptr() }, aad.len(), &mut dec, ) }; assert_eq!(rc, OK); assert!(!dec.is_null()); dec } // ── Null-pointer guards ────────────────────────────────────────────────── #[test] fn stream_encrypt_init_null_key() { let mut enc: *mut SolitonStreamEncryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_encrypt_init(ptr::null(), 32, ptr::null(), 0, false, &mut enc) }; assert_eq!(rc, E_NULL); } #[test] fn stream_encrypt_init_null_out() { let key = [0x42u8; 32]; let rc = unsafe { soliton_stream_encrypt_init(key.as_ptr(), 32, ptr::null(), 0, false, ptr::null_mut()) }; assert_eq!(rc, E_NULL); } #[test] fn stream_encrypt_init_null_aad_nonzero_len() { let key = [0x42u8; 32]; let mut enc: *mut SolitonStreamEncryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_encrypt_init(key.as_ptr(), 32, ptr::null(), 10, false, &mut enc) }; assert_eq!(rc, E_NULL); } #[test] fn stream_encrypt_header_null_enc() { let mut hdr = [0u8; HDR_SZ]; let rc = unsafe { soliton_stream_encrypt_header(ptr::null(), hdr.as_mut_ptr(), HDR_SZ) }; assert_eq!(rc, E_NULL); } #[test] fn stream_encrypt_header_null_out() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let rc = unsafe { soliton_stream_encrypt_header(enc, ptr::null_mut(), HDR_SZ) }; assert_eq!(rc, E_NULL); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_chunk_null_enc() { let pt = vec![0u8; CHUNK_SZ]; let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( ptr::null_mut(), pt.as_ptr(), pt.len(), true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, E_NULL); } #[test] fn stream_encrypt_chunk_null_out() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = vec![0u8; CHUNK_SZ]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, ptr::null_mut(), ENC_MAX, &mut written, ) }; assert_eq!(rc, E_NULL); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_chunk_null_out_written() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = vec![0u8; CHUNK_SZ]; let mut out = vec![0u8; ENC_MAX]; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, out.as_mut_ptr(), out.len(), ptr::null_mut(), ) }; assert_eq!(rc, E_NULL); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_chunk_null_plaintext_nonzero_len() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, ptr::null(), 1, true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, E_NULL); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_chunk_null_plaintext_zero_len_final() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, ptr::null(), 0, true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, OK); assert!(written > 0); // tag_byte + AEAD tag unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_is_finalized_null() { let mut out = false; assert_eq!( unsafe { soliton_stream_encrypt_is_finalized(ptr::null(), &mut out) }, E_NULL ); assert_eq!( unsafe { soliton_stream_encrypt_is_finalized(ptr::null(), ptr::null_mut()) }, E_NULL ); } #[test] fn stream_encrypt_free_null() { // Outer null is a no-op (returns 0), matching ratchet_free/keyring_free/call_keys_free. assert_eq!(unsafe { soliton_stream_encrypt_free(ptr::null_mut()) }, OK); let mut null: *mut SolitonStreamEncryptor = ptr::null_mut(); assert_eq!(unsafe { soliton_stream_encrypt_free(&mut null) }, OK); } #[test] fn stream_decrypt_init_null_key() { let hdr = [0u8; HDR_SZ]; let mut dec: *mut SolitonStreamDecryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_decrypt_init( ptr::null(), 32, hdr.as_ptr(), HDR_SZ, ptr::null(), 0, &mut dec, ) }; assert_eq!(rc, E_NULL); } #[test] fn stream_decrypt_init_null_header() { let key = [0x42u8; 32]; let mut dec: *mut SolitonStreamDecryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_decrypt_init( key.as_ptr(), 32, ptr::null(), HDR_SZ, ptr::null(), 0, &mut dec, ) }; assert_eq!(rc, E_NULL); } #[test] fn stream_decrypt_init_null_out() { let key = [0x42u8; 32]; let hdr = [0u8; HDR_SZ]; let rc = unsafe { soliton_stream_decrypt_init( key.as_ptr(), 32, hdr.as_ptr(), HDR_SZ, ptr::null(), 0, ptr::null_mut(), ) }; assert_eq!(rc, E_NULL); } #[test] fn stream_decrypt_chunk_nulls() { let mut out = vec![0u8; CHUNK_SZ]; let mut written = 0usize; let mut last = false; let chunk = [0u8; 17]; assert_eq!( unsafe { soliton_stream_decrypt_chunk( ptr::null_mut(), chunk.as_ptr(), 17, out.as_mut_ptr(), CHUNK_SZ, &mut written, &mut last, ) }, E_NULL ); } #[test] fn stream_encrypt_chunk_at_null_enc() { let pt = b"data"; let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; assert_eq!( unsafe { soliton_stream_encrypt_chunk_at( ptr::null(), 0, pt.as_ptr(), pt.len(), true, out.as_mut_ptr(), out.len(), &mut written, ) }, E_NULL ); } #[test] fn stream_encrypt_chunk_at_null_out() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"data"; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk_at( enc, 0, pt.as_ptr(), pt.len(), true, ptr::null_mut(), ENC_MAX, &mut written, ) }; assert_eq!(rc, E_NULL); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_chunk_at_null_out_written() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"data"; let mut out = vec![0u8; ENC_MAX]; let rc = unsafe { soliton_stream_encrypt_chunk_at( enc, 0, pt.as_ptr(), pt.len(), true, out.as_mut_ptr(), out.len(), ptr::null_mut(), ) }; assert_eq!(rc, E_NULL); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_chunk_at_round_trip() { // Encrypt a chunk via encrypt_chunk_at, decrypt via sequential decrypt_chunk. let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"encrypt_chunk_at round trip"; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk_at( enc, 0, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); assert!(written > 0); // finalized must NOT be set — encrypt_chunk_at does not seal the stream. let mut fin = true; let rc = unsafe { soliton_stream_encrypt_is_finalized(enc, &mut fin) }; assert_eq!(rc, OK); assert!(!fin); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); assert_eq!(&dec_out[..dec_written], pt); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn stream_encrypt_chunk_at_wrong_index_aead_fails() { // Ciphertext produced at index 0 cannot be decrypted at index 1. let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"index mismatch"; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk_at( enc, 0, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; // Feed chunk encrypted at index 0 to decrypt_chunk_at with index 1. let rc = unsafe { soliton_stream_decrypt_chunk_at( dec, 1, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, E_AEAD); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn stream_encrypt_chunk_at_out_too_small() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"x"; let mut out = vec![0u8; ENC_MAX - 1]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk_at( enc, 0, pt.as_ptr(), pt.len(), true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, E_LEN); assert_eq!(written, 0); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn miri_capi_stream_encrypt_chunk_at_small() { // MIRI-friendly: small plaintext, exercises the full encrypt_chunk_at → // decrypt_chunk_at round-trip through the FFI boundary. let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"miri encrypt_chunk_at"; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk_at( enc, 0, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk_at( dec, 0, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); assert_eq!(&dec_out[..dec_written], pt); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn stream_decrypt_chunk_at_nulls() { let mut out = vec![0u8; CHUNK_SZ]; let mut written = 0usize; let mut last = false; let chunk = [0u8; 17]; assert_eq!( unsafe { soliton_stream_decrypt_chunk_at( ptr::null(), 0, chunk.as_ptr(), 17, out.as_mut_ptr(), CHUNK_SZ, &mut written, &mut last, ) }, E_NULL ); } #[test] fn stream_decrypt_is_finalized_null() { let mut out = false; assert_eq!( unsafe { soliton_stream_decrypt_is_finalized(ptr::null(), &mut out) }, E_NULL ); } #[test] fn stream_decrypt_expected_index_null() { let mut out = 0u64; assert_eq!( unsafe { soliton_stream_decrypt_expected_index(ptr::null(), &mut out) }, E_NULL ); } #[test] fn stream_decrypt_free_null() { // Outer null is a no-op (returns 0), matching ratchet_free/keyring_free/call_keys_free. assert_eq!(unsafe { soliton_stream_decrypt_free(ptr::null_mut()) }, OK); let mut null: *mut SolitonStreamDecryptor = ptr::null_mut(); assert_eq!(unsafe { soliton_stream_decrypt_free(&mut null) }, OK); } // ── Length validation ──────────────────────────────────────────────────── #[test] fn stream_encrypt_init_wrong_key_len() { let key = [0x42u8; 31]; let mut enc: *mut SolitonStreamEncryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_encrypt_init(key.as_ptr(), 31, ptr::null(), 0, false, &mut enc) }; assert_eq!(rc, E_LEN); } #[test] fn stream_decrypt_init_wrong_key_len() { let key = [0x42u8; 31]; let hdr = [0u8; HDR_SZ]; let mut dec: *mut SolitonStreamDecryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_decrypt_init( key.as_ptr(), 31, hdr.as_ptr(), HDR_SZ, ptr::null(), 0, &mut dec, ) }; assert_eq!(rc, E_LEN); } #[test] fn stream_decrypt_init_wrong_header_len() { let key = [0x42u8; 32]; let hdr = [0u8; 25]; let mut dec: *mut SolitonStreamDecryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_decrypt_init(key.as_ptr(), 32, hdr.as_ptr(), 25, ptr::null(), 0, &mut dec) }; assert_eq!(rc, E_LEN); } #[test] fn stream_encrypt_init_aad_too_large() { // AAD exceeding 256 MiB cap returns InvalidLength. // Pass a non-null pointer with an oversized length — the function checks // aad_len before dereferencing, so we use a 1-byte dummy pointer. let key = [0x42u8; 32]; let dummy_aad: u8 = 0; let mut enc: *mut SolitonStreamEncryptor = ptr::null_mut(); let rc = unsafe { soliton_stream_encrypt_init( key.as_ptr(), 32, &dummy_aad as *const u8, 256 * 1024 * 1024 + 1, false, &mut enc, ) }; assert_eq!(rc, E_LEN); assert!(enc.is_null()); } #[test] fn stream_encrypt_chunk_out_too_small() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = vec![0xAAu8; CHUNK_SZ]; let mut out = vec![0u8; ENC_MAX - 1]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, E_LEN); assert_eq!(written, 0); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_decrypt_chunk_out_too_small() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = vec![0xBBu8; CHUNK_SZ]; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ - 1]; // too small let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, E_LEN); assert_eq!(dec_written, 0); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } // ── Output zeroing ─────────────────────────────────────────────────────── #[test] fn stream_decrypt_chunk_zeros_output_on_failure() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = vec![0xAAu8; CHUNK_SZ]; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ); } // Corrupt the chunk. enc_out[5] ^= 0xFF; let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0xABu8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, E_AEAD); assert_eq!(dec_written, 0); assert!(!last); assert!(is_zeroed(&dec_out)); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn stream_encrypt_chunk_zeros_output_on_failure() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; // Oversized non-final chunk → InvalidData. let bad_pt = vec![0u8; CHUNK_SZ + 1]; let mut out = vec![0xABu8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, bad_pt.as_ptr(), bad_pt.len(), false, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, E_DATA); assert_eq!(written, 0); assert!(is_zeroed(&out)); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } // ── Round-trip ─────────────────────────────────────────────────────────── #[test] fn capi_stream_round_trip_uncompressed() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; // Encrypt 2 non-final + 1 final. let pt0 = vec![0x01u8; CHUNK_SZ]; let pt1 = vec![0x02u8; CHUNK_SZ]; let pt2 = vec![0x03u8; 500]; let mut enc_bufs: Vec<(Vec, usize)> = Vec::new(); for (pt, is_last) in [(&pt0, false), (&pt1, false), (&pt2, true)] { let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), is_last, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, OK); assert!(written > 0); enc_bufs.push((out, written)); } // Decrypt sequentially. let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; for (i, (pt, _is_last_expected)) in [(&pt0, false), (&pt1, false), (&pt2, true)] .iter() .enumerate() { let (ref enc_buf, enc_written) = enc_bufs[i]; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_buf.as_ptr(), enc_written, dec_out.as_mut_ptr(), dec_out.len(), &mut written, &mut last, ) }; assert_eq!(rc, OK); assert_eq!(&dec_out[..written], pt.as_slice()); assert_eq!(last, *_is_last_expected); } unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn capi_stream_round_trip_compressed() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], true) }; let pt = vec![0xCCu8; CHUNK_SZ]; // compressible let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); assert_eq!(&dec_out[..dec_written], &pt); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn capi_stream_round_trip_empty_file() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, ptr::null(), 0, true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, OK); assert_eq!(written, 17); // tag_byte + AEAD tag let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); assert_eq!(dec_written, 0); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn capi_stream_random_access() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt0 = vec![0x10u8; CHUNK_SZ]; let pt1 = vec![0x20u8; CHUNK_SZ]; let pt2 = vec![0x30u8; CHUNK_SZ]; let pt3 = vec![0x40u8; 200]; let mut chunks: Vec<(Vec, usize)> = Vec::new(); for (pt, is_last) in [(&pt0, false), (&pt1, false), (&pt2, false), (&pt3, true)] { let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), is_last, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, OK); chunks.push((out, written)); } let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; // Random-access decrypt chunk 2. let mut dec_out = vec![0u8; CHUNK_SZ]; let mut written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk_at( dec, 2, chunks[2].0.as_ptr(), chunks[2].1, dec_out.as_mut_ptr(), dec_out.len(), &mut written, &mut last, ) }; assert_eq!(rc, OK); assert!(!last); assert_eq!(&dec_out[..written], &pt2); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } // ── Getters ────────────────────────────────────────────────────────────── #[test] fn stream_encrypt_is_finalized_lifecycle() { let key = [0x42u8; 32]; let (enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let mut fin = true; let rc = unsafe { soliton_stream_encrypt_is_finalized(enc, &mut fin) }; assert_eq!(rc, OK); assert!(!fin); let mut out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, ptr::null(), 0, true, out.as_mut_ptr(), out.len(), &mut written, ) }; assert_eq!(rc, OK); let rc = unsafe { soliton_stream_encrypt_is_finalized(enc, &mut fin) }; assert_eq!(rc, OK); assert!(fin); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_decrypt_is_finalized_lifecycle() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, ptr::null(), 0, true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut fin = true; let rc = unsafe { soliton_stream_decrypt_is_finalized(dec, &mut fin) }; assert_eq!(rc, OK); assert!(!fin); let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); let rc = unsafe { soliton_stream_decrypt_is_finalized(dec, &mut fin) }; assert_eq!(rc, OK); assert!(fin); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn stream_decrypt_expected_index_increments() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt0 = vec![0x01u8; CHUNK_SZ]; let pt1 = [0x02u8; 100]; let mut c0 = vec![0u8; ENC_MAX]; let mut c1 = vec![0u8; ENC_MAX]; let mut w0 = 0usize; let mut w1 = 0usize; unsafe { soliton_stream_encrypt_chunk( enc, pt0.as_ptr(), pt0.len(), false, c0.as_mut_ptr(), c0.len(), &mut w0, ); soliton_stream_encrypt_chunk( enc, pt1.as_ptr(), pt1.len(), true, c1.as_mut_ptr(), c1.len(), &mut w1, ); } let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut idx = 99u64; let rc = unsafe { soliton_stream_decrypt_expected_index(dec, &mut idx) }; assert_eq!(rc, OK); assert_eq!(idx, 0); let mut dec_out = vec![0u8; CHUNK_SZ]; let mut written = 0usize; let mut last = false; unsafe { soliton_stream_decrypt_chunk( dec, c0.as_ptr(), w0, dec_out.as_mut_ptr(), dec_out.len(), &mut written, &mut last, ); } let rc = unsafe { soliton_stream_decrypt_expected_index(dec, &mut idx) }; assert_eq!(rc, OK); assert_eq!(idx, 1); unsafe { soliton_stream_decrypt_chunk( dec, c1.as_ptr(), w1, dec_out.as_mut_ptr(), dec_out.len(), &mut written, &mut last, ); } let rc = unsafe { soliton_stream_decrypt_expected_index(dec, &mut idx) }; assert_eq!(rc, OK); assert_eq!(idx, 2); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } // ── Handle safety ──────────────────────────────────────────────────────── #[test] fn stream_encrypt_free_nullifies_pointer() { let key = [0x42u8; 32]; let (mut enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; let rc = unsafe { soliton_stream_encrypt_free(&mut enc) }; assert_eq!(rc, OK); assert!(enc.is_null()); } #[test] fn stream_decrypt_free_nullifies_pointer() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let mut dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let rc = unsafe { soliton_stream_decrypt_free(&mut dec) }; assert_eq!(rc, OK); assert!(dec.is_null()); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } #[test] fn stream_encrypt_double_free_noop() { let key = [0x42u8; 32]; let (mut enc, _) = unsafe { stream_enc_setup(&key, &[], false) }; assert_eq!(unsafe { soliton_stream_encrypt_free(&mut enc) }, OK); assert_eq!(unsafe { soliton_stream_encrypt_free(&mut enc) }, OK); // null → no-op } #[test] fn stream_decrypt_double_free_noop() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let mut dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; assert_eq!(unsafe { soliton_stream_decrypt_free(&mut dec) }, OK); assert_eq!(unsafe { soliton_stream_decrypt_free(&mut dec) }, OK); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)) }; } // ═══════════════════════════════════════════════════════════════════════════ // Small-data MIRI-friendly streaming tests // ═══════════════════════════════════════════════════════════════════════════ // // The full-chunk streaming tests (capi_stream_round_trip_*, random_access, // expected_index_increments) use 1 MiB AEAD and time out under MIRI. These // variants exercise the same CAPI paths with tiny plaintext. #[test] fn miri_capi_stream_round_trip_small() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, b"ctx", false) }; let pt = b"small miri test"; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); assert!(written > 0); let dec = unsafe { stream_dec_setup(&key, &hdr, b"ctx") }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); assert_eq!(&dec_out[..dec_written], pt); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn miri_capi_stream_round_trip_compressed_small() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], true) }; let pt = b"compressible data for miri"; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); assert_eq!(&dec_out[..dec_written], pt); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn miri_capi_stream_random_access_small() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"random access miri"; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk_at( dec, 0, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); assert_eq!(&dec_out[..dec_written], pt); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } #[test] fn miri_capi_stream_expected_index_small() { let key = [0x42u8; 32]; let (enc, hdr) = unsafe { stream_enc_setup(&key, &[], false) }; let pt = b"index test"; let mut enc_out = vec![0u8; ENC_MAX]; let mut written = 0usize; let rc = unsafe { soliton_stream_encrypt_chunk( enc, pt.as_ptr(), pt.len(), true, enc_out.as_mut_ptr(), enc_out.len(), &mut written, ) }; assert_eq!(rc, OK); let dec = unsafe { stream_dec_setup(&key, &hdr, &[]) }; let mut idx: u64 = 99; let rc = unsafe { soliton_stream_decrypt_expected_index(dec, &mut idx) }; assert_eq!(rc, OK); assert_eq!(idx, 0); let mut dec_out = vec![0u8; CHUNK_SZ]; let mut dec_written = 0usize; let mut last = false; let rc = unsafe { soliton_stream_decrypt_chunk( dec, enc_out.as_ptr(), written, dec_out.as_mut_ptr(), dec_out.len(), &mut dec_written, &mut last, ) }; assert_eq!(rc, OK); assert!(last); let rc = unsafe { soliton_stream_decrypt_expected_index(dec, &mut idx) }; assert_eq!(rc, OK); assert_eq!(idx, 1); unsafe { soliton_stream_encrypt_free(&mut (enc as *mut _)); soliton_stream_decrypt_free(&mut (dec as *mut _)); } } // ═══════════════════════════════════════════════════════════════════════════ // Header freshness verification // ═══════════════════════════════════════════════════════════════════════════ /// Verify that the checked-in `soliton.h` matches what cbindgen would /// generate from the current source. Run `cargo test -p libsoliton_capi` /// after changing any `extern "C"` function or `#[repr(C)]` type to /// regenerate the header. #[test] fn header_up_to_date() { let crate_dir = env!("CARGO_MANIFEST_DIR"); let config_path = format!("{crate_dir}/cbindgen.toml"); let header_path = format!("{crate_dir}/soliton.h"); let config = cbindgen::Config::from_file(&config_path).expect("failed to read cbindgen.toml"); // cbindgen::Bindings has no Display impl — write to a temp file and // read back for comparison. let tmp = std::env::temp_dir().join("soliton_header_check.h"); cbindgen::Builder::new() .with_crate(crate_dir) .with_config(config) .generate() .expect("cbindgen failed to generate bindings") .write_to_file(&tmp); let generated = std::fs::read_to_string(&tmp).expect("failed to read generated header from temp file"); let _ = std::fs::remove_file(&tmp); let checked_in = std::fs::read_to_string(&header_path) .expect("soliton.h not found — run `cargo test -p libsoliton_capi` to generate it"); if generated != checked_in { // Overwrite the stale header so the next test run passes. std::fs::write(&header_path, &generated).expect("failed to write soliton.h"); panic!( "soliton.h was out of date and has been regenerated. \ Please review the diff and commit the updated header." ); } }