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

7394 lines
217 KiB
Rust

//! 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<const N: usize>() -> [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::<u8>(),
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::<SolitonEncryptedMessage>::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::<SolitonInitiatedSession>();
// 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::<SolitonEncryptedMessage>::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::<SolitonEncryptedMessage>(),
);
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::<SolitonEncryptedMessage>::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::<SolitonEncryptedMessage>::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<SolitonEncryptedMessage> = 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<SolitonEncryptedMessage> = 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::<SolitonInitiatedSession>::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<u8> {
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::<SolitonEncryptedMessage>();
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::<SolitonEncryptedMessage>();
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::<SolitonInitiatedSession>();
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<SolitonEncryptedMessage> = 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<SolitonEncryptedMessage> = 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::<SolitonInitiatedSession>();
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<SolitonEncryptedMessage> = 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<SolitonEncryptedMessage> = 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<u8>, 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<u8>, 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."
);
}
}