#![forbid(unsafe_code)]
use ml_kem::{
array::Array,
kem::{Decapsulate, Encapsulate, Kem, KeyExport},
ml_kem_512::{self, MlKem512},
};
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305};
use zeroize::Zeroizing;
pub(crate) const NOISE_PARAMS: &str = "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s";
#[derive(Debug, Clone)]
pub struct Keypair {
pub private: [u8; 32],
pub public: [u8; 32],
}
pub fn generate_keypair() -> Keypair {
let kp = snow::Builder::new(NOISE_PARAMS.parse().expect("valid params"))
.generate_keypair()
.expect("keypair generation");
let mut private = [0u8; 32];
let mut public = [0u8; 32];
private.copy_from_slice(&kp.private);
public.copy_from_slice(&kp.public);
Keypair { private, public }
}
pub const PQ_ENCAPS_KEY_LEN: usize = 800;
pub const PQ_CIPHERTEXT_LEN: usize = 768;
pub const PQ_SHARED_SECRET_LEN: usize = 32;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PqEncapsKey(pub [u8; PQ_ENCAPS_KEY_LEN]);
pub struct PqDecapsKey(Box<ml_kem_512::DecapsulationKey>);
impl std::fmt::Debug for PqDecapsKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("PqDecapsKey([REDACTED])")
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PqCiphertext(pub [u8; PQ_CIPHERTEXT_LEN]);
pub fn generate_pq_keypair() -> (PqEncapsKey, PqDecapsKey) {
let (dk, ek) = MlKem512::generate_keypair();
let ek_bytes = ek.to_bytes();
let mut ek_arr = [0u8; PQ_ENCAPS_KEY_LEN];
ek_arr.copy_from_slice(ek_bytes.as_slice());
(PqEncapsKey(ek_arr), PqDecapsKey(Box::new(dk)))
}
pub fn pq_encapsulate(
ek: &PqEncapsKey,
) -> Result<(PqCiphertext, Zeroizing<[u8; PQ_SHARED_SECRET_LEN]>), CryptoError> {
for chunk in ek.0[..768].as_chunks::<3>().0 {
let d0 = u16::from(chunk[0]) | ((u16::from(chunk[1]) & 0x0F) << 8);
let d1 = (u16::from(chunk[1]) >> 4) | (u16::from(chunk[2]) << 4);
if d0 >= 3329 || d1 >= 3329 {
return Err(CryptoError::Handshake);
}
}
let ek_arr = Array::from(ek.0);
let key = ml_kem_512::EncapsulationKey::new(&ek_arr).map_err(|_| CryptoError::Handshake)?;
let (ct, ss) = key.encapsulate();
let ss = Zeroizing::new(ss);
let mut ct_arr = [0u8; PQ_CIPHERTEXT_LEN];
ct_arr.copy_from_slice(ct.as_slice());
let mut ss_arr = Zeroizing::new([0u8; PQ_SHARED_SECRET_LEN]);
ss_arr.copy_from_slice(ss.as_slice());
Ok((PqCiphertext(ct_arr), ss_arr))
}
pub fn pq_decapsulate(
dk: &PqDecapsKey,
ct: &PqCiphertext,
) -> Result<Zeroizing<[u8; PQ_SHARED_SECRET_LEN]>, CryptoError> {
let ct_arr = Array::from(ct.0);
let ss = Zeroizing::new(dk.0.decapsulate(&ct_arr));
let mut ss_arr = Zeroizing::new([0u8; PQ_SHARED_SECRET_LEN]);
ss_arr.copy_from_slice(ss.as_slice());
Ok(ss_arr)
}
pub const REPLAY_WINDOW_BITS_STANDARD: u64 = 8_192;
pub const REPLAY_WORDS_STANDARD: usize = 128;
pub const REPLAY_WINDOW_BITS_HIGH_THROUGHPUT: u64 = 131_072;
pub const REPLAY_WORDS_HIGH_THROUGHPUT: usize = 2048;
pub const REPLAY_WINDOW_BITS: u64 = REPLAY_WINDOW_BITS_HIGH_THROUGHPUT;
pub const REPLAY_WORDS: usize = REPLAY_WORDS_HIGH_THROUGHPUT;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayProfile {
Standard,
HighThroughput,
}
impl ReplayProfile {
#[inline]
pub const fn bits(self) -> u64 {
match self {
Self::Standard => REPLAY_WINDOW_BITS_STANDARD,
Self::HighThroughput => REPLAY_WINDOW_BITS_HIGH_THROUGHPUT,
}
}
#[inline]
pub const fn words(self) -> usize {
match self {
Self::Standard => REPLAY_WORDS_STANDARD,
Self::HighThroughput => REPLAY_WORDS_HIGH_THROUGHPUT,
}
}
}
#[derive(Clone)]
pub struct ReplayWindow {
profile: ReplayProfile,
latest: u64,
bitmap: Box<[u64]>,
started: bool,
}
impl ReplayWindow {
pub fn new() -> Self {
Self::new_with_profile(ReplayProfile::HighThroughput)
}
pub fn new_with_profile(profile: ReplayProfile) -> Self {
let words = profile.words();
Self {
profile,
latest: 0,
bitmap: vec![0u64; words].into_boxed_slice(),
started: false,
}
}
pub fn profile(&self) -> ReplayProfile {
self.profile
}
pub fn promote_to_high_throughput(&mut self) {
if self.profile == ReplayProfile::HighThroughput {
return;
}
let old_words = self.bitmap.len();
let new_words = ReplayProfile::HighThroughput.words();
let mut new_bitmap = vec![0u64; new_words].into_boxed_slice();
if self.started {
let latest_word = self.latest / 64;
let start_word = latest_word.saturating_sub((old_words - 1) as u64);
for w in start_word..=latest_word {
let old_idx = (w as usize) & (old_words - 1);
let new_idx = (w as usize) & (new_words - 1);
new_bitmap[new_idx] = self.bitmap[old_idx];
}
}
self.profile = ReplayProfile::HighThroughput;
self.bitmap = new_bitmap;
}
#[inline]
fn word_idx(&self, counter: u64) -> usize {
((counter / 64) as usize) & (self.bitmap.len() - 1)
}
#[inline]
fn bit_mask(counter: u64) -> u64 {
1u64 << (counter % 64)
}
pub fn check(&self, counter: u64) -> bool {
if !self.started {
return true;
}
if counter > self.latest {
true
} else {
let diff = self.latest - counter;
if diff >= self.profile.bits() {
return false; }
let idx = self.word_idx(counter);
let mask = Self::bit_mask(counter);
(self.bitmap[idx] & mask) == 0
}
}
pub fn commit(&mut self, counter: u64) {
if !self.started {
self.started = true;
self.latest = counter;
let idx = self.word_idx(counter);
self.bitmap[idx] = Self::bit_mask(counter);
return;
}
if counter > self.latest {
let diff = counter - self.latest;
let window_bits = self.profile.bits();
let total_words = self.bitmap.len();
if diff >= window_bits {
self.bitmap.fill(0);
} else {
let old_word = self.latest / 64;
let new_word = counter / 64;
if new_word > old_word {
let words_to_clear = ((new_word - old_word) as usize).min(total_words);
for w in 1..=words_to_clear {
let idx = ((old_word + w as u64) as usize) & (total_words - 1);
self.bitmap[idx] = 0;
}
}
}
self.latest = counter;
let idx = self.word_idx(counter);
self.bitmap[idx] |= Self::bit_mask(counter);
} else {
let diff = self.latest - counter;
if diff < self.profile.bits() {
let idx = self.word_idx(counter);
self.bitmap[idx] |= Self::bit_mask(counter);
}
}
}
pub fn check_and_set(&mut self, counter: u64) -> bool {
if self.check(counter) {
self.commit(counter);
true
} else {
false
}
}
}
impl Default for ReplayWindow {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CryptoError {
#[error("decryption failed")]
Decrypt,
#[error("replayed message")]
Replay,
#[error("handshake failed")]
Handshake,
}
enum HandshakePqRole {
Initiator {
dk: Option<PqDecapsKey>,
ek: PqEncapsKey,
},
Responder {
pending_ct: Option<PqCiphertext>,
},
}
pub struct Handshake {
inner: std::sync::Mutex<snow::HandshakeState>,
role: HandshakePqRole,
}
impl Handshake {
pub fn initiator(
local_private: &[u8; 32],
peer_public: &[u8; 32],
) -> Result<Handshake, CryptoError> {
let inner = snow::Builder::new(NOISE_PARAMS.parse().map_err(|_| CryptoError::Handshake)?)
.local_private_key(local_private)
.map_err(|_| CryptoError::Handshake)?
.remote_public_key(peer_public)
.map_err(|_| CryptoError::Handshake)?
.build_initiator()
.map_err(|_| CryptoError::Handshake)?;
let (ek, dk) = generate_pq_keypair();
Ok(Handshake {
inner: std::sync::Mutex::new(inner),
role: HandshakePqRole::Initiator { dk: Some(dk), ek },
})
}
pub fn responder(local_private: &[u8; 32]) -> Result<Handshake, CryptoError> {
let inner = snow::Builder::new(NOISE_PARAMS.parse().map_err(|_| CryptoError::Handshake)?)
.local_private_key(local_private)
.map_err(|_| CryptoError::Handshake)?
.build_responder()
.map_err(|_| CryptoError::Handshake)?;
Ok(Handshake {
inner: std::sync::Mutex::new(inner),
role: HandshakePqRole::Responder { pending_ct: None },
})
}
pub fn write_message(&mut self, payload: &[u8]) -> Result<Vec<u8>, CryptoError> {
let mut buf = [0u8; 4096];
match &mut self.role {
HandshakePqRole::Initiator { ek, .. } => {
let mut full_payload = Vec::with_capacity(PQ_ENCAPS_KEY_LEN + payload.len());
full_payload.extend_from_slice(&ek.0);
full_payload.extend_from_slice(payload);
let n = self
.inner
.get_mut()
.expect("handshake lock")
.write_message(&full_payload, &mut buf)
.map_err(|_| CryptoError::Handshake)?;
Ok(buf[..n].to_vec())
}
HandshakePqRole::Responder { pending_ct } => {
let ct = pending_ct.take().ok_or(CryptoError::Handshake)?;
let n = self
.inner
.get_mut()
.expect("handshake lock")
.write_message(payload, &mut buf)
.map_err(|_| CryptoError::Handshake)?;
let mut out = Vec::with_capacity(PQ_CIPHERTEXT_LEN + n);
out.extend_from_slice(&ct.0);
out.extend_from_slice(&buf[..n]);
Ok(out)
}
}
}
pub fn read_message(&mut self, msg: &[u8]) -> Result<Vec<u8>, CryptoError> {
let mut buf = [0u8; 4096];
match &mut self.role {
HandshakePqRole::Responder { pending_ct } => {
let n = self
.inner
.get_mut()
.expect("handshake lock")
.read_message(msg, &mut buf)
.map_err(|_| CryptoError::Handshake)?;
if n < PQ_ENCAPS_KEY_LEN {
return Err(CryptoError::Handshake);
}
let ek_bytes: [u8; PQ_ENCAPS_KEY_LEN] = buf[..PQ_ENCAPS_KEY_LEN]
.try_into()
.map_err(|_| CryptoError::Handshake)?;
let ek = PqEncapsKey(ek_bytes);
let (ct, ss) = pq_encapsulate(&ek)?;
self.inner
.get_mut()
.expect("handshake lock")
.set_psk(2, &ss[..])
.map_err(|_| CryptoError::Handshake)?;
*pending_ct = Some(ct);
Ok(buf[PQ_ENCAPS_KEY_LEN..n].to_vec())
}
HandshakePqRole::Initiator { dk, .. } => {
let ct_slice = msg.get(..PQ_CIPHERTEXT_LEN).ok_or(CryptoError::Handshake)?;
let ct_bytes: [u8; PQ_CIPHERTEXT_LEN] =
ct_slice.try_into().map_err(|_| CryptoError::Handshake)?;
let ct = PqCiphertext(ct_bytes);
let decaps_key = dk.take().ok_or(CryptoError::Handshake)?;
let ss = pq_decapsulate(&decaps_key, &ct)?;
drop(decaps_key);
let inner = self.inner.get_mut().expect("handshake lock");
inner
.set_psk(2, &ss[..])
.map_err(|_| CryptoError::Handshake)?;
let n = inner
.read_message(&msg[PQ_CIPHERTEXT_LEN..], &mut buf)
.map_err(|_| CryptoError::Handshake)?;
Ok(buf[..n].to_vec())
}
}
}
pub fn is_finished(&self) -> bool {
self.inner
.lock()
.expect("handshake lock")
.is_handshake_finished()
}
pub fn remote_static(&self) -> Option<[u8; 32]> {
self.inner
.lock()
.expect("handshake lock")
.get_remote_static()
.map(|k| {
let mut out = [0u8; 32];
out.copy_from_slice(k);
out
})
}
pub fn channel_binding(&self) -> [u8; 32] {
let mut out = [0u8; 32];
out.copy_from_slice(
self.inner
.lock()
.expect("handshake lock")
.get_handshake_hash(),
);
out
}
pub fn raw_split_keys(&self) -> ([u8; 32], [u8; 32]) {
let mut inner = self.inner.lock().expect("handshake lock");
let is_initiator = inner.is_initiator();
let (k0, k1) = inner.dangerously_get_raw_split();
if is_initiator {
(k0, k1)
} else {
(k1, k0)
}
}
pub fn into_session(self) -> Result<Session, CryptoError> {
let (k_send, k_recv) = self.raw_split_keys();
Session::from_raw_keys(&k_send, &k_recv, 0, 1)
}
#[cfg(test)]
pub(crate) fn to_session_for_test(&self) -> Result<Session, CryptoError> {
let (k_send, k_recv) = self.raw_split_keys();
Session::from_raw_keys(&k_send, &k_recv, 0, 1)
}
#[cfg(test)]
pub(crate) fn into_inner_snow_for_test(self) -> snow::HandshakeState {
self.inner.into_inner().expect("handshake lock")
}
}
fn noise_nonce(counter: u64) -> Nonce {
let mut n = [0u8; 12];
n[4..].copy_from_slice(&counter.to_le_bytes());
Nonce::assume_unique_for_key(n)
}
#[derive(Debug, Clone)]
pub struct Sealed {
pub counter: u64,
pub ciphertext: Vec<u8>,
}
pub struct Session {
send_key: LessSafeKey,
recv_key: LessSafeKey,
send_counter: u64,
stride: u64,
replay: ReplayWindow,
}
impl Session {
pub fn from_raw_keys(
k_send: &[u8; 32],
k_recv: &[u8; 32],
start_counter: u64,
stride: u64,
) -> Result<Self, CryptoError> {
let send =
UnboundKey::new(&CHACHA20_POLY1305, k_send).map_err(|_| CryptoError::Handshake)?;
let recv =
UnboundKey::new(&CHACHA20_POLY1305, k_recv).map_err(|_| CryptoError::Handshake)?;
Ok(Session {
send_key: LessSafeKey::new(send),
recv_key: LessSafeKey::new(recv),
send_counter: start_counter,
stride: if stride == 0 { 1 } else { stride },
replay: ReplayWindow::new(),
})
}
pub fn set_stride(&mut self, start_counter: u64, stride: u64) {
let stride = if stride == 0 { 1 } else { stride };
if self.send_counter == 0 {
self.send_counter = start_counter;
} else {
let rem = self.send_counter % stride;
let target_rem = start_counter % stride;
let diff = if rem <= target_rem {
target_rem - rem
} else {
stride - (rem - target_rem)
};
self.send_counter = self.send_counter.saturating_add(diff);
}
self.stride = stride;
}
pub fn stride(&self) -> u64 {
self.stride
}
pub fn send_counter(&self) -> u64 {
self.send_counter
}
pub fn seal(&mut self, plaintext: &[u8]) -> Result<Sealed, CryptoError> {
let counter = self.send_counter;
let mut buf = plaintext.to_vec();
self.send_key
.seal_in_place_append_tag(noise_nonce(counter), Aad::empty(), &mut buf)
.map_err(|_| CryptoError::Decrypt)?;
self.send_counter = self
.send_counter
.checked_add(self.stride)
.ok_or(CryptoError::Decrypt)?;
Ok(Sealed {
counter,
ciphertext: buf,
})
}
pub fn open(&mut self, counter: u64, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
if !self.replay.check(counter) {
return Err(CryptoError::Replay);
}
let mut buf = ciphertext.to_vec();
let plain = self
.recv_key
.open_in_place(noise_nonce(counter), Aad::empty(), &mut buf)
.map_err(|_| CryptoError::Decrypt)?;
self.replay.commit(counter);
Ok(plain.to_vec())
}
#[inline]
pub fn check_replay(&self, counter: u64) -> bool {
self.replay.check(counter)
}
pub fn seal_into(&mut self, plaintext: &[u8], out: &mut Vec<u8>) -> Result<u64, CryptoError> {
let counter = self.send_counter;
out.clear();
out.extend_from_slice(plaintext);
self.send_key
.seal_in_place_append_tag(noise_nonce(counter), Aad::empty(), out)
.map_err(|_| CryptoError::Decrypt)?;
self.send_counter = self
.send_counter
.checked_add(self.stride)
.ok_or(CryptoError::Decrypt)?;
Ok(counter)
}
pub fn seal_with_counter(&self, counter: u64, plaintext: &[u8]) -> Result<Sealed, CryptoError> {
let mut buf = plaintext.to_vec();
self.send_key
.seal_in_place_append_tag(noise_nonce(counter), Aad::empty(), &mut buf)
.map_err(|_| CryptoError::Decrypt)?;
Ok(Sealed {
counter,
ciphertext: buf,
})
}
pub fn seal_into_with_counter(
&self,
counter: u64,
plaintext: &[u8],
out: &mut Vec<u8>,
) -> Result<(), CryptoError> {
out.clear();
out.extend_from_slice(plaintext);
self.send_key
.seal_in_place_append_tag(noise_nonce(counter), Aad::empty(), out)
.map_err(|_| CryptoError::Decrypt)?;
Ok(())
}
pub fn open_with_window(
&self,
counter: u64,
ciphertext: &[u8],
replay: &mut ReplayWindow,
) -> Result<Vec<u8>, CryptoError> {
if !replay.check(counter) {
return Err(CryptoError::Replay);
}
let mut buf = ciphertext.to_vec();
let plain = self
.recv_key
.open_in_place(noise_nonce(counter), Aad::empty(), &mut buf)
.map_err(|_| CryptoError::Decrypt)?;
replay.commit(counter);
Ok(plain.to_vec())
}
pub fn open_into_with_window(
&self,
counter: u64,
ciphertext: &[u8],
replay: &mut ReplayWindow,
out: &mut Vec<u8>,
) -> Result<(), CryptoError> {
if !replay.check(counter) {
return Err(CryptoError::Replay);
}
out.clear();
out.extend_from_slice(ciphertext);
let n = {
let plain = self
.recv_key
.open_in_place(noise_nonce(counter), Aad::empty(), out)
.map_err(|_| CryptoError::Decrypt)?;
plain.len()
};
replay.commit(counter);
out.truncate(n);
Ok(())
}
pub fn open_into(
&mut self,
counter: u64,
ciphertext: &[u8],
out: &mut Vec<u8>,
) -> Result<(), CryptoError> {
if !self.replay.check(counter) {
return Err(CryptoError::Replay);
}
out.clear();
out.extend_from_slice(ciphertext);
let n = {
let plain = self
.recv_key
.open_in_place(noise_nonce(counter), Aad::empty(), out)
.map_err(|_| CryptoError::Decrypt)?;
plain.len()
};
self.replay.commit(counter);
out.truncate(n);
Ok(())
}
pub fn seal_in_place(
&mut self,
buf: &mut [u8],
plaintext_len: usize,
) -> Result<u64, CryptoError> {
let counter = self.send_counter;
let total_len = plaintext_len.checked_add(16).ok_or(CryptoError::Decrypt)?;
if buf.len() < total_len {
return Err(CryptoError::Decrypt);
}
let (in_out, tag_out) = buf[..total_len].split_at_mut(plaintext_len);
let tag = self
.send_key
.seal_in_place_separate_tag(noise_nonce(counter), Aad::empty(), in_out)
.map_err(|_| CryptoError::Decrypt)?;
tag_out.copy_from_slice(tag.as_ref());
self.send_counter = self
.send_counter
.checked_add(self.stride)
.ok_or(CryptoError::Decrypt)?;
Ok(counter)
}
pub fn open_in_place(
&mut self,
counter: u64,
buf: &mut [u8],
sealed_len: usize,
) -> Result<usize, CryptoError> {
if !self.replay.check(counter) {
return Err(CryptoError::Replay);
}
if sealed_len < 16 || buf.len() < sealed_len {
return Err(CryptoError::Decrypt);
}
let plain = self
.recv_key
.open_in_place(noise_nonce(counter), Aad::empty(), &mut buf[..sealed_len])
.map_err(|_| CryptoError::Decrypt)?;
self.replay.commit(counter);
Ok(plain.len())
}
}
pub struct ChaCha20Poly1305Cipher {
key: LessSafeKey,
}
impl std::fmt::Debug for ChaCha20Poly1305Cipher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChaCha20Poly1305Cipher")
.finish_non_exhaustive()
}
}
impl ChaCha20Poly1305Cipher {
pub fn new(key: [u8; 32]) -> Self {
let unbound = UnboundKey::new(&CHACHA20_POLY1305, &key)
.expect("32-byte key is valid for ChaCha20-Poly1305");
Self {
key: LessSafeKey::new(unbound),
}
}
pub fn seal_in_place(
&self,
counter: u64,
buf: &mut [u8],
plaintext_len: usize,
) -> Result<usize, CryptoError> {
let total_len = plaintext_len.checked_add(16).ok_or(CryptoError::Decrypt)?;
if buf.len() < total_len {
return Err(CryptoError::Decrypt);
}
let (in_out, tag_out) = buf[..total_len].split_at_mut(plaintext_len);
let tag = self
.key
.seal_in_place_separate_tag(noise_nonce(counter), Aad::empty(), in_out)
.map_err(|_| CryptoError::Decrypt)?;
tag_out.copy_from_slice(tag.as_ref());
Ok(total_len)
}
pub fn open_in_place(
&self,
counter: u64,
buf: &mut [u8],
sealed_len: usize,
) -> Result<usize, CryptoError> {
if sealed_len < 16 || buf.len() < sealed_len {
return Err(CryptoError::Decrypt);
}
let plain = self
.key
.open_in_place(noise_nonce(counter), Aad::empty(), &mut buf[..sealed_len])
.map_err(|_| CryptoError::Decrypt)?;
Ok(plain.len())
}
pub fn open_in_place_with_window(
&self,
counter: u64,
buf: &mut [u8],
sealed_len: usize,
replay: &mut ReplayWindow,
) -> Result<usize, CryptoError> {
if !replay.check(counter) {
return Err(CryptoError::Replay);
}
let plain_len = self.open_in_place(counter, buf, sealed_len)?;
replay.commit(counter);
Ok(plain_len)
}
}
#[cfg(test)]
pub(crate) fn test_session_pair() -> (Session, Session) {
let resp_kp = generate_keypair();
let init_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let m1 = ini.write_message(&[]).unwrap();
let _ = res.read_message(&m1).unwrap();
let m2 = res.write_message(&[]).unwrap();
let _ = ini.read_message(&m2).unwrap();
(ini.into_session().unwrap(), res.into_session().unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_keypairs_are_distinct_32_byte_keys() {
let a = generate_keypair();
let b = generate_keypair();
assert_eq!(a.private.len(), 32);
assert_eq!(a.public.len(), 32);
assert_ne!(a.private, b.private, "two keypairs differ");
assert_ne!(a.public, [0u8; 32], "public key is not all-zero");
}
#[test]
fn session_seal_and_open_in_place_with_stride() {
let (k_send, k_recv) = ([0x11u8; 32], [0x22u8; 32]);
let mut s_tx = Session::from_raw_keys(&k_send, &k_recv, 2, 4).unwrap();
let mut s_rx = Session::from_raw_keys(&k_recv, &k_send, 0, 1).unwrap();
let mut buf = vec![0u8; 64];
buf[..10].copy_from_slice(b"0123456789");
let c1 = s_tx.seal_in_place(&mut buf, 10).unwrap();
assert_eq!(c1, 2);
let plain_len = s_rx.open_in_place(c1, &mut buf, 26).unwrap();
assert_eq!(plain_len, 10);
assert_eq!(&buf[..10], b"0123456789");
buf[..10].copy_from_slice(b"abcdefghij");
let c2 = s_tx.seal_in_place(&mut buf, 10).unwrap();
assert_eq!(c2, 6);
let plain_len2 = s_rx.open_in_place(c2, &mut buf, 26).unwrap();
assert_eq!(plain_len2, 10);
assert_eq!(&buf[..10], b"abcdefghij");
assert!(s_rx.open_in_place(c2, &mut buf, 26).is_err());
}
#[test]
fn replay_window_accepts_fresh_rejects_replays_and_old() {
let mut w = ReplayWindow::new();
assert!(w.check_and_set(0), "first counter accepted");
assert!(!w.check_and_set(0), "exact replay rejected");
assert!(w.check_and_set(1), "next in order accepted");
assert!(w.check_and_set(5), "jump ahead accepted");
assert!(w.check_and_set(3), "in-window out-of-order accepted");
assert!(!w.check_and_set(3), "replay of out-of-order rejected");
assert!(w.check_and_set(100), "large advance accepted");
assert!(
!w.check_and_set(5),
"counter now far below window rejected as too old"
);
}
#[test]
fn replay_window_advance_then_replay_old_counter_rejected() {
let mut w = ReplayWindow::new();
assert!(w.check_and_set(10), "counter 10 accepted as first");
assert!(w.check_and_set(15), "advance to 15 accepted");
assert!(
!w.check_and_set(10),
"replay of counter 10 after advance to 15 rejected"
);
}
#[test]
fn replay_window_new_latest_immediately_replayable_rejected() {
let mut w = ReplayWindow::new();
assert!(w.check_and_set(10), "first packet at 10");
assert!(w.check_and_set(9), "counter 9 accepted in-order-ish");
assert!(w.check_and_set(11), "advance to 11 accepted");
assert!(!w.check_and_set(11), "replay of new latest 11 rejected");
}
#[test]
fn replay_window_in_window_replay_rejected_after_advance() {
let mut w = ReplayWindow::new();
assert!(w.check_and_set(10), "first packet");
assert!(w.check_and_set(15), "advance to 15");
assert!(!w.check_and_set(10), "in-window replay at diff=5 rejected");
}
#[test]
fn handshake_not_finished_before_message_exchange() {
let kp = generate_keypair();
let res = Handshake::responder(&kp.private).unwrap();
assert!(
!res.is_finished(),
"responder reports not-finished before any messages"
);
let ini = Handshake::initiator(&kp.private, &kp.public).unwrap();
assert!(
!ini.is_finished(),
"initiator reports not-finished before any messages"
);
}
#[test]
fn handshake_payload_round_trips_and_sessions_match() {
let resp_kp = generate_keypair();
let init_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let m1 = ini.write_message(b"cert-A").unwrap();
let got_a = res.read_message(&m1).unwrap();
assert_eq!(got_a, b"cert-A");
let m2 = res.write_message(b"cert-B").unwrap();
let got_b = ini.read_message(&m2).unwrap();
assert_eq!(got_b, b"cert-B");
assert_eq!(ini.channel_binding(), res.channel_binding());
let mut ini_session = ini.into_session().unwrap();
let mut res_session = res.into_session().unwrap();
let sealed = ini_session.seal(b"payload round-trip ok").unwrap();
assert_eq!(
res_session
.open(sealed.counter, &sealed.ciphertext)
.unwrap(),
b"payload round-trip ok"
);
}
#[test]
fn session_seals_and_opens_roundtrip() {
let (mut a, mut b) = test_session_pair();
let s = a.seal(b"inner packet").unwrap();
assert_eq!(s.counter, 0, "first counter is 0");
assert_eq!(b.open(s.counter, &s.ciphertext).unwrap(), b"inner packet");
}
#[test]
fn session_opens_out_of_order() {
let (mut a, mut b) = test_session_pair();
let s0 = a.seal(b"zero").unwrap();
let s1 = a.seal(b"one").unwrap();
assert_eq!(s1.counter, 1);
assert_eq!(b.open(s1.counter, &s1.ciphertext).unwrap(), b"one");
assert_eq!(b.open(s0.counter, &s0.ciphertext).unwrap(), b"zero");
}
#[test]
fn session_rejects_replay() {
let (mut a, mut b) = test_session_pair();
let s = a.seal(b"x").unwrap();
assert!(b.open(s.counter, &s.ciphertext).is_ok());
assert_eq!(b.open(s.counter, &s.ciphertext), Err(CryptoError::Replay));
}
#[test]
fn session_rejects_tampered_ciphertext() {
let (mut a, mut b) = test_session_pair();
let s = a.seal(b"y").unwrap();
let mut bad = s.ciphertext.clone();
bad[0] ^= 0x01;
assert_eq!(b.open(s.counter, &bad), Err(CryptoError::Decrypt));
}
#[test]
fn forged_frame_does_not_advance_replay_window() {
let (mut a, mut b) = test_session_pair();
let s0 = a.seal(b"zero").unwrap();
let s1 = a.seal(b"one").unwrap();
assert_eq!(b.open(s0.counter, &s0.ciphertext).unwrap(), b"zero");
let garbage = vec![0u8; s0.ciphertext.len()];
assert_eq!(
b.open(1_000_000, &garbage),
Err(CryptoError::Decrypt),
"forged frame fails AEAD"
);
assert_eq!(
b.open(s1.counter, &s1.ciphertext).unwrap(),
b"one",
"legit frame still opens after forged far-future injection"
);
}
#[test]
fn forged_frame_does_not_advance_replay_window_open_into() {
let (mut a, mut b) = test_session_pair();
let s0 = a.seal(b"zero").unwrap();
let s1 = a.seal(b"one").unwrap();
let mut out = Vec::new();
b.open_into(s0.counter, &s0.ciphertext, &mut out).unwrap();
assert_eq!(out, b"zero");
let garbage = vec![0u8; s0.ciphertext.len()];
assert_eq!(
b.open_into(1_000_000, &garbage, &mut out),
Err(CryptoError::Decrypt),
"forged frame fails AEAD"
);
b.open_into(s1.counter, &s1.ciphertext, &mut out).unwrap();
assert_eq!(
out, b"one",
"legit frame still opens after forged injection"
);
}
#[test]
fn channel_binding_matches_on_both_peers() {
let resp_kp = generate_keypair();
let init_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let m1 = ini.write_message(&[]).unwrap();
let _ = res.read_message(&m1).unwrap();
let m2 = res.write_message(&[]).unwrap();
let _ = ini.read_message(&m2).unwrap();
assert!(ini.is_finished() && res.is_finished());
assert_eq!(
ini.channel_binding(),
res.channel_binding(),
"both peers derive the same binding"
);
assert_ne!(ini.channel_binding(), [0u8; 32]);
}
#[test]
fn channel_binding_is_transcript_dependent_not_constant() {
fn complete_binding() -> [u8; 32] {
let resp_kp = generate_keypair();
let init_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let m1 = ini.write_message(&[]).unwrap();
let _ = res.read_message(&m1).unwrap();
let m2 = res.write_message(&[]).unwrap();
let _ = ini.read_message(&m2).unwrap();
assert!(ini.is_finished() && res.is_finished());
let binding = ini.channel_binding();
assert_eq!(binding, res.channel_binding());
binding
}
let binding_a = complete_binding();
let binding_b = complete_binding();
assert_ne!(
binding_a, [1u8; 32],
"channel_binding must not be the constant [1; 32]"
);
assert_ne!(
binding_b, [1u8; 32],
"channel_binding must not be the constant [1; 32]"
);
assert_ne!(
binding_a, binding_b,
"two independent handshakes (different keys/transcripts) must \
derive different channel bindings"
);
}
#[test]
fn ik_handshake_completes_and_authenticates_initiator() {
let resp_kp = generate_keypair();
let init_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let msg1 = ini.write_message(&[]).unwrap();
let _ = res.read_message(&msg1).unwrap();
let msg2 = res.write_message(&[]).unwrap();
let _ = ini.read_message(&msg2).unwrap();
assert!(ini.is_finished() && res.is_finished());
assert_eq!(res.remote_static(), Some(init_kp.public));
}
#[test]
fn seal_is_byte_identical_across_a_reference_session() {
let (mut a, mut b) = crate::test_session_pair();
for ctr in 0u64..8 {
let s = a.seal(&[0x5Au8; 64]).unwrap();
assert_eq!(s.counter, ctr);
assert_eq!(b.open(s.counter, &s.ciphertext).unwrap(), vec![0x5Au8; 64]);
}
}
#[test]
fn open_rejects_tampered_ciphertext() {
let (mut a, mut b) = crate::test_session_pair();
let s = a.seal(b"secret").unwrap();
let mut bad = s.ciphertext.clone();
bad[0] ^= 1;
assert_eq!(b.open(s.counter, &bad), Err(CryptoError::Decrypt));
}
#[test]
fn open_rejects_replay_and_opens_out_of_order() {
let (mut a, mut b) = crate::test_session_pair();
let s0 = a.seal(b"zero").unwrap();
let s1 = a.seal(b"one").unwrap();
assert_eq!(b.open(s1.counter, &s1.ciphertext).unwrap(), b"one"); assert_eq!(b.open(s0.counter, &s0.ciphertext).unwrap(), b"zero");
assert_eq!(b.open(s1.counter, &s1.ciphertext), Err(CryptoError::Replay));
}
#[test]
fn session_seal_is_byte_identical_to_snow_write_message_both_directions() {
let resp_kp = generate_keypair();
let init_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let m1 = ini.write_message(&[]).unwrap();
let _ = res.read_message(&m1).unwrap();
let m2 = res.write_message(&[]).unwrap();
let _ = ini.read_message(&m2).unwrap();
assert!(ini.is_finished() && res.is_finished());
let (ini_send, ini_recv) = ini.raw_split_keys();
let (res_send, res_recv) = res.raw_split_keys();
assert_eq!(
ini_send, res_recv,
"initiator send key matches responder recv key"
);
assert_eq!(
ini_recv, res_send,
"initiator recv key matches responder send key"
);
let mut ini_session = ini.to_session_for_test().unwrap();
let mut res_session = res.to_session_for_test().unwrap();
let snow_ini_ref = ini
.into_inner_snow_for_test()
.into_stateless_transport_mode()
.unwrap();
let snow_res_ref = res
.into_inner_snow_for_test()
.into_stateless_transport_mode()
.unwrap();
let plaintext: Vec<u8> = (0u8..200).collect();
for ctr in 0u64..=4 {
let sealed = ini_session.seal(&plaintext).unwrap();
assert_eq!(sealed.counter, ctr);
let mut snow_out = vec![0u8; plaintext.len() + 16];
let n = snow_ini_ref
.write_message(ctr, &plaintext, &mut snow_out)
.unwrap();
snow_out.truncate(n);
assert_eq!(
sealed.ciphertext, snow_out,
"initiator->responder ciphertext byte-identical to snow at counter {ctr}"
);
assert_eq!(
res_session
.open(sealed.counter, &sealed.ciphertext)
.unwrap(),
plaintext,
"responder opens what the initiator sealed at counter {ctr}"
);
}
for ctr in 0u64..=4 {
let sealed = res_session.seal(&plaintext).unwrap();
assert_eq!(sealed.counter, ctr);
let mut snow_out = vec![0u8; plaintext.len() + 16];
let n = snow_res_ref
.write_message(ctr, &plaintext, &mut snow_out)
.unwrap();
snow_out.truncate(n);
assert_eq!(
sealed.ciphertext, snow_out,
"responder->initiator ciphertext byte-identical to snow at counter {ctr}"
);
assert_eq!(
ini_session
.open(sealed.counter, &sealed.ciphertext)
.unwrap(),
plaintext,
"initiator opens what the responder sealed at counter {ctr}"
);
}
}
#[test]
fn seal_into_matches_seal_and_opens() {
let (mut a, mut b) = crate::test_session_pair();
let mut sbuf = Vec::new();
let ctr = a.seal_into(b"reuse me", &mut sbuf).unwrap();
let mut obuf = Vec::new();
b.open_into(ctr, &sbuf, &mut obuf).unwrap();
assert_eq!(obuf, b"reuse me");
}
#[test]
fn test_wide_replay_window_jitter_and_rejection() {
let mut w = ReplayWindow::new();
assert!(w.check(0));
w.commit(0);
assert!(w.check(100_000));
w.commit(100_000);
assert!(w.check(50_000));
w.commit(50_000);
assert!(!w.check(50_000));
assert!(!w.check(0));
assert!(w.check(250_000));
w.commit(250_000);
assert!(!w.check(100_000));
}
#[test]
fn test_seal_with_counter_and_open_with_window() {
let (a, b) = crate::test_session_pair();
let payload = b"concurrent multi-worker payload";
let sealed = a.seal_with_counter(42, payload).unwrap();
assert_eq!(sealed.counter, 42);
let mut replay = ReplayWindow::new();
let decrypted = b
.open_with_window(sealed.counter, &sealed.ciphertext, &mut replay)
.unwrap();
assert_eq!(decrypted, payload);
assert_eq!(
b.open_with_window(sealed.counter, &sealed.ciphertext, &mut replay),
Err(CryptoError::Replay)
);
}
#[test]
fn test_chacha20_poly1305_cipher_in_place() {
let key = [0x5au8; 32];
let cipher = ChaCha20Poly1305Cipher::new(key);
let mut buffer = [0u8; 128];
let plaintext = b"zero-copy in-place packet buffer payload";
buffer[..plaintext.len()].copy_from_slice(plaintext);
let counter = 100u64;
let sealed_len = cipher
.seal_in_place(counter, &mut buffer, plaintext.len())
.expect("seal_in_place should succeed");
assert_eq!(sealed_len, plaintext.len() + 16);
assert_ne!(&buffer[..plaintext.len()], plaintext);
let mut replay = ReplayWindow::new();
let plain_len = cipher
.open_in_place_with_window(counter, &mut buffer, sealed_len, &mut replay)
.expect("open_in_place_with_window should succeed");
assert_eq!(plain_len, plaintext.len());
assert_eq!(&buffer[..plain_len], plaintext);
assert_eq!(
cipher.open_in_place_with_window(counter, &mut buffer, sealed_len, &mut replay),
Err(CryptoError::Replay)
);
}
#[test]
fn test_chacha20_poly1305_cipher_debug_and_boundaries() {
let key = [0x5au8; 32];
let cipher = ChaCha20Poly1305Cipher::new(key);
let debug_str = format!("{cipher:?}");
assert!(debug_str.contains("ChaCha20Poly1305Cipher"));
let mut buf = [0u8; 32];
assert_eq!(cipher.seal_in_place(1, &mut buf[..16], 0).unwrap(), 16);
assert_eq!(
cipher.seal_in_place(1, &mut buf[..15], 0),
Err(CryptoError::Decrypt)
);
assert_eq!(
cipher.open_in_place(1, &mut buf, 15),
Err(CryptoError::Decrypt)
);
assert_eq!(
cipher.open_in_place(1, &mut buf, 0),
Err(CryptoError::Decrypt)
);
assert_eq!(
cipher.open_in_place(1, &mut buf[..10], 16),
Err(CryptoError::Decrypt)
);
let mut exact = [0u8; 16];
cipher.seal_in_place(2, &mut exact, 0).unwrap();
assert_eq!(cipher.open_in_place(2, &mut exact, 16).unwrap(), 0);
}
#[test]
fn test_seal_into_with_counter_and_open_into_with_window() {
let (a, b) = crate::test_session_pair();
let payload = b"buffer test payload";
let mut sealed_buf = Vec::new();
a.seal_into_with_counter(42, payload, &mut sealed_buf)
.unwrap();
assert_ne!(sealed_buf, payload);
assert_eq!(sealed_buf.len(), payload.len() + 16);
let mut replay = ReplayWindow::new();
let mut out = Vec::new();
b.open_into_with_window(42, &sealed_buf, &mut replay, &mut out)
.unwrap();
assert_eq!(out, payload);
out.clear();
let err = b.open_into_with_window(42, &sealed_buf, &mut replay, &mut out);
assert_eq!(err, Err(CryptoError::Replay));
assert!(out.is_empty());
let mut bad_buf = sealed_buf.clone();
bad_buf[0] ^= 1;
let mut fresh_replay = ReplayWindow::new();
let bad_err = b.open_into_with_window(43, &bad_buf, &mut fresh_replay, &mut out);
assert_eq!(bad_err, Err(CryptoError::Decrypt));
}
#[test]
fn test_replay_window_word_idx_distinct_words() {
let mut w = ReplayWindow::new();
w.commit(0);
assert!(w.check(64));
assert!(w.check(128));
w.commit(64);
assert!(!w.check(64));
assert!(!w.check(0));
assert!(w.check(128));
}
#[test]
fn test_promote_to_high_throughput_preserves_multi_word_state() {
let mut w = ReplayWindow::new_with_profile(ReplayProfile::Standard);
w.commit(100);
w.commit(200);
w.commit(1000);
w.commit(1500);
w.promote_to_high_throughput();
assert_eq!(w.profile(), ReplayProfile::HighThroughput);
assert!(!w.check(100));
assert!(!w.check(200));
assert!(!w.check(1000));
assert!(!w.check(1500));
assert!(w.check(101));
assert!(w.check(201));
assert!(w.check(1001));
assert!(w.check(1499));
}
#[test]
fn test_promote_to_high_throughput_large_latest() {
let mut w = ReplayWindow::new_with_profile(ReplayProfile::Standard);
let latest = 200 * 64 + 10;
w.commit(latest);
w.promote_to_high_throughput();
assert!(
w.check(72 * 64 + 10),
"counter 72 * 64 + 10 was never committed and must be acceptable"
);
let mut w2 = ReplayWindow::new_with_profile(ReplayProfile::Standard);
w2.commit(73 * 64 + 10);
w2.commit(latest);
w2.promote_to_high_throughput();
assert!(!w2.check(latest), "latest must still be marked seen");
assert!(
!w2.check(73 * 64 + 10),
"tail counter in word 73 must be marked seen"
);
}
#[test]
fn test_replay_window_circular_word_clearing() {
let mut w = ReplayWindow::new_with_profile(ReplayProfile::Standard);
w.commit(69);
assert!(!w.check(69));
w.commit(8192 + 74);
assert!(!w.check(8192 + 74));
assert!(
w.check(8192 + 69),
"counter 8192 + 69 must not be blocked by stale bit from counter 69"
);
}
#[test]
fn test_replay_window_consecutive_word_clearing() {
let mut w = ReplayWindow::new_with_profile(ReplayProfile::Standard);
w.commit(130);
w.commit(8192 + 70);
w.commit(8192 + 135);
assert!(
w.check(8192 + 130),
"counter 8192 + 130 must be accepted after word 2 is overtaken and cleared"
);
w.commit(3000);
w.commit(3001);
w.commit(3500); w.commit(5000);
w.commit(5001);
w.commit(5100);
assert!(!w.check(5001), "counter 5001 must still be marked seen");
}
#[test]
fn test_session_set_stride_and_send_counter() {
let (mut a, _) = crate::test_session_pair();
assert_eq!(a.send_counter(), 0);
assert_ne!(a.send_counter(), 1);
a.set_stride(5, 4);
assert_eq!(a.stride(), 4);
assert_eq!(a.send_counter(), 5);
a.set_stride(10, 0);
assert_eq!(a.stride(), 1);
assert_eq!(a.send_counter(), 5);
a.set_stride(3, 4);
assert_eq!(a.stride(), 4);
assert_eq!(a.send_counter(), 7);
assert_eq!(a.send_counter() % 4, 3);
a.set_stride(1, 8);
assert_eq!(a.stride(), 8);
assert_eq!(a.send_counter(), 9);
assert_eq!(a.send_counter() % 8, 1);
}
#[test]
fn test_session_seal_in_place_and_open_in_place_boundaries() {
let (mut a, mut b) = crate::test_session_pair();
let mut buf = [0u8; 32];
let ctr = a.seal_in_place(&mut buf[..16], 0).unwrap();
assert_eq!(ctr, 0);
assert_eq!(
a.seal_in_place(&mut buf[..15], 0),
Err(CryptoError::Decrypt)
);
assert_eq!(b.open_in_place(0, &mut buf, 15), Err(CryptoError::Decrypt));
assert_eq!(b.open_in_place(0, &mut buf, 0), Err(CryptoError::Decrypt));
assert_eq!(
b.open_in_place(0, &mut buf[..10], 16),
Err(CryptoError::Decrypt)
);
let mut exact = [0u8; 16];
let ctr2 = a.seal_in_place(&mut exact, 0).unwrap();
assert_eq!(b.open_in_place(ctr2, &mut exact, 16).unwrap(), 0);
}
#[test]
fn pq_keypair_encapsulate_decapsulate_roundtrip() {
let (ek, dk) = generate_pq_keypair();
assert_eq!(ek.0.len(), PQ_ENCAPS_KEY_LEN);
let (ct, ss_sender) = pq_encapsulate(&ek).expect("encapsulation succeeds");
assert_eq!(ct.0.len(), PQ_CIPHERTEXT_LEN);
assert_eq!(ss_sender.len(), PQ_SHARED_SECRET_LEN);
let ss_recipient = pq_decapsulate(&dk, &ct).expect("decapsulation succeeds");
assert_eq!(
*ss_sender, *ss_recipient,
"both sides agree on 32-byte shared secret"
);
}
#[test]
fn pq_encapsulate_rejects_fips203_invalid_modulus() {
let (mut ek, _dk) = generate_pq_keypair();
ek.0[0] = 0xFF;
ek.0[1] = 0x0F;
let res = pq_encapsulate(&ek);
assert!(
res.is_err(),
"encapsulation must reject coefficient >= 3329"
);
}
#[test]
fn pq_decapsulate_implicit_rejection_on_tampered_ciphertext() {
let (ek, dk) = generate_pq_keypair();
let (mut ct, ss_sender) = pq_encapsulate(&ek).expect("encapsulation succeeds");
ct.0[0] ^= 0x01;
let ss_recipient =
pq_decapsulate(&dk, &ct).expect("decapsulation succeeds via implicit rejection");
assert_ne!(
*ss_sender, *ss_recipient,
"FIPS 203 implicit rejection must yield a distinct pseudorandom secret on tampered ciphertext"
);
}
#[test]
fn pq_decaps_key_debug_redacted() {
let (_ek, dk) = generate_pq_keypair();
assert_eq!(format!("{:?}", dk), "PqDecapsKey([REDACTED])");
}
#[test]
fn pq_encapsulate_rejects_fips203_invalid_modulus_second_coeff() {
let (mut ek, _dk) = generate_pq_keypair();
ek.0[0] = 0x00;
ek.0[1] = 0xF0;
ek.0[2] = 0xFF;
let res = pq_encapsulate(&ek);
assert!(
res.is_err(),
"encapsulation must reject second coefficient >= 3329"
);
}
#[test]
fn pq_hybrid_handshake_roundtrip_with_payload() {
let init_kp = generate_keypair();
let resp_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let cert_a = b"initiator-test-certificate-128b";
let cert_b = b"responder-test-certificate-128b";
let msg1 = ini.write_message(cert_a).unwrap();
assert_eq!(msg1.len(), 32 + 48 + PQ_ENCAPS_KEY_LEN + cert_a.len() + 16);
let p1 = res.read_message(&msg1).unwrap();
assert_eq!(p1, cert_a);
assert_eq!(res.remote_static(), Some(init_kp.public));
let msg2 = res.write_message(cert_b).unwrap();
assert_eq!(msg2.len(), PQ_CIPHERTEXT_LEN + 32 + cert_b.len() + 16);
let p2 = ini.read_message(&msg2).unwrap();
assert_eq!(p2, cert_b);
assert_eq!(ini.remote_static(), Some(resp_kp.public));
assert!(ini.is_finished());
assert!(res.is_finished());
assert_eq!(ini.channel_binding(), res.channel_binding());
let (ini_send, ini_recv) = ini.raw_split_keys();
let (res_send, res_recv) = res.raw_split_keys();
assert_eq!(ini_send, res_recv);
assert_eq!(ini_recv, res_send);
let mut ini_session = ini.into_session().unwrap();
let mut res_session = res.into_session().unwrap();
let sealed = ini_session.seal(b"quantum safe transport payload").unwrap();
let opened = res_session
.open(sealed.counter, &sealed.ciphertext)
.unwrap();
assert_eq!(opened, b"quantum safe transport payload");
}
#[test]
fn pq_hybrid_handshake_rejects_tampered_ciphertext() {
let init_kp = generate_keypair();
let resp_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let msg1 = ini.write_message(b"certA").unwrap();
let _ = res.read_message(&msg1).unwrap();
let mut msg2 = res.write_message(b"certB").unwrap();
msg2[10] ^= 0x01;
let err = ini.read_message(&msg2);
assert!(err.is_err(), "tampered ct must fail handshake verification");
}
#[test]
fn pq_handshake_read_message_rejects_truncated_message() {
let init_kp = generate_keypair();
let resp_kp = generate_keypair();
let mut ini = Handshake::initiator(&init_kp.private, &resp_kp.public).unwrap();
let mut res = Handshake::responder(&resp_kp.private).unwrap();
let msg1 = ini.write_message(b"certA").unwrap();
assert!(res.read_message(&msg1[..10]).is_err());
let _ = res.read_message(&msg1).unwrap();
let msg2 = res.write_message(b"certB").unwrap();
let truncated = vec![0u8; PQ_CIPHERTEXT_LEN - 1];
assert!(ini.read_message(&truncated).is_err());
assert!(ini.read_message(&msg2[..PQ_CIPHERTEXT_LEN]).is_err());
}
#[test]
fn session_check_replay_is_non_committing() {
let (a, mut b) = crate::test_session_pair();
assert!(b.check_replay(1));
assert!(b.check_replay(1));
let sealed = a.seal_with_counter(1, b"hello").unwrap();
let opened = b.open(sealed.counter, &sealed.ciphertext).unwrap();
assert_eq!(opened, b"hello");
assert!(!b.check_replay(1));
}
}