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