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
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
7394 lines
217 KiB
Rust
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."
|
|
);
|
|
}
|
|
}
|