1use 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)]
26pub struct CertOrderInput {
28 pub domain: Domain,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct CertOrderOutput {
35 pub crt_pem_base64: String,
36 pub key_pem_base64: String,
37}
38
39#[derive(Debug)]
40pub struct CertIssuerHttpClient {
46 endpoint: Uri,
47 allow_list: Option<DomainTrie<Option<Domain>>>,
51 http_client: BoxService<Request, Response, OpaqueError>,
52}
53
54impl CertIssuerHttpClient {
55 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 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 pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
155 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 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 pub async fn prefetch_certs(&self) {
182 if let Some(allow_list) = &self.allow_list {
183 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 Some(wildcard) => wildcard.clone(),
262 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}