Skip to content

Commit fc99bd5

Browse files
feat: expose the negotiated TLS version via TlsInfo (#3067)
`TlsInfo` already carried the peer certificate and was already gated behind `ClientBuilder::tls_info(true)`, but the negotiated protocol version -- which the rustls backend has in hand at the point `TlsInfo` is built -- was never surfaced. `Response::version()` answers "which HTTP version?"; there was no equivalent for TLS. Add a `version` field to `TlsInfo` with a `version()` accessor, and populate it in the rustls `TlsInfoFactory` impls from `CommonState::protocol_version()`, translated through the existing `Version::from_rustls`. native-tls exposes no accessor for the negotiated version, so it reports `None` -- the same "`Some` where the backend can report it, `None` otherwise" contract `peer_certificate()` already has. Closes #3066
1 parent 26394b4 commit fc99bd5

3 files changed

Lines changed: 135 additions & 31 deletions

File tree

src/connect.rs

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,10 @@ impl TlsInfoFactory for tokio_native_tls::TlsStream<TokioIo<TokioIo<tokio::net::
984984
.ok()
985985
.flatten()
986986
.and_then(|c| c.to_der().ok());
987-
Some(crate::tls::TlsInfo { peer_certificate })
987+
Some(crate::tls::TlsInfo {
988+
peer_certificate,
989+
version: None,
990+
})
988991
}
989992
}
990993

@@ -1001,7 +1004,10 @@ impl TlsInfoFactory
10011004
.ok()
10021005
.flatten()
10031006
.and_then(|c| c.to_der().ok());
1004-
Some(crate::tls::TlsInfo { peer_certificate })
1007+
Some(crate::tls::TlsInfo {
1008+
peer_certificate,
1009+
version: None,
1010+
})
10051011
}
10061012
}
10071013

@@ -1018,13 +1024,18 @@ impl TlsInfoFactory for hyper_tls::MaybeHttpsStream<TokioIo<tokio::net::TcpStrea
10181024
#[cfg(feature = "__rustls")]
10191025
impl TlsInfoFactory for tokio_rustls::client::TlsStream<TokioIo<TokioIo<tokio::net::TcpStream>>> {
10201026
fn tls_info(&self) -> Option<crate::tls::TlsInfo> {
1021-
let peer_certificate = self
1022-
.get_ref()
1023-
.1
1027+
let conn = &self.get_ref().1;
1028+
let peer_certificate = conn
10241029
.peer_certificates()
10251030
.and_then(|certs| certs.first())
10261031
.map(|c| c.to_vec());
1027-
Some(crate::tls::TlsInfo { peer_certificate })
1032+
let version = conn
1033+
.protocol_version()
1034+
.and_then(crate::tls::Version::from_rustls);
1035+
Some(crate::tls::TlsInfo {
1036+
peer_certificate,
1037+
version,
1038+
})
10281039
}
10291040
}
10301041

@@ -1035,13 +1046,18 @@ impl TlsInfoFactory
10351046
>
10361047
{
10371048
fn tls_info(&self) -> Option<crate::tls::TlsInfo> {
1038-
let peer_certificate = self
1039-
.get_ref()
1040-
.1
1049+
let conn = &self.get_ref().1;
1050+
let peer_certificate = conn
10411051
.peer_certificates()
10421052
.and_then(|certs| certs.first())
10431053
.map(|c| c.to_vec());
1044-
Some(crate::tls::TlsInfo { peer_certificate })
1054+
let version = conn
1055+
.protocol_version()
1056+
.and_then(crate::tls::Version::from_rustls);
1057+
Some(crate::tls::TlsInfo {
1058+
peer_certificate,
1059+
version,
1060+
})
10451061
}
10461062
}
10471063

@@ -1075,7 +1091,10 @@ impl TlsInfoFactory for tokio_native_tls::TlsStream<TokioIo<TokioIo<tokio::net::
10751091
.ok()
10761092
.flatten()
10771093
.and_then(|c| c.to_der().ok());
1078-
Some(crate::tls::TlsInfo { peer_certificate })
1094+
Some(crate::tls::TlsInfo {
1095+
peer_certificate,
1096+
version: None,
1097+
})
10791098
}
10801099
}
10811100

