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::graceful::WeakScope;
5use crate::http::{
6    BodyExtractExt as _, Request, Response, StatusCode, client::EasyHttpWebClient,
7    service::client::HttpClientExt as _,
8};
9use crate::net::address::{AsDomainRef, Domain, DomainTrie};
10use crate::net::uri::Uri;
11use crate::telemetry::tracing;
12use crate::tls::server::{
13    CertificateIdentity, CertificateIssuanceContext, DynamicCertIssuer, ServerAuthData,
14};
15use crate::{Service, service::BoxService};
16
17use base64::Engine;
18use base64::engine::general_purpose::STANDARD as ENGINE;
19use rama_core::error::extra::OpaqueError;
20use rama_core::layer::MapErr;
21use rama_crypto::pki_types::pem::PemObject;
22use rama_crypto::pki_types::{CertificateDer, PrivateKeyDer};
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    // `normalize_identity` can return the wildcard as the cache key.
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(scope: impl Into<WeakScope>, endpoint: Uri) -> Self {
57        let scope = scope.into();
58        Self::new_with_client(endpoint, EasyHttpWebClient::default_with_scope(scope))
59    }
60
61    #[cfg(feature = "boring")]
62    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
63    pub fn try_from_env(scope: impl Into<WeakScope>) -> Result<Self, BoxError> {
64        let scope = scope.into();
65        use crate::{
66            Layer as _,
67            http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
68            net::user::Bearer,
69            tls::boring::{
70                client::BoringClientConfigExt as _,
71                core::x509::{X509, store::X509StoreBuilder},
72            },
73            tls::client::TlsClientConfig,
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 = TlsClientConfig::new().with_alpn_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            .with_default_dns_connector()
100            .without_tls_proxy_support()
101            .without_proxy_support()
102            .with_tls_support_using_boringssl(tls_config)
103            .with_default_http_connector(scope)
104            .without_connection_pool()
105            .build_client();
106
107        let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
108        let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
109            Self::new_with_client(
110                uri,
111                SetRequestHeaderLayer::overriding_typed(Authorization::new(
112                    Bearer::try_from(auth_raw)
113                        .context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
114                ))
115                .into_layer(client),
116            )
117        } else {
118            Self::new_with_client(uri, client)
119        };
120
121        if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
122            for raw_cn_str in allow_cn_csv_raw.split(',') {
123                let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
124                client.set_allow_domain(cn);
125            }
126        }
127
128        Ok(client)
129    }
130
131    /// Create a new [`CertIssuerHttpClient`] using a custom http client.
132    ///
133    /// The custom http client allows you to add whatever layers and client implementation
134    /// you wish, to allow for custom headers, behaviour and security measures
135    /// such as authorization.
136    pub fn new_with_client(
137        endpoint: Uri,
138        client: impl Service<
139            Request,
140            Output = Response,
141            Error: std::error::Error + Send + Sync + 'static,
142        >,
143    ) -> Self {
144        let http_client = MapErr::into_opaque_error(client).boxed();
145        Self {
146            endpoint,
147            allow_list: None,
148            http_client,
149        }
150    }
151
152    crate::utils::macros::generate_set_and_with! {
153        /// Only allow fetching certs for the given domain.
154        ///
155        /// By default, if none of the `allow_*` setters are called
156        /// the client will fetch for any client. This is a local pre-filter;
157        /// the remote issuer remains responsible for authorizing each order.
158        pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
159            // The trie's smart insert handles "*.x" -> subtree at x and bare
160            // "x" -> exact at x. The stored value is just the wildcard form
161            // for subtree entries (so normalize_identity can return it).
162            let wildcard_form = domain.as_wildcard();
163            self.allow_list
164                .get_or_insert_default()
165                .insert_domain(domain, wildcard_form);
166            self
167        }
168    }
169
170    crate::utils::macros::generate_set_and_with! {
171        /// Only allow fetching certs for the given domains.
172        ///
173        /// By default, if none of the `allow_*` setters are called
174        /// the client will fetch for any client. This is a local pre-filter;
175        /// the remote issuer remains responsible for authorizing each order.
176        pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
177            for domain in domains {
178                self.set_allow_domain(domain);
179            }
180            self
181        }
182    }
183
184    /// Prefetch all certificates, useful to warm them up at startup time.
185    pub async fn prefetch_certs(&self) {
186        if let Some(allow_list) = &self.allow_list {
187            // iter() yields the wildcard form for subtree entries and the
188            // apex for exact entries; both are the issuing form we want.
189            for (domain, _) in allow_list.iter() {
190                match self.fetch_certs(domain.clone()).await {
191                    Ok(_) => tracing::debug!("prefetched certificates for domain: {domain}"),
192                    Err(err) => tracing::error!(
193                        "failed to prefetch certificates for domain '{domain}': {err}"
194                    ),
195                }
196            }
197        }
198    }
199
200    async fn fetch_certs(&self, domain: Domain) -> Result<ServerAuthData, BoxError> {
201        let response = self
202            .http_client
203            .post(self.endpoint.clone())
204            .json(&CertOrderInput { domain })
205            .send()
206            .await
207            .context("send order request")?;
208
209        let status = response.status();
210        if status != StatusCode::OK {
211            return Err(
212                BoxError::from_static_str("unexpected dinocert order response")
213                    .context_field("status", status),
214            );
215        }
216
217        let CertOrderOutput {
218            crt_pem_base64,
219            key_pem_base64,
220        } = response
221            .into_body()
222            .try_into_json()
223            .await
224            .context("fetch json crt order response")?;
225
226        let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
227        let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
228
229        let cert_chain = CertificateDer::pem_slice_iter(&crt)
230            .collect::<Result<Vec<_>, _>>()
231            .context("parse crt pem chain")?;
232        let private_key =
233            PrivateKeyDer::from_pem_slice(key.as_slice()).context("parse private key")?;
234
235        Ok(ServerAuthData {
236            cert_chain,
237            private_key,
238            ocsp: None,
239        })
240    }
241}
242
243impl DynamicCertIssuer for CertIssuerHttpClient {
244    async fn issue_cert(
245        &self,
246        context: CertificateIssuanceContext,
247    ) -> Result<ServerAuthData, BoxError> {
248        let domain = match context.server_identity {
249            Some(CertificateIdentity::Dns(domain)) => {
250                if let Some(ref allow_list) = self.allow_list {
251                    match allow_list.get(&domain) {
252                        None => {
253                            return Err(BoxError::from_static_str(
254                                "sni found: unexpected unknown domain",
255                            )
256                            .with_context_field("domain", || domain.clone()));
257                        }
258                        Some(m) => match m.value {
259                            // Subtree match — issue using the stored wildcard form.
260                            Some(wildcard) => wildcard.clone(),
261                            // Exact match — issue for the queried domain itself.
262                            None => domain,
263                        },
264                    }
265                } else {
266                    domain
267                }
268            }
269            Some(CertificateIdentity::Ip(ip)) => {
270                return Err(BoxError::from_static_str(
271                    "remote certificate issuer only supports DNS identities",
272                )
273                .context_field("ip", ip));
274            }
275            None => {
276                return Err(BoxError::from_static_str("no server identity found"));
277            }
278        };
279
280        self.fetch_certs(domain).await
281    }
282
283    fn normalize_identity(&self, identity: &CertificateIdentity) -> Option<CertificateIdentity> {
284        let CertificateIdentity::Dns(domain) = identity else {
285            return None;
286        };
287        self.allow_list
288            .as_ref()?
289            .get(domain)
290            .and_then(|m| m.value.as_ref())
291            .cloned()
292            .map(CertificateIdentity::Dns)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_issuer_kind_normalize_identity() {
302        let issuer = CertIssuerHttpClient::new(
303            WeakScope::detached(),
304            Uri::from_static("http://example.com"),
305        )
306        .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
307        for (input, expected) in [
308            ("example.com", None),
309            ("www.foo.com", Some("*.foo.com")),
310            ("bar.foo.com", Some("*.foo.com")),
311            ("bar.example.io", Some("*.example.io")),
312            ("example.net", None),
313            ("foo.example.net", None),
314            ("foo.bar.org", None),
315            ("bar.org", None),
316        ] {
317            let output = issuer
318                .normalize_identity(&CertificateIdentity::Dns(Domain::from_static(input)))
319                .and_then(|identity| match identity {
320                    CertificateIdentity::Dns(domain) => Some(domain),
321                    CertificateIdentity::Ip(_) => None,
322                });
323            assert_eq!(
324                output.as_ref().map(Domain::as_str),
325                expected,
326                "{input:?} ; {expected:?}"
327            )
328        }
329    }
330}