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::NonEmptyString;
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(endpoint: Uri) -> Self {
57        Self::new_with_client(endpoint, EasyHttpWebClient::default().boxed())
58    }
59
60    #[cfg(feature = "boring")]
61    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
62    pub fn try_from_env() -> Result<Self, OpaqueError> {
63        use crate::{
64            Layer as _,
65            http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
66            net::user::Bearer,
67            tls::boring::{
68                client::TlsConnectorDataBuilder,
69                core::x509::{X509, store::X509StoreBuilder},
70            },
71        };
72        use std::sync::Arc;
73
74        let uri_raw = std::env::var("RAMA_TLS_REMOTE").context("RAMA_TLS_REMOTE is undefined")?;
75
76        let mut tls_config = TlsConnectorDataBuilder::new_http_auto();
77
78        if let Ok(remote_ca_raw) = std::env::var("RAMA_TLS_REMOTE_CA") {
79            let mut store_builder = X509StoreBuilder::new().context("build x509 store builder")?;
80            store_builder
81                .add_cert(
82                    X509::from_pem(
83                        &ENGINE
84                            .decode(remote_ca_raw)
85                            .context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
86                    )
87                    .context("load CA cert")?,
88                )
89                .context("add CA cert to store builder")?;
90            let store = store_builder.build();
91            tls_config.set_server_verify_cert_store(Arc::new(store));
92        }
93
94        let client = EasyHttpWebClient::builder()
95            .with_default_transport_connector()
96            .without_tls_proxy_support()
97            .without_proxy_support()
98            .with_tls_support_using_boringssl(Some(Arc::new(tls_config)))
99            .with_default_http_connector()
100            .build();
101
102        let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
103        let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
104            Self::new_with_client(
105                uri,
106                SetRequestHeaderLayer::overriding_typed(Authorization::new(
107                    Bearer::new(auth_raw)
108                        .context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
109                ))
110                .into_layer(client)
111                .boxed(),
112            )
113        } else {
114            Self::new_with_client(uri, client.boxed())
115        };
116
117        if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
118            for raw_cn_str in allow_cn_csv_raw.split(',') {
119                let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
120                client.set_allow_domain(cn);
121            }
122        }
123
124        Ok(client)
125    }
126
127    /// Create a new [`CertIssuerHttpClient`] using a custom http client.
128    ///
129    /// The custom http client allows you to add whatever layers and client implementation
130    /// you wish, to allow for custom headers, behaviour and security measures
131    /// such as authorization.
132    pub fn new_with_client(
133        endpoint: Uri,
134        client: BoxService<Request, Response, OpaqueError>,
135    ) -> 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 fn prefetch_certs_in_background(&self, exec: &Executor) {
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                let http_client = self.http_client.clone();
181                let uri = self.endpoint.clone();
182                exec.spawn_task(async move {
183                    match fetch_certs(http_client, domain.clone(), uri).await {
184                        Ok(_) => tracing::debug!("prefetched certificates for domain: {domain}"),
185                        Err(err) => tracing::error!(
186                            "failed to prefetch certificates for domain '{domain}': {err}"
187                        ),
188                    }
189                });
190            }
191        }
192    }
193}
194
195impl DynamicCertIssuer for CertIssuerHttpClient {
196    fn issue_cert(
197        &self,
198        client_hello: ClientHello,
199        _server_name: Option<Domain>,
200    ) -> impl Future<Output = Result<ServerAuthData, OpaqueError>> + Send + Sync + '_ {
201        let domain = match client_hello.ext_server_name() {
202            Some(domain) => {
203                if let Some(ref allow_list) = self.allow_list {
204                    match allow_list.match_parent(domain) {
205                        None => {
206                            return Either::A(std::future::ready(Err(OpaqueError::from_display(
207                                "sni found: unexpected unknown domain",
208                            ))));
209                        }
210                        Some(DomainParentMatch {
211                            value: &DomainAllowMode::Exact,
212                            is_exact,
213                            ..
214                        }) => {
215                            if is_exact {
216                                domain.clone()
217                            } else {
218                                return Either::A(std::future::ready(Err(
219                                    OpaqueError::from_display("sni found: unexpected child domain"),
220                                )));
221                            }
222                        }
223                        Some(DomainParentMatch {
224                            value: DomainAllowMode::Parent(wildcard_domain),
225                            ..
226                        }) => wildcard_domain.clone(),
227                    }
228                } else {
229                    domain.clone()
230                }
231            }
232            None => {
233                return Either::A(std::future::ready(Err(OpaqueError::from_display(
234                    "no SNI found: failure",
235                ))));
236            }
237        };
238
239        let (tx, rx) = tokio::sync::oneshot::channel();
240        let http_client = self.http_client.clone();
241        let uri = self.endpoint.clone();
242
243        tokio::spawn(async move {
244            if let Err(err) = tx.send(fetch_certs(http_client, domain, uri).await) {
245                tracing::debug!("failed to send result back to callee: {err:?}");
246            }
247        });
248
249        Either::B(async move { rx.await.context("await crt order result")? })
250    }
251
252    fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
253        if let Some(ref allow_list) = self.allow_list {
254            match allow_list.match_parent(domain) {
255                None
256                | Some(DomainParentMatch {
257                    value: &DomainAllowMode::Exact,
258                    ..
259                }) => None,
260                Some(DomainParentMatch {
261                    value: DomainAllowMode::Parent(wildcard_domain),
262                    ..
263                }) => Some(wildcard_domain),
264            }
265        } else {
266            None
267        }
268    }
269}
270
271async fn fetch_certs(
272    client: BoxService<Request, Response, OpaqueError>,
273    domain: Domain,
274    uri: Uri,
275) -> Result<ServerAuthData, OpaqueError> {
276    let response = client
277        .post(uri)
278        .json(&CertOrderInput { domain })
279        .send()
280        .await
281        .context("send order request")?;
282
283    let status = response.status();
284    if status != StatusCode::OK {
285        return Err(OpaqueError::from_display(format!(
286            "unexpected dinocert order response status code: {status}"
287        )));
288    }
289
290    let CertOrderOutput {
291        crt_pem_base64,
292        key_pem_base64,
293    } = response
294        .into_body()
295        .try_into_json()
296        .await
297        .context("fetch json crt order response")?;
298
299    let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
300    let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
301
302    Ok(ServerAuthData {
303        cert_chain: DataEncoding::Pem(
304            NonEmptyString::try_from(
305                String::from_utf8(crt).context("concert crt pem to utf8 string")?,
306            )
307            .context("convert crt utf8 string to non-empty")?,
308        ),
309        private_key: DataEncoding::Pem(
310            NonEmptyString::try_from(
311                String::from_utf8(key).context("concert private key pem to utf8 string")?,
312            )
313            .context("convert privatek key pem utf8 string to non-empty")?,
314        ),
315        ocsp: None,
316    })
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_issuer_kind_norm_cn() {
325        let issuer = CertIssuerHttpClient::new(Uri::from_static("http://example.com"))
326            .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
327        for (input, expected) in [
328            ("example.com", None),
329            ("www.foo.com", Some("*.foo.com")),
330            ("bar.foo.com", Some("*.foo.com")),
331            ("bar.example.io", Some("*.example.io")),
332            ("example.net", None),
333            ("foo.example.net", None),
334            ("foo.bar.org", None),
335            ("bar.org", None),
336        ] {
337            let output = issuer
338                .norm_cn(&Domain::from_static(input))
339                .map(|d| d.as_str());
340            assert_eq!(output, expected, "{input:?} ; {expected:?}")
341        }
342    }
343}