|
| 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 | +} |
0 commit comments