@@ -1093,7 +1112,10 @@ impl TlsInfoFactory
10931112
.ok()
10941113
.flatten()
10951114
.and_then(|c| c.to_der().ok());
1096-
Some(crate::tls::TlsInfo { peer_certificate })
1115+
Some(crate::tls::TlsInfo {
1116+
peer_certificate,
1117+
version: None,
1118+
})
10971119
}
10981120
}
10991121

@@ -1112,13 +1134,18 @@ impl TlsInfoFactory for hyper_tls::MaybeHttpsStream<TokioIo<tokio::net::UnixStre
11121134
#[cfg(unix)]
11131135
impl TlsInfoFactory for tokio_rustls::client::TlsStream<TokioIo<TokioIo<tokio::net::UnixStream>>> {
11141136
fn tls_info(&self) -> Option<crate::tls::TlsInfo> {
1115-
let peer_certificate = self
1116-
.get_ref()
1117-
.1
1137+
let conn = &self.get_ref().1;
1138+
let peer_certificate = conn
11181139
.peer_certificates()
11191140
.and_then(|certs| certs.first())
11201141
.map(|c| c.to_vec());
1121-
Some(crate::tls::TlsInfo { peer_certificate })
1142+
let version = conn
1143+
.protocol_version()
1144+
.and_then(crate::tls::Version::from_rustls);
1145+
Some(crate::tls::TlsInfo {
1146+
peer_certificate,
1147+
version,
1148+
})
11221149
}
11231150
}
11241151

@@ -1130,13 +1157,18 @@ impl TlsInfoFactory
11301157
>
11311158
{
11321159
fn tls_info(&self) -> Option<crate::tls::TlsInfo> {
1133-
let peer_certificate = self
1134-
.get_ref()
1135-
.1
1160+
let conn = &self.get_ref().1;
1161+
let peer_certificate = conn
11361162
.peer_certificates()
11371163
.and_then(|certs| certs.first())
11381164
.map(|c| c.to_vec());
1139-
Some(crate::tls::TlsInfo { peer_certificate })
1165+
let version = conn
1166+
.protocol_version()
1167+
.and_then(crate::tls::Version::from_rustls);
1168+
Some(crate::tls::TlsInfo {
1169+
peer_certificate,
1170+
version,
1171+
})
11401172
}
11411173
}
11421174

@@ -1175,7 +1207,10 @@ impl TlsInfoFactory
11751207
.ok()
11761208
.flatten()
11771209
.and_then(|c| c.to_der().ok());
1178-
Some(crate::tls::TlsInfo { peer_certificate })
1210+
Some(crate::tls::TlsInfo {
1211+
peer_certificate,
1212+
version: None,
1213+
})
11791214
}
11801215
}
11811216

@@ -1195,7 +1230,10 @@ impl TlsInfoFactory
11951230
.ok()
11961231
.flatten()
11971232
.and_then(|c| c.to_der().ok());
1198-
Some(crate::tls::TlsInfo { peer_certificate })
1233+
Some(crate::tls::TlsInfo {
1234+
peer_certificate,
1235+
version: None,
1236+
})
11991237
}
12001238
}
12011239

@@ -1220,13 +1258,18 @@ impl TlsInfoFactory
12201258
>
12211259
{
12221260
fn tls_info(&self) -> Option<crate::tls::TlsInfo> {
1223-
let peer_certificate = self
1224-
.get_ref()
1225-
.1
1261+
let conn = &self.get_ref().1;
1262+
let peer_certificate = conn
12261263
.peer_certificates()
12271264
.and_then(|certs| certs.first())
12281265
.map(|c| c.to_vec());
1229-
Some(crate::tls::TlsInfo { peer_certificate })
1266+
let version = conn
1267+
.protocol_version()
1268+
.and_then(crate::tls::Version::from_rustls);
1269+
Some(crate::tls::TlsInfo {
1270+
peer_certificate,
1271+
version,
1272+
})
12301273
}
12311274
}
12321275

