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