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, 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, 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<DomainAllowMode>>,
47 http_client: BoxService<Request, Response, OpaqueError>,
48}
49
50#[derive(Debug, Clone)]
51enum DomainAllowMode {
52 Exact,
53 Parent(Domain),
54}
55
56impl CertIssuerHttpClient {
57 pub fn new(exec: Executor, endpoint: Uri) -> Self {
59 Self::new_with_client(endpoint, EasyHttpWebClient::default_with_executor(exec))
60 }
61
62 #[cfg(feature = "boring")]
63 #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
64 pub fn try_from_env(exec: Executor) -> Result<Self, BoxError> {
65 use crate::{
66 Layer as _,
67 http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
68 net::user::Bearer,
69 tls::boring::{
70 client::TlsConnectorDataBuilder,
71 core::x509::{X509, store::X509StoreBuilder},
72 },
73 };
74 use std::sync::Arc;
75
76 let uri_raw = std::env::var("RAMA_TLS_REMOTE").context("RAMA_TLS_REMOTE is undefined")?;
77
78 let mut tls_config = TlsConnectorDataBuilder::new_http_auto();
79
80 if let Ok(remote_ca_raw) = std::env::var("RAMA_TLS_REMOTE_CA") {
81 let mut store_builder = X509StoreBuilder::new().context("build x509 store builder")?;
82 store_builder
83 .add_cert(
84 X509::from_pem(
85 &ENGINE
86 .decode(remote_ca_raw)
87 .context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
88 )
89 .context("load CA cert")?,
90 )
91 .context("add CA cert to store builder")?;
92 let store = store_builder.build();
93 tls_config.set_server_verify_cert_store(Arc::new(store));
94 }
95
96 let client = EasyHttpWebClient::connector_builder()
97 .with_default_transport_connector()
98 .without_tls_proxy_support()
99 .without_proxy_support()
100 .with_tls_support_using_boringssl(Some(Arc::new(tls_config)))
101 .with_default_http_connector(exec)
102 .build_client();
103
104 let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
105 let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
106 Self::new_with_client(
107 uri,
108 SetRequestHeaderLayer::overriding_typed(Authorization::new(
109 Bearer::try_from(auth_raw)
110 .context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
111 ))
112 .into_layer(client),
113 )
114 } else {
115 Self::new_with_client(uri, client)
116 };
117
118 if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
119 for raw_cn_str in allow_cn_csv_raw.split(',') {
120 let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
121 client.set_allow_domain(cn);
122 }
123 }
124
125 Ok(client)
126 }
127
128 pub fn new_with_client(
134 endpoint: Uri,
135 client: impl Service<
136 Request,
137 Output = Response,
138 Error: std::error::Error + Send + Sync + 'static,
139 >,
140 ) -> Self {
141 let http_client = MapErr::into_opaque_error(client).boxed();
142 Self {
143 endpoint,
144 allow_list: None,
145 http_client,
146 }
147 }
148
149 crate::utils::macros::generate_set_and_with! {
150 pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
155 if let Some(parent) = domain.as_wildcard_parent() && let Ok(domain) = parent.try_as_wildcard() {
156 self.allow_list.get_or_insert_default().insert_domain(parent, DomainAllowMode::Parent(domain));
157 } else {
158 self.allow_list.get_or_insert_default().insert_domain(domain, DomainAllowMode::Exact);
159 }
160 self
161 }
162 }
163
164 crate::utils::macros::generate_set_and_with! {
165 pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
170 for domain in domains {
171 self.set_allow_domain(domain);
172 }
173 self
174 }
175 }
176
177 pub async fn prefetch_certs(&self) {
179 if let Some(allow_list) = &self.allow_list {
180 for (domain_key, mode) in allow_list.iter() {
181 let domain = match mode {
182 DomainAllowMode::Exact => domain_key,
184 DomainAllowMode::Parent(domain) => domain.clone(),
185 };
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(BoxError::from("unexpected dinocert order response")
208 .context_field("status", status));
209 }
210
211 let CertOrderOutput {
212 crt_pem_base64,
213 key_pem_base64,
214 } = response
215 .into_body()
216 .try_into_json()
217 .await
218 .context("fetch json crt order response")?;
219
220 let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
221 let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
222
223 Ok(ServerAuthData {
224 cert_chain: DataEncoding::Pem(
225 NonEmptyStr::try_from(
226 String::from_utf8(crt).context("concert crt pem to utf8 string")?,
227 )
228 .context("convert crt utf8 string to non-empty")?,
229 ),
230 private_key: DataEncoding::Pem(
231 NonEmptyStr::try_from(
232 String::from_utf8(key).context("concert private key pem to utf8 string")?,
233 )
234 .context("convert privatek key pem utf8 string to non-empty")?,
235 ),
236 ocsp: None,
237 })
238 }
239}
240
241impl DynamicCertIssuer for CertIssuerHttpClient {
242 async fn issue_cert(
243 &self,
244 client_hello: ClientHello,
245 _server_name: Option<Domain>,
246 ) -> Result<ServerAuthData, BoxError> {
247 let domain = match client_hello.ext_server_name() {
248 Some(domain) => {
249 if let Some(ref allow_list) = self.allow_list {
250 match allow_list.match_parent(domain) {
251 None => {
252 return Err(BoxError::from("sni found: unexpected unknown domain")
253 .with_context_field("domain", || domain.clone()));
254 }
255 Some(DomainParentMatch {
256 value: &DomainAllowMode::Exact,
257 is_exact,
258 ..
259 }) => {
260 if is_exact {
261 domain.clone()
262 } else {
263 return Err(BoxError::from("sni found: unexpected child domain")
264 .with_context_field("domain", || domain.clone()));
265 }
266 }
267 Some(DomainParentMatch {
268 value: DomainAllowMode::Parent(wildcard_domain),
269 ..
270 }) => wildcard_domain.clone(),
271 }
272 } else {
273 domain.clone()
274 }
275 }
276 None => {
277 return Err(BoxError::from("no SNI found"));
278 }
279 };
280
281 self.fetch_certs(domain).await
282 }
283
284 fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
285 if let Some(ref allow_list) = self.allow_list {
286 match allow_list.match_parent(domain) {
287 None
288 | Some(DomainParentMatch {
289 value: &DomainAllowMode::Exact,
290 ..
291 }) => None,
292 Some(DomainParentMatch {
293 value: DomainAllowMode::Parent(wildcard_domain),
294 ..
295 }) => Some(wildcard_domain),
296 }
297 } else {
298 None
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_issuer_kind_norm_cn() {
309 let issuer =
310 CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
311 .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
312 for (input, expected) in [
313 ("example.com", None),
314 ("www.foo.com", Some("*.foo.com")),
315 ("bar.foo.com", Some("*.foo.com")),
316 ("bar.example.io", Some("*.example.io")),
317 ("example.net", None),
318 ("foo.example.net", None),
319 ("foo.bar.org", None),
320 ("bar.org", None),
321 ] {
322 let output = issuer
323 .norm_cn(&Domain::from_static(input))
324 .map(|d| d.as_str());
325 assert_eq!(output, expected, "{input:?} ; {expected:?}")
326 }
327 }
328}