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