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