Skip to content

Commit 354a6ee

Browse files
johnhurthargut
andcommitted
Add preliminary rustls support
Co-authored-by: Harald Gutmann <[email protected]>
1 parent dd99314 commit 354a6ee

File tree

17 files changed

+1299
-17
lines changed

17 files changed

+1299
-17
lines changed

.bleep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
46b4788e5c9c00dfaa327800b22cb84c8f695b5c
1+
3919de32fe7a184847dd6ce7da98247ec9e1eb86

pingora-core/src/connectors/tls/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ mod boringssl_openssl;
1818
#[cfg(feature = "openssl_derived")]
1919
pub use boringssl_openssl::*;
2020

21+
#[cfg(feature = "rustls")]
22+
mod rustls;
23+
24+
#[cfg(feature = "rustls")]
25+
pub use rustls::*;
26+
2127
/// OpenSSL considers underscores in hostnames non-compliant.
2228
/// We replace the underscore in the leftmost label as we must support these
2329
/// hostnames for wildcard matches and we have not patched OpenSSL.
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright 2024 Cloudflare, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::sync::Arc;
16+
17+
use log::debug;
18+
use pingora_error::{
19+
Error,
20+
ErrorType::{ConnectTimedout, InvalidCert},
21+
OrErr, Result,
22+
};
23+
use pingora_rustls::{
24+
load_ca_file_into_store, load_certs_key_file, load_platform_certs_incl_env_into_store, version,
25+
CertificateDer, ClientConfig as RusTlsClientConfig, PrivateKeyDer, RootCertStore,
26+
TlsConnector as RusTlsConnector,
27+
};
28+
29+
use crate::protocols::tls::{client::handshake, TlsStream};
30+
use crate::{connectors::ConnectorOptions, listeners::ALPN, protocols::IO, upstreams::peer::Peer};
31+
32+
use super::replace_leftmost_underscore;
33+
34+
#[derive(Clone)]
35+
pub struct Connector {
36+
pub ctx: Arc<TlsConnector>,
37+
}
38+
39+
impl Connector {
40+
pub fn new(config_opt: Option<ConnectorOptions>) -> Self {
41+
TlsConnector::build_connector(config_opt)
42+
}
43+
}
44+
45+
pub(crate) struct TlsConnector {
46+
config: RusTlsClientConfig,
47+
ca_certs: RootCertStore,
48+
}
49+
50+
impl TlsConnector {
51+
pub(crate) fn build_connector(options: Option<ConnectorOptions>) -> Connector
52+
where
53+
Self: Sized,
54+
{
55+
// NOTE: Rustls only supports TLS 1.2 & 1.3
56+
57+
// TODO: currently using Rustls defaults
58+
// - support SSLKEYLOGFILE
59+
// - set supported ciphers/algorithms/curves
60+
// - add options for CRL/OCSP validation
61+
62+
let (ca_certs, certs_key) = {
63+
let mut ca_certs = RootCertStore::empty();
64+
let mut certs_key = None;
65+
66+
if let Some(conf) = options.as_ref() {
67+
if let Some(ca_file_path) = conf.ca_file.as_ref() {
68+
load_ca_file_into_store(ca_file_path, &mut ca_certs);
69+
} else {
70+
load_platform_certs_incl_env_into_store(&mut ca_certs);
71+
}
72+
if let Some((cert, key)) = conf.cert_key_file.as_ref() {
73+
certs_key = load_certs_key_file(cert, key);
74+
}
75+
// TODO: support SSLKEYLOGFILE
76+
} else {
77+
load_platform_certs_incl_env_into_store(&mut ca_certs);
78+
}
79+
80+
(ca_certs, certs_key)
81+
};
82+
83+
// TODO: WebPkiServerVerifier for CRL/OCSP validation
84+
let builder =
85+
RusTlsClientConfig::builder_with_protocol_versions(&[&version::TLS12, &version::TLS13])
86+
.with_root_certificates(ca_certs.clone());
87+
88+
let config = match certs_key {
89+
Some((certs, key)) => {
90+
match builder.with_client_auth_cert(certs.clone(), key.clone_key()) {
91+
Ok(config) => config,
92+
Err(err) => {
93+
// TODO: is there a viable alternative to the panic?
94+
// falling back to no client auth... does not seem to be reasonable.
95+
panic!(
96+
"{}",
97+
format!("Failed to configure client auth cert/key. Error: {}", err)
98+
);
99+
}
100+
}
101+
}
102+
None => builder.with_no_client_auth(),
103+
};
104+
105+
Connector {
106+
ctx: Arc::new(TlsConnector { config, ca_certs }),
107+
}
108+
}
109+
}
110+
111+
pub async fn connect<T, P>(
112+
stream: T,
113+
peer: &P,
114+
alpn_override: Option<ALPN>,
115+
tls_ctx: &TlsConnector,
116+
) -> Result<TlsStream<T>>
117+
where
118+
T: IO,
119+
P: Peer + Send + Sync,
120+
{
121+
let mut config = tls_ctx.config.clone();
122+
123+
// TODO: setup CA/verify cert store from peer
124+
// looks like the fields are always None
125+
// peer.get_ca()
126+
127+
let key_pair = peer.get_client_cert_key();
128+
let updated_config: Option<RusTlsClientConfig> = match key_pair {
129+
None => None,
130+
Some(key_arc) => {
131+
debug!("setting client cert and key");
132+
133+
let mut cert_chain = vec![];
134+
debug!("adding leaf certificate to mTLS cert chain");
135+
cert_chain.push(key_arc.leaf().to_owned());
136+
137+
debug!("adding intermediate certificates to mTLS cert chain");
138+
key_arc
139+
.intermediates()
140+
.to_owned()
141+
.iter()
142+
.map(|i| i.to_vec())
143+
.for_each(|i| cert_chain.push(i));
144+
145+
let certs: Vec<CertificateDer> = cert_chain
146+
.into_iter()
147+
.map(|c| c.as_slice().to_owned().into())
148+
.collect();
149+
let private_key: PrivateKeyDer =
150+
key_arc.key().as_slice().to_owned().try_into().unwrap();
151+
152+
let builder = RusTlsClientConfig::builder_with_protocol_versions(&[
153+
&version::TLS12,
154+
&version::TLS13,
155+
])
156+
.with_root_certificates(tls_ctx.ca_certs.clone());
157+
158+
let updated_config = builder
159+
.with_client_auth_cert(certs, private_key)
160+
.explain_err(InvalidCert, |e| {
161+
format!(
162+
"Failed to use peer cert/key to update Rustls config: {:?}",
163+
e
164+
)
165+
})?;
166+
Some(updated_config)
167+
}
168+
};
169+
170+
if let Some(alpn) = alpn_override.as_ref().or(peer.get_alpn()) {
171+
config.alpn_protocols = alpn.to_wire_protocols();
172+
}
173+
174+
// TODO: curve setup from peer
175+
// - second key share from peer, currently only used in boringssl with PQ features
176+
177+
let tls_conn = if let Some(cfg) = updated_config {
178+
RusTlsConnector::from(Arc::new(cfg))
179+
} else {
180+
RusTlsConnector::from(Arc::new(config))
181+
};
182+
183+
// TODO: for consistent behaviour between TLS providers some additions are required
184+
// - allowing to disable verification
185+
// - the validation/replace logic would need adjustments to match the boringssl/openssl behaviour
186+
// implementing a custom certificate_verifier could be used to achieve matching behaviour
187+
//let d_conf = config.dangerous();
188+
//d_conf.set_certificate_verifier(...);
189+
190+
let mut domain = peer.sni().to_string();
191+
if peer.sni().is_empty() {
192+
// use ip in case SNI is not present
193+
// TODO: disable validation
194+
domain = peer.address().as_inet().unwrap().ip().to_string()
195+
}
196+
197+
if peer.verify_cert() && peer.verify_hostname() {
198+
// TODO: streamline logic with replacing first underscore within TLS implementations
199+
if let Some(sni_s) = replace_leftmost_underscore(peer.sni()) {
200+
domain = sni_s;
201+
}
202+
if let Some(alt_cn) = peer.alternative_cn() {
203+
if !alt_cn.is_empty() {
204+
domain = alt_cn.to_string();
205+
// TODO: streamline logic with replacing first underscore within TLS implementations
206+
if let Some(alt_cn_s) = replace_leftmost_underscore(alt_cn) {
207+
domain = alt_cn_s;
208+
}
209+
}
210+
}
211+
}
212+
213+
let connect_future = handshake(&tls_conn, &domain, stream);
214+
215+
match peer.connection_timeout() {
216+
Some(t) => match pingora_timeout::timeout(t, connect_future).await {
217+
Ok(res) => res,
218+
Err(_) => Error::e_explain(
219+
ConnectTimedout,
220+
format!("connecting to server {}, timeout {:?}", peer, t),
221+
),
222+
},
223+
None => connect_future.await,
224+
}
225+
}

