@@ -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 ] ,
0 commit comments