Visitar URL original
reject a second ClientHello that changes the cipher suite · rustls/rustls@185a063 · GitHub
Skip to content

Commit 185a063

Browse files
oleksii-udovychenkoctz
authored andcommitted
reject a second ClientHello that changes the cipher suite
1 parent 9fafe6d commit 185a063

4 files changed

Lines changed: 86 additions & 1 deletion

File tree

‎rustls/src/error.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ impl From<InvalidMessage> for AlertDescription {
237237
pub enum PeerMisbehaved {
238238
AttemptedDowngradeToTls12WhenTls13IsSupported,
239239
BadCertChainExtensions,
240+
CipherSuiteDifferedOnRetry,
240241
DisallowedEncryptedExtension,
241242
DuplicateClientHelloExtensions,
242243
DuplicateEncryptedExtensions,

‎rustls/src/server/hs.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ pub(super) struct ExpectClientHello {
315315
pub(super) using_ems: bool,
316316
pub(super) done_retry: bool,
317317
pub(super) offered_psk_before_retry: bool,
318+
pub(super) suite_before_retry: Option<CipherSuite>,
318319
pub(super) send_tickets: usize,
319320
}
320321

@@ -339,6 +340,7 @@ impl ExpectClientHello {
339340
using_ems: false,
340341
done_retry: false,
341342
offered_psk_before_retry: false,
343+
suite_before_retry: None,
342344
send_tickets: 0,
343345
}
344346
}
@@ -472,6 +474,17 @@ impl ExpectClientHello {
472474
.send_fatal_alert(AlertDescription::HandshakeFailure, incompat)
473475
})?;
474476

477+
// RFC 9846 section 4.2.4: the server must negotiate the same cipher suite it
478+
// named in its HelloRetryRequest
479+
if let Some(before_retry) = self.suite_before_retry {
480+
if before_retry != suite.suite() {
481+
return Err(cx.common.send_fatal_alert(
482+
AlertDescription::IllegalParameter,
483+
PeerMisbehaved::CipherSuiteDifferedOnRetry,
484+
));
485+
}
486+
}
487+
475488
debug!("decided upon suite {suite:?}");
476489
cx.common.suite = Some(suite);
477490
cx.common.kx_state = KxState::Start(skxg);

‎rustls/src/server/test.rs‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ mod tests {
7878
use crate::server::{AlwaysResolvesServerRawPublicKeys, ServerConfig, ServerConnection};
7979
use crate::sign::CertifiedKey;
8080
use crate::sync::Arc;
81-
use crate::{CipherSuiteCommon, SupportedCipherSuite, Tls12CipherSuite, version};
81+
use crate::{
82+
CipherSuiteCommon, SupportedCipherSuite, Tls12CipherSuite, Tls13CipherSuite, version,
83+
};
8284

8385
#[cfg(feature = "tls12")]
8486
#[test]
@@ -295,6 +297,74 @@ mod tests {
295297
);
296298
}
297299

300+
#[test]
301+
fn second_client_hello_cannot_change_cipher_suite() {
302+
// RFC 9846 section 4.2.4 requires the server to negotiate the same cipher suite it
303+
// named in its HelloRetryRequest, and section 4.2.2 does not let the client vary its
304+
// offer, so a second hello that withdraws the retried suite cannot be honoured.
305+
let provider = CryptoProvider {
306+
cipher_suites: vec![
307+
super::provider::cipher_suite::TLS13_AES_128_GCM_SHA256,
308+
TLS13_AES_128_GCM_SHA256_ALT,
309+
],
310+
..super::provider::default_provider()
311+
};
312+
let config = ServerConfig::builder_with_provider(provider.into())
313+
.with_protocol_versions(&[&version::TLS13])
314+
.unwrap()
315+
.with_no_client_auth()
316+
.with_single_cert(server_cert(), server_key())
317+
.unwrap();
318+
let mut conn = ServerConnection::new(config.into()).unwrap();
319+
320+
let encode = |hello| {
321+
Message {
322+
version: ProtocolVersion::TLSv1_3,
323+
payload: MessagePayload::handshake(HandshakeMessagePayload(
324+
HandshakePayload::ClientHello(hello),
325+
)),
326+
}
327+
.into_wire_bytes()
328+
};
329+
330+
// this hello has no key share for a group we support, so it draws a
331+
// HelloRetryRequest naming `TLS13_AES_128_GCM_SHA256`.
332+
let mut first = minimal_client_hello();
333+
first.cipher_suites = vec![CipherSuite::TLS13_AES_128_GCM_SHA256];
334+
first.extensions.key_shares = Some(vec![]);
335+
conn.read_tls(&mut encode(first).as_slice())
336+
.unwrap();
337+
conn.process_new_packets().unwrap();
338+
339+
// the second hello follows the retry, but offers only a different suite. It shares
340+
// the retried suite's hash, so the transcript stays valid and nothing else objects.
341+
let mut second = minimal_client_hello();
342+
second.cipher_suites = vec![TLS13_AES_128_GCM_SHA256_ALT.suite()];
343+
conn.read_tls(&mut encode(second).as_slice())
344+
.unwrap();
345+
346+
assert_eq!(
347+
conn.process_new_packets().unwrap_err(),
348+
PeerMisbehaved::CipherSuiteDifferedOnRetry.into(),
349+
);
350+
}
351+
352+
static TLS13_AES_128_GCM_SHA256_ALT: SupportedCipherSuite =
353+
SupportedCipherSuite::Tls13(&TLS13_AES_128_GCM_SHA256_ALT_INNER);
354+
355+
/// Differs from `TLS13_AES_128_GCM_SHA256` only in its code point: same hash, same everything else.
356+
static TLS13_AES_128_GCM_SHA256_ALT_INNER: Tls13CipherSuite =
357+
match &super::provider::cipher_suite::TLS13_AES_128_GCM_SHA256 {
358+
SupportedCipherSuite::Tls13(provider) => Tls13CipherSuite {
359+
common: CipherSuiteCommon {
360+
suite: CipherSuite::Unknown(0xff14),
361+
..provider.common
362+
},
363+
..**provider
364+
},
365+
_ => unreachable!(),
366+
};
367+
298368
fn server_config_for_rpk() -> ServerConfig {
299369
let x25519_provider = CryptoProvider {
300370
kx_groups: vec![super::provider::kx_group::X25519],

‎rustls/src/server/tls13.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ mod client_hello {
255255
offered_psk_before_retry: client_hello
256256
.preshared_key_offer
257257
.is_some(),
258+
suite_before_retry: Some(self.suite.common.suite),
258259
send_tickets: self.send_tickets,
259260
extra_exts: self.extra_exts,
260261
});

0 commit comments

Comments
 (0)