pingora-core/src/listeners/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub trait TlsAccept {
4444
/// This function is called in the middle of a TLS handshake. Structs who
4545
/// implement this function should provide tls certificate and key to the
4646
/// [TlsRef] via `ssl_use_certificate` and `ssl_use_private_key`.
47+
/// Note. This is only supported for openssl and boringssl
4748
async fn certificate_callback(&self, _ssl: &mut TlsRef) -> () {
4849
// does nothing by default
4950
}

pingora-core/src/listeners/tls/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ mod boringssl_openssl;
1717

1818
#[cfg(feature = "openssl_derived")]
1919
pub use boringssl_openssl::*;
20+
21+
#[cfg(feature = "rustls")]
22+
mod rustls;
23+
24+
#[cfg(feature = "rustls")]
25+
pub use rustls::*;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2024 Cloudflare, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::sync::Arc;
16+
17+
use crate::listeners::TlsAcceptCallbacks;
18+
use crate::protocols::tls::{server::handshake, server::handshake_with_callback, TlsStream};
19+
use log::debug;
20+
use pingora_error::ErrorType::InternalError;
21+
use pingora_error::{Error, ErrorSource, ImmutStr, OrErr, Result};
22+
use pingora_rustls::load_certs_key_file;
23+
use pingora_rustls::ServerConfig;
24+
use pingora_rustls::{version, TlsAcceptor as RusTlsAcceptor};
25+
26+
use crate::protocols::{ALPN, IO};
27+
28+
/// The TLS settings of a listening endpoint
29+
pub struct TlsSettings {
30+
alpn_protocols: Option<Vec<Vec<u8>>>,
31+
cert_path: String,
32+
key_path: String,
33+
}
34+
35+
pub struct Acceptor {
36+
pub acceptor: RusTlsAcceptor,
37+
callbacks: Option<TlsAcceptCallbacks>,
38+
}
39+
40+
impl TlsSettings {
41+
pub fn build(self) -> Acceptor {
42+
let (certs, key) =
43+
load_certs_key_file(&self.cert_path, &self.key_path).unwrap_or_else(|| {
44+
panic!(
45+
"Failed to load provided certificates \"{}\" or key \"{}\".",
46+
self.cert_path, self.key_path
47+
)
48+
});
49+
50+
let mut config =
51+
ServerConfig::builder_with_protocol_versions(&[&version::TLS12, &version::TLS13])
52+
.with_no_client_auth()
53+
.with_single_cert(certs, key)
54+
.explain_err(InternalError, |e| {
55+
format!("Failed to create server listener config: {}", e)
56+
})
57+
.unwrap();
58+
59+
if let Some(alpn_protocols) = self.alpn_protocols {
60+
config.alpn_protocols = alpn_protocols;
61+
}
62+
63+
Acceptor {
64+
acceptor: RusTlsAcceptor::from(Arc::new(config)),
65+
callbacks: None,
66+
}
67+
}
68+
69+
/// Enable HTTP/2 support for this endpoint, which is default off.
70+
/// This effectively sets the ALPN to prefer HTTP/2 with HTTP/1.1 allowed
71+
pub fn enable_h2(&mut self) {
72+
self.set_alpn(ALPN::H2H1);
73+
}
74+
75+
fn set_alpn(&mut self, alpn: ALPN) {
76+
self.alpn_protocols = Some(alpn.to_wire_protocols());
77+
}
78+
79+
pub fn intermediate(cert_path: &str, key_path: &str) -> Result<Self>
80+
where
81+
Self: Sized,
82+
{
83+
Ok(TlsSettings {
84+
alpn_protocols: None,
85+
cert_path: cert_path.to_string(),
86+
key_path: key_path.to_string(),
87+
})
88+
}
89+
90+
pub fn with_callbacks() -> Result<Self>
91+
where
92+
Self: Sized,
93+
{
94+
// TODO: verify if/how callback in handshake can be done using Rustls
95+
Err(Error::create(
96+
InternalError,
97+
ErrorSource::Internal,
98+
Some(ImmutStr::from(
99+
"Certificate callbacks are not supported with feature \"rustls\".",
100+
)),
101+
None,
102+
))
103+
}
104+
}
105+
106+
impl Acceptor {
107+
pub async fn tls_handshake<S: IO>(&self, stream: S) -> Result<TlsStream<S>> {
108+
debug!("new tls session");
109+
// TODO: be able to offload this handshake in a thread pool
110+
if let Some(cb) = self.callbacks.as_ref() {
111+
handshake_with_callback(self, stream, cb).await
112+
} else {
113+
handshake(self, stream).await
114+
}
115+
}
116+
}

pingora-core/src/protocols/tls/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ mod boringssl_openssl;
2323
#[cfg(feature = "openssl_derived")]
2424
pub use boringssl_openssl::*;
2525

26+
#[cfg(feature = "rustls")]
27+
mod rustls;
28+
29+
#[cfg(feature = "rustls")]
30+
pub use rustls::*;
31+
2632
#[cfg(not(feature = "any_tls"))]
2733
pub mod noop_tls;
2834

0 commit comments

Comments
 (0)