use crate::error::{BoxError, BoxErrorExt, ErrorContext as _, ErrorExt as _};
use crate::http::{
BodyExtractExt as _, Request, Response, StatusCode, client::EasyHttpWebClient,
service::client::HttpClientExt as _,
};
use crate::net::address::{AsDomainRef, Domain, DomainTrie};
use crate::net::uri::Uri;
use crate::rt::Executor;
use crate::telemetry::tracing;
use crate::tls::{
client::ClientHello,
server::{DynamicCertIssuer, ServerAuthData},
};
use crate::{Service, service::BoxService};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as ENGINE;
use rama_core::error::extra::OpaqueError;
use rama_core::layer::MapErr;
use rama_crypto::pki_types::pem::PemObject;
use rama_crypto::pki_types::{CertificateDer, PrivateKeyDer};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertOrderInput {
pub domain: Domain,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CertOrderOutput {
pub crt_pem_base64: String,
pub key_pem_base64: String,
}
#[derive(Debug)]
pub struct CertIssuerHttpClient {
endpoint: Uri,
allow_list: Option<DomainTrie<Option<Domain>>>,
http_client: BoxService<Request, Response, OpaqueError>,
}
impl CertIssuerHttpClient {
pub fn new(exec: Executor, endpoint: Uri) -> Self {
Self::new_with_client(endpoint, EasyHttpWebClient::default_with_executor(exec))
}
#[cfg(feature = "boring")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
pub fn try_from_env(exec: Executor) -> Result<Self, BoxError> {
use crate::{
Layer as _,
http::{headers::Authorization, layer::set_header::SetRequestHeaderLayer},
net::user::Bearer,
tls::boring::{
client::BoringClientConfigExt as _,
core::x509::{X509, store::X509StoreBuilder},
},
tls::client::TlsClientConfig,
};
use std::sync::Arc;
let uri_raw = std::env::var("RAMA_TLS_REMOTE").context("RAMA_TLS_REMOTE is undefined")?;
let mut tls_config = TlsClientConfig::new().with_alpn_http_auto();
if let Ok(remote_ca_raw) = std::env::var("RAMA_TLS_REMOTE_CA") {
let mut store_builder = X509StoreBuilder::new().context("build x509 store builder")?;
store_builder
.add_cert(
X509::from_pem(
&ENGINE
.decode(remote_ca_raw)
.context("base64 decode RAMA_TLS_REMOTE_CA")?[..],
)
.context("load CA cert")?,
)
.context("add CA cert to store builder")?;
let store = store_builder.build();
tls_config.set_server_verify_cert_store(Arc::new(store));
}
let client = EasyHttpWebClient::connector_builder()
.with_default_transport_connector()
.with_default_dns_connector()
.without_tls_proxy_support()
.without_proxy_support()
.with_tls_support_using_boringssl(tls_config)
.with_default_http_connector(exec)
.build_client();
let uri: Uri = uri_raw.parse().context("parse RAMA_TLS_REMOTE as URI")?;
let mut client = if let Ok(auth_raw) = std::env::var("RAMA_TLS_REMOTE_AUTH") {
Self::new_with_client(
uri,
SetRequestHeaderLayer::overriding_typed(Authorization::new(
Bearer::try_from(auth_raw)
.context("try to create Bearer using RAMA_TLS_REMOTE_AUTH")?,
))
.into_layer(client),
)
} else {
Self::new_with_client(uri, client)
};
if let Ok(allow_cn_csv_raw) = std::env::var("RAMA_TLS_REMOTE_CN_CSV") {
for raw_cn_str in allow_cn_csv_raw.split(',') {
let cn: Domain = raw_cn_str.parse().context("parse CN as a a valid domain")?;
client.set_allow_domain(cn);
}
}
Ok(client)
}
pub fn new_with_client(
endpoint: Uri,
client: impl Service<
Request,
Output = Response,
Error: std::error::Error + Send + Sync + 'static,
>,
) -> Self {
let http_client = MapErr::into_opaque_error(client).boxed();
Self {
endpoint,
allow_list: None,
http_client,
}
}
crate::utils::macros::generate_set_and_with! {
pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
let wildcard_form = domain.as_wildcard();
self.allow_list
.get_or_insert_default()
.insert_domain(domain, wildcard_form);
self
}
}
crate::utils::macros::generate_set_and_with! {
pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
for domain in domains {
self.set_allow_domain(domain);
}
self
}
}
pub async fn prefetch_certs(&self) {
if let Some(allow_list) = &self.allow_list {
for (domain, _) in allow_list.iter() {
match self.fetch_certs(domain.clone()).await {
Ok(_) => tracing::debug!("prefetched certificates for domain: {domain}"),
Err(err) => tracing::error!(
"failed to prefetch certificates for domain '{domain}': {err}"
),
}
}
}
}
async fn fetch_certs(&self, domain: Domain) -> Result<ServerAuthData, BoxError> {
let response = self
.http_client
.post(self.endpoint.clone())
.json(&CertOrderInput { domain })
.send()
.await
.context("send order request")?;
let status = response.status();
if status != StatusCode::OK {
return Err(
BoxError::from_static_str("unexpected dinocert order response")
.context_field("status", status),
);
}
let CertOrderOutput {
crt_pem_base64,
key_pem_base64,
} = response
.into_body()
.try_into_json()
.await
.context("fetch json crt order response")?;
let crt = ENGINE.decode(crt_pem_base64).context("base64 decode crt")?;
let key = ENGINE.decode(key_pem_base64).context("base64 decode crt")?;
let cert_chain = CertificateDer::pem_slice_iter(&crt)
.collect::<Result<Vec<_>, _>>()
.context("parse crt pem chain")?;
let private_key =
PrivateKeyDer::from_pem_slice(key.as_slice()).context("parse private key")?;
Ok(ServerAuthData {
cert_chain,
private_key,
ocsp: None,
})
}
}
impl DynamicCertIssuer for CertIssuerHttpClient {
async fn issue_cert(
&self,
client_hello: ClientHello,
_server_name: Option<Domain>,
) -> Result<ServerAuthData, BoxError> {
let domain = match client_hello.ext_server_name() {
Some(domain) => {
if let Some(ref allow_list) = self.allow_list {
match allow_list.get(domain) {
None => {
return Err(BoxError::from_static_str(
"sni found: unexpected unknown domain",
)
.with_context_field("domain", || domain.clone()));
}
Some(m) => match m.value {
Some(wildcard) => wildcard.clone(),
None => domain.clone(),
},
}
} else {
domain.clone()
}
}
None => {
return Err(BoxError::from_static_str("no SNI found"));
}
};
self.fetch_certs(domain).await
}
fn norm_cn(&self, domain: &Domain) -> Option<&Domain> {
self.allow_list
.as_ref()?
.get(domain)
.and_then(|m| m.value.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_issuer_kind_norm_cn() {
let issuer =
CertIssuerHttpClient::new(Executor::default(), Uri::from_static("http://example.com"))
.with_allow_domains(["*.foo.com", "bar.org", "*.example.io", "example.net"]);
for (input, expected) in [
("example.com", None),
("www.foo.com", Some("*.foo.com")),
("bar.foo.com", Some("*.foo.com")),
("bar.example.io", Some("*.example.io")),
("example.net", None),
("foo.example.net", None),
("foo.bar.org", None),
("bar.org", None),
] {
let output = issuer
.norm_cn(&Domain::from_static(input))
.map(|d| d.as_str());
assert_eq!(output, expected, "{input:?} ; {expected:?}")
}
}
}