nusadb 0.1.0

Fast, stable native Rust driver and ORM for NusaDB (Nusa Wire Protocol 1.1).
Documentation
//! TLS configuration. The Nusa Wire Protocol uses **implicit** TLS (§5.1): the client opens TLS
//! 1.3 immediately, then sends `Startup` inside the encrypted stream — there is no in-band
//! negotiation step. [`TlsConfig`] is plain data (always available); the rustls plumbing that turns
//! it into a client config is compiled only with the `tls` feature.

/// How to verify the server's certificate.
#[derive(Debug, Clone, Default)]
pub struct TlsConfig {
    /// PEM-encoded CA certificate(s) to trust in addition to (or instead of) the public roots. When
    /// set, only these roots are trusted (use it for a private/self-signed server CA).
    pub ca_cert_pem: Option<Vec<u8>>,
    /// **Danger:** accept any server certificate without verification. Dev/test only — this disables
    /// authentication of the server and exposes you to MITM. Never enable in production.
    pub danger_accept_invalid_certs: bool,
}

impl TlsConfig {
    /// Trust the Mozilla/webpki public root store (the default when no CA is supplied).
    #[must_use]
    pub fn with_public_roots() -> Self {
        Self::default()
    }

    /// Trust exactly the given PEM CA certificate(s).
    #[must_use]
    pub fn with_ca_pem(pem: impl Into<Vec<u8>>) -> Self {
        Self {
            ca_cert_pem: Some(pem.into()),
            danger_accept_invalid_certs: false,
        }
    }

    /// **Danger:** skip certificate verification entirely (dev/test only).
    #[must_use]
    pub fn danger_insecure() -> Self {
        Self {
            ca_cert_pem: None,
            danger_accept_invalid_certs: true,
        }
    }
}

#[cfg(feature = "tls")]
pub(crate) use imp::{client_config, server_name};

#[cfg(feature = "tls")]
mod imp {
    use std::sync::Arc;

    use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
    use rustls::crypto::{ring, CryptoProvider};
    use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
    use rustls::{ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme};

    use super::TlsConfig;
    use crate::error::{Error, Result};

    /// Build a rustls `ClientConfig` (TLS 1.3, `ring` provider) for `tls`.
    pub(crate) fn client_config(tls: &TlsConfig) -> Result<Arc<ClientConfig>> {
        let provider = Arc::new(ring::default_provider());
        let builder = ClientConfig::builder_with_provider(Arc::clone(&provider))
            .with_safe_default_protocol_versions()
            .map_err(|e| Error::Config(format!("rustls config: {e}")))?;

        let config = if tls.danger_accept_invalid_certs {
            builder
                .dangerous()
                .with_custom_certificate_verifier(Arc::new(NoVerify(provider)))
                .with_no_client_auth()
        } else {
            let mut roots = RootCertStore::empty();
            if let Some(pem) = &tls.ca_cert_pem {
                let mut cursor = std::io::Cursor::new(pem);
                for cert in rustls_pemfile::certs(&mut cursor) {
                    let cert = cert.map_err(|e| Error::Config(format!("invalid CA PEM: {e}")))?;
                    roots
                        .add(cert)
                        .map_err(|e| Error::Config(format!("add CA: {e}")))?;
                }
            } else {
                roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
            }
            builder.with_root_certificates(roots).with_no_client_auth()
        };
        Ok(Arc::new(config))
    }

    /// Resolve a host string to a rustls `ServerName` (handles DNS names and IP literals).
    pub(crate) fn server_name(host: &str) -> Result<ServerName<'static>> {
        ServerName::try_from(host.to_owned())
            .map_err(|_| Error::Config(format!("invalid TLS server name {host:?}")))
    }

    /// A verifier that accepts every certificate — only reachable via `danger_accept_invalid_certs`.
    #[derive(Debug)]
    struct NoVerify(Arc<CryptoProvider>);

    impl ServerCertVerifier for NoVerify {
        fn verify_server_cert(
            &self,
            _end_entity: &CertificateDer<'_>,
            _intermediates: &[CertificateDer<'_>],
            _server_name: &ServerName<'_>,
            _ocsp: &[u8],
            _now: UnixTime,
        ) -> std::result::Result<ServerCertVerified, rustls::Error> {
            Ok(ServerCertVerified::assertion())
        }

        fn verify_tls12_signature(
            &self,
            _message: &[u8],
            _cert: &CertificateDer<'_>,
            _dss: &DigitallySignedStruct,
        ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
            Ok(HandshakeSignatureValid::assertion())
        }

        fn verify_tls13_signature(
            &self,
            _message: &[u8],
            _cert: &CertificateDer<'_>,
            _dss: &DigitallySignedStruct,
        ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
            Ok(HandshakeSignatureValid::assertion())
        }

        fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
            self.0.signature_verification_algorithms.supported_schemes()
        }
    }

    #[cfg(test)]
    mod tests {
        use super::super::TlsConfig;
        use super::{client_config, server_name};

        #[test]
        fn builds_public_root_config() {
            assert!(client_config(&TlsConfig::with_public_roots()).is_ok());
        }

        #[test]
        fn builds_insecure_config() {
            assert!(client_config(&TlsConfig::danger_insecure()).is_ok());
        }

        #[test]
        fn rejects_garbage_ca_pem() {
            // No valid certificate in the PEM → empty root store is fine, but malformed bytes that
            // *look* like a cert block fail to parse.
            let bad = TlsConfig::with_ca_pem(
                b"-----BEGIN CERTIFICATE-----\nnot-base64!!!\n-----END CERTIFICATE-----\n".to_vec(),
            );
            assert!(client_config(&bad).is_err());
        }

        #[test]
        fn resolves_server_names() {
            assert!(server_name("db.example.com").is_ok());
            assert!(server_name("127.0.0.1").is_ok());
        }
    }
}