rama 0.3.0-rc1

modular service framework
//! tls features provided from the http layer.

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)]
/// Json input used as http (POST) request payload sent by the [`CertIssuerHttpClient`].
pub struct CertOrderInput {
    pub domain: Domain,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
/// Json payload expected in
/// the http (POST) response payload as received by the [`CertIssuerHttpClient`].
pub struct CertOrderOutput {
    pub crt_pem_base64: String,
    pub key_pem_base64: String,
}

#[derive(Debug)]
/// An http client used to fetch certs dynamically ([`DynamicCertIssuer`]).
///
/// There is no server implementation in Rama.
/// It is up to the user of this client to provide their own server, including
/// authentication and authorization for every certificate order.
pub struct CertIssuerHttpClient {
    endpoint: Uri,
    // Trie value `None` means an exact entry; `Some(wildcard)` is a subtree
    // entry, storing the issuing-form wildcard (e.g. `"*.foo.com"`) so
    // `norm_cn` can hand it back as a borrowed reference.
    allow_list: Option<DomainTrie<Option<Domain>>>,
    http_client: BoxService<Request, Response, OpaqueError>,
}

impl CertIssuerHttpClient {
    /// Create a new [`CertIssuerHttpClient`] using the default [`EasyHttpWebClient`].
    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)
    }

    /// Create a new [`CertIssuerHttpClient`] using a custom http client.
    ///
    /// The custom http client allows you to add whatever layers and client implementation
    /// you wish, to allow for custom headers, behaviour and security measures
    /// such as authorization.
    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! {
        /// Only allow fetching certs for the given domain.
        ///
        /// By default, if none of the `allow_*` setters are called
        /// the client will fetch for any client. This is a local pre-filter;
        /// the remote issuer remains responsible for authorizing each order.
        pub fn allow_domain(mut self, domain: impl AsDomainRef) -> Self {
            // The trie's smart insert handles "*.x" -> subtree at x and bare
            // "x" -> exact at x. The stored value is just the wildcard form
            // for subtree entries (so norm_cn can return a borrowed ref).
            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! {
        /// Only allow fetching certs for the given domains.
        ///
        /// By default, if none of the `allow_*` setters are called
        /// the client will fetch for any client. This is a local pre-filter;
        /// the remote issuer remains responsible for authorizing each order.
        pub fn allow_domains(mut self, domains: impl IntoIterator<Item: AsDomainRef>) -> Self {
            for domain in domains {
                self.set_allow_domain(domain);
            }
            self
        }
    }

    /// Prefetch all certificates, useful to warm them up at startup time.
    pub async fn prefetch_certs(&self) {
        if let Some(allow_list) = &self.allow_list {
            // iter() yields the wildcard form for subtree entries and the
            // apex for exact entries; both are the issuing form we want.
            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 {
                            // Subtree match — issue using the stored wildcard form.
                            Some(wildcard) => wildcard.clone(),
                            // Exact match — issue for the queried domain itself.
                            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:?}")
        }
    }
}