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(
208                OpaqueError::from_static_str("unexpected dinocert order response")
209                    .context_field("status", status),
210            );
211        }
212
213        let CertOrderOutput {
214            crt_pem_base64,
215            key_pem_base64,
216        } = response
217            .into_body()
218            .try_into_json()
219            .await
220            .context("fetch json crt order response")?;
221
222        let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
223        let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
224
225        Ok(ServerAuthData {
226            cert_chain: DataEncoding::Pem(
227                NonEmptyStr::try_from(
228                    String::from_utf8(crt).context("concert crt pem to utf8 string")?,
229                )
230                .context("convert crt utf8 string to non-empty")?,
231            ),
232            private_key: DataEncoding::Pem(
233                NonEmptyStr::try_from(
234                    String::from_utf8(key).context("concert private key pem to utf8 string")?,
235                )
236                .context("convert privatek key pem utf8 string to non-empty")?,
237            ),
238            ocsp: None,
239        })
240    }
241}
242
243impl DynamicCertIssuer for CertIssuerHttpClient {
244    async fn issue_cert(
245        &self,
246        client_hello: ClientHello,
247        _server_name: Option<Domain>,
248    ) -> Result<ServerAuthData, BoxError> {
249        let domain = match client_hello.ext_server_name() {
250            Some(domain) => {
251                if let Some(ref allow_list) = self.allow_list {
252                    match allow_list.match_parent(domain) {
253                        None => {
254                            return Err(OpaqueError::from_static_str(
255                                "sni found: unexpected unknown domain",
256                            )
257                            .with_context_field("domain", || domain.clone()));
258                        }
259                        Some(DomainParentMatch {
260                            value: &DomainAllowMode::Exact,
261                            is_exact,
262                            ..
263                        }) => {
264                            if is_exact {
265                                domain.clone()
266                            } else {
267                                return Err(OpaqueError::from_static_str(
268                                    "sni found: unexpected child domain",
269                                )
270                                .with_context_field("domain", || domain.clone()));
271                            }
272                        }
273                        Some(DomainParentMatch {
274                            value: DomainAllowMode::Parent(wildcard_domain),
275                            ..
276                        }) => wildcard_domain.clone(),
277                    }
278                } else {
279                    domain.clone()
280                }
281            }
282            None => {
283                return Err(OpaqueError::from_static_str("no SNI found").into_box_error());
284            }
285        };
286
287        self.fetch_certs(domain).await
288    }
289
290    fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
291        if let Some(ref allow_list) = self.allow_list {
292            match allow_list.match_parent(domain) {
293                None
294                | Some(DomainParentMatch {
295                    value: &DomainAllowMode::Exact,
296                    ..
297                }) => None,
298                Some(DomainParentMatch {
299                    value: DomainAllowMode::Parent(wildcard_domain),
300                    ..
301                }) => Some(wildcard_domain),
302            }
303        } else {
304            None
305        }
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_issuer_kind_norm_cn() {
315        let issuer =
316            CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
317                .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
318        for (input, expected) in [
319            ("example.com", None),
320            ("www.foo.com", Some("*.foo.com")),
321            ("bar.foo.com", Some("*.foo.com")),
322            ("bar.example.io", Some("*.example.io")),
323            ("example.net", None),
324            ("foo.example.net", None),
325            ("foo.bar.org", None),
326            ("bar.org", None),
327        ] {
328            let output = issuer
329                .norm_cn(&Domain::from_static(input))
330                .map(|d| d.as_str());
331            assert_eq!(output, expected, "{input:?} ; {expected:?}")
332        }
333    }
334}