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 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, BoxError>,
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, BoxError> {
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(endpoint: Uri, client: BoxService<Request, Response, BoxError>) -> Self {
136        Self {
137            endpoint,
138            allow_list: None,
139            http_client: client,
140        }
141    }
142
143    crate::utils::macros::generate_set_and_with! {
144        /// Only allow fetching certs for the given domain.
145        ///
146        /// By default, if none of the `allow_*` setters are called
147        /// the client will fetch for any client.
148        pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
149            if let Some(parent) = domain.as_wildcard_parent() && let Ok(domain) = parent.try_as_wildcard() {
150                self.allow_list.get_or_insert_default().insert_domain(parent, DomainAllowMode::Parent(domain));
151            } else {
152                self.allow_list.get_or_insert_default().insert_domain(domain, DomainAllowMode::Exact);
153            }
154            self
155        }
156    }
157
158    crate::utils::macros::generate_set_and_with! {
159        /// Only allow fetching certs for the given domains.
160        ///
161        /// By default, if none of the `allow_*` setters are called
162        /// the client will fetch for any client.
163        pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
164            for domain in domains {
165                self.set_allow_domain(domain);
166            }
167            self
168        }
169    }
170
171    /// Prefetch all certificates, useful to warm them up at startup time.
172    pub async fn prefetch_certs(&self) {
173        if let Some(allow_list) = &self.allow_list {
174            for (domain_key, mode) in allow_list.iter() {
175                let domain = match mode {
176                    // assumption: only valid domains in trie possible
177                    DomainAllowMode::Exact => domain_key,
178                    DomainAllowMode::Parent(domain) => domain.clone(),
179                };
180                match self.fetch_certs(domain.clone()).await {
181                    Ok(_) => tracing::debug!("prefetched certificates for domain: {domain}"),
182                    Err(err) => tracing::error!(
183                        "failed to prefetch certificates for domain '{domain}': {err}"
184                    ),
185                }
186            }
187        }
188    }
189
190    async fn fetch_certs(&self, domain: Domain) -> Result<ServerAuthData, BoxError> {
191        let req = self
192            .http_client
193            .post(self.endpoint.clone())
194            .json(&CertOrderInput { domain })
195            .try_into_request()
196            .context("builld request")?;
197
198        let response = self
199            .http_client
200            .serve(req)
201            .await
202            .context("send order request")?;
203
204        let status = response.status();
205        if status != StatusCode::OK {
206            return Err(BoxError::from("unexpected dinocert order response")
207                .context_field("status", status));
208        }
209
210        let CertOrderOutput {
211            crt_pem_base64,
212            key_pem_base64,
213        } = response
214            .into_body()
215            .try_into_json()
216            .await
217            .context("fetch json crt order response")?;
218
219        let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
220        let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
221
222        Ok(ServerAuthData {
223            cert_chain: DataEncoding::Pem(
224                NonEmptyStr::try_from(
225                    String::from_utf8(crt).context("concert crt pem to utf8 string")?,
226                )
227                .context("convert crt utf8 string to non-empty")?,
228            ),
229            private_key: DataEncoding::Pem(
230                NonEmptyStr::try_from(
231                    String::from_utf8(key).context("concert private key pem to utf8 string")?,
232                )
233                .context("convert privatek key pem utf8 string to non-empty")?,
234            ),
235            ocsp: None,
236        })
237    }
238}
239
240impl DynamicCertIssuer for CertIssuerHttpClient {
241    async fn issue_cert(
242        &self,
243        client_hello: ClientHello,
244        _server_name: Option<Domain>,
245    ) -> Result<ServerAuthData, BoxError> {
246        let domain = match client_hello.ext_server_name() {
247            Some(domain) => {
248                if let Some(ref allow_list) = self.allow_list {
249                    match allow_list.match_parent(domain) {
250                        None => {
251                            return Err(BoxError::from("sni found: unexpected unknown domain")
252                                .with_context_field("domain", || domain.clone()));
253                        }
254                        Some(DomainParentMatch {
255                            value: &DomainAllowMode::Exact,
256                            is_exact,
257                            ..
258                        }) => {
259                            if is_exact {
260                                domain.clone()
261                            } else {
262                                return Err(BoxError::from("sni found: unexpected child domain")
263                                    .with_context_field("domain", || domain.clone()));
264                            }
265                        }
266                        Some(DomainParentMatch {
267                            value: DomainAllowMode::Parent(wildcard_domain),
268                            ..
269                        }) => wildcard_domain.clone(),
270                    }
271                } else {
272                    domain.clone()
273                }
274            }
275            None => {
276                return Err(BoxError::from("no SNI found"));
277            }
278        };
279
280        self.fetch_certs(domain).await
281    }
282
283    fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
284        if let Some(ref allow_list) = self.allow_list {
285            match allow_list.match_parent(domain) {
286                None
287                | Some(DomainParentMatch {
288                    value: &DomainAllowMode::Exact,
289                    ..
290                }) => None,
291                Some(DomainParentMatch {
292                    value: DomainAllowMode::Parent(wildcard_domain),
293                    ..
294                }) => Some(wildcard_domain),
295            }
296        } else {
297            None
298        }
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_issuer_kind_norm_cn() {
308        let issuer =
309            CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
310                .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
311        for (input, expected) in [
312            ("example.com", None),
313            ("www.foo.com", Some("*.foo.com")),
314            ("bar.foo.com", Some("*.foo.com")),
315            ("bar.example.io", Some("*.example.io")),
316            ("example.net", None),
317            ("foo.example.net", None),
318            ("foo.bar.org", None),
319            ("bar.org", None),
320        ] {
321            let output = issuer
322                .norm_cn(&Domain::from_static(input))
323                .map(|d| d.as_str());
324            assert_eq!(output, expected, "{input:?} ; {expected:?}")
325        }
326    }
327}