Skip to content

Commit 2a8313f

Browse files
authored
tests(integration): cases for TLS 1.3 group selection (#5652)
1 parent f6ca8f0 commit 2a8313f

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

bindings/rust/standard/integration/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ tokio = { version = "1", features = ["macros", "test-util"] }
3232
tokio-openssl = { version = "0.6.5" }
3333
rustls = "0.23"
3434

35+
brass-aphid-wire-decryption = "0.0.1"
36+
brass-aphid-wire-messages = "0.0.1"
37+
3538
tracing = "0.1"
3639
tracing-subscriber = "0.3"
3740
test-log = { version = "0.2", default-features = false, features = ["trace"]}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! These integration tests look at our group negotiation logic.
5+
//!
6+
//! s2n-tls has some non-standard negotiation behaviors, and will prefer to negotiate
7+
//! PQ where possible (even at the cost of an RTT) and also has a "strongly preferred
8+
//! groups" feature that serves as a blunt instrument to force clients to negotiate
9+
//! a particular group whenever possible.
10+
11+
use std::sync::LazyLock;
12+
13+
use brass_aphid_wire_decryption::decryption::key_manager::KeyManager;
14+
use brass_aphid_wire_messages::iana;
15+
use openssl::ssl::SslContextBuilder;
16+
use s2n_tls::security::Policy;
17+
use tls_harness::{
18+
cohort::{OpenSslConnection, S2NConnection},
19+
harness::TlsConfigBuilderPair,
20+
TlsConnPair,
21+
};
22+
23+
use crate::capability_check::{required_capability, Capability};
24+
25+
struct Trial {
26+
client_group_configuration: String,
27+
server_policy: &'static Policy,
28+
}
29+
30+
impl Trial {
31+
/// * `client_groups`: groups to set on the openssl client, e.g. "SecP384r1MLKEM1024:x25519"
32+
/// * `server_policy`: the s2n-tls security policy set on the server, e.g. "20251014"
33+
fn new(client_groups: String, server_policy: &'static Policy) -> Self {
34+
Self {
35+
client_group_configuration: client_groups,
36+
server_policy,
37+
}
38+
}
39+
}
40+
41+
#[derive(Debug)]
42+
struct Outcome {
43+
/// The key shares from the first client hello
44+
client_key_shares: Vec<iana::Group>,
45+
server_selected_group: iana::Group,
46+
hello_retry_request: bool,
47+
}
48+
49+
/// supported KEMS -> [X25519MLKEM768, Secp256r1MLKEM768, Secp384r1MLKEM1024]
50+
/// supported curves -> [secp256r1, x25519, secp384r1, secp521r1]
51+
static PQ_ENABLED_POLICY: LazyLock<s2n_tls::security::Policy> =
52+
LazyLock::new(|| Policy::from_version("20251014").unwrap());
53+
54+
/// supported KEMS -> []
55+
/// strongly preferred groups -> [secp384r1]
56+
/// supported curves -> [secp384r1, secp256r1, secp521r1]
57+
static STRONGLY_PREFERRED_GROUPS: LazyLock<s2n_tls::security::Policy> =
58+
LazyLock::new(|| Policy::from_version("20251117").unwrap());
59+
60+
impl Trial {
61+
fn handshake(&self) -> Outcome {
62+
let key_manager = KeyManager::new();
63+
let mut pair: TlsConnPair<OpenSslConnection, S2NConnection> = {
64+
let mut configs =
65+
TlsConfigBuilderPair::<SslContextBuilder, s2n_tls::config::Builder>::default();
66+
configs
67+
.server
68+
.set_security_policy(self.server_policy)
69+
.unwrap();
70+
key_manager.enable_s2n_logging(&mut configs.server);
71+
configs
72+
.client
73+
.set_groups_list(&self.client_group_configuration)
74+
.unwrap();
75+
configs.connection_pair()
76+
};
77+
pair.io.enable_recording();
78+
pair.io.enable_decryption(key_manager.clone());
79+
80+
pair.handshake().unwrap();
81+
pair.shutdown().unwrap();
82+
83+
let decrypted_stream = pair.io.decrypter.borrow();
84+
let transcript = decrypted_stream.as_ref().unwrap().transcript();
85+
86+
let client_hello = transcript.client_hellos().first().unwrap().clone();
87+
let key_shares = client_hello.key_share().unwrap();
88+
89+
let server_hello = transcript.server_hello();
90+
Outcome {
91+
client_key_shares: key_shares,
92+
server_selected_group: server_hello.selected_group().unwrap().unwrap(),
93+
hello_retry_request: transcript.hello_retry_request().is_some(),
94+
}
95+
}
96+
}
97+
98+
/// Classical Key Share Preference:
99+
///
100+
/// As long as the client key share is supported (but not necessarily preferred)
101+
/// by the server then it will be selected, and there will be no HRR.
102+
#[test]
103+
fn classical_group_selection() {
104+
required_capability(&[Capability::Tls13], || {
105+
let trial = Trial::new(
106+
"secp256r1:secp384r1:secp521r1".to_owned(),
107+
&PQ_ENABLED_POLICY,
108+
);
109+
let outcome = trial.handshake();
110+
assert_eq!(outcome.client_key_shares, vec![iana::constants::secp256r1]);
111+
assert_eq!(outcome.server_selected_group, iana::constants::secp256r1);
112+
assert!(!outcome.hello_retry_request);
113+
114+
let trial = Trial::new(
115+
"secp384r1:secp521r1:secp256r1".to_owned(),
116+
&PQ_ENABLED_POLICY,
117+
);
118+
let outcome = trial.handshake();
119+
assert_eq!(outcome.client_key_shares, vec![iana::constants::secp384r1]);
120+
assert_eq!(outcome.server_selected_group, iana::constants::secp384r1);
121+
assert!(!outcome.hello_retry_request);
122+
});
123+
}
124+
125+
/// PQ Key Share Preference:
126+
///
127+
/// When the client and server both support PQ but the client didn't send a key share
128+
/// for its PQ algorithm, then we will force an additional round trip to negotiate PQ.
129+
#[test]
130+
fn pq_group_selection() {
131+
required_capability(&[Capability::Tls13, Capability::MLKem], || {
132+
let trial = Trial::new("secp384r1:X25519MLKEM768".to_owned(), &PQ_ENABLED_POLICY);
133+
let outcome = trial.handshake();
134+
assert_eq!(outcome.client_key_shares, vec![iana::constants::secp384r1]);
135+
assert_eq!(
136+
outcome.server_selected_group,
137+
iana::constants::X25519MLKEM768
138+
);
139+
assert!(outcome.hello_retry_request);
140+
});
141+
}
142+
143+
/// Strongly Preferred Groups:
144+
///
145+
/// If the server's strongly preferred group is in the client's supported groups,
146+
/// then the strongly preferred group will be negotiated even at the cost of an HRR.
147+
///
148+
/// Otherwise normal group negotiation logic applies
149+
#[test]
150+
fn strongly_preferred_groups() {
151+
required_capability(&[Capability::Tls13], || {
152+
// happy path: strongly preferred group is client key share
153+
let trial = Trial::new("secp384r1:secp256r1".to_owned(), &STRONGLY_PREFERRED_GROUPS);
154+
let outcome = trial.handshake();
155+
assert_eq!(outcome.client_key_shares, vec![iana::constants::secp384r1]);
156+
assert_eq!(outcome.server_selected_group, iana::constants::secp384r1);
157+
assert!(!outcome.hello_retry_request);
158+
159+
// forced negotiation of strongly preferred group
160+
let trial = Trial::new("secp256r1:secp384r1".to_owned(), &STRONGLY_PREFERRED_GROUPS);
161+
let outcome = trial.handshake();
162+
assert_eq!(outcome.client_key_shares, vec![iana::constants::secp256r1]);
163+
assert_eq!(outcome.server_selected_group, iana::constants::secp384r1);
164+
assert!(outcome.hello_retry_request);
165+
166+
// client doesn't support strongly preferred group: 1RTT negotiation
167+
let trial = Trial::new("secp256r1:x448".to_owned(), &STRONGLY_PREFERRED_GROUPS);
168+
let outcome = trial.handshake();
169+
assert_eq!(outcome.client_key_shares, vec![iana::constants::secp256r1]);
170+
assert_eq!(outcome.server_selected_group, iana::constants::secp256r1);
171+
assert!(!outcome.hello_retry_request);
172+
173+
// client doesn't support strongly preferred group: HRR negotiation
174+
let trial = Trial::new("x448:secp256r1".to_owned(), &STRONGLY_PREFERRED_GROUPS);
175+
let outcome = trial.handshake();
176+
assert_eq!(outcome.client_key_shares, vec![iana::constants::x448]);
177+
assert_eq!(outcome.server_selected_group, iana::constants::secp256r1);
178+
assert!(outcome.hello_retry_request);
179+
});
180+
}

bindings/rust/standard/integration/src/features/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
mod dynamic_record_sizing;
5+
mod group_negotiation;
56
#[cfg(feature = "pq")]
67
mod pq;
78
mod record_padding;

0 commit comments

Comments
 (0)