diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 000000000..c39191114 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,25 @@ +# Cargo audit configuration +# +# IMPORTANT: The three RUSTSEC advisories below (RUSTSEC-2026-0098, 0099, 0104) are +# DEVELOPMENT DEPENDENCY ONLY and do NOT affect production users. +# +# Root cause: azure_identity (dev-dep only) -> reqwest 0.11 -> rustls 0.21 -> +# rustls-webpki 0.101.7 (vulnerable). This chain is NOT in production code. +# +# Production rustls stack: tokio-rustls 0.26 -> rustls 0.23 -> rustls-webpki +# 0.103.13 (secure, all CVEs fixed). +# +# These ignores are justified because: +# 1. The vulnerable rustls-webpki 0.101.7 comes ONLY via dev-dependency +# azure_identity, not the production rustls feature +# 2. The production rustls feature uses rustls 0.23 with the secure +# rustls-webpki 0.103.13 +# 3. Upgrading azure_identity is out of scope -- it's an external dependency +# with its own constraints and not part of tiberius' public API surface + +[advisories] +ignore = [ + "RUSTSEC-2026-0098", + "RUSTSEC-2026-0099", + "RUSTSEC-2026-0104", +] diff --git a/Cargo.toml b/Cargo.toml index 0caaac815..4d079a9c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,16 +118,11 @@ version = "1.12.0" optional = true [dependencies.tokio-rustls] -version = "0.24.0" -optional = true -features = ["dangerous_configuration"] - -[dependencies.rustls-pemfile] -version = "1" +version = "0.26" optional = true [dependencies.rustls-native-certs] -version = "0.6" +version = "0.8" optional = true [dependencies.opentls] @@ -199,6 +194,6 @@ sql-browser-tokio = ["tokio", "tokio-util"] sql-browser-smol = ["async-io", "async-net", "futures-lite"] integrated-auth-gssapi = ["libgssapi"] bigdecimal = ["bigdecimal_"] -rustls = ["tokio-rustls", "tokio-util", "rustls-pemfile", "rustls-native-certs"] +rustls = ["tokio-rustls", "tokio-util", "rustls-native-certs"] native-tls = ["async-native-tls"] vendored-openssl = ["opentls"] diff --git a/examples/aad-auth.rs b/examples/aad-auth.rs index 8ef41c472..e280a8b94 100644 --- a/examples/aad-auth.rs +++ b/examples/aad-auth.rs @@ -9,7 +9,6 @@ //! - TENANT_ID: tenant id of service principal and sql instance; //! - SERVER: SQL server URI use azure_identity::client_credentials_flow; -use oauth2::{ClientId, ClientSecret}; use std::{env, sync::Arc}; use tiberius::{AuthMethod, Client, Config, Query}; use tokio::net::TcpStream; @@ -17,16 +16,12 @@ use tokio_util::compat::TokioAsyncWriteCompatExt; #[tokio::main] async fn main() -> anyhow::Result<()> { - // following code will retrive token with AAD Service Principal Auth - let client_id = - ClientId::new(env::var("CLIENT_ID").expect("Missing CLIENT_ID environment variable.")); - let client_secret = ClientSecret::new( - env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET environment variable."), - ); + let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID environment variable."); + let client_secret = + env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET environment variable."); let tenant_id = env::var("TENANT_ID").expect("Missing TENANT_ID environment variable."); let client = Arc::new(reqwest::Client::new()); - // This will give you the final token to use in authorization. let token = client_credentials_flow::perform( client, &client_id, @@ -41,7 +36,7 @@ async fn main() -> anyhow::Result<()> { config.host(server); config.port(1433); config.authentication(AuthMethod::AADToken( - token.access_token().secret().to_string(), + token.access_token().secret().to_owned(), )); config.trust_cert(); diff --git a/src/client/tls.rs b/src/client/tls.rs index 7a22d4333..3c8ff9bd7 100644 --- a/src/client/tls.rs +++ b/src/client/tls.rs @@ -4,18 +4,44 @@ feature = "vendored-openssl" ))] use super::tls_stream::TlsStream; +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] use crate::tds::{ codec::{Decode, Encode, PacketHeader, PacketStatus, PacketType}, HEADER_BYTES, }; +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] use bytes::BytesMut; use futures_util::io::{AsyncRead, AsyncWrite}; +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] use futures_util::ready; +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] +use std::cmp; use std::{ - cmp, io, + io, pin::Pin, task::{self, Poll}, }; +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] use tracing::{event, Level}; /// A wrapper to handle either TLS or bare connections. @@ -114,6 +140,11 @@ impl AsyncWrite for MaybeTlsStream /// /// What it does is it interferes on handshake for TDS packet handling, /// and when complete, just passes the calls to the underlying connection. +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] pub(crate) struct TlsPreloginWrapper { stream: Option, pending_handshake: bool, @@ -150,6 +181,11 @@ impl TlsPreloginWrapper { } } +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] impl AsyncRead for TlsPreloginWrapper { fn poll_read( mut self: Pin<&mut Self>, @@ -212,6 +248,11 @@ impl AsyncRead for TlsPreloginWrapper< } } +#[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" +))] impl AsyncWrite for TlsPreloginWrapper { fn poll_write( mut self: Pin<&mut Self>, diff --git a/src/client/tls_stream/rustls_tls_stream.rs b/src/client/tls_stream/rustls_tls_stream.rs index e417583a6..edabe9a98 100644 --- a/src/client/tls_stream/rustls_tls_stream.rs +++ b/src/client/tls_stream/rustls_tls_stream.rs @@ -9,16 +9,17 @@ use std::{ pin::Pin, sync::Arc, task::{Context, Poll}, - time::SystemTime, }; use tokio_rustls::{ rustls::{ client::{ - HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier, - WantsTransparencyPolicyOrClientCert, + danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + WantsClientCert, }, - Certificate, ClientConfig, ConfigBuilder, DigitallySignedStruct, Error as RustlsError, - RootCertStore, ServerName, WantsVerifier, + crypto::aws_lc_rs, + pki_types::{pem::PemObject, CertificateDer, ServerName, UnixTime}, + version, ClientConfig, ConfigBuilder, DigitallySignedStruct, Error as RustlsError, + RootCertStore, SignatureScheme, WantsVerifier, }, TlsConnector, }; @@ -35,17 +36,17 @@ pub(crate) struct TlsStream( Compat>>, ); +#[derive(Debug)] struct NoCertVerifier; impl ServerCertVerifier for NoCertVerifier { fn verify_server_cert( &self, - _end_entity: &Certificate, - _intermediates: &[Certificate], - _server_name: &ServerName, - _scts: &mut dyn Iterator, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, _ocsp_response: &[u8], - _now: SystemTime, + _now: UnixTime, ) -> Result { Ok(ServerCertVerified::assertion()) } @@ -53,16 +54,41 @@ impl ServerCertVerifier for NoCertVerifier { fn verify_tls12_signature( &self, _message: &[u8], - _cert: &Certificate, + _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } } -fn get_server_name(config: &Config) -> crate::Result { +fn get_server_name(config: &Config) -> crate::Result> { match (ServerName::try_from(config.get_host()), &config.trust) { - (Ok(sn), _) => Ok(sn), + (Ok(sn), _) => Ok(sn.to_owned()), (Err(_), TrustConfig::TrustAll) => { Ok(ServerName::try_from("placeholder.domain.com").unwrap()) } @@ -74,36 +100,54 @@ impl TlsStream { pub(super) async fn new(config: &Config, stream: S) -> crate::Result { event!(Level::INFO, "Performing a TLS handshake"); - let builder = ClientConfig::builder().with_safe_defaults(); + let builder = ClientConfig::builder_with_provider(Arc::new(aws_lc_rs::default_provider())) + .with_protocol_versions(&[&version::TLS12]) + .map_err(|e| crate::Error::Tls(e.to_string()))?; let client_config = 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" => - { - let pem_cert = rustls_pemfile::certs(&mut buf.as_slice())?; - if pem_cert.len() != 1 { - return Err(crate::Error::Io { - kind: IoErrorKind::InvalidInput, - message: format!("Certificate file {} contain 0 or more than 1 certs", path.to_string_lossy()), - }); - } - - Certificate(pem_cert.into_iter().next().unwrap()) - } - Some(ext) if ext.to_ascii_lowercase() == "der" => { - Certificate(buf) + Some(ext) + if ext.eq_ignore_ascii_case("pem") + || ext.eq_ignore_ascii_case("crt") => + { + let pem_certs: Vec< + CertificateDer<'static>, + > = CertificateDer::pem_slice_iter(&buf) + .collect::, _>>() + .map_err(|e| crate::Error::Io { + kind: IoErrorKind::InvalidData, + message: format!( + "Failed to parse PEM certificate: {e}" + ), + })?; + if pem_certs.len() != 1 { + return Err(crate::Error::Io { + kind: IoErrorKind::InvalidInput, + message: format!( + "Certificate file {} must contain exactly one certificate", + path.to_string_lossy() + ), + }); } - Some(_) | None => return Err(crate::Error::Io { + + pem_certs.into_iter().next().unwrap() + } + Some(ext) + if ext.eq_ignore_ascii_case("der") => + { + CertificateDer::from(buf) + } + Some(_) | None => { + return Err(crate::Error::Io { kind: IoErrorKind::InvalidInput, message: "Provided CA certificate with unsupported file-extension! Supported types are pem, crt and der.".to_string(), - }), - }; + }) + } + }; let mut cert_store = RootCertStore::empty(); - cert_store.add(&cert)?; + cert_store.add(cert)?; builder .with_root_certificates(cert_store) .with_no_client_auth() @@ -119,14 +163,10 @@ impl TlsStream { Level::WARN, "Trusting the server certificate without validation." ); - let mut config = builder - .with_root_certificates(RootCertStore::empty()) - .with_no_client_auth(); - config + builder .dangerous() - .set_certificate_verifier(Arc::new(NoCertVerifier {})); - // config.enable_sni = false; - config + .with_custom_certificate_verifier(Arc::new(NoCertVerifier)) + .with_no_client_auth() } TrustConfig::Default => { event!(Level::INFO, "Using default trust configuration."); @@ -181,28 +221,26 @@ impl AsyncWrite for TlsStream { } trait ConfigBuilderExt { - fn with_native_roots(self) -> ConfigBuilder; + fn with_native_roots(self) -> ConfigBuilder; } impl ConfigBuilderExt for ConfigBuilder { - fn with_native_roots(self) -> ConfigBuilder { + fn with_native_roots(self) -> ConfigBuilder { let mut roots = RootCertStore::empty(); let mut valid_count = 0; let mut invalid_count = 0; for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") { - let cert = Certificate(cert.0); - match roots.add(&cert) { + match roots.add(cert) { Ok(_) => valid_count += 1, Err(err) => { - tracing::event!(Level::TRACE, "invalid cert der {:?}", cert.0); - tracing::event!(Level::DEBUG, "certificate parsing failed: {:?}", err); + event!(Level::DEBUG, "certificate parsing failed: {:?}", err); invalid_count += 1 } } } - tracing::event!( + event!( Level::TRACE, "with_native_roots processed {} valid and {} invalid certs", valid_count, diff --git a/src/tds/codec/header.rs b/src/tds/codec/header.rs index 719fc158b..fcee5b09f 100644 --- a/src/tds/codec/header.rs +++ b/src/tds/codec/header.rs @@ -112,6 +112,11 @@ impl PacketHeader { self.status = status; } + #[cfg(any( + feature = "rustls", + feature = "native-tls", + feature = "vendored-openssl" + ))] pub fn set_type(&mut self, ty: PacketType) { self.ty = ty; }