1use crate::error::{BoxError, BoxErrorExt, ErrorContext as _, ErrorExt as _};
4use crate::http::{
5 BodyExtractExt as _, Request, Response, StatusCode, client::EasyHttpWebClient,
6 service::client::HttpClientExt as _,
7};
8use crate::net::address::{AsDomainRef, Domain, DomainTrie};
9use crate::net::uri::Uri;
10use crate::rt::Executor;
11use crate::telemetry::tracing;
12use crate::tls::server::{
13 CertificateIdentity, CertificateIssuanceContext, DynamicCertIssuer, ServerAuthData,
14};
15use crate::{Service, service::BoxService};
16
17use base64::Engine;
18use base64::engine::general_purpose::STANDARD as ENGINE;
19use rama_core::error::extra::OpaqueError;
20use rama_core::layer::MapErr;
21use rama_crypto::pki_types::pem::PemObject;
22use rama_crypto::pki_types::{CertificateDer, PrivateKeyDer};
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 {
46 endpoint: Uri,
47 allow_list: Option<DomainTrie<Option<Domain>>>,
51 http_client: BoxService<Request, Response, OpaqueError>,
52}
53
54impl CertIssuerHttpClient {
55 pub fn new(exec: Executor, endpoint: Uri) -> Self {
57 Self::new_with_client(endpoint, EasyHttpWebClient::default_with_executor(exec))
58 }
59
60 #[cfg(feature = "boring")]
61 #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
62 pub fn try_from_env(exec: Executor) -> Result<Self, BoxError> {
63 use crate::{
64 Layer as _,
65 http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
66 net::user::Bearer,
67 tls::boring::{
68 client::BoringClientConfigExt as _,
69 core::x509::{X509, store::X509StoreBuilder},
70 },
71 tls::client::TlsClientConfig,
72 };
73 use std::sync::Arc;
74
75 let uri_raw = std::env::var("RAMA_TLS_REMOTE").context("RAMA_TLS_REMOTE is undefined")?;
76
77 let mut tls_config = TlsClientConfig::new().with_alpn_http_auto();
78
79 if let Ok(remote_ca_raw) = std::env::var("RAMA_TLS_REMOTE_CA") {
80 let mut store_builder = X509StoreBuilder::new().context("build x509 store builder")?;
81 store_builder
82 .add_cert(
83 X509::from_pem(
84 &ENGINE
85 .decode(remote_ca_raw)
86 .context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
87 )
88 .context("load CA cert")?,
89 )
90 .context("add CA cert to store builder")?;
91 let store = store_builder.build();
92 tls_config.set_server_verify_cert_store(Arc::new(store));
93 }
94
95 let client = EasyHttpWebClient::connector_builder()
96 .with_default_transport_connector()
97 .with_default_dns_connector()
98 .without_tls_proxy_support()
99 .without_proxy_support()
100 .with_tls_support_using_boringssl(tls_config)
101 .with_default_http_connector(exec)
102 .without_connection_pool()
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 )
115 } else {
116 Self::new_with_client(uri, client)
117 };
118
119 if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
120 for raw_cn_str in allow_cn_csv_raw.split(',') {
121 let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
122 client.set_allow_domain(cn);
123 }
124 }
125
126 Ok(client)
127 }
128
129 pub fn new_with_client(
135 endpoint: Uri,
136 client: impl Service<
137 Request,
138 Output = Response,
139 Error: std::error::Error + Send + Sync + 'static,
140 >,
141 ) -> Self {
142 let http_client = MapErr::into_opaque_error(client).boxed();
143 Self {
144 endpoint,
145 allow_list: None,
146 http_client,
147 }
148 }
149
150 crate::utils::macros::generate_set_and_with! {
151 pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
157 let wildcard_form = domain.as_wildcard();
161 self.allow_list
162 .get_or_insert_default()
163 .insert_domain(domain, wildcard_form);
164 self
165 }
166 }
167
168 crate::utils::macros::generate_set_and_with! {
169 pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
175 for domain in domains {
176 self.set_allow_domain(domain);
177 }
178 self
179 }
180 }
181
182 pub async fn prefetch_certs(&self) {
184 if let Some(allow_list) = &self.allow_list {
185 for (domain, _) in allow_list.iter() {
188 match self.fetch_certs(domain.clone()).await {
189 Ok(_) => tracing::debug!("prefetched certificates for domain: {domain}"),
190 Err(err) => tracing::error!(
191 "failed to prefetch certificates for domain '{domain}': {err}"
192 ),
193 }
194 }
195 }
196 }
197
198 async fn fetch_certs(&self, domain: Domain) -> Result<ServerAuthData, BoxError> {
199 let response = self
200 .http_client
201 .post(self.endpoint.clone())
202 .json(&CertOrderInput { domain })
203 .send()
204 .await
205 .context("send order request")?;
206
207 let status = response.status();
208 if status != StatusCode::OK {
209 return Err(
210 BoxError::from_static_str("unexpected dinocert order response")
211 .context_field("status", status),
212 );
213 }
214
215 let CertOrderOutput {
216 crt_pem_base64,
217 key_pem_base64,
218 } = response
219 .into_body()
220 .try_into_json()
221 .await
222 .context("fetch json crt order response")?;
223
224 let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
225 let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
226
227 let cert_chain = CertificateDer::pem_slice_iter(&crt)
228 .collect::<Result<Vec<_>, _>>()
229 .context("parse crt pem chain")?;
230 let private_key =
231 PrivateKeyDer::from_pem_slice(key.as_slice()).context("parse private key")?;
232
233 Ok(ServerAuthData {
234 cert_chain,
235 private_key,
236 ocsp: None,
237 })
238 }
239}
240
241impl DynamicCertIssuer for CertIssuerHttpClient {
242 async fn issue_cert(
243 &self,
244 context: CertificateIssuanceContext,
245 ) -> Result<ServerAuthData, BoxError> {
246 let domain = match context.server_identity {
247 Some(CertificateIdentity::Dns(domain)) => {
248 if let Some(ref allow_list) = self.allow_list {
249 match allow_list.get(&domain) {
250 None => {
251 return Err(BoxError::from_static_str(
252 "sni found: unexpected unknown domain",
253 )
254 .with_context_field("domain", || domain.clone()));
255 }
256 Some(m) => match m.value {
257 Some(wildcard) => wildcard.clone(),
259 None => domain,
261 },
262 }
263 } else {
264 domain
265 }
266 }
267 Some(CertificateIdentity::Ip(ip)) => {
268 return Err(BoxError::from_static_str(
269 "remote certificate issuer only supports DNS identities",
270 )
271 .context_field("ip", ip));
272 }
273 None => {
274 return Err(BoxError::from_static_str("no server identity found"));
275 }
276 };
277
278 self.fetch_certs(domain).await
279 }
280
281 fn normalize_identity(&self, identity: &CertificateIdentity) -> Option<CertificateIdentity> {
282 let CertificateIdentity::Dns(domain) = identity else {
283 return None;
284 };
285 self.allow_list
286 .as_ref()?
287 .get(domain)
288 .and_then(|m| m.value.as_ref())
289 .cloned()
290 .map(CertificateIdentity::Dns)
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_issuer_kind_normalize_identity() {
300 let issuer =
301 CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
302 .with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
303 for (input, expected) in [
304 ("example.com", None),
305 ("www.foo.com", Some("*.foo.com")),
306 ("bar.foo.com", Some("*.foo.com")),
307 ("bar.example.io", Some("*.example.io")),
308 ("example.net", None),
309 ("foo.example.net", None),
310 ("foo.bar.org", None),
311 ("bar.org", None),
312 ] {
313 let output = issuer
314 .normalize_identity(&CertificateIdentity::Dns(Domain::from_static(input)))
315 .and_then(|identity| match identity {
316 CertificateIdentity::Dns(domain) => Some(domain),
317 CertificateIdentity::Ip(_) => None,
318 });
319 assert_eq!(
320 output.as_ref().map(Domain::as_str),
321 expected,
322 "{input:?} ; {expected:?}"
323 )
324 }
325 }
326}