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::NonEmptyString;
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(endpoint: Uri) -> Self {
57 Self::new_with_client(endpoint, EasyHttpWebClient::default().boxed())
58 }
59
60 #[cfg(feature = "boring")]
61 #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
62 pub fn try_from_env() -> Result<Self, OpaqueError> {
63 use crate::{
64 Layer as _,
65 http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
66 net::user::Bearer,
67 tls::boring::{
68 client::TlsConnectorDataBuilder,
69 core::x509::{X509, store::X509StoreBuilder},
70 },
71 };
72 use std::sync::Arc;
73
74 let uri_raw = std::env::var("RAMA_TLS_REMOTE").context("RAMA_TLS_REMOTE is undefined")?;
75
76 let mut tls_config = TlsConnectorDataBuilder::new_http_auto();
77
78 if let Ok(remote_ca_raw) = std::env::var("RAMA_TLS_REMOTE_CA") {
79 let mut store_builder = X509StoreBuilder::new().context("build x509 store builder")?;
80 store_builder
81 .add_cert(
82 X509::from_pem(
83 &ENGINE
84 .decode(remote_ca_raw)
85 .context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
86 )
87 .context("load CA cert")?,
88 )
89 .context("add CA cert to store builder")?;
90 let store = store_builder.build();
91 tls_config.set_server_verify_cert_store(Arc::new(store));
92 }
93
94 let client = EasyHttpWebClient::builder()
95 .with_default_transport_connector()
96 .without_tls_proxy_support()
97 .without_proxy_support()
98 .with_tls_support_using_boringssl(Some(Arc::new(tls_config)))
99 .with_default_http_connector()
100 .build();
101
102 let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
103 let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
104 Self::new_with_client(
105 uri,
106 SetRequestHeaderLayer::overriding_typed(Authorization::new(
107 Bearer::new(auth_raw)
108 .context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
109 ))
110 .into_layer(client)
111 .boxed(),
112 )
113 } else {
114 Self::new_with_client(uri, client.boxed())
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: BoxService<Request, Response, OpaqueError>,
135 ) -> Self {
136 Self {
137 endpoint,
138 allow_list: None,
139 http_client: client,
140 }
141 }
142
143 crate::utils::macros::generate_set_and_with! {
144 pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
149 if let Some(parent) = domain.as_wildcard_parent() && let Ok(domain) = parent.try_as_wildcard() {
150 self.allow_list.get_or_insert_default().insert_domain(parent, DomainAllowMode::Parent(domain));
151 } else {
152 self.allow_list.get_or_insert_default().insert_domain(domain, DomainAllowMode::Exact);
153 }
154 self
155 }
156 }
157
158 crate::utils::macros::generate_set_and_with! {
159 pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
164 for domain in domains {
165 self.set_allow_domain(domain);
166 }
167 self
168 }
169 }
170
171 pub fn prefetch_certs_in_background(&self, exec: &Executor) {
173 if let Some(allow_list) = &self.allow_list {
174 for (domain_key, mode) in allow_list.iter() {
175 let domain = match mode {
176 DomainAllowMode::Exact => domain_key,
178 DomainAllowMode::Parent(domain) => domain.clone(),
179 };
180 let http_client = self.http_client.clone();
181 let uri = self.endpoint.clone();
182 exec.spawn_task(async move {
183 match fetch_certs(http_client, domain.clone(), uri).await {
184 Ok(_) => tracing::debug!("prefetched certificates for domain: {domain}"),
185 Err(err) => tracing::error!(
186 "failed to prefetch certificates for domain '{domain}': {err}"
187 ),
188 }
189 });
190 }
191 }
192 }
193}
194
195impl DynamicCertIssuer for CertIssuerHttpClient {
196 fn issue_cert(
197 &self,
198 client_hello: ClientHello,
199 _server_name: Option<Domain>,
200 ) -> impl Future<Output = Result<ServerAuthData, OpaqueError>> + Send + Sync + '_ {
201 let domain = match client_hello.ext_server_name() {
202 Some(domain) => {
203 if let Some(ref allow_list) = self.allow_list {
204 match allow_list.match_parent(domain) {
205 None => {
206 return Either::A(std::future::ready(Err(OpaqueError::from_display(
207 "sni found: unexpected unknown domain",
208 ))));
209 }
210 Some(DomainParentMatch {
211 value: &DomainAllowMode::Exact,
212 is_exact,
213 ..
214 }) => {
215 if is_exact {
216 domain.clone()
217 } else {
218 return Either::A(std::future::ready(Err(
219 OpaqueError::from_display("sni found: unexpected child domain"),
220 )));
221 }
222 }
223 Some(DomainParentMatch {
224 value: DomainAllowMode::Parent(wildcard_domain),
225 ..
226 }) => wildcard_domain.clone(),
227 }
228 } else {
229 domain.clone()
230 }
231 }
232 None => {
233 return Either::A(std::future::ready(Err(OpaqueError::from_display(
234 "no SNI found: failure",
235 ))));
236 }
237 };
238
239 let (tx, rx) = tokio::sync::oneshot::channel();
240 let http_client = self.http_client.clone();
241 let uri = self.endpoint.clone();
242
243 tokio::spawn(async move {
244 if let Err(err) = tx.send(fetch_certs(http_client, domain, uri).await) {
245 tracing::debug!("failed to send result back to callee: {err:?}");
246 }
247 });
248
249 Either::B(async move { rx.await.context("await crt order result")? })
250 }
251
252 fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
253 if let Some(ref allow_list) = self.allow_list {
254 match allow_list.match_parent(domain) {
255 None
256 | Some(DomainParentMatch {
257 value: &DomainAllowMode::Exact,
258 ..
259 }) => None,
260 Some(DomainParentMatch {
261 value: DomainAllowMode::Parent(wildcard_domain),
262 ..
263 }) => Some(wildcard_domain),
264 }
265 } else {
266 None
267 }
268 }
269}
270
271async fn fetch_certs(
272 client: BoxService<Request, Response, OpaqueError>,
273 domain: Domain,
274 uri: Uri,
275) -> Result<ServerAuthData, OpaqueError> {
276 let response = client
277 .post(uri)
278 .json(&CertOrderInput { domain })
279 .send()
280 .await
281 .context("send order request")?;
282
283 let status = response.status();
284 if status != StatusCode::OK {
285 return Err(OpaqueError::from_display(format!(
286 "unexpected dinocert order response status code: {status}"
287 )));
288 }
289
290 let CertOrderOutput {
291 crt_pem_base64,
292 key_pem_base64,
293 } = response
294 .into_body()
295 .try_into_json()
296 .await
297 .context("fetch json crt order response")?;
298
299 let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
300 let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
301
302 Ok(ServerAuthData {
303 cert_chain: DataEncoding::Pem(
304 NonEmptyString::try_from(
305 String::from_utf8(crt).context("concert crt pem to utf8 string")?,
306 )
307 .context("convert crt utf8 string to non-empty")?,
308 ),
309 private_key: DataEncoding::Pem(
310 NonEmptyString::try_from(
311 String::from_utf8(key).context("concert private key pem to utf8 string")?,
312 )
313 .context("convert privatek key pem utf8 string to non-empty")?,
314 ),
315 ocsp: None,
316 })
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_issuer_kind_norm_cn() {
325 let issuer = CertIssuerHttpClient::new(Uri::from_static("http://example.com"))
326 .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
327 for (input, expected) in [
328 ("example.com", None),
329 ("www.foo.com", Some("*.foo.com")),
330 ("bar.foo.com", Some("*.foo.com")),
331 ("bar.example.io", Some("*.example.io")),
332 ("example.net", None),
333 ("foo.example.net", None),
334 ("foo.bar.org", None),
335 ("bar.org", None),
336 ] {
337 let output = issuer
338 .norm_cn(&Domain::from_static(input))
339 .map(|d| d.as_str());
340 assert_eq!(output, expected, "{input:?} ; {expected:?}")
341 }
342 }
343}