Skip to main content

rama/http/
tls.rs

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