diff --git a/rama-net/src/tls/client/config.rs b/rama-net/src/tls/client/config.rs index 869c1e43..a938a49a 100644 --- a/rama-net/src/tls/client/config.rs +++ b/rama-net/src/tls/client/config.rs @@ -21,6 +21,8 @@ pub struct ClientConfig { pub client_auth: Option, /// key log intent pub key_logger: Option, + /// if enabled server certificates will be stored in [`NegotiatedTlsParameters`] + pub store_server_certificate_chain: bool, } impl ClientConfig { diff --git a/rama-net/src/tls/client/mod.rs b/rama-net/src/tls/client/mod.rs index bb36b6cc..c751f0cf 100644 --- a/rama-net/src/tls/client/mod.rs +++ b/rama-net/src/tls/client/mod.rs @@ -19,7 +19,7 @@ mod config; #[doc(inline)] pub use config::{ClientAuth, ClientAuthData, ClientConfig, ServerVerifyMode}; -use super::{ApplicationProtocol, ProtocolVersion}; +use super::{ApplicationProtocol, DataEncoding, ProtocolVersion}; #[derive(Debug, Clone)] /// Indicate (some) of the negotiated tls parameters that @@ -35,6 +35,8 @@ pub struct NegotiatedTlsParameters { /// /// e.g. [`ApplicationProtocol::HTTP_2`] pub application_layer_protocol: Option, + /// Certificate chain provided the peer (only stored if config requested this) + pub peer_certificate_chain: Option, } /// Merge extension lists A and B, with diff --git a/rama-net/src/tls/mod.rs b/rama-net/src/tls/mod.rs index 1e723051..f84ff548 100644 --- a/rama-net/src/tls/mod.rs +++ b/rama-net/src/tls/mod.rs @@ -95,3 +95,58 @@ pub enum DataEncoding { /// Privacy Enhanced Mail (PEM) (plain text) Pem(NonEmptyString), } + +#[cfg(feature = "boring")] +mod boring { + use super::*; + use ::boring::stack::StackRef; + use ::boring::x509::X509; + use rama_core::error::{ErrorContext, OpaqueError}; + + impl TryFrom<&X509> for DataEncoding { + type Error = OpaqueError; + + fn try_from(value: &X509) -> Result { + let der = value.to_der().context("boring X509 to Der DataEncoding")?; + Ok(DataEncoding::Der(der)) + } + } + + impl TryFrom<&StackRef> for DataEncoding { + type Error = OpaqueError; + + fn try_from(value: &StackRef) -> Result { + let der = value + .into_iter() + .map(|cert| { + cert.to_der() + .context("boring X509 stackref to DerStack DataEncoding") + }) + .collect::>, _>>()?; + Ok(DataEncoding::DerStack(der)) + } + } +} + +#[cfg(feature = "rustls")] +mod rustls { + use super::*; + use ::rustls::pki_types::CertificateDer; + + impl From<&CertificateDer<'static>> for DataEncoding { + fn from(value: &CertificateDer<'static>) -> Self { + DataEncoding::Der(value.as_ref().into()) + } + } + + impl From<&[CertificateDer<'static>]> for DataEncoding { + fn from(value: &[CertificateDer<'static>]) -> Self { + DataEncoding::DerStack( + value + .into_iter() + .map(|cert| Into::>::into(cert.as_ref())) + .collect(), + ) + } + } +} diff --git a/rama-net/src/tls/server/config.rs b/rama-net/src/tls/server/config.rs index 805a2998..13dec035 100644 --- a/rama-net/src/tls/server/config.rs +++ b/rama-net/src/tls/server/config.rs @@ -28,6 +28,9 @@ pub struct ServerConfig { /// key log intent pub key_logger: KeyLogIntent, + + /// store client certificate chain + pub store_client_certificate_chain: bool, } impl ServerConfig { @@ -40,6 +43,7 @@ impl ServerConfig { application_layer_protocol_negotiation: None, client_verify_mode: ClientVerifyMode::default(), key_logger: KeyLogIntent::default(), + store_client_certificate_chain: false, } } } diff --git a/rama-tls/src/boring/client/connector.rs b/rama-tls/src/boring/client/connector.rs index 66b55c4a..eb66c427 100644 --- a/rama-tls/src/boring/client/connector.rs +++ b/rama-tls/src/boring/client/connector.rs @@ -406,7 +406,8 @@ impl TlsConnector { where T: Stream + Unpin, { - let client_config_data = match connector_data.as_ref().or(self.connector_data.as_ref()) { + let connector_data = connector_data.as_ref().or(self.connector_data.as_ref()); + let client_config_data = match connector_data { Some(connector_data) => connector_data.try_to_build_config()?, None => TlsConnectorData::new_http_auto()?.try_to_build_config()?, }; @@ -437,9 +438,22 @@ impl TlsConnector { if let Some(ref proto) = application_layer_protocol { tracing::trace!(%proto, "boring client (connector) has selected ALPN"); } + + let store_server_cert_chain = connector_data + .is_some_and(|data| data.connect_config_input.store_server_certificate_chain); + + let server_certificate_chain = match store_server_cert_chain + .then(|| stream.ssl().peer_cert_chain()) + .flatten() + { + Some(chain) => Some(chain.try_into()?), + None => None, + }; + NegotiatedTlsParameters { protocol_version, application_layer_protocol, + peer_certificate_chain: server_certificate_chain, } } None => { diff --git a/rama-tls/src/boring/client/connector_data.rs b/rama-tls/src/boring/client/connector_data.rs index e44871f4..5cc48603 100644 --- a/rama-tls/src/boring/client/connector_data.rs +++ b/rama-tls/src/boring/client/connector_data.rs @@ -42,6 +42,7 @@ pub(super) struct ConnectConfigurationInput { pub(super) verify_algorithm_prefs: Option>, pub(super) server_verify_mode: Option, pub(super) client_auth: Option, + pub(super) store_server_certificate_chain: bool, } #[derive(Debug, Clone)] @@ -233,6 +234,9 @@ impl TlsConnectorData { .client_auth .clone() .or_else(|| self.connect_config_input.client_auth.clone()), + store_server_certificate_chain: other + .connect_config_input + .store_server_certificate_chain, }), server_name: other .server_name @@ -491,6 +495,7 @@ impl TryFrom for TlsConnectorData { verify_algorithm_prefs, server_verify_mode: value.server_verify_mode, client_auth, + store_server_certificate_chain: value.store_server_certificate_chain, }), server_name, }) diff --git a/rama-tls/src/boring/server/acceptor_data.rs b/rama-tls/src/boring/server/acceptor_data.rs index 366ed680..d19dbe72 100644 --- a/rama-tls/src/boring/server/acceptor_data.rs +++ b/rama-tls/src/boring/server/acceptor_data.rs @@ -45,6 +45,8 @@ pub(super) struct TlsConfig { pub(super) protocol_versions: Option>, /// optionally define client certificates in case client auth is enabled pub(super) client_cert_chain: Option>, + /// store client certificate chain if true and client provided this + pub store_client_certificate_chain: bool, } #[derive(Debug, Clone)] @@ -320,6 +322,7 @@ impl TryFrom for TlsAcceptorData { keylog_intent: value.key_logger, protocol_versions: value.protocol_versions.clone(), client_cert_chain, + store_client_certificate_chain: value.store_client_certificate_chain, }), }) } diff --git a/rama-tls/src/boring/server/service.rs b/rama-tls/src/boring/server/service.rs index bfd12508..d74eea6a 100644 --- a/rama-tls/src/boring/server/service.rs +++ b/rama-tls/src/boring/server/service.rs @@ -15,7 +15,7 @@ use rama_core::{ use rama_net::{ http::RequestContext, stream::Stream, - tls::{client::NegotiatedTlsParameters, ApplicationProtocol}, + tls::{client::NegotiatedTlsParameters, ApplicationProtocol, DataEncoding}, transport::TransportContext, }; use rama_utils::macros::define_inner_service_accessors; @@ -214,9 +214,36 @@ where .ssl() .selected_alpn_protocol() .map(ApplicationProtocol::from); + + let client_certificate_chain = if let Some(certificate) = tls_config + .store_client_certificate_chain + .then(|| stream.ssl().peer_certificate()) + .flatten() + { + // peer_cert_chain doesn't contain the leaf certificate in a server ctx + let mut chain = stream.ssl().peer_cert_chain().map_or(Ok(vec![]), |chain| { + chain + .into_iter() + .map(|cert| { + cert.to_der() + .context("boring ssl session: failed to convert peer certificates to der") + }) + .collect::>, _>>() + })?; + + let certificate = certificate + .to_der() + .context("boring ssl session: failed to convert peer certificate to der")?; + chain.insert(0, certificate); + Some(DataEncoding::DerStack(chain)) + } else { + None + }; + ctx.insert(NegotiatedTlsParameters { protocol_version, application_layer_protocol, + peer_certificate_chain: client_certificate_chain, }); } None => { diff --git a/rama-tls/src/rustls/client/connector.rs b/rama-tls/src/rustls/client/connector.rs index f84b29ab..168061cb 100644 --- a/rama-tls/src/rustls/client/connector.rs +++ b/rama-tls/src/rustls/client/connector.rs @@ -414,7 +414,8 @@ impl TlsConnector { where T: Stream + Unpin, { - let client_config_data = match connector_data.as_ref().or(self.connector_data.as_ref()) { + let connector_data = connector_data.as_ref().or(self.connector_data.as_ref()); + let client_config_data = match connector_data { Some(connector_data) => connector_data.try_to_build_config()?, None => TlsConnectorData::new_http_auto()?.try_to_build_config()?, }; @@ -428,6 +429,15 @@ impl TlsConnector { let (_, conn_data_ref) = stream.get_ref(); + let store_server_cert_chain = connector_data + .is_some_and(|data| data.client_config_input.store_server_certificate_chain); + + let server_certificate_chain = if store_server_cert_chain { + conn_data_ref.peer_certificates().map(Into::into) + } else { + None + }; + let params = NegotiatedTlsParameters { protocol_version: conn_data_ref .protocol_version() @@ -436,6 +446,7 @@ impl TlsConnector { application_layer_protocol: conn_data_ref .alpn_protocol() .map(ApplicationProtocol::from), + peer_certificate_chain: server_certificate_chain, }; Ok((stream, params)) diff --git a/rama-tls/src/rustls/client/connector_data.rs b/rama-tls/src/rustls/client/connector_data.rs index 826ca85d..699f6c1e 100644 --- a/rama-tls/src/rustls/client/connector_data.rs +++ b/rama-tls/src/rustls/client/connector_data.rs @@ -20,17 +20,18 @@ use tracing::trace; /// Created by converting a [`rustls::ClientConfig`] into it directly, /// or by trying to turn the _rama_ opiniated [`rama_net::tls::client::ClientConfig`] into it. pub struct TlsConnectorData { - client_config_input: Arc, - server_name: Option, + pub(super) client_config_input: Arc, + pub(super) server_name: Option, } #[derive(Debug, Default)] -struct ClientConfigInput { - protocol_versions: Option>, - client_auth: Option<(Vec>, PrivateKeyDer<'static>)>, - key_logger: Option, - alpn_protos: Option>>, - cert_verifier: Option>, +pub(super) struct ClientConfigInput { + pub(super) protocol_versions: Option>, + pub(super) client_auth: Option<(Vec>, PrivateKeyDer<'static>)>, + pub(super) key_logger: Option, + pub(super) alpn_protos: Option>>, + pub(super) cert_verifier: Option>, + pub(super) store_server_certificate_chain: bool, } impl TlsConnectorData { @@ -171,6 +172,9 @@ impl TlsConnectorData { .cert_verifier .clone() .or_else(|| self.client_config_input.cert_verifier.clone()), + store_server_certificate_chain: other + .client_config_input + .store_server_certificate_chain, }), server_name: other .server_name @@ -304,6 +308,7 @@ impl TryFrom for TlsConnectorData { .into_file_path(), alpn_protos, cert_verifier, + store_server_certificate_chain: value.store_server_certificate_chain, }), server_name, }) diff --git a/rama-tls/src/rustls/server/service.rs b/rama-tls/src/rustls/server/service.rs index 1aea8544..de7051a6 100644 --- a/rama-tls/src/rustls/server/service.rs +++ b/rama-tls/src/rustls/server/service.rs @@ -95,6 +95,8 @@ where application_layer_protocol: conn_data_ref .alpn_protocol() .map(ApplicationProtocol::from), + // Currently not supported as this would mean we need to wrap rustls config + peer_certificate_chain: None, }); ctx.insert(secure_transport);