Clatter has a PSK Validity Rule Violation issue
Description
Clatter is a no_std compatible, pure Rust implementation of the Noise protocol framework with post-quantum support. Versiosn prior to2.2.0 have a protocol compliance vulnerability. The library allowed post-quantum handshake patterns that violated the PSK validity rule (Noise Protocol Framework Section 9.3). This could allow PSK-derived keys to be used for encryption without proper randomization by self-chosen ephemeral randomness, weakening security guarantees and potentially allowing catastrophic key reuse. Affected default patterns include noise_pqkk_psk0, noise_pqkn_psk0, noise_pqnk_psk0, noise_pqnn_psk0, and some hybrid variants. Users of these patterns may have been using handshakes that do not meet the intended security properties. The issue is fully patched and released in Clatter v2.2.0. The fixed version includes runtime checks to detect offending handshake patterns. As a workaround, avoid using offending *_psk0` variants of post-quantum patterns. Review custom handshake patterns carefully.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
clattercrates.io | < 2.2.0 | 2.2.0 |
Affected products
1Patches
1b65ae6e9b801Pattern validity rules (#19)
10 files changed · +1198 −148
fuzz/src/lib.rs+0 −6 modified@@ -71,14 +71,10 @@ pub fn pq_handshake_patterns() -> Vec<HandshakePattern> { noise_pqin_psk1(), noise_pqin_psk2(), noise_pqix_psk2(), - noise_pqkk_psk0(), noise_pqkk_psk2(), - noise_pqkn_psk0(), noise_pqkn_psk2(), noise_pqkx_psk2(), - noise_pqnk_psk0(), noise_pqnk_psk2(), - noise_pqnn_psk0(), noise_pqnn_psk2(), noise_pqnx_psk2(), noise_pqxk_psk3(), @@ -107,12 +103,10 @@ pub fn hybrid_handshake_patterns() -> Vec<HandshakePattern> { noise_hybrid_in_psk1(), noise_hybrid_in_psk2(), noise_hybrid_ix_psk2(), - noise_hybrid_kk_psk0(), noise_hybrid_kk_psk2(), noise_hybrid_kn_psk0(), noise_hybrid_kn_psk2(), noise_hybrid_kx_psk2(), - noise_hybrid_nk_psk0(), noise_hybrid_nk_psk2(), noise_hybrid_nn_psk0(), noise_hybrid_nn_psk2(),
README.md+15 −0 modified@@ -45,6 +45,21 @@ This crate tracks Noise protocol framework **revision 34**. As of now we omit su * Deferred pattern support - Can be implemented by the user * Fallback pattern support - Can be implemented by the user +### PSK validity rule + +Clatter adopts a modified interpretation of the PSK validity rule for post-quantum patterns, the original rule being: + +> A party may not send any encrypted data after it processes a "psk" token unless it +> has previously sent an ephemeral public key (an "e" token), either before or after +> the "psk" token. + +When post-quantum patterns are used, sending either an `e` or `ekem` token provides the required +self-chosen randomness equivalent to the `e` token in classic Noise patterns. Both tokens result +in the `MixKey` Noise function being called with derived key material from a self-chosen randomness +source **before** any encryption occurs, ensuring the same security guarantees as the original rule. + +`skem` also satisfies this requirement if it comes before any `psk` tokens in the message pattern. + ## PQ? NQ? Why should I care? This crate refers to classical Noise handshakes as NQ handshakes (non-post-quantum). But what does a
src/error.rs+14 −0 modified@@ -30,6 +30,8 @@ pub enum HandshakeError { Cipher(#[from] CipherError), /// Transport error: {0} Transport(#[from] TransportError), + /// Handshake pattern error: {0} + Pattern(#[from] PatternError), } /// Handshake operation result type @@ -92,3 +94,15 @@ pub enum CipherError { /// Cipher operation result type pub type CipherResult<T> = Result<T, CipherError>; + +/// Errors that can happen as a result from using invalid handshake patterns +#[derive(Debug, Error, Display)] +pub enum PatternError { + /// PSK validity rule violation + PskValidityViolation, + /// PQ token ordering violation + PqTokenOrderViolation, +} + +/// Pattern operation result type +pub type PatternResult<T> = Result<T, PatternError>;
src/handshakepattern.rs+417 −132 modified@@ -3,6 +3,7 @@ use arrayvec::ArrayVec; use crate::constants::{MAX_HS_MESSAGES_PER_ROLE, MAX_TOKENS_PER_HS_MESSAGE}; +use crate::error::{PatternError, PatternResult}; /// Handshake pattern type #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -110,13 +111,17 @@ impl HandshakePattern { /// # Panics /// * If the pattern is invalid (empty pattern, no DH or KEM operations at all) /// * If the message sequences are too long, limits in [`crate::constants`] - pub fn new( + /// + /// # Errors + /// * [`PatternError::PskValidityViolation`] - If the pattern violates the PSK validity rule (Noise Protocol Framework Specification section 9.3) + /// * [`PatternError::PqTokenOrderViolation`] - If the pattern violates the PQ token ordering rule (PQNoise paper section 2.4) + pub fn try_new( name: &'static str, pre_initiator: &[Token], pre_responder: &[Token], initiator: &[&[Token]], responder: &[&[Token]], - ) -> Self { + ) -> PatternResult<Self> { let message_pattern = MessagePattern { initiator: initiator .iter() @@ -138,21 +143,150 @@ impl HandshakePattern { (false, false) => unreachable!("Invalid handshake pattern"), }; - Self { + // Validate PSK validity rule if pattern has PSK + if message_pattern.has_psk() { + Self::validate_psk_rule(&message_pattern.initiator)?; + Self::validate_psk_rule(&message_pattern.responder)?; + } + + if has_kem { + Self::validate_pq_token_order_rule(&message_pattern.initiator)?; + Self::validate_pq_token_order_rule(&message_pattern.responder)?; + } + + Ok(Self { name, hs_type, has_psk: message_pattern.has_psk(), message_pattern, pre_initiator: pre_initiator.iter().copied().collect(), pre_responder: pre_responder.iter().copied().collect(), + }) + } + + /// Initialize a new handshake pattern + /// + /// # Arguments + /// * `name` - Pattern name + /// * `pre_initiator` - Tokens shared by initiator pre handshake + /// * `pre_responder` - Tokens shared by responder pre handshake + /// * `initiator` - Initiator messages + /// * `responder` - Responder messages + /// + /// # Panics + /// * If the pattern is invalid (empty pattern, no DH or KEM operations at all) + /// * If the message sequences are too long, limits in [`crate::constants`] + /// * If the pattern violates the PSK validity rule (Noise Protocol Framework Specification section 9.3) + /// * If the pattern violates the PQ token ordering rule (PQNoise paper section 2.4) + pub fn new( + name: &'static str, + pre_initiator: &[Token], + pre_responder: &[Token], + initiator: &[&[Token]], + responder: &[&[Token]], + ) -> Self { + Self::try_new(name, pre_initiator, pre_responder, initiator, responder) + .unwrap_or_else(|e| panic!("Handshake pattern error: {}", e)) + } + + /// Validate PQ token ordering rule for a party's message pattern + /// + /// The validity rule expressed by PQNoise authors is as follows: + /// + /// > Within a message ekem always precedes skem which + /// > always precedes all public keys and the payload + /// + /// # Errors + /// * [`PatternError::PqTokenOrderViolation`] if the rule is violated + pub fn validate_pq_token_order_rule( + messages: &[ArrayVec<Token, MAX_TOKENS_PER_HS_MESSAGE>], + ) -> PatternResult<()> { + for message in messages.iter() { + let mut skem_seen = false; + let mut public_key_seen = false; + for token in message.iter() { + match token { + Token::Ekem => { + if public_key_seen { + return Err(PatternError::PqTokenOrderViolation); + } + + if skem_seen { + return Err(PatternError::PqTokenOrderViolation); + } + } + Token::Skem => { + skem_seen = true; + if public_key_seen { + return Err(PatternError::PqTokenOrderViolation); + } + } + Token::E | Token::S => { + public_key_seen = true; + } + _ => {} + } + } } + + Ok(()) } - pub(crate) fn get_initiator_pattern_len(&self) -> usize { + /// Validate PSK validity rule for a party's message pattern + /// + /// The relaxed rule applied by Clatter: A party may not send any encrypted data after it processes a "psk" token + /// unless it has previously sent an "e" token or "ekem" token, either before or after the "psk" token. + /// + /// Encrypted data includes: + /// - `skem` token (encrypts its ciphertext if a key exists, then applies randomness) + /// - `s` token (encrypts static public key if a key exists) + /// + /// # Errors + /// * [`PatternError::PskValidityViolation`] if the rule is violated + pub fn validate_psk_rule( + messages: &[ArrayVec<Token, MAX_TOKENS_PER_HS_MESSAGE>], + ) -> PatternResult<()> { + let mut psk_sent = false; + for message in messages.iter() { + for token in message.iter() { + match token { + Token::Psk => { + psk_sent = true; + } + Token::E | Token::Ekem => { + // E or Ekem token applies randomness before any encryption, safe to early return + return Ok(()); + } + Token::Skem => { + if psk_sent { + return Err(PatternError::PskValidityViolation); + } else { + // If Skem comes before PSK, it satisfies the requirement of applying randomness before any encryption + return Ok(()); + } + } + Token::S => { + if psk_sent { + return Err(PatternError::PskValidityViolation); + } + + // S does not apply randomness, continue validation... + } + _ => {} + } + } + } + + Ok(()) + } + + /// Get initiator message pattern length + pub fn get_initiator_pattern_len(&self) -> usize { self.message_pattern.initiator.len() } - pub(crate) fn get_responder_pattern_len(&self) -> usize { + /// Get responder message pattern length + pub fn get_responder_pattern_len(&self) -> usize { self.message_pattern.responder.len() } @@ -170,20 +304,20 @@ impl HandshakePattern { /// /// # Panics /// Panics if message index is larger than the pattern length - pub(crate) fn get_initiator_pattern(&self, index: usize) -> &[Token] { + pub fn get_initiator_pattern(&self, index: usize) -> &[Token] { &self.message_pattern.initiator[index] } /// Get responder message pattern based on message index /// /// # Panics /// Panics if message index is larger than the pattern length - pub(crate) fn get_responder_pattern(&self, index: usize) -> &[Token] { + pub fn get_responder_pattern(&self, index: usize) -> &[Token] { &self.message_pattern.responder[index] } /// Check if the pattern includes PSKs - pub(crate) fn has_psk(&self) -> bool { + pub fn has_psk(&self) -> bool { self.has_psk } @@ -206,6 +340,10 @@ impl HandshakePattern { /// /// PSK placement is identical to the one defined in the Noise spec. To /// include PSK0 and PSK2 in a pattern, pass in `psks = [0, 2]`. + /// + /// # Panics + /// * If the resulting pattern violates the PSK validity rule (Noise Protocol Framework Specification section 9.3) + /// * If the resulting pattern violates the PQ token ordering rule (PQNoise paper section 2.4) pub fn add_psks(&self, psks: &[usize], name: &'static str) -> Self { let mut initiator = self.message_pattern.initiator.clone(); let mut responder = self.message_pattern.responder.clone(); @@ -223,17 +361,19 @@ impl HandshakePattern { } } - Self { + let initiator_slices: ArrayVec<&[Token], MAX_HS_MESSAGES_PER_ROLE> = + initiator.iter().map(|v| v.as_slice()).collect(); + let responder_slices: ArrayVec<&[Token], MAX_HS_MESSAGES_PER_ROLE> = + responder.iter().map(|v| v.as_slice()).collect(); + + Self::try_new( name, - hs_type: self.hs_type, - has_psk: true, - pre_initiator: self.pre_initiator.clone(), - pre_responder: self.pre_responder.clone(), - message_pattern: MessagePattern { - initiator, - responder, - }, - } + self.pre_initiator.as_slice(), + self.pre_responder.as_slice(), + &initiator_slices, + &responder_slices, + ) + .unwrap_or_else(|e| panic!("Handshake pattern error in add_psks: {}", e)) } } @@ -425,14 +565,6 @@ pub fn noise_pqix() -> HandshakePattern { // PQ patterns with PSKs: -/// ```text -/// -> psk, e -/// <- ekem -/// ``` -pub fn noise_pqnn_psk0() -> HandshakePattern { - noise_pqnn().add_psks(&[0], "pqNNpsk0") -} - /// ```text /// -> e /// <- ekem, psk @@ -441,16 +573,6 @@ pub fn noise_pqnn_psk2() -> HandshakePattern { noise_pqnn().add_psks(&[2], "pqNNpsk2") } -/// ```text -/// <- s -/// ... -/// -> psk, skem, e -/// <- ekem -/// ``` -pub fn noise_pqnk_psk0() -> HandshakePattern { - noise_pqnk().add_psks(&[0], "pqNKpsk0") -} - /// ```text /// <- s /// ... @@ -501,16 +623,6 @@ pub fn noise_pqxx_psk3() -> HandshakePattern { noise_pqxx().add_psks(&[3], "pqXXpsk3") } -/// ```text -/// -> s -/// ... -/// -> psk, e -/// <- ekem, skem -/// ``` -pub fn noise_pqkn_psk0() -> HandshakePattern { - noise_pqkn().add_psks(&[0], "pqKNpsk0") -} - /// ```text /// -> s /// ... @@ -521,17 +633,6 @@ pub fn noise_pqkn_psk2() -> HandshakePattern { noise_pqkn().add_psks(&[2], "pqKNpsk2") } -/// ```text -/// -> s -/// <- s -/// ... -/// -> psk, skem, e -/// <- ekem, skem -/// ``` -pub fn noise_pqkk_psk0() -> HandshakePattern { - noise_pqkk().add_psks(&[0], "pqKKpsk0") -} - /// ```text /// -> s /// <- s @@ -1022,37 +1123,37 @@ pub fn noise_ix_psk2() -> HandshakePattern { /// ```text /// -> e -/// <- e, ee, ekem +/// <- ekem, e, ee /// ``` pub fn noise_hybrid_nn() -> HandshakePattern { HandshakePattern::new( "hybridNN", &[], &[], &[&[Token::E]], - &[&[Token::E, Token::EE, Token::Ekem]], + &[&[Token::Ekem, Token::E, Token::EE]], ) } /// ```text /// <- s /// ... /// -> skem, e, es -/// <- e, ee, ekem +/// <- ekem, e, ee /// ``` pub fn noise_hybrid_nk() -> HandshakePattern { HandshakePattern::new( "hybridNK", &[], &[Token::S], &[&[Token::Skem, Token::E, Token::ES]], - &[&[Token::E, Token::EE, Token::Ekem]], + &[&[Token::Ekem, Token::E, Token::EE]], ) } /// ```text /// -> e -/// <- e, ee, ekem, s, es +/// <- ekem, e, ee, s, es /// -> skem /// ``` pub fn noise_hybrid_nx() -> HandshakePattern { @@ -1061,23 +1162,23 @@ pub fn noise_hybrid_nx() -> HandshakePattern { &[], &[], &[&[Token::E], &[Token::Skem]], - &[&[Token::E, Token::EE, Token::Ekem, Token::S, Token::ES]], + &[&[Token::Ekem, Token::E, Token::EE, Token::S, Token::ES]], ) } /// ```text /// -> s /// ... /// -> e -/// <- e, ee, se, ekem, skem +/// <- ekem, skem, e, ee, se /// ``` pub fn noise_hybrid_kn() -> HandshakePattern { HandshakePattern::new( "hybridKN", &[Token::S], &[], &[&[Token::E]], - &[&[Token::E, Token::EE, Token::SE, Token::Ekem, Token::Skem]], + &[&[Token::Ekem, Token::Skem, Token::E, Token::EE, Token::SE]], ) } @@ -1086,23 +1187,23 @@ pub fn noise_hybrid_kn() -> HandshakePattern { /// <- s /// ... /// -> skem, e, es, ss -/// <- e, ee, se, ekem, skem +/// <- ekem, skem, e, ee, se /// ``` pub fn noise_hybrid_kk() -> HandshakePattern { HandshakePattern::new( "hybridKK", &[Token::S], &[Token::S], &[&[Token::Skem, Token::E, Token::ES, Token::SS]], - &[&[Token::E, Token::EE, Token::SE, Token::Ekem, Token::Skem]], + &[&[Token::Ekem, Token::Skem, Token::E, Token::EE, Token::SE]], ) } /// ```text /// -> s /// ... /// -> e -/// <- e, ee, se, ekem, skem, s, es +/// <- ekem, skem, e, ee, se, s, es /// -> skem /// ``` pub fn noise_hybrid_kx() -> HandshakePattern { @@ -1112,11 +1213,11 @@ pub fn noise_hybrid_kx() -> HandshakePattern { &[], &[&[Token::E], &[Token::Skem]], &[&[ + Token::Ekem, + Token::Skem, Token::E, Token::EE, Token::SE, - Token::Ekem, - Token::Skem, Token::S, Token::ES, ]], @@ -1125,7 +1226,7 @@ pub fn noise_hybrid_kx() -> HandshakePattern { /// ```text /// -> e -/// <- e, ee, ekem +/// <- ekem, e, ee /// -> s, se /// <- skem /// ``` @@ -1135,15 +1236,15 @@ pub fn noise_hybrid_xn() -> HandshakePattern { &[], &[], &[&[Token::E], &[Token::S, Token::SE]], - &[&[Token::E, Token::EE, Token::Ekem], &[Token::Skem]], + &[&[Token::Ekem, Token::E, Token::EE], &[Token::Skem]], ) } /// ```text /// <- s /// ... /// -> skem, e, es -/// <- e, ee, ekem +/// <- ekem, e, ee /// -> s, se /// <- skem /// ``` @@ -1153,13 +1254,13 @@ pub fn noise_hybrid_xk() -> HandshakePattern { &[], &[Token::S], &[&[Token::Skem, Token::E, Token::ES], &[Token::S, Token::SE]], - &[&[Token::E, Token::EE, Token::Ekem], &[Token::Skem]], + &[&[Token::Ekem, Token::E, Token::EE], &[Token::Skem]], ) } /// ```text /// -> e -/// <- e, ee, ekem, s, es +/// <- ekem, e, ee, s, es /// -> skem, s, se /// <- skem /// ``` @@ -1170,45 +1271,45 @@ pub fn noise_hybrid_xx() -> HandshakePattern { &[], &[&[Token::E], &[Token::Skem, Token::S, Token::SE]], &[ - &[Token::E, Token::EE, Token::Ekem, Token::S, Token::ES], + &[Token::Ekem, Token::E, Token::EE, Token::S, Token::ES], &[Token::Skem], ], ) } /// ```text /// -> e, s -/// <- e, ee, se, ekem, skem +/// <- ekem, skem, e, ee, se /// ``` pub fn noise_hybrid_in() -> HandshakePattern { HandshakePattern::new( "hybridIN", &[], &[], &[&[Token::E, Token::S]], - &[&[Token::E, Token::EE, Token::SE, Token::Ekem, Token::Skem]], + &[&[Token::Ekem, Token::Skem, Token::E, Token::EE, Token::SE]], ) } /// ```text /// <- s /// ... /// -> skem, e, es, s, ss -/// <- e, ee, se, ekem, skem +/// <- ekem, skem, e, ee, se /// ``` pub fn noise_hybrid_ik() -> HandshakePattern { HandshakePattern::new( "hybridIK", &[], &[Token::S], &[&[Token::Skem, Token::E, Token::ES, Token::S, Token::SS]], - &[&[Token::E, Token::EE, Token::SE, Token::Ekem, Token::Skem]], + &[&[Token::Ekem, Token::Skem, Token::E, Token::EE, Token::SE]], ) } /// ```text /// -> e, s -/// <- e, ee, se, ekem, skem, s, es +/// <- ekem, skem, e, ee, se, s, es /// -> skem /// ``` pub fn noise_hybrid_ix() -> HandshakePattern { @@ -1218,11 +1319,11 @@ pub fn noise_hybrid_ix() -> HandshakePattern { &[], &[&[Token::E, Token::S], &[Token::Skem]], &[&[ + Token::Ekem, + Token::Skem, Token::E, Token::EE, Token::SE, - Token::Ekem, - Token::Skem, Token::S, Token::ES, ]], @@ -1233,15 +1334,15 @@ pub fn noise_hybrid_ix() -> HandshakePattern { /// ```text /// -> psk, e -/// <- e, ee, ekem +/// <- ekem, e, ee /// ``` pub fn noise_hybrid_nn_psk0() -> HandshakePattern { noise_hybrid_nn().add_psks(&[0], "hybridNNpsk0") } /// ```text /// -> e -/// <- e, ee, ekem, psk +/// <- ekem, e, ee, psk /// ``` pub fn noise_hybrid_nn_psk2() -> HandshakePattern { noise_hybrid_nn().add_psks(&[2], "hybridNNpsk2") @@ -1250,37 +1351,26 @@ pub fn noise_hybrid_nn_psk2() -> HandshakePattern { /// ```text /// <- s /// ... -/// -> psk, e, es, skem -/// <- e, ee, ekem -/// ``` -pub fn noise_hybrid_nk_psk0() -> HandshakePattern { - noise_hybrid_nk().add_psks(&[0], "hybridNKpsk0") -} - -/// ```text -/// <- s -/// ... -/// -> e, es, skem -/// <- e, ee, ekem, psk +/// -> skem, e, es +/// <- ekem, e, ee, psk /// ``` pub fn noise_hybrid_nk_psk2() -> HandshakePattern { noise_hybrid_nk().add_psks(&[2], "hybridNKpsk2") } /// ```text /// -> e -/// <- e, ee, ekem, s, es, psk -/// -> se, skem +/// <- ekem, e, ee, s, es, psk +/// -> skem /// ``` pub fn noise_hybrid_nx_psk2() -> HandshakePattern { noise_hybrid_nx().add_psks(&[2], "hybridNXpsk2") } /// ```text /// -> e -/// <- e, ee, ekem -/// -> s, se, skem, psk -/// <- skem +/// <- ekem, e, ee, s, es +/// -> skem, psk /// ``` pub fn noise_hybrid_xn_psk3() -> HandshakePattern { noise_hybrid_xn().add_psks(&[3], "hybridXNpsk3") @@ -1289,9 +1379,9 @@ pub fn noise_hybrid_xn_psk3() -> HandshakePattern { /// ```text /// <- s /// ... -/// -> e, es, skem -/// <- e, ee, ekem -/// -> s, se, skem, psk +/// -> skem, e, es +/// <- ekem, e, ee +/// -> s, se, psk /// <- skem /// ``` pub fn noise_hybrid_xk_psk3() -> HandshakePattern { @@ -1300,8 +1390,8 @@ pub fn noise_hybrid_xk_psk3() -> HandshakePattern { /// ```text /// -> e -/// <- e, ee, ekem, s, es -/// -> se, skem, s, ss, psk +/// <- ekem, e, ee, s, es +/// -> skem, s, se, psk /// <- skem /// ``` pub fn noise_hybrid_xx_psk3() -> HandshakePattern { @@ -1312,7 +1402,7 @@ pub fn noise_hybrid_xx_psk3() -> HandshakePattern { /// -> s /// ... /// -> psk, e -/// <- e, ee, ekem, se, skem +/// <- ekem, skem, e, ee, se /// ``` pub fn noise_hybrid_kn_psk0() -> HandshakePattern { noise_hybrid_kn().add_psks(&[0], "hybridKNpsk0") @@ -1322,7 +1412,7 @@ pub fn noise_hybrid_kn_psk0() -> HandshakePattern { /// -> s /// ... /// -> e -/// <- e, ee, ekem, se, skem, psk +/// <- ekem, skem, e, ee, se, psk /// ``` pub fn noise_hybrid_kn_psk2() -> HandshakePattern { noise_hybrid_kn().add_psks(&[2], "hybridKNpsk2") @@ -1332,19 +1422,8 @@ pub fn noise_hybrid_kn_psk2() -> HandshakePattern { /// -> s /// <- s /// ... -/// -> psk, e, es, ss, skem -/// <- e, ee, ekem, se, skem -/// ``` -pub fn noise_hybrid_kk_psk0() -> HandshakePattern { - noise_hybrid_kk().add_psks(&[0], "hybridKKpsk0") -} - -/// ```text -/// -> s -/// <- s -/// ... -/// -> e, es, ss, skem -/// <- e, ee, ekem, se, skem, psk +/// -> skem, e, es, ss +/// <- ekem, skem, e, ee, se, psk /// ``` pub fn noise_hybrid_kk_psk2() -> HandshakePattern { noise_hybrid_kk().add_psks(&[2], "hybridKKpsk2") @@ -1354,24 +1433,24 @@ pub fn noise_hybrid_kk_psk2() -> HandshakePattern { /// -> s /// ... /// -> e -/// <- e, ee, ekem, se, skem, s, es, psk -/// -> ss, skem +/// <- ekem, skem, e, ee, se, s, es, psk +/// -> skem /// ``` pub fn noise_hybrid_kx_psk2() -> HandshakePattern { noise_hybrid_kx().add_psks(&[2], "hybridKXpsk2") } /// ```text /// -> e, s, psk -/// <- e, ee, ekem, se, skem +/// <- ekem, skem, e, ee, se /// ``` pub fn noise_hybrid_in_psk1() -> HandshakePattern { noise_hybrid_in().add_psks(&[1], "hybridINpsk1") } /// ```text /// -> e, s -/// <- e, ee, ekem, se, skem, psk +/// <- ekem, skem, e, ee, se, psk /// ``` pub fn noise_hybrid_in_psk2() -> HandshakePattern { noise_hybrid_in().add_psks(&[2], "hybridINpsk2") @@ -1380,8 +1459,8 @@ pub fn noise_hybrid_in_psk2() -> HandshakePattern { /// ```text /// <- s /// ... -/// -> e, es, s, ss, skem, psk -/// <- e, ee, ekem, se, skem +/// -> skem, e, es, s, ss, psk +/// <- ekem, skem, e, ee, se /// ``` pub fn noise_hybrid_ik_psk1() -> HandshakePattern { noise_hybrid_ik().add_psks(&[1], "hybridIKpsk1") @@ -1390,17 +1469,17 @@ pub fn noise_hybrid_ik_psk1() -> HandshakePattern { /// ```text /// <- s /// ... -/// -> e, es, s, ss, skem -/// <- e, ee, ekem, se, skem, psk +/// -> skem, e, es, s, ss +/// <- ekem, skem, e, ee, se, psk /// ``` pub fn noise_hybrid_ik_psk2() -> HandshakePattern { noise_hybrid_ik().add_psks(&[2], "hybridIKpsk2") } /// ```text /// -> e, s -/// <- e, ee, ekem, se, skem, s, es, psk -/// -> ss, skem +/// <- ekem, skem, e, ee, se, s, es, psk +/// -> skem /// ``` pub fn noise_hybrid_ix_psk2() -> HandshakePattern { noise_hybrid_ix().add_psks(&[2], "hybridIXpsk2") @@ -1470,4 +1549,210 @@ mod tests { &[], ); } + + // PSK validity rule tests + #[test] + fn psk_validity_e_before_psk() { + // E before PSK is valid + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::E, Token::Psk]], + &[&[Token::E, Token::EE]], + ); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_e_after_psk() { + // E after PSK is valid + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::Psk, Token::E]], + &[&[Token::E, Token::EE]], + ); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_ekem_after_psk() { + // Ekem after PSK is valid + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::Psk, Token::Ekem]], + &[&[Token::Ekem]], + ); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_skem_before_psk() { + // Skem before PSK is valid (applies randomness before encryption) + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::Skem, Token::Psk]], + &[&[Token::Ekem]], + ); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_violation_s_after_psk() { + // S after PSK without E/Ekem is invalid + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::Psk, Token::S]], + &[&[Token::E, Token::EE]], + ); + assert!(matches!( + result, + Err(crate::error::PatternError::PskValidityViolation) + )); + } + + #[test] + fn psk_validity_violation_skem_after_psk() { + // Skem after PSK is invalid (encrypts before applying randomness) + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::Psk, Token::Skem]], + &[&[Token::Ekem]], + ); + assert!(matches!( + result, + Err(crate::error::PatternError::PskValidityViolation) + )); + } + + // PQ token ordering rule tests + #[test] + fn pq_token_order_valid() { + // Valid: ekem before skem before public keys + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::Ekem, Token::Skem, Token::E]], + &[&[Token::Ekem]], + ); + assert!(result.is_ok()); + } + + #[test] + fn pq_token_order_violation_ekem_after_skem() { + // Invalid: ekem must come before skem + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::Skem, Token::Ekem]], + &[&[Token::Ekem]], + ); + assert!(matches!( + result, + Err(crate::error::PatternError::PqTokenOrderViolation) + )); + } + + #[test] + fn pq_token_order_violation_ekem_after_public_key() { + // Invalid: ekem must come before public keys + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::E, Token::Ekem]], + &[&[Token::Ekem]], + ); + assert!(matches!( + result, + Err(crate::error::PatternError::PqTokenOrderViolation) + )); + } + + #[test] + fn pq_token_order_violation_skem_after_public_key() { + // Invalid: skem must come before public keys + let result = HandshakePattern::try_new( + "test", + &[], + &[], + &[&[Token::E, Token::Skem]], + &[&[Token::Ekem]], + ); + assert!(matches!( + result, + Err(crate::error::PatternError::PqTokenOrderViolation) + )); + } + + // Tests for add_psks validation + #[test] + #[should_panic(expected = "PSK validity rule violation")] + fn add_psks_psk_validity_violation_skem_after_psk() { + // This test verifies that add_psks validates PSK validity rule + // Creating a pattern with Skem, then adding PSK at position 0 + // should fail because PSK comes before Skem without E/Ekem + let pattern = HandshakePattern::new("test", &[], &[], &[&[Token::Skem]], &[&[Token::Ekem]]); + // Adding PSK at position 0 (first message, first token) creates [Psk, Skem] + // which violates PSK validity rule: Skem encrypts data after PSK without E/Ekem + let _ = pattern.add_psks(&[0], "invalid"); + } + + #[test] + #[should_panic(expected = "PSK validity rule violation")] + fn add_psks_psk_validity_violation_s_after_psk() { + // This test verifies that add_psks validates PSK validity rule + // Creating a pattern with S, then adding PSK at position 0 + // should fail because PSK comes before S without E/Ekem + let pattern = + HandshakePattern::new("test", &[], &[], &[&[Token::S]], &[&[Token::E, Token::EE]]); + // Adding PSK at position 0 creates [Psk, S] + // which violates PSK validity rule: S encrypts data after PSK without E/Ekem + let _ = pattern.add_psks(&[0], "invalid"); + } + + #[test] + fn add_psks_psk_validity_ok_with_e_before() { + // Valid: E before PSK + let pattern = + HandshakePattern::new("test", &[], &[], &[&[Token::E]], &[&[Token::E, Token::EE]]); + // Adding PSK at position 1 (after E) creates [E, Psk] which is valid + let result = pattern.add_psks(&[1], "valid"); + assert!(result.has_psk()); + } + + #[test] + fn add_psks_psk_validity_ok_with_e_after() { + // Valid: E after PSK + let pattern = + HandshakePattern::new("test", &[], &[], &[&[Token::E]], &[&[Token::E, Token::EE]]); + // Adding PSK at position 0 (before E) creates [Psk, E] which is valid + let result = pattern.add_psks(&[0], "valid"); + assert!(result.has_psk()); + } + + #[test] + fn add_psks_validates_full_pattern() { + // This test verifies that add_psks calls try_new which validates + // the entire pattern, including PQ token ordering if applicable. + // Since PSK placement doesn't typically affect ekem/skem ordering, + // we verify that add_psks works correctly on valid patterns. + let pattern = HandshakePattern::new("test", &[], &[], &[&[Token::Ekem]], &[&[Token::Ekem]]); + // Adding PSK should work and the resulting pattern should be valid + let result = pattern.add_psks(&[0], "valid"); + assert!(result.has_psk()); + assert_eq!(result.get_type(), HandshakeType::KEM); + } }
src/handshakestate/hybrid.rs+154 −1 modified@@ -8,7 +8,7 @@ use super::HandshakeInternals; use crate::bytearray::ByteArray; use crate::cipherstate::CipherStates; use crate::constants::{MAX_PSKS, PSK_LEN}; -use crate::error::{CipherResult, HandshakeError, HandshakeResult}; +use crate::error::{CipherResult, HandshakeError, HandshakeResult, PatternError}; use crate::handshakepattern::{HandshakePattern, HandshakeType, Token}; use crate::handshakestate::HandshakeStatus; use crate::symmetricstate::SymmetricState; @@ -369,6 +369,8 @@ where responder_pattern_index: 0, psks: ArrayVec::<[u8; PSK_LEN], MAX_PSKS>::new(), rng: RNG::default(), + psk_applied: false, + own_randomness_applied: false, }; Ok(Self { @@ -485,12 +487,17 @@ where } out[cur..cur + EKEM::PubKey::len()].copy_from_slice(e_kem_pub.as_slice()); cur += EKEM::PubKey::len(); + + // Sending E satisfies the requirement of applying self-chosen randomness + self.dh_internals.own_randomness_applied = true; } Token::S => { if self.dh_internals.s.is_none() || self.kem_s.is_none() { return Err(HandshakeError::MissingMaterial); } + self.dh_internals.psk_validity_check()?; + let has_key = self.dh_internals.symmetricstate.has_key(); // Send DH static key @@ -523,6 +530,7 @@ where } else { return Err(HandshakeError::PskMissing); } + self.dh_internals.psk_applied = true; } t @ (Token::EE | Token::ES | Token::SE | Token::SS) => { // Perform DH @@ -544,13 +552,18 @@ where self.dh_internals.symmetricstate.mix_key(ss.as_slice()); out[cur..cur + EKEM::Ct::len()].copy_from_slice(ct.as_slice()); cur += EKEM::Ct::len(); + + // Sending Ekem satisfies the requirement of applying self-chosen randomness + self.dh_internals.own_randomness_applied = true; } Token::Skem => { // Should have peer s if self.kem_rs.is_none() { return Err(HandshakeError::MissingMaterial); } + self.dh_internals.psk_validity_check()?; + let rs_kem_pk = self.kem_rs.as_ref().unwrap(); let len = if self.dh_internals.symmetricstate.has_key() { SKEM::Ct::len() + C::tag_len() @@ -568,10 +581,20 @@ where .symmetricstate .mix_key_and_hash(ss.as_slice()); cur += len; + + // Sending Skem satisfies the requirement of applying self-chosen randomness + self.dh_internals.own_randomness_applied = true; } } } + if !payload.is_empty() + && self.dh_internals.psk_applied + && !self.dh_internals.own_randomness_applied + { + return Err(PatternError::PskValidityViolation.into()); + } + self.dh_internals .symmetricstate .encrypt_and_hash(payload, &mut out[cur..out_len])?; @@ -659,6 +682,7 @@ where } else { return Err(HandshakeError::PskMissing); } + self.dh_internals.psk_applied = true; } t @ (Token::EE | Token::ES | Token::SE | Token::SS) => { // Perform DH @@ -853,3 +877,132 @@ where &mut self.dh_internals.symmetricstate } } + +#[cfg(test)] +mod tests { + use crate::crypto::cipher::ChaChaPoly; + use crate::crypto::dh::X25519; + use crate::crypto::hash::Sha256; + use crate::crypto::kem::rust_crypto_ml_kem; + use crate::error::{HandshakeError, PatternError}; + use crate::handshakepattern::{HandshakePattern, Token}; + use crate::traits::Handshaker; + + type TestKem = rust_crypto_ml_kem::MlKem1024; + + #[test] + fn psk_validity_violation_with_payload() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::Psk]], + &[&[Token::Ekem, Token::E, Token::EE]], + ); + + let params = crate::handshakestate::hybrid::HybridHandshakeParams::new(pattern, true) + .with_prologue(&[]); + let mut handshake = crate::handshakestate::hybrid::HybridHandshakeCore::< + X25519, + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(params) + .unwrap(); + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(matches!( + result, + Err(HandshakeError::Pattern(PatternError::PskValidityViolation)) + )); + } + + #[test] + fn psk_validity_ok_with_e_before_psk() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::E, Token::Psk]], + &[&[Token::Ekem, Token::E, Token::EE]], + ); + + let params = crate::handshakestate::hybrid::HybridHandshakeParams::new(pattern, true) + .with_prologue(&[]); + let mut handshake = crate::handshakestate::hybrid::HybridHandshakeCore::< + X25519, + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(params) + .unwrap(); + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_ok_with_e_after_psk() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::Psk, Token::E]], + &[&[Token::Ekem, Token::E, Token::EE]], + ); + + let params = crate::handshakestate::hybrid::HybridHandshakeParams::new(pattern, true) + .with_prologue(&[]); + let mut handshake = crate::handshakestate::hybrid::HybridHandshakeCore::< + X25519, + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(params) + .unwrap(); + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_ok_with_empty_payload() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::Psk]], + &[&[Token::Ekem, Token::E, Token::EE]], + ); + + let params = crate::handshakestate::hybrid::HybridHandshakeParams::new(pattern, true) + .with_prologue(&[]); + let mut handshake = crate::handshakestate::hybrid::HybridHandshakeCore::< + X25519, + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(params) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(&[], &mut out); + assert!(result.is_ok()); + } +}
src/handshakestate/mod.rs+11 −1 modified@@ -4,7 +4,7 @@ use zeroize::Zeroize; use crate::bytearray::ByteArray; use crate::cipherstate::CipherStates; use crate::constants::{MAX_PSKS, PSK_LEN}; -use crate::error::{CipherResult, HandshakeError, HandshakeResult}; +use crate::error::{CipherResult, HandshakeError, HandshakeResult, PatternError}; use crate::handshakepattern::{HandshakePattern, Token}; use crate::symmetricstate::SymmetricState; use crate::traits::{Cipher, Hash, Rng}; @@ -52,6 +52,8 @@ where responder_pattern_index: usize, psks: ArrayVec<[u8; PSK_LEN], MAX_PSKS>, rng: RNG, + psk_applied: bool, + own_randomness_applied: bool, } impl<C, H, RNG, K, P, EK, EP> HandshakeInternals<C, H, RNG, K, P, EK, EP> @@ -120,4 +122,12 @@ where fn push_psk(&mut self, psk: &[u8]) { self.psks.push(ByteArray::from_slice(psk)); } + + fn psk_validity_check(&self) -> HandshakeResult<()> { + if self.psk_applied && !self.own_randomness_applied { + Err(PatternError::PskValidityViolation.into()) + } else { + Ok(()) + } + } }
src/handshakestate/nq.rs+130 −1 modified@@ -8,7 +8,7 @@ use super::HandshakeInternals; use crate::bytearray::ByteArray; use crate::cipherstate::CipherStates; use crate::constants::{MAX_PSKS, PSK_LEN}; -use crate::error::{CipherResult, HandshakeError, HandshakeResult}; +use crate::error::{CipherResult, HandshakeError, HandshakeResult, PatternError}; use crate::handshakepattern::{HandshakePattern, HandshakeType, Token}; use crate::handshakestate::HandshakeStatus; use crate::symmetricstate::SymmetricState; @@ -176,6 +176,8 @@ where responder_pattern_index: 0, psks: ArrayVec::<[u8; PSK_LEN], MAX_PSKS>::new(), rng: RNG::default(), + psk_applied: false, + own_randomness_applied: false, }; let this = Self { internals }; @@ -271,12 +273,17 @@ where } out[cur..cur + DH::PubKey::len()].copy_from_slice(e_pub.as_slice()); cur += DH::PubKey::len(); + + // Sending E satisfies the requirement of applying self-chosen randomness + self.internals.own_randomness_applied = true; } Token::S => { if self.internals.s.is_none() { return Err(HandshakeError::MissingMaterial); } + self.internals.psk_validity_check()?; + let len = if self.internals.symmetricstate.has_key() { DH::PubKey::len() + C::tag_len() } else { @@ -296,6 +303,7 @@ where } else { return Err(HandshakeError::PskMissing); } + self.internals.psk_applied = true; } t @ (Token::EE | Token::ES | Token::SE | Token::SS) => { let dh_result = self.map_dh(t)?; @@ -305,6 +313,13 @@ where } } + if !payload.is_empty() + && self.internals.psk_applied + && !self.internals.own_randomness_applied + { + return Err(PatternError::PskValidityViolation.into()); + } + self.internals .symmetricstate .encrypt_and_hash(payload, &mut out[cur..out_len])?; @@ -373,6 +388,7 @@ where } else { return Err(HandshakeError::PskMissing); } + self.internals.psk_applied = true; } t @ (Token::EE | Token::ES | Token::SE | Token::SS) => { let dh_result = self.map_dh(t)?; @@ -500,3 +516,116 @@ where &mut self.internals.symmetricstate } } + +#[cfg(test)] +mod tests { + + use crate::crypto::cipher::ChaChaPoly; + use crate::crypto::dh::X25519; + use crate::crypto::hash::Sha256; + use crate::error::{HandshakeError, PatternError}; + use crate::handshakepattern::{HandshakePattern, Token}; + use crate::traits::Handshaker; + + #[test] + fn psk_validity_violation_with_payload() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::Psk]], + &[&[Token::E, Token::EE]], + ); + + let mut handshake = crate::handshakestate::nq::NqHandshakeCore::< + X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern, + &[], + true, // initiator + None, + None, + None, + None, + ) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(matches!( + result, + Err(HandshakeError::Pattern(PatternError::PskValidityViolation)) + )); + } + + #[test] + fn psk_validity_ok_with_e_before_psk() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::E, Token::Psk]], + &[&[Token::E, Token::EE]], + ); + + let mut handshake = crate::handshakestate::nq::NqHandshakeCore::< + X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern, + &[], + true, // initiator + None, + None, + None, + None, + ) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_ok_with_e_after_psk() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::Psk, Token::E]], + &[&[Token::E, Token::EE]], + ); + + let mut handshake = crate::handshakestate::nq::NqHandshakeCore::< + X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern, + &[], + true, // initiator + None, + None, + None, + None, + ) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(result.is_ok()); + } +}
src/handshakestate/pq.rs+165 −1 modified@@ -8,7 +8,7 @@ use super::HandshakeInternals; use crate::bytearray::ByteArray; use crate::cipherstate::CipherStates; use crate::constants::{MAX_PSKS, PSK_LEN}; -use crate::error::{CipherResult, HandshakeError, HandshakeResult}; +use crate::error::{CipherResult, HandshakeError, HandshakeResult, PatternError}; use crate::handshakepattern::{HandshakePattern, HandshakeType, Token}; use crate::handshakestate::HandshakeStatus; use crate::symmetricstate::SymmetricState; @@ -180,6 +180,8 @@ where responder_pattern_index: 0, psks: ArrayVec::<[u8; PSK_LEN], MAX_PSKS>::new(), rng: RNG::default(), + psk_applied: false, + own_randomness_applied: false, }; let this = Self { internals }; @@ -238,12 +240,17 @@ where } out[cur..cur + EKEM::PubKey::len()].copy_from_slice(e_pub.as_slice()); cur += EKEM::PubKey::len(); + + // Sending E satisfies the requirement of applying self-chosen randomness + self.internals.own_randomness_applied = true; } Token::S => { if self.internals.s.is_none() { return Err(HandshakeError::MissingMaterial); } + self.internals.psk_validity_check()?; + let len = if self.internals.symmetricstate.has_key() { SKEM::PubKey::len() + C::tag_len() } else { @@ -268,6 +275,7 @@ where } else { return Err(HandshakeError::PskMissing); } + self.internals.psk_applied = true; } Token::Ekem => { // Should have peer e @@ -283,13 +291,18 @@ where self.internals.symmetricstate.mix_key(ss.as_slice()); out[cur..cur + EKEM::Ct::len()].copy_from_slice(ct.as_slice()); cur += EKEM::Ct::len(); + + // Sending Ekem satisfies the requirement of applying self-chosen randomness + self.internals.own_randomness_applied = true; } Token::Skem => { // Should have peer s if self.internals.rs.is_none() { return Err(HandshakeError::MissingMaterial); } + self.internals.psk_validity_check()?; + let len = if self.internals.symmetricstate.has_key() { SKEM::Ct::len() + C::tag_len() } else { @@ -308,11 +321,21 @@ where .symmetricstate .mix_key_and_hash(ss.as_slice()); cur += len; + + // Sending Skem satisfies the requirement of applying self-chosen randomness + self.internals.own_randomness_applied = true; } _ => panic!("Incompatible pattern"), } } + if !payload.is_empty() + && self.internals.psk_applied + && !self.internals.own_randomness_applied + { + return Err(PatternError::PskValidityViolation.into()); + } + self.internals .symmetricstate .encrypt_and_hash(payload, &mut out[cur..out_len])?; @@ -377,6 +400,7 @@ where } else { return Err(HandshakeError::PskMissing); } + self.internals.psk_applied = true; } Token::Ekem => { let ct = get(EKEM::Ct::len()); @@ -557,3 +581,143 @@ where &mut self.internals.symmetricstate } } + +#[cfg(test)] +mod tests { + + use crate::crypto::cipher::ChaChaPoly; + use crate::crypto::hash::Sha256; + use crate::crypto::kem::rust_crypto_ml_kem; + use crate::error::{HandshakeError, PatternError}; + use crate::handshakepattern::{HandshakePattern, Token}; + use crate::traits::Handshaker; + + type TestKem = rust_crypto_ml_kem::MlKem1024; + + #[test] + fn psk_validity_violation_with_payload() { + let pattern = HandshakePattern::new("test", &[], &[], &[&[Token::Psk]], &[&[Token::Ekem]]); + + let mut handshake = crate::handshakestate::pq::PqHandshakeCore::< + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern, + &[], + true, // initiator + None, + None, + None, + None, + ) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(matches!( + result, + Err(HandshakeError::Pattern(PatternError::PskValidityViolation)) + )); + } + + #[test] + fn psk_validity_ok_with_e_before_psk() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::E, Token::Psk]], + &[&[Token::Ekem]], + ); + + let mut handshake = crate::handshakestate::pq::PqHandshakeCore::< + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern, + &[], + true, // initiator + None, + None, + None, + None, + ) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_ok_with_e_after_psk() { + let pattern = HandshakePattern::new( + "test", + &[], + &[], + &[&[Token::Psk, Token::E]], + &[&[Token::Ekem]], + ); + + let mut handshake = crate::handshakestate::pq::PqHandshakeCore::< + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern, + &[], + true, // initiator + None, + None, + None, + None, + ) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(b"test payload", &mut out); + assert!(result.is_ok()); + } + + #[test] + fn psk_validity_ok_with_empty_payload() { + let pattern = HandshakePattern::new("test", &[], &[], &[&[Token::Psk]], &[&[Token::Ekem]]); + + let mut handshake = crate::handshakestate::pq::PqHandshakeCore::< + TestKem, + TestKem, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern, + &[], + true, // initiator + None, + None, + None, + None, + ) + .unwrap(); + + handshake.push_psk(&[0u8; 32]); + + let mut out = [0u8; 2048]; + let result = handshake.write_message(&[], &mut out); + assert!(result.is_ok()); + } +}
src/transportstate.rs+292 −0 modified@@ -351,3 +351,295 @@ impl<C: Cipher, H: Hash> TransportState<C, H> { self.cipherstates } } + +#[cfg(test)] +mod tests { + use super::TransportState; + use crate::crypto::cipher::ChaChaPoly; + use crate::crypto::hash::Sha256; + use crate::error::{HandshakeError, TransportError}; + use crate::handshakepattern::noise_nn; + use crate::handshakestate::nq::NqHandshakeCore; + use crate::traits::{Dh, Handshaker}; + + fn create_transport_states() -> ( + TransportState<ChaChaPoly, Sha256>, + TransportState<ChaChaPoly, Sha256>, + ) { + let pattern = noise_nn(); + let mut alice = NqHandshakeCore::< + crate::crypto::dh::X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(pattern.clone(), &[], true, None, None, None, None) + .unwrap(); + let mut bob = NqHandshakeCore::< + crate::crypto::dh::X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(pattern, &[], false, None, None, None, None) + .unwrap(); + + let mut alice_buf = [0u8; 2048]; + let mut bob_buf = [0u8; 2048]; + + // Complete handshake + let n = alice.write_message(&[], &mut alice_buf).unwrap(); + let _ = bob.read_message(&alice_buf[..n], &mut bob_buf).unwrap(); + let n = bob.write_message(&[], &mut bob_buf).unwrap(); + let _ = alice.read_message(&bob_buf[..n], &mut alice_buf).unwrap(); + + let alice_transport = alice.finalize().unwrap(); + let bob_transport = bob.finalize().unwrap(); + + (alice_transport, bob_transport) + } + + #[test] + fn transport_basic_send_receive() { + let (mut alice, mut bob) = create_transport_states(); + + let msg = b"Hello, world!"; + let mut send_buf = [0u8; 2048]; + let mut recv_buf = [0u8; 2048]; + + // Alice sends to Bob + let n = alice.send(msg, &mut send_buf).unwrap(); + let m = bob.receive(&send_buf[..n], &mut recv_buf).unwrap(); + assert_eq!(&recv_buf[..m], msg); + + // Bob sends to Alice + let n = bob.send(msg, &mut send_buf).unwrap(); + let m = alice.receive(&send_buf[..n], &mut recv_buf).unwrap(); + assert_eq!(&recv_buf[..m], msg); + } + + #[test] + fn transport_buffer_too_small_send() { + let (mut alice, _) = create_transport_states(); + + let msg = b"test message"; + let mut buf = [0u8; 5]; // Too small + + let result = alice.send(msg, &mut buf); + assert!(matches!(result, Err(TransportError::BufferTooSmall))); + } + + #[test] + fn transport_buffer_too_small_receive() { + let (mut alice, mut bob) = create_transport_states(); + + let msg = b"test message"; + let mut send_buf = [0u8; 2048]; + let mut recv_buf = [0u8; 5]; // Too small + + let n = alice.send(msg, &mut send_buf).unwrap(); + let result = bob.receive(&send_buf[..n], &mut recv_buf); + assert!(matches!(result, Err(TransportError::BufferTooSmall))); + } + + #[test] + fn transport_too_short_receive() { + let (_, mut bob) = create_transport_states(); + + let mut recv_buf = [0u8; 2048]; + let short_msg = [0u8; 10]; // Too short (less than tag_len) + + let result = bob.receive(&short_msg, &mut recv_buf); + assert!(matches!(result, Err(TransportError::TooShort))); + } + + #[test] + fn transport_invalid_state_from_unfinished_handshake() { + let pattern = noise_nn(); + let alice = NqHandshakeCore::< + crate::crypto::dh::X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(pattern, &[], true, None, None, None, None) + .unwrap(); + + // Try to finalize before handshake is complete + let result = alice.finalize(); + assert!(matches!(result, Err(HandshakeError::InvalidState))); + } + + #[test] + fn transport_one_way_violation_responder_send() { + // Create a one-way handshake (N pattern) + // N pattern: <- s (responder's static key is known to initiator) + let pattern = crate::handshakepattern::noise_n(); + let mut rng = crate::crypto::rng::DefaultRng::default(); + let bob_static = crate::crypto::dh::X25519::genkey_rng(&mut rng).unwrap(); + let bob_static_pub = bob_static.public.clone(); + let mut alice = NqHandshakeCore::< + crate::crypto::dh::X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern.clone(), + &[], + true, + None, + None, + Some(bob_static_pub), + None, + ) + .unwrap(); + let mut bob = NqHandshakeCore::< + crate::crypto::dh::X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(pattern, &[], false, Some(bob_static), None, None, None) + .unwrap(); + + let mut alice_buf = [0u8; 2048]; + let mut bob_buf = [0u8; 2048]; + + // Complete one-way handshake + let n = alice.write_message(&[], &mut alice_buf).unwrap(); + let _ = bob.read_message(&alice_buf[..n], &mut bob_buf).unwrap(); + + let mut bob_transport = bob.finalize().unwrap(); + + // Bob (responder) should not be able to send in one-way handshake + let msg = b"test"; + let mut buf = [0u8; 2048]; + let result = bob_transport.send(msg, &mut buf); + assert!(matches!(result, Err(TransportError::OneWayViolation))); + } + + #[test] + fn transport_one_way_violation_initiator_receive() { + // Create a one-way handshake (N pattern) + // N pattern: <- s (responder's static key is known to initiator) + let pattern = crate::handshakepattern::noise_n(); + let mut rng = crate::crypto::rng::DefaultRng::default(); + let bob_static = crate::crypto::dh::X25519::genkey_rng(&mut rng).unwrap(); + let bob_static_pub = bob_static.public.clone(); + let mut alice = NqHandshakeCore::< + crate::crypto::dh::X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new( + pattern.clone(), + &[], + true, + None, + None, + Some(bob_static_pub), + None, + ) + .unwrap(); + let mut bob = NqHandshakeCore::< + crate::crypto::dh::X25519, + ChaChaPoly, + Sha256, + crate::crypto::rng::DefaultRng, + >::new(pattern, &[], false, Some(bob_static), None, None, None) + .unwrap(); + + let mut alice_buf = [0u8; 2048]; + let mut bob_buf = [0u8; 2048]; + + // Complete one-way handshake + let n = alice.write_message(&[], &mut alice_buf).unwrap(); + let _ = bob.read_message(&alice_buf[..n], &mut bob_buf).unwrap(); + + let mut alice_transport = alice.finalize().unwrap(); + + // Alice (initiator) should not be able to receive in one-way handshake + let mut buf = [0u8; 2048]; + let fake_msg = [0u8; 32]; // Fake encrypted message + let result = alice_transport.receive(&fake_msg, &mut buf); + assert!(matches!(result, Err(TransportError::OneWayViolation))); + } + + #[test] + fn transport_nonce_management() { + let (mut alice, mut bob) = create_transport_states(); + + // Check initial nonces + assert_eq!(alice.sending_nonce(), 0); + assert_eq!(alice.receiving_nonce(), 0); + assert_eq!(bob.sending_nonce(), 0); + assert_eq!(bob.receiving_nonce(), 0); + + // Send/receive messages to increment nonces + let msg = b"test"; + let mut send_buf = [0u8; 2048]; + let mut recv_buf = [0u8; 2048]; + + let n = alice.send(msg, &mut send_buf).unwrap(); + bob.receive(&send_buf[..n], &mut recv_buf).unwrap(); + + // Nonces should have incremented + assert_eq!(alice.sending_nonce(), 1); + assert_eq!(bob.receiving_nonce(), 1); + + // Test nonce setting + alice.set_receiving_nonce(42); + assert_eq!(alice.receiving_nonce(), 42); + } + + #[test] + fn transport_rekey() { + let (mut alice, mut bob) = create_transport_states(); + + let msg = b"test"; + let mut send_buf = [0u8; 2048]; + let mut recv_buf = [0u8; 2048]; + + // Send a message + let n = alice.send(msg, &mut send_buf).unwrap(); + bob.receive(&send_buf[..n], &mut recv_buf).unwrap(); + + // Rekey sender + alice.rekey_sender().unwrap(); + bob.rekey_receiver().unwrap(); + + // Should still be able to communicate after rekey + let n = alice.send(msg, &mut send_buf).unwrap(); + let m = bob.receive(&send_buf[..n], &mut recv_buf).unwrap(); + assert_eq!(&recv_buf[..m], msg); + } + + #[test] + fn transport_send_in_place() { + let (mut alice, mut bob) = create_transport_states(); + + let mut msg = [0u8; 2048]; + msg[..13].copy_from_slice(b"Hello, world!"); + let msg_len = 13; + + let mut recv_buf = [0u8; 2048]; + + // Send in-place + let n = alice.send_in_place(&mut msg, msg_len).unwrap(); + let m = bob.receive(&msg[..n], &mut recv_buf).unwrap(); + assert_eq!(&recv_buf[..m], b"Hello, world!"); + } + + #[test] + fn transport_receive_in_place() { + let (mut alice, mut bob) = create_transport_states(); + + let msg = b"Hello, world!"; + let mut send_buf = [0u8; 2048]; + let mut recv_buf = [0u8; 2048]; + + // Send normally + let n = alice.send(msg, &mut send_buf).unwrap(); + recv_buf[..n].copy_from_slice(&send_buf[..n]); + + // Receive in-place + let m = bob.receive_in_place(&mut recv_buf, n).unwrap(); + assert_eq!(&recv_buf[..m], msg); + } +}
tests/src/lib.rs+0 −6 modified@@ -69,14 +69,10 @@ pub fn pq_handshake_patterns() -> Vec<HandshakePattern> { noise_pqin_psk1(), noise_pqin_psk2(), noise_pqix_psk2(), - noise_pqkk_psk0(), noise_pqkk_psk2(), - noise_pqkn_psk0(), noise_pqkn_psk2(), noise_pqkx_psk2(), - noise_pqnk_psk0(), noise_pqnk_psk2(), - noise_pqnn_psk0(), noise_pqnn_psk2(), noise_pqnx_psk2(), noise_pqxk_psk3(), @@ -104,12 +100,10 @@ pub fn hybrid_handshake_patterns() -> Vec<HandshakePattern> { noise_hybrid_in_psk1(), noise_hybrid_in_psk2(), noise_hybrid_ix_psk2(), - noise_hybrid_kk_psk0(), noise_hybrid_kk_psk2(), noise_hybrid_kn_psk0(), noise_hybrid_kn_psk2(), noise_hybrid_kx_psk2(), - noise_hybrid_nk_psk0(), noise_hybrid_nk_psk2(), noise_hybrid_nn_psk0(), noise_hybrid_nn_psk2(),
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-253q-9q78-63x4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-24785ghsaADVISORY
- github.com/jmlepisto/clatter/commit/b65ae6e9b8019bed5407771e21f89ddff17c5a71ghsax_refsource_MISCWEB
- github.com/jmlepisto/clatter/security/advisories/GHSA-253q-9q78-63x4ghsax_refsource_CONFIRMWEB
- noiseprotocol.org/noise.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.