Skip to content

Commit 6c8e756

Browse files
complexspacesmcginty
authored andcommitted
Fix nonce incrementing in stateful transport to match the specification
1 parent 3b33f0a commit 6c8e756

File tree

3 files changed

+93
-4
lines changed

3 files changed

+93
-4
lines changed

src/cipherstate.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ impl CipherState {
3535
return Err(StateProblem::MissingKeyMaterial.into());
3636
}
3737

38+
validate_nonce(self.n)?;
3839
let len = self.cipher.encrypt(self.n, authtext, plaintext, out);
39-
self.n = self.n.checked_add(1).ok_or(StateProblem::Exhausted)?;
40+
41+
// We have validated this will not wrap around.
42+
self.n += 1;
4043

4144
Ok(len)
4245
}
@@ -55,8 +58,11 @@ impl CipherState {
5558
return Err(StateProblem::MissingKeyMaterial.into());
5659
}
5760

61+
validate_nonce(self.n)?;
5862
let len = self.cipher.decrypt(self.n, authtext, ciphertext, out);
59-
self.n = self.n.checked_add(1).ok_or(StateProblem::Exhausted)?;
63+
64+
// We have validated this will not wrap around.
65+
self.n += 1;
6066

6167
len
6268
}
@@ -131,6 +137,8 @@ impl StatelessCipherState {
131137
return Err(StateProblem::MissingKeyMaterial.into());
132138
}
133139

140+
validate_nonce(nonce)?;
141+
134142
Ok(self.cipher.encrypt(nonce, authtext, plaintext, out))
135143
}
136144

@@ -149,6 +157,8 @@ impl StatelessCipherState {
149157
return Err(StateProblem::MissingKeyMaterial.into());
150158
}
151159

160+
validate_nonce(nonce)?;
161+
152162
self.cipher.decrypt(nonce, authtext, ciphertext, out)
153163
}
154164

@@ -169,6 +179,19 @@ impl StatelessCipherState {
169179
}
170180
}
171181

182+
/// Validates that a nonce value has not exceeded the maximum
183+
/// defined by the Noise spec.
184+
fn validate_nonce(current: u64) -> Result<(), Error> {
185+
// 2^64-1 is reserved and may not be used in the state machine (5.1).
186+
//
187+
// It is used by the default cipher rekey function (4.2).
188+
if current == u64::MAX {
189+
Err(Error::State(StateProblem::Exhausted))
190+
} else {
191+
Ok(())
192+
}
193+
}
194+
172195
impl From<CipherState> for StatelessCipherState {
173196
fn from(other: CipherState) -> Self {
174197
Self { cipher: other.cipher, has_key: other.has_key }

src/types.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ pub trait Cipher: Send + Sync {
6060
/// implementation guaranteed to be secure for all ciphers.
6161
fn rekey(&mut self) {
6262
let mut ciphertext = [0; CIPHERKEYLEN + TAGLEN];
63-
let ciphertext_len =
64-
self.encrypt(u64::max_value(), &[], &[0; CIPHERKEYLEN], &mut ciphertext);
63+
let ciphertext_len = self.encrypt(u64::MAX, &[], &[0; CIPHERKEYLEN], &mut ciphertext);
6564
assert_eq!(ciphertext_len, ciphertext.len());
6665
self.set(&ciphertext[..CIPHERKEYLEN]);
6766
}

tests/general.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,3 +803,70 @@ fn test_handshake_read_oob_error() {
803803
// This shouldn't panic, but it *should* return an error.
804804
let _ = h_i.read_message(&buffer_msg[..len], &mut buffer_out);
805805
}
806+
807+
#[test]
808+
fn test_stateful_nonce_maxiumum_behavior() {
809+
let params: NoiseParams = "Noise_NN_25519_ChaChaPoly_SHA256".parse().unwrap();
810+
let mut h_i = Builder::new(params.clone()).build_initiator().unwrap();
811+
let mut h_r = Builder::new(params).build_responder().unwrap();
812+
813+
let mut buffer_msg = [0u8; 200];
814+
let mut buffer_out = [0u8; 200];
815+
let len = h_i.write_message(b"abc", &mut buffer_msg).unwrap();
816+
h_r.read_message(&buffer_msg[..len], &mut buffer_out).unwrap();
817+
818+
let len = h_r.write_message(b"defg", &mut buffer_msg).unwrap();
819+
h_i.read_message(&buffer_msg[..len], &mut buffer_out).unwrap();
820+
821+
let h_i = h_i.into_stateless_transport_mode().unwrap();
822+
let mut h_r = h_r.into_transport_mode().unwrap();
823+
824+
let mut sender_nonce = u64::MAX - 2;
825+
let len = h_i.write_message(sender_nonce, b"xyz", &mut buffer_msg).unwrap();
826+
827+
h_r.set_receiving_nonce(sender_nonce);
828+
h_r.read_message(&buffer_msg[..len], &mut buffer_out).unwrap();
829+
830+
// Simulate exhausting the nonce space for the stateful transport.
831+
sender_nonce += 1;
832+
let len = h_i.write_message(sender_nonce, b"abc", &mut buffer_msg).unwrap();
833+
834+
h_r.set_receiving_nonce(sender_nonce + 1); // u64::MAX
835+
836+
// This should fail because we've simulated exhausting the nonce space, as the spec says 2^64-1 is reserved
837+
// and may not be used in the `CipherState` object.
838+
assert!(matches!(
839+
dbg!(h_r.read_message(&buffer_msg[..len], &mut buffer_out)),
840+
Err(snow::Error::State(snow::error::StateProblem::Exhausted))
841+
));
842+
}
843+
844+
#[test]
845+
fn test_stateless_nonce_maximum_behavior() {
846+
let params: NoiseParams = "Noise_NN_25519_ChaChaPoly_SHA256".parse().unwrap();
847+
let mut h_i = Builder::new(params.clone()).build_initiator().unwrap();
848+
let mut h_r = Builder::new(params).build_responder().unwrap();
849+
850+
let mut buffer_msg = [0u8; 200];
851+
let mut buffer_out = [0u8; 200];
852+
let len = h_i.write_message(b"abc", &mut buffer_msg).unwrap();
853+
h_r.read_message(&buffer_msg[..len], &mut buffer_out).unwrap();
854+
855+
let len = h_r.write_message(b"defg", &mut buffer_msg).unwrap();
856+
h_i.read_message(&buffer_msg[..len], &mut buffer_out).unwrap();
857+
858+
let h_i = h_i.into_stateless_transport_mode().unwrap();
859+
let h_r = h_r.into_stateless_transport_mode().unwrap();
860+
861+
let max_nonce = u64::MAX;
862+
863+
assert!(matches!(
864+
h_i.write_message(max_nonce, b"xyz", &mut buffer_msg),
865+
Err(snow::Error::State(snow::error::StateProblem::Exhausted))
866+
));
867+
868+
assert!(matches!(
869+
h_r.read_message(max_nonce, &buffer_msg, &mut buffer_out),
870+
Err(snow::Error::State(snow::error::StateProblem::Exhausted))
871+
));
872+
}

0 commit comments

Comments
 (0)