Skip to main content

rama/http/
tls.rs

1//! tls features provided from the http layer.
2
3use crate::error::{BoxError, ErrorContext as _, ErrorExt as _};
4use crate::http::{
5    BodyExtractExt as _, Request, Response, StatusCode, Uri, client::EasyHttpWebClient,
6    service::client::HttpClientExt as _,
7};
8use crate::net::address::{AsDomainRef, Domain, DomainParentMatch, DomainTrie};
9use crate::net::tls::{
10    DataEncoding,
11    client::ClientHello,
12    server::{DynamicCertIssuer, ServerAuthData},
13};
14use crate::rt::Executor;
15use crate::telemetry::tracing;
16use crate::utils::str::NonEmptyStr;
17use crate::{Service, service::BoxService};
18
19use base64::Engine;
20use base64::engine::general_purpose::STANDARD as ENGINE;
21use rama_core::error::extra::OpaqueError;
22use rama_core::layer::MapErr;
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26/// Json input used as http (POST) request payload sent by the [`CertIssuerHttpClient`].
27pub struct CertOrderInput {
28    pub domain: Domain,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32/// Json payload expected in
33/// the http (POST) response payload as received by the [`CertIssuerHttpClient`].
34pub struct CertOrderOutput {
35    pub crt_pem_base64: String,
36    pub key_pem_base64: String,
37}
38
39#[derive(Debug)]
40/// An http client used to fetch certs dynamically ([`DynamicCertIssuer`]).
41///
42/// There is no server implementation in Rama.
43/// It is up to the user of this client to provide their own server.
44pub struct CertIssuerHttpClient {
45    endpoint: Uri,
46    allow_list: Option<DomainTrie<DomainAllowMode>>,
47    http_client: BoxService<Request, Response, OpaqueError>,
48}
49
50#[derive(Debug, Clone)]
51enum DomainAllowMode {
52    Exact,
53    Parent(Domain),
54}
55
56impl CertIssuerHttpClient {
57    /// Create a new [`CertIssuerHttpClient`] using the default [`EasyHttpWebClient`].
58    pub fn new(exec: Executor, endpoint: Uri) -> Self {
59        Self::new_with_client(endpoint, EasyHttpWebClient::default_with_executor(exec))
60    }
61
62    #[cfg(feature = "boring")]
63    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
64    pub fn try_from_env(exec: Executor) -> Result<Self, BoxError> {
65        use crate::{
66            Layer as _,
67            http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
68            net::user::Bearer,
69            tls::boring::{
70                client::TlsConnectorDataBuilder,
71                core::x509::{X509, store::X509StoreBuilder},
72            },
73        };
74        use std::sync::Arc;
75
76        let uri_raw = std::env::var("RAMA_TLS_REMOTE").context("RAMA_TLS_REMOTE is undefined")?;
77
78        let mut tls_config = TlsConnectorDataBuilder::new_http_auto();
79
80        if let Ok(remote_ca_raw) = std::env::var("RAMA_TLS_REMOTE_CA") {
81            let mut store_builder = X509StoreBuilder::new().context("build x509 store builder")?;
82            store_builder
83                .add_cert(
84                    X509::from_pem(
85                        &ENGINE
86                            .decode(remote_ca_raw)
87                            .context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
88                    )
89                    .context("load CA cert")?,
90                )
91                .context("add CA cert to store builder")?;
92            let store = store_builder.build();
93            tls_config.set_server_verify_cert_store(Arc::new(store));
94        }
95
96        let client = EasyHttpWebClient::connector_builder()
97            .with_default_transport_connector()
98            .without_tls_proxy_support()
99            .without_proxy_support()
100            .with_tls_support_using_boringssl(Some(Arc::new(tls_config)))
101            .with_default_http_connector(exec)
102            .build_client();
103
104        let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
105        let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
106            Self::new_with_client(
107                uri,
108                SetRequestHeaderLayer::overriding_typed(Authorization::new(
109                    Bearer::try_from(auth_raw)
110                        .context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
111                ))
112                .into_layer(client),
113            )
114        } else {
115            Self::new_with_client(uri, client)
116        };
117
118        if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
119            for raw_cn_str in allow_cn_csv_raw.split(',') {
120                let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
121                client.set_allow_domain(cn);
122            }
123        }
124
125        Ok(client)
126    }
127
128    /// Create a new [`CertIssuerHttpClient`] using a custom http client.
129    ///
130    /// The custom http client allows you to add whatever layers and client implementation
131    /// you wish, to allow for custom headers, behaviour and security measures
132    /// such as authorization.
133    pub fn new_with_client(
134        endpoint: Uri,
135        client: impl Service<
136            Request,
137            Output = Response,
138            Error: std::error::Error + Send + Sync + 'static,
139        >,
140    ) -> Self {
141        let http_client = MapErr::into_opaque_error(client).boxed();
142        Self {
143            endpoint,
144            allow_list: None,
145            http_client,
146        }
147    }
148
149    crate::utils::macros::generate_set_and_with! {
150        /// Only allow fetching certs for the given domain.
151        ///
152        /// By default, if none of the `allow_*` setters are called
153        /// the client will fetch for any client.
154        pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
155            if let Some(parent) = domain.as_wildcard_parent() && let Ok(domain) = parent.try_as_wildcard() {
156                self.allow_list.get_or_insert_default().insert_domain(parent, DomainAllowMode::Parent(domain));
157            } else {
158                self.allow_list.get_or_insert_default().insert_domain(domain, DomainAllowMode::Exact);
159            }
160            self
161        }
162    }
163
164    crate::utils::macros::generate_set_and_with! {
165        /// Only allow fetching certs for the given domains.
166        ///
167        /// By default, if none of the `allow_*` setters are called
168        /// the client will fetch for any client.
169        pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
170            for domain in domains {
171                self.set_allow_domain(domain);
172            }
173            self
174        }
175    }
176
177    /// Prefetch all certificates, useful to warm them up at startup time.
178    pub async fn prefetch_certs(&self) {
179        if let Some(allow_list) = &self.allow_list {
180            for (domain_key, mode) in allow_list.iter() {
181                let domain = match mode {
182                    // assumption: only valid domains in trie possible
183                    DomainAllowMode::Exact => domain_key,
184                    DomainAllowMode::Parent(domain) => domain.clone(),
185                };
186                match self.fetch_certs(domain.clone()).await {
187                    Ok(_) => tracing::debug!("prefetched certificates for domain: {domain}"),
188                    Err(err) => tracing::error!(
189                        "failed to prefetch certificates for domain '{domain}': {err}"
190                    ),
191                }
192            }
193        }
194    }
195
196    async fn fetch_certs(&self, domain: Domain) -> Result<ServerAuthData, BoxError> {
197        let response = self
198            .http_client
199            .post(self.endpoint.clone())
200            .json(&CertOrderInput { domain })
201            .send()
202            .await
203            .context("send order request")?;
204
205        let status = response.status();
206        if status != StatusCode::OK {
207            return Err(BoxError::from("unexpected dinocert order response")
208                .context_field("status", status));
209        }
210
211        let CertOrderOutput {
212            crt_pem_base64,
213            key_pem_base64,
214        } = response
215            .into_body()
216            .try_into_json()
217            .await
218            .context("fetch json crt order response")?;
219
220        let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
221        let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
222
223        Ok(ServerAuthData {
224            cert_chain: DataEncoding::Pem(
225                NonEmptyStr::try_from(
226                    String::from_utf8(crt).context("concert crt pem to utf8 string")?,
227                )
228                .context("convert crt utf8 string to non-empty")?,
229            ),
230            private_key: DataEncoding::Pem(
231                NonEmptyStr::try_from(
232                    String::from_utf8(key).context("concert private key pem to utf8 string")?,
233                )
234                .context("convert privatek key pem utf8 string to non-empty")?,
235            ),
236            ocsp: None,
237        })
238    }
239}
240
241impl DynamicCertIssuer for CertIssuerHttpClient {
242    async fn issue_cert(
243        &self,
244        client_hello: ClientHello,
245        _server_name: Option<Domain>,
246    ) -> Result<ServerAuthData, BoxError> {
247        let domain = match client_hello.ext_server_name() {
248            Some(domain) => {
249                if let Some(ref allow_list) = self.allow_list {
250                    match allow_list.match_parent(domain) {
251                        None => {
252                            return Err(BoxError::from("sni found: unexpected unknown domain")
253                                .with_context_field("domain", || domain.clone()));
254                        }
255                        Some(DomainParentMatch {
256                            value: &DomainAllowMode::Exact,
257                            is_exact,
258                            ..
259                        }) => {
260                            if is_exact {
261                                domain.clone()
262                            } else {
263                                return Err(BoxError::from("sni found: unexpected child domain")
264                                    .with_context_field("domain", || domain.clone()));
265                            }
266                        }
267                        Some(DomainParentMatch {
268                            value: DomainAllowMode::Parent(wildcard_domain),
269                            ..
270                        }) => wildcard_domain.clone(),
271                    }
272                } else {
273                    domain.clone()
274                }
275            }
276            None => {
277                return Err(BoxError::from("no SNI found"));
278            }
279        };
280
281        self.fetch_certs(domain).await
282    }
283
284    fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
285        if let Some(ref allow_list) = self.allow_list {
286            match allow_list.match_parent(domain) {
287                None
288                | Some(DomainParentMatch {
289                    value: &DomainAllowMode::Exact,
290                    ..
291                }) => None,
292                Some(DomainParentMatch {
293                    value: DomainAllowMode::Parent(wildcard_domain),
294                    ..
295                }) => Some(wildcard_domain),
296            }
297        } else {
298            None
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_issuer_kind_norm_cn() {
309        let issuer =
310            CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
311                .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
312        for (input, expected) in [
313            ("example.com", None),
314            ("www.foo.com", Some("*.foo.com")),
315            ("bar.foo.com", Some("*.foo.com")),
316            ("bar.example.io", Some("*.example.io")),
317            ("example.net", None),
318            ("foo.example.net", None),
319            ("foo.bar.org", None),
320            ("bar.org", None),
321        ] {
322            let output = issuer
323                .norm_cn(&Domain::from_static(input))
324                .map(|d| d.as_str());
325            assert_eq!(output, expected, "{input:?} ; {expected:?}")
326        }
327    }
328}