1use 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::NonEmptyStr;
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)]
24pub struct CertOrderInput {
26 pub domain: Domain,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct CertOrderOutput {
33 pub crt_pem_base64: String,
34 pub key_pem_base64: String,
35}
36
37#[derive(Debug)]
38pub 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 pub fn new(exec: Executor, endpoint: Uri) -> Self {
57 Self::new_with_client(
58 endpoint,
59 EasyHttpWebClient::default_with_executor(exec).boxed(),
60 )
61 }
62
63 #[cfg(feature = "boring")]
64 #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
65 pub fn try_from_env(exec: Executor) -> Result<Self, OpaqueError> {
66 use crate::{
67 Layer as _,
68 http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
69 net::user::Bearer,
70 tls::boring::{
71 client::TlsConnectorDataBuilder,
72 core::x509::{X509, store::X509StoreBuilder},
73 },
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 = TlsConnectorDataBuilder::new_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 .without_tls_proxy_support()
100 .without_proxy_support()
101 .with_tls_support_using_boringssl(Some(Arc::new(tls_config)))
102 .with_default_http_connector(exec)
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 .boxed(),
115 )
116 } else {
117 Self::new_with_client(uri, client.boxed())
118 };
119
120 if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
121 for raw_cn_str in allow_cn_csv_raw.split(',') {
122 let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
123 client.set_allow_domain(cn);
124 }
125 }
126
127 Ok(client)
128 }
129
130 pub fn new_with_client(
136 endpoint: Uri,
137 client: BoxService<Request, Response, OpaqueError>,
138 ) -> Self {
139 Self {
140 endpoint,
141 allow_list: None,
142 http_client: client,
143 }
144 }
145
146 crate::utils::macros::generate_set_and_with! {
147 pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
152 if let Some(parent) = domain.as_wildcard_parent() && let Ok(domain) = parent.try_as_wildcard() {
153 self.allow_list.get_or_insert_default().insert_domain(parent, DomainAllowMode::Parent(domain));
154 } else {
155 self.allow_list.get_or_insert_default().insert_domain(domain, DomainAllowMode::Exact);
156 }
157 self
158 }
159 }
160
161 crate::utils::macros::generate_set_and_with! {
162 pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
167 for domain in domains {
168 self.set_allow_domain(domain);
169 }
170 self
171 }
172 }
173
174 pub fn prefetch_certs_in_background(&self, exec: &Executor) {
176 if let Some(allow_list) = &self.allow_list {
177 for (domain_key, mode) in allow_list.iter() {
178 let domain = match mode {
179 DomainAllowMode::Exact => domain_key,
181 DomainAllowMode::Parent(domain) => domain.clone(),
182 };
183 let http_client = self.http_client.clone();
184 let uri = self.endpoint.clone();
185 exec.spawn_task(async move {
186 match fetch_certs(http_client, domain.clone(), uri).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}
197
198impl DynamicCertIssuer for CertIssuerHttpClient {
199 fn issue_cert(
200 &self,
201 client_hello: ClientHello,
202 _server_name: Option<Domain>,
203 ) -> impl Future<Output = Result<ServerAuthData, OpaqueError>> + Send + Sync + '_ {
204 let domain = match client_hello.ext_server_name() {
205 Some(domain) => {
206 if let Some(ref allow_list) = self.allow_list {
207 match allow_list.match_parent(domain) {
208 None => {
209 return Either::A(std::future::ready(Err(OpaqueError::from_display(
210 "sni found: unexpected unknown domain",
211 ))));
212 }
213 Some(DomainParentMatch {
214 value: &DomainAllowMode::Exact,
215 is_exact,
216 ..
217 }) => {
218 if is_exact {
219 domain.clone()
220 } else {
221 return Either::A(std::future::ready(Err(
222 OpaqueError::from_display("sni found: unexpected child domain"),
223 )));
224 }
225 }
226 Some(DomainParentMatch {
227 value: DomainAllowMode::Parent(wildcard_domain),
228 ..
229 }) => wildcard_domain.clone(),
230 }
231 } else {
232 domain.clone()
233 }
234 }
235 None => {
236 return Either::A(std::future::ready(Err(OpaqueError::from_display(
237 "no SNI found: failure",
238 ))));
239 }
240 };
241
242 let (tx, rx) = tokio::sync::oneshot::channel();
243 let http_client = self.http_client.clone();
244 let uri = self.endpoint.clone();
245
246 tokio::spawn(async move {
247 if let Err(err) = tx.send(fetch_certs(http_client, domain, uri).await) {
248 tracing::debug!("failed to send result back to callee: {err:?}");
249 }
250 });
251
252 Either::B(async move { rx.await.context("await crt order result")? })
253 }
254
255 fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
256 if let Some(ref allow_list) = self.allow_list {
257 match allow_list.match_parent(domain) {
258 None
259 | Some(DomainParentMatch {
260 value: &DomainAllowMode::Exact,
261 ..
262 }) => None,
263 Some(DomainParentMatch {
264 value: DomainAllowMode::Parent(wildcard_domain),
265 ..
266 }) => Some(wildcard_domain),
267 }
268 } else {
269 None
270 }
271 }
272}
273
274async fn fetch_certs(
275 client: BoxService<Request, Response, OpaqueError>,
276 domain: Domain,
277 uri: Uri,
278) -> Result<ServerAuthData, OpaqueError> {
279 let response = client
280 .post(uri)
281 .json(&CertOrderInput { domain })
282 .send()
283 .await
284 .context("send order request")?;
285
286 let status = response.status();
287 if status != StatusCode::OK {
288 return Err(OpaqueError::from_display(format!(
289 "unexpected dinocert order response status code: {status}"
290 )));
291 }
292
293 let CertOrderOutput {
294 crt_pem_base64,
295 key_pem_base64,
296 } = response
297 .into_body()
298 .try_into_json()
299 .await
300 .context("fetch json crt order response")?;
301
302 let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
303 let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
304
305 Ok(ServerAuthData {
306 cert_chain: DataEncoding::Pem(
307 NonEmptyStr::try_from(
308 String::from_utf8(crt).context("concert crt pem to utf8 string")?,
309 )
310 .context("convert crt utf8 string to non-empty")?,
311 ),
312 private_key: DataEncoding::Pem(
313 NonEmptyStr::try_from(
314 String::from_utf8(key).context("concert private key pem to utf8 string")?,
315 )
316 .context("convert privatek key pem utf8 string to non-empty")?,
317 ),
318 ocsp: None,
319 })
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_issuer_kind_norm_cn() {
328 let issuer =
329 CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
330 .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
331 for (input, expected) in [
332 ("example.com", None),
333 ("www.foo.com", Some("*.foo.com")),
334 ("bar.foo.com", Some("*.foo.com")),
335 ("bar.example.io", Some("*.example.io")),
336 ("example.net", None),
337 ("foo.example.net", None),
338 ("foo.bar.org", None),
339 ("bar.org", None),
340 ] {
341 let output = issuer
342 .norm_cn(&Domain::from_static(input))
343 .map(|d| d.as_str());
344 assert_eq!(output, expected, "{input:?} ; {expected:?}")
345 }
346 }
347}