Skip to main content

rama/http/
tls.rs

1//! tls features provided from the http layer.
2
3use crate::error::{BoxError, BoxErrorExt, 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, 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, including
44/// authentication and authorization for every certificate order.
45pub struct CertIssuerHttpClient {
46    endpoint: Uri,
47    // Trie value `None` means an exact entry; `Some(wildcard)` is a subtree
48    // entry, storing the issuing-form wildcard (e.g. `"*.foo.com"`) so
49    // `norm_cn` can hand it back as a borrowed reference.
50    allow_list: Option<DomainTrie<Option<Domain>>>,
51    http_client: BoxService<Request, Response, OpaqueError>,
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(endpoint, EasyHttpWebClient::default_with_executor(exec))
58    }
59
60    #[cfg(feature = "boring")]
61    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
62    pub fn try_from_env(exec: Executor) -> Result<Self, BoxError> {
63        use crate::{
64            Layer as _,
65            http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
66            net::tls::client::TlsClientConfig,
67            net::user::Bearer,
68            tls::boring::{
69                client::BoringClientConfigExt as _,
70                core::x509::{X509, store::X509StoreBuilder},
71            },
72        };
73        use std::sync::Arc;
74
75        let uri_raw = std::env::var("RAMA_TLS_REMOTE").context("RAMA_TLS_REMOTE is undefined")?;
76
77        let mut tls_config = TlsClientConfig::new().with_alpn_http_auto();
78
79        if let Ok(remote_ca_raw) = std::env::var("RAMA_TLS_REMOTE_CA") {
80            let mut store_builder = X509StoreBuilder::new().context("build x509 store builder")?;
81            store_builder
82                .add_cert(
83                    X509::from_pem(
84                        &ENGINE
85                            .decode(remote_ca_raw)
86                            .context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
87                    )
88                    .context("load CA cert")?,
89                )
90                .context("add CA cert to store builder")?;
91            let store = store_builder.build();
92            tls_config.set_server_verify_cert_store(Arc::new(store));
93        }
94
95        let client = EasyHttpWebClient::connector_builder()
96            .with_default_transport_connector()
97            .without_tls_proxy_support()
98            .without_proxy_support()
99            .with_tls_support_using_boringssl(tls_config)
100            .with_default_http_connector(exec)
101            .build_client();
102
103        let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
104        let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
105            Self::new_with_client(
106                uri,
107                SetRequestHeaderLayer::overriding_typed(Authorization::new(
108                    Bearer::try_from(auth_raw)
109                        .context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
110                ))
111                .into_layer(client),
112            )
113        } else {
114            Self::new_with_client(uri, client)
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: impl Service<
135            Request,
136            Output = Response,
137            Error: std::error::Error + Send + Sync + 'static,
138        >,
139    ) -> Self {
140        let http_client = MapErr::into_opaque_error(client).boxed();
141        Self {
142            endpoint,
143            allow_list: None,
144            http_client,
145        }
146    }
147
148    crate::utils::macros::generate_set_and_with! {
149        /// Only allow fetching certs for the given domain.
150        ///
151        /// By default, if none of the `allow_*` setters are called
152        /// the client will fetch for any client. This is a local pre-filter;
153        /// the remote issuer remains responsible for authorizing each order.
154        pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
155            // The trie's smart insert handles "*.x" -> subtree at x and bare
156            // "x" -> exact at x. The stored value is just the wildcard form
157            // for subtree entries (so norm_cn can return a borrowed ref).
158            let wildcard_form = domain.as_wildcard();
159            self.allow_list
160                .get_or_insert_default()
161                .insert_domain(domain, wildcard_form);
162            self
163        }
164    }
165
166    crate::utils::macros::generate_set_and_with! {
167        /// Only allow fetching certs for the given domains.
168        ///
169        /// By default, if none of the `allow_*` setters are called
170        /// the client will fetch for any client. This is a local pre-filter;
171        /// the remote issuer remains responsible for authorizing each order.
172        pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
173            for domain in domains {
174                self.set_allow_domain(domain);
175            }
176            self
177        }
178    }
179
180    /// Prefetch all certificates, useful to warm them up at startup time.
181    pub async fn prefetch_certs(&self) {
182        if let Some(allow_list) = &self.allow_list {
183            // iter() yields the wildcard form for subtree entries and the
184            // apex for exact entries; both are the issuing form we want.
185            for (domain, _) in allow_list.iter() {
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                BoxError::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.get(domain) {
253                        None => {
254                            return Err(BoxError::from_static_str(
255                                "sni found: unexpected unknown domain",
256                            )
257                            .with_context_field("domain", || domain.clone()));
258                        }
259                        Some(m) => match m.value {
260                            // Subtree match — issue using the stored wildcard form.
261                            Some(wildcard) => wildcard.clone(),
262                            // Exact match — issue for the queried domain itself.
263                            None => domain.clone(),
264                        },
265                    }
266                } else {
267                    domain.clone()
268                }
269            }
270            None => {
271                return Err(BoxError::from_static_str("no SNI found"));
272            }
273        };
274
275        self.fetch_certs(domain).await
276    }
277
278    fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
279        self.allow_list
280            .as_ref()?
281            .get(domain)
282            .and_then(|m| m.value.as_ref())
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_issuer_kind_norm_cn() {
292        let issuer =
293            CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
294                .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
295        for (input, expected) in [
296            ("example.com", None),
297            ("www.foo.com", Some("*.foo.com")),
298            ("bar.foo.com", Some("*.foo.com")),
299            ("bar.example.io", Some("*.example.io")),
300            ("example.net", None),
301            ("foo.example.net", None),
302            ("foo.bar.org", None),
303            ("bar.org", None),
304        ] {
305            let output = issuer
306                .norm_cn(&Domain::from_static(input))
307                .map(|d| d.as_str());
308            assert_eq!(output, expected, "{input:?} ; {expected:?}")
309        }
310    }
311}