1use crate::error::{BoxError, 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 {
45 endpoint: Uri,
46 allow_list: Option<DomainTrie<Option<Domain>>>,
50 http_client: BoxService<Request, Response, OpaqueError>,
51}
52
53impl CertIssuerHttpClient {
54 pub fn new(exec: Executor, endpoint: Uri) -> Self {
56 Self::new_with_client(endpoint, EasyHttpWebClient::default_with_executor(exec))
57 }
58
59 #[cfg(feature = "boring")]
60 #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
61 pub fn try_from_env(exec: Executor) -> Result<Self, BoxError> {
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().context("build x509 store builder")?;
79 store_builder
80 .add_cert(
81 X509::from_pem(
82 &ENGINE
83 .decode(remote_ca_raw)
84 .context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
85 )
86 .context("load CA cert")?,
87 )
88 .context("add CA cert to store builder")?;
89 let store = store_builder.build();
90 tls_config.set_server_verify_cert_store(Arc::new(store));
91 }
92
93 let client = EasyHttpWebClient::connector_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 .with_default_http_connector(exec)
99 .build_client();
100
101 let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
102 let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
103 Self::new_with_client(
104 uri,
105 SetRequestHeaderLayer::overriding_typed(Authorization::new(
106 Bearer::try_from(auth_raw)
107 .context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
108 ))
109 .into_layer(client),
110 )
111 } else {
112 Self::new_with_client(uri, client)
113 };
114
115 if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
116 for raw_cn_str in allow_cn_csv_raw.split(',') {
117 let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
118 client.set_allow_domain(cn);
119 }
120 }
121
122 Ok(client)
123 }
124
125 pub fn new_with_client(
131 endpoint: Uri,
132 client: impl Service<
133 Request,
134 Output = Response,
135 Error: std::error::Error + Send + Sync + 'static,
136 >,
137 ) -> Self {
138 let http_client = MapErr::into_opaque_error(client).boxed();
139 Self {
140 endpoint,
141 allow_list: None,
142 http_client,
143 }
144 }
145
146 crate::utils::macros::generate_set_and_with! {
147 pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
152 let wildcard_form = domain.as_wildcard();
156 self.allow_list
157 .get_or_insert_default()
158 .insert_domain(domain, wildcard_form);
159 self
160 }
161 }
162
163 crate::utils::macros::generate_set_and_with! {
164 pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
169 for domain in domains {
170 self.set_allow_domain(domain);
171 }
172 self
173 }
174 }
175
176 pub async fn prefetch_certs(&self) {
178 if let Some(allow_list) = &self.allow_list {
179 for (domain, _) in allow_list.iter() {
182 match self.fetch_certs(domain.clone()).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 async fn fetch_certs(&self, domain: Domain) -> Result<ServerAuthData, BoxError> {
193 let response = self
194 .http_client
195 .post(self.endpoint.clone())
196 .json(&CertOrderInput { domain })
197 .send()
198 .await
199 .context("send order request")?;
200
201 let status = response.status();
202 if status != StatusCode::OK {
203 return Err(
204 OpaqueError::from_static_str("unexpected dinocert order response")
205 .context_field("status", status),
206 );
207 }
208
209 let CertOrderOutput {
210 crt_pem_base64,
211 key_pem_base64,
212 } = response
213 .into_body()
214 .try_into_json()
215 .await
216 .context("fetch json crt order response")?;
217
218 let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
219 let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
220
221 Ok(ServerAuthData {
222 cert_chain: DataEncoding::Pem(
223 NonEmptyStr::try_from(
224 String::from_utf8(crt).context("concert crt pem to utf8 string")?,
225 )
226 .context("convert crt utf8 string to non-empty")?,
227 ),
228 private_key: DataEncoding::Pem(
229 NonEmptyStr::try_from(
230 String::from_utf8(key).context("concert private key pem to utf8 string")?,
231 )
232 .context("convert privatek key pem utf8 string to non-empty")?,
233 ),
234 ocsp: None,
235 })
236 }
237}
238
239impl DynamicCertIssuer for CertIssuerHttpClient {
240 async fn issue_cert(
241 &self,
242 client_hello: ClientHello,
243 _server_name: Option<Domain>,
244 ) -> Result<ServerAuthData, BoxError> {
245 let domain = match client_hello.ext_server_name() {
246 Some(domain) => {
247 if let Some(ref allow_list) = self.allow_list {
248 match allow_list.get(domain) {
249 None => {
250 return Err(OpaqueError::from_static_str(
251 "sni found: unexpected unknown domain",
252 )
253 .with_context_field("domain", || domain.clone()));
254 }
255 Some(m) => match m.value {
256 Some(wildcard) => wildcard.clone(),
258 None => domain.clone(),
260 },
261 }
262 } else {
263 domain.clone()
264 }
265 }
266 None => {
267 return Err(OpaqueError::from_static_str("no SNI found").into_box_error());
268 }
269 };
270
271 self.fetch_certs(domain).await
272 }
273
274 fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
275 self.allow_list
276 .as_ref()?
277 .get(domain)
278 .and_then(|m| m.value.as_ref())
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_issuer_kind_norm_cn() {
288 let issuer =
289 CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
290 .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
291 for (input, expected) in [
292 ("example.com", None),
293 ("www.foo.com", Some("*.foo.com")),
294 ("bar.foo.com", Some("*.foo.com")),
295 ("bar.example.io", Some("*.example.io")),
296 ("example.net", None),
297 ("foo.example.net", None),
298 ("foo.bar.org", None),
299 ("bar.org", None),
300 ] {
301 let output = issuer
302 .norm_cn(&Domain::from_static(input))
303 .map(|d| d.as_str());
304 assert_eq!(output, expected, "{input:?} ; {expected:?}")
305 }
306 }
307}