@@ -1242,13 +1285,18 @@ impl TlsInfoFactory
12421285
>
12431286
{
12441287
fn tls_info(&self) -> Option<crate::tls::TlsInfo> {
1245-
let peer_certificate = self
1246-
.get_ref()
1247-
.1
1288+
let conn = &self.get_ref().1;
1289+
let peer_certificate = conn
12481290
.peer_certificates()
12491291
.and_then(|certs| certs.first())
12501292
.map(|c| c.to_vec());
1251-
Some(crate::tls::TlsInfo { peer_certificate })
1293+
let version = conn
1294+
.protocol_version()
1295+
.and_then(crate::tls::Version::from_rustls);
1296+
Some(crate::tls::TlsInfo {
1297+
peer_certificate,
1298+
version,
1299+
})
12521300
}
12531301
}
12541302

src/tls.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,18 +789,29 @@ impl ServerCertVerifier for IgnoreHostname {
789789
#[derive(Clone)]
790790
pub struct TlsInfo {
791791
pub(crate) peer_certificate: Option<Vec<u8>>,
792+
pub(crate) version: Option<Version>,
792793
}
793794

794795
impl TlsInfo {
795796
/// Get the DER encoded leaf certificate of the peer.
796797
pub fn peer_certificate(&self) -> Option<&[u8]> {
797798
self.peer_certificate.as_ref().map(|der| &der[..])
798799
}
800+
801+
/// Get the TLS protocol version negotiated with the peer.
802+
///
803+
/// Returns `None` if the TLS backend cannot report it. The `native-tls`
804+
/// backend never reports a version.
805+
pub fn version(&self) -> Option<Version> {
806+
self.version
807+
}
799808
}
800809

801810
impl std::fmt::Debug for TlsInfo {
802811
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
803-
f.debug_struct("TlsInfo").finish()
812+
f.debug_struct("TlsInfo")
813+
.field("version", &self.version)
814+
.finish()
804815
}
805816
}
806817

tests/client.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,51 @@ async fn test_tls_info() {
521521
assert!(tls_info.is_none());
522522
}
523523

524+
#[cfg(all(feature = "__rustls", not(feature = "rustls-no-provider")))]
525+
#[tokio::test]
526+
async fn test_tls_info_version_rustls() {
527+
let resp = reqwest::Client::builder()
528+
.tls_backend_rustls()
529+
.tls_info(true)
530+
.build()
531+
.expect("client builder")
532+
.get("https://google.com")
533+
.send()
534+
.await
535+
.expect("response");
536+
let tls_info = resp
537+
.extensions()
538+
.get::<reqwest::tls::TlsInfo>()
539+
.expect("tls info");
540+
let version = tls_info.version().expect("negotiated version");
541+
assert!(
542+
version >= reqwest::tls::Version::TLS_1_2,
543+
"negotiated {version:?}"
544+
);
545+
}
546+
547+
// native-tls cannot report the negotiated version, so it stays `None` even
548+
// though the rest of the `TlsInfo` is populated.
549+
#[cfg(feature = "__native-tls")]
550+
#[tokio::test]
551+
async fn test_tls_info_version_native_tls() {
552+
let resp = reqwest::Client::builder()
553+
.tls_backend_native()
554+
.tls_info(true)
555+
.build()
556+
.expect("client builder")
557+
.get("https://google.com")
558+
.send()
559+
.await
560+
.expect("response");
561+
let tls_info = resp
562+
.extensions()
563+
.get::<reqwest::tls::TlsInfo>()
564+
.expect("tls info");
565+
assert!(tls_info.peer_certificate().is_some());
566+
assert!(tls_info.version().is_none());
567+
}
568+
524569
#[tokio::test]
525570
async fn close_connection_after_idle_timeout() {
526571
let mut server = server::http(move |_| async move { http::Response::default() });

0 commit comments

Comments
 (0)