tiberius-ng 0.13.1

A TDS (Microsoft SQL Server) driver for Rust — actively-maintained community continuation of tiberius
Documentation
use crate::{
    client::{
        config::{ClientCertSource, ClientCertificate, Config},
        TrustConfig,
    },
    error::{Error, IoErrorKind},
};
use futures_util::io::{AsyncRead, AsyncWrite};
pub(crate) use opentls::async_io::{TlsConnector, TlsStream};
use opentls::{Certificate, Identity};
use std::fs;
use tracing::{event, Level};

/// Loads a client identity from the configured source for the `opentls`
/// (vendored OpenSSL) backend.
///
/// `opentls` only exposes `Identity::from_pkcs12`, so only a PKCS#12 / PFX
/// bundle (supplied via [`Config::client_certificate_pkcs12`]) is supported;
/// separate PEM/DER certificate and key files cannot be loaded by this backend.
fn load_identity(cert: &ClientCertificate) -> crate::Result<Identity> {
    match &cert.source {
        ClientCertSource::Pkcs12 { path, password } => {
            let buf = fs::read(path).map_err(|e| Error::Io {
                kind: IoErrorKind::InvalidData,
                message: format!(
                    "Could not read PKCS#12 identity {}: {e}",
                    path.to_string_lossy()
                ),
            })?;
            Ok(Identity::from_pkcs12(&buf, password)?)
        }
        ClientCertSource::CertAndKey { .. } => Err(Error::Tls(
            "The vendored-openssl (opentls) backend does not support separate \
             certificate/key files for client authentication; supply a PKCS#12 \
             bundle via `Config::client_certificate_pkcs12` instead."
                .to_string(),
        )),
    }
}

/// The Application-Layer Protocol Negotiation (ALPN) protocol identifier used
/// by SQL Server to negotiate TDS 8.0 ("strict" encryption), as defined in
/// [MS-TDS] 2.2.6.5 "Prelogin" / the TDS 8.0 addendum. A client that wishes to
/// speak TDS 8.0 advertises this protocol in the TLS `ClientHello`.
pub(crate) const TDS80_ALPN_PROTOCOL: &str = "tds/8.0";

/// Whether the `opentls` (vendored OpenSSL) TLS backend is able to advertise
/// ALPN protocols during the TLS handshake.
///
/// The `opentls` crate (v0.2.x) does not expose any way to set the ALPN
/// protocol list on its `TlsConnector` (there is no equivalent of
/// `openssl::ssl::SslConnectorBuilder::set_alpn_protos`, and the wrapped
/// `SslConnector` is a private field with no accessor). As a result this
/// backend cannot advertise the [`TDS80_ALPN_PROTOCOL`] identifier and cannot
/// participate in TDS 8.0 strict-encryption ALPN negotiation.
///
/// The `native-tls` and `rustls` backends do not currently advertise ALPN
/// either, but — unlike `opentls` — their underlying libraries expose the
/// necessary API, so this constant is deliberately scoped to the `opentls`
/// backend to document its specific limitation.
pub(crate) const fn supports_alpn() -> bool {
    false
}

pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
    config: &Config,
    stream: S,
) -> crate::Result<TlsStream<S>> {
    if !supports_alpn() {
        // The `opentls` backend has no API to set the ALPN protocol list, so we
        // cannot advertise `tds/8.0`. TDS 8.0 strict encryption relies on ALPN
        // to select the protocol before the TDS PRELOGIN is exchanged; without
        // it the connection silently falls back to the classic (pre-8.0) TLS
        // negotiation. Surface this clearly instead of failing opaquely later.
        event!(
            Level::WARN,
            "The `vendored-openssl` (opentls) TLS backend cannot advertise the \
             `{TDS80_ALPN_PROTOCOL}` ALPN protocol; TDS 8.0 strict-encryption \
             negotiation is unavailable. Use the `native-tls` or `rustls` \
             backend if TDS 8.0 ALPN negotiation is required."
        );
    }

    let mut builder = TlsConnector::new();

    if matches!(config.encryption, crate::EncryptionLevel::Strict) {
        event!(
            Level::WARN,
            "OpenTLS does not support ALPN, so the TDS 8.0 ALPN protocol will not be requested. SQL Server will assume TDS 8.0."
        );
    }

    if let Some(cert) = config.get_client_certificate() {
        event!(
            Level::DEBUG,
            "Presenting a client certificate for mutual TLS."
        );
        builder = builder.identity(load_identity(cert)?);
    }

    match &config.trust {
        TrustConfig::CaCertificateLocation(path) => {
            if let Ok(buf) = fs::read(path) {
                let cert = match path.extension() {
                        Some(ext)
                        if ext.to_ascii_lowercase() == "pem"
                            || ext.to_ascii_lowercase() == "crt" =>
                            {
                                Some(Certificate::from_pem(&buf)?)
                            }
                        Some(ext) if ext.to_ascii_lowercase() == "der" => {
                            Some(Certificate::from_der(&buf)?)
                        }
                        Some(_) | None => return Err(Error::Io {
                            kind: IoErrorKind::InvalidInput,
                            message: "Provided CA certificate with unsupported file-extension! Supported types are pem, crt and der.".to_string()}),
                    };
                if let Some(c) = cert {
                    builder = builder.add_root_certificate(c);
                }
            } else {
                return Err(Error::Io {
                    kind: IoErrorKind::InvalidData,
                    message: "Could not read provided CA certificate!".to_string(),
                });
            }
        }
        TrustConfig::TrustAll => {
            event!(
                Level::WARN,
                "Trusting the server certificate without validation."
            );

            builder = builder.danger_accept_invalid_certs(true);
            builder = builder.danger_accept_invalid_hostnames(true);
            builder = builder.use_sni(false);
        }
        TrustConfig::Default => {
            event!(Level::DEBUG, "Using default trust configuration.");
        }
    }

    Ok(builder
        .connect(config.get_hostname_in_certificate(), stream)
        .await?)
}

#[cfg(test)]
mod tests {
    use super::{supports_alpn, TDS80_ALPN_PROTOCOL};

    #[test]
    fn tds80_alpn_identifier_is_stable() {
        // The identifier is fixed by the TDS 8.0 specification and must not
        // drift; SQL Server matches it byte-for-byte during ALPN negotiation.
        assert_eq!(TDS80_ALPN_PROTOCOL, "tds/8.0");
    }

    #[test]
    fn opentls_backend_cannot_advertise_alpn() {
        // Documents (and guards against silent regressions of) the fact that
        // the opentls backend has no ALPN API. If a future opentls release adds
        // one and this backend is updated to use it, this assertion should be
        // flipped together with the `supports_alpn` implementation.
        assert!(!supports_alpn());
    }
}