Skip to content
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ winauth = { version = "0.0.4", optional = true }

[target.'cfg(unix)'.dependencies]
libgssapi = { version = "0.8.1", optional = true, default-features = false }
libc = "0.2"

[dependencies.async-native-tls]
version = "0.4"
Expand Down Expand Up @@ -191,7 +192,8 @@ all = [
"bigdecimal",
"native-tls",
]
default = ["tds73", "winauth", "native-tls"]
default = ["tds80", "winauth", "native-tls"]
tds80 = ["tds73"]
Comment on lines +195 to +196
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Document the breaking change in default features.

Changing the default feature set from tds73 to tds80 is a breaking change for downstream users. Users who relied on the previous defaults will now automatically receive TDS 8.0 behavior, including:

  • New EncryptionLevel::Strict support (requires TLS handshake with ALPN)
  • TDS 8.0 protocol capabilities
  • Different connection behavior when using defaults

Users who need the previous behavior must explicitly opt out of default features and select tds73 in their Cargo.toml:

[dependencies]
tiberius = { version = "0.12", default-features = false, features = ["tds73", "winauth", "native-tls"] }

This change should be prominently documented in the CHANGELOG/release notes with migration guidance.

Note: Since tds80 = ["tds73"], the tds80 feature is additive and preserves tds73 behavior while enabling additional functionality. However, the new code paths (Strict encryption, ALPN negotiation) may introduce behavioral differences that users should be aware of.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` around lines 195 - 196, The change to the Cargo.toml default
features now enabling tds80 (default = ["tds80", "winauth", "native-tls"]) is a
breaking change for downstream users; update the CHANGELOG/release notes to
clearly document the migration: explain that tds80 is now default, that tds80
includes tds73 but introduces new behavior (e.g., EncryptionLevel::Strict,
ALPN/TLS handshake differences and TDS 8.0 protocol capabilities), provide the
exact opt-out example using default-features = false and features = ["tds73",
"winauth", "native-tls"], and add a short guidance section listing likely
behavioral differences and how to test/restore previous behavior.

tds73 = []
docs = []
sql-browser-async-std = ["async-std"]
Expand Down
35 changes: 31 additions & 4 deletions src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct Config {
pub(crate) trust: TrustConfig,
pub(crate) auth: AuthMethod,
pub(crate) readonly: bool,
pub(crate) hostname_in_certificate: Option<String>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -65,6 +66,7 @@ impl Default for Config {
trust: TrustConfig::Default,
auth: AuthMethod::None,
readonly: false,
hostname_in_certificate: None,
}
}
}
Expand Down Expand Up @@ -149,14 +151,22 @@ impl Config {
/// Will panic in case `trust_cert` was called before.
///
/// - Defaults to validating the server certificate is validated against system's certificate storage.
pub fn trust_cert_ca(&mut self, path: impl ToString) {
pub fn trust_cert_ca(&mut self, path: impl Into<PathBuf>) {
if let TrustConfig::TrustAll = &self.trust {
panic!("'trust_cert' and 'trust_cert_ca' are mutual exclusive! Only use one.")
} else {
self.trust = TrustConfig::CaCertificateLocation(PathBuf::from(path.to_string()))
self.trust = TrustConfig::CaCertificateLocation(path.into())
}
}

/// Sets the hostname to be used for certificate validation.
/// If not set, the hostname from `host` will be used.
///
/// - Defaults to the value of `host`.
pub fn hostname_in_certificate(&mut self, hostname: impl ToString) {
self.hostname_in_certificate = Some(hostname.to_string());
}

/// Sets the authentication method.
///
/// - Defaults to `None`.
Expand Down Expand Up @@ -190,6 +200,12 @@ impl Config {
}
}

pub(crate) fn get_hostname_in_certificate(&self) -> &str {
self.hostname_in_certificate
.as_deref()
.unwrap_or_else(|| self.get_host())
}

/// Get the host address including port
pub fn get_addr(&self) -> String {
format!("{}:{}", self.get_host(), self.get_port())
Expand All @@ -210,7 +226,7 @@ impl Config {
/// |`database`|`<string>`|The name of the database.|
/// |`TrustServerCertificate`|`true`,`false`,`yes`,`no`|Specifies whether the driver trusts the server certificate when connecting using TLS. Cannot be used toghether with `TrustServerCertificateCA`|
/// |`TrustServerCertificateCA`|`<path>`|Path to a `pem`, `crt` or `der` certificate file. Cannot be used together with `TrustServerCertificate`|
/// |`encrypt`|`true`,`false`,`yes`,`no`,`DANGER_PLAINTEXT`|Specifies whether the driver uses TLS to encrypt communication.|
/// |`encrypt`|`strict`,`true`,`false`,`yes`,`no`,`DANGER_PLAINTEXT`|Specifies whether the driver uses TLS to encrypt communication.|
/// |`Application Name`, `ApplicationName`|`<string>`|Sets the application name for the connection.|
///
/// [ADO.NET connection string]: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings
Expand Down Expand Up @@ -265,6 +281,10 @@ impl Config {
builder.trust_cert_ca(ca);
}

if let Some(hostname_in_cert) = s.host_name_in_certificate() {
builder.hostname_in_certificate(hostname_in_cert);
}

builder.encryption(s.encrypt()?);

builder.readonly(s.readonly());
Expand Down Expand Up @@ -346,6 +366,12 @@ pub(crate) trait ConfigString {
.map(|ca| ca.to_string())
}

fn host_name_in_certificate(&self) -> Option<String> {
self.dict()
.get("hostnameincertificate")
.map(|ca| ca.to_string())
}

#[cfg(any(
feature = "rustls",
feature = "native-tls",
Expand All @@ -358,9 +384,10 @@ pub(crate) trait ConfigString {
Ok(true) => Ok(EncryptionLevel::Required),
Ok(false) => Ok(EncryptionLevel::Off),
Err(_) if val == "DANGER_PLAINTEXT" => Ok(EncryptionLevel::NotSupported),
Err(_) if val.eq_ignore_ascii_case("strict") => Ok(EncryptionLevel::Strict),
Err(e) => Err(e),
})
.unwrap_or(Ok(EncryptionLevel::Off))
.unwrap_or(Ok(EncryptionLevel::Required))
}

#[cfg(not(any(
Expand Down
79 changes: 50 additions & 29 deletions src/client/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,18 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
context
};

let transport = Framed::new(MaybeTlsStream::Raw(tcp_stream), PacketCodec);
// let transport = Framed::new(MaybeTlsStream::Raw(tcp_stream), PacketCodec);
Comment thread
olback marked this conversation as resolved.
Outdated
let transport = match config.encryption {
EncryptionLevel::Strict => {
event!(Level::INFO, "Performing a TLS handshake");
let mut pre_login_stream = TlsPreloginWrapper::new(tcp_stream);
pre_login_stream.handshake_complete();
let stream = create_tls_stream(&config, pre_login_stream).await?;
event!(Level::INFO, "TLS handshake successful");
Framed::new(MaybeTlsStream::Tls(stream), PacketCodec)
}
_ => Framed::new(MaybeTlsStream::Raw(tcp_stream), PacketCodec),
};

let mut connection = Self {
transport,
Expand Down Expand Up @@ -444,37 +455,47 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> Connection<S> {
config: &Config,
encryption: EncryptionLevel,
) -> crate::Result<Self> {
if encryption != EncryptionLevel::NotSupported {
event!(Level::INFO, "Performing a TLS handshake");

let Self {
transport, context, ..
} = self;
let mut stream = match transport.into_inner() {
MaybeTlsStream::Raw(tcp) => {
create_tls_stream(config, TlsPreloginWrapper::new(tcp)).await?
}
_ => unreachable!(),
};

stream.get_mut().handshake_complete();
event!(Level::INFO, "TLS handshake successful");
match encryption {
EncryptionLevel::NotSupported => {
event!(
Level::WARN,
"TLS encryption is not enabled. All traffic including the login credentials are not encrypted."
);
Ok(self)
}
EncryptionLevel::Strict => {
// In Strict mode, we should already be in TLS stream after prelogin, so just return self.
event!(
Level::TRACE,
"Already in TLS stream due to Strict encryption level, skipping handshake."
);
Ok(self)
}
EncryptionLevel::Off | EncryptionLevel::On | EncryptionLevel::Required => {
event!(Level::INFO, "Performing a TLS handshake");

let Self {
transport, context, ..
} = self;
let mut stream = match transport.into_inner() {
MaybeTlsStream::Raw(tcp) => {
create_tls_stream(config, TlsPreloginWrapper::new(tcp)).await?
}
_ => unreachable!(),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let transport = Framed::new(MaybeTlsStream::Tls(stream), PacketCodec);
stream.get_mut().handshake_complete();
event!(Level::INFO, "TLS handshake successful");

Ok(Self {
transport,
context,
flushed: false,
buf: BytesMut::new(),
})
} else {
event!(
Level::WARN,
"TLS encryption is not enabled. All traffic including the login credentials are not encrypted."
);
let transport = Framed::new(MaybeTlsStream::Tls(stream), PacketCodec);

Ok(self)
Ok(Self {
transport,
context,
flushed: false,
buf: BytesMut::new(),
})
}
}
}

Expand Down
15 changes: 11 additions & 4 deletions src/client/tls_stream/native_tls_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,22 @@ pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
) -> crate::Result<TlsStream<S>> {
let mut builder = TlsConnector::new();

#[cfg(not(feature = "tds80"))]
{
builder = builder.max_protocol_version(Some(async_native_tls::Protocol::Tlsv12));
}
Comment thread
olback marked this conversation as resolved.
Outdated
Comment on lines +17 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

async-native-tls 0.5 request_alpns method TlsConnector

💡 Result:

In async-native-tls 0.5.x, request_alpns is a builder-style method on async_native_tls::TlsConnector that lets you advertise ALPN protocols (e.g. h2, http/1.1) during the TLS handshake:

use async_native_tls::TlsConnector;

let tls = TlsConnector::new()
    .request_alpns(&["h2", "http/1.1"]);

request_alpns ultimately comes from native-tls and is only available when the native-tls crate is built with its alpn feature enabled. In native-tls it’s documented as “Available on crate feature alpn only.” [2] (Reqwest uses the same underlying request_alpns call on a TlsConnector::builder() for ALPN selection.) [3]

Sources: async-native-tls::TlsConnector docs (shows request_alpns(self, protocols: &[&str]) -> Self) [1], native-tls::TlsConnectorBuilder::request_alpns (feature-gated by alpn) [2], reqwest source using tls.request_alpns(...) [3].


🏁 Script executed:

#!/bin/bash
# Check Cargo.toml for async-native-tls dependency and alpn feature
find . -name "Cargo.toml" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && grep -A 5 "async-native-tls" {}'

Repository: prisma/tiberius

Length of output: 304


🏁 Script executed:

#!/bin/bash
# Find the TDS_ALPN_PROTOCOL_NAME constant definition
rg "TDS_ALPN_PROTOCOL_NAME" -B 2 -A 2

Repository: prisma/tiberius

Length of output: 1053


🏁 Script executed:

#!/bin/bash
# Check if there are any feature flags or default settings that might enable alpn
rg "alpn" -i --type toml

Repository: prisma/tiberius

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check async-native-tls documentation or source for default features
# Search for any build script or conditional compilation related to alpn
rg "request_alpns" -B 5 -A 5 src/client/tls_stream/native_tls_stream.rs

Repository: prisma/tiberius

Length of output: 429


Add missing alpn feature to async-native-tls dependency.

The code at lines 17-19 calls builder.request_alpns(), which requires the alpn feature on the native-tls crate. However, the async-native-tls dependency in Cargo.toml only enables runtime-async-std:

[dependencies.async-native-tls]
version = "0.5"
features = ["runtime-async-std"]

Update to:

[dependencies.async-native-tls]
version = "0.5"
features = ["runtime-async-std", "alpn"]

Without this feature flag, the request_alpns method will not be available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/tls_stream/native_tls_stream.rs` around lines 17 - 19, The
async-native-tls dependency is missing the "alpn" feature required for
builder.request_alpns to be available; update the Cargo.toml dependency entry
for async-native-tls to include the "alpn" feature in its features list (e.g.,
add "alpn" alongside "runtime-async-std") so calls like
builder.request_alpns(&[super::TDS_ALPN_PROTOCOL_NAME]) compile successfully.


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" =>
if ext.eq_ignore_ascii_case("pem")
|| ext.eq_ignore_ascii_case("crt") =>
{
Some(Certificate::from_pem(&buf)?)
}
Some(ext) if ext.to_ascii_lowercase() == "der" => {
Some(ext) if ext.eq_ignore_ascii_case("der") => {
Some(Certificate::from_der(&buf)?)
}
Some(_) | None => return Err(Error::Io {
Expand Down Expand Up @@ -56,5 +61,7 @@ pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
}
}

Ok(builder.connect(config.get_host(), stream).await?)
Ok(builder
.connect(config.get_hostname_in_certificate(), stream)
.await?)
}
4 changes: 3 additions & 1 deletion src/client/tls_stream/opentls_tls_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,7 @@ pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
}
}

Ok(builder.connect(config.get_host(), stream).await?)
Ok(builder
.connect(config.get_hostname_in_certificate(), stream)
.await?)
}
22 changes: 17 additions & 5 deletions src/client/tls_stream/rustls_tls_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,25 @@ impl ServerCertVerifier for NoCertVerifier {
) -> Result<HandshakeSignatureValid, RustlsError> {
Ok(HandshakeSignatureValid::assertion())
}

fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &Certificate,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, RustlsError> {
Ok(HandshakeSignatureValid::assertion())
}
}

fn get_server_name(config: &Config) -> crate::Result<ServerName> {
match (ServerName::try_from(config.get_host()), &config.trust) {
match (
ServerName::try_from(config.get_hostname_in_certificate()),
&config.trust,
) {
(Ok(sn), _) => Ok(sn),
(Err(_), TrustConfig::TrustAll) => {
Ok(ServerName::try_from("placeholder.domain.com").unwrap())
Ok(ServerName::try_from("placeholder.example.com").unwrap())
}
(Err(e), _) => Err(crate::Error::Tls(e.to_string())),
}
Expand All @@ -81,8 +93,8 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> TlsStream<S> {
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" =>
if ext.eq_ignore_ascii_case("pem")
|| ext.eq_ignore_ascii_case("crt") =>
{
let pem_cert = rustls_pemfile::certs(&mut buf.as_slice())?;
if pem_cert.len() != 1 {
Expand All @@ -94,7 +106,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin + Send> TlsStream<S> {

Certificate(pem_cert.into_iter().next().unwrap())
}
Some(ext) if ext.to_ascii_lowercase() == "der" => {
Some(ext) if ext.eq_ignore_ascii_case("der") => {
Certificate(buf)
}
Some(_) | None => return Err(crate::Error::Io {
Expand Down
10 changes: 10 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,16 @@
#![doc(test(attr(deny(rust_2018_idioms, warnings))))]
#![doc(test(attr(allow(unused_extern_crates, unused_variables))))]

#[cfg(all(
feature = "tds80",
not(any(
feature = "rustls",
feature = "native-tls",
feature = "vendored-openssl"
))
))]
compile_error!("The `tds80` feature requires one of the TLS features to be enabled.");

#[cfg(feature = "bigdecimal")]
pub(crate) extern crate bigdecimal_ as bigdecimal;

Expand Down
2 changes: 2 additions & 0 deletions src/tds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ uint_enum! {
NotSupported = 2,
/// Encrypt everything and fail if not possible
Required = 3,
/// Start encryption before TDS prelogin and encrypt everything, fail if not possible
Strict = 4,
Comment thread
olback marked this conversation as resolved.
}

}
51 changes: 51 additions & 0 deletions src/tds/codec/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,61 @@ impl<'a> LoginMessage<'a> {
option_flags_2: OptionFlag2::InitLangFatal | OptionFlag2::OdbcDriver,
option_flags_3: BitFlags::from_flag(OptionFlag3::UnknownCollationHandling),
app_name: "tiberius".into(),
hostname: Self::get_hostname(),
..Default::default()
}
}

fn get_hostname() -> Cow<'static, str> {
#[cfg(windows)]
fn get_computer_name() -> io::Result<String> {
unsafe extern "C" {
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcomputernamea
fn GetComputerNameA(lpBuffer: *mut u8, nSize: *mut u32) -> i32;
}

// MAX_COMPUTERNAME_LENGTH is 15 and we need 1 byte for the null terminator
let mut buffer = [0u8; 15 + 1];
let mut size = buffer.len() as u32;
let result = unsafe { GetComputerNameA(buffer.as_mut_ptr(), &mut size) };
if result == 0 {
let lerr = io::Error::last_os_error();
tracing::error!("GetComputerNameA failed: {lerr}");
Err(lerr)
} else {
Ok(String::from_utf8_lossy(&buffer[..size as usize]).into_owned())
Comment thread
olback marked this conversation as resolved.
Outdated
}
}

#[cfg(target_family = "unix")]
fn get_computer_name() -> io::Result<String> {
unsafe extern "C" {
// Extract from the man page of gethostname():
// gethostname() returns the null-terminated hostname in the character array name, which has a length of len bytes. If the null-terminated hostname is too large to fit, then the
// name is truncated, and no error is returned (but see NOTES below). POSIX.1 says that if such truncation occurs, then it is unspecified whether the returned buffer includes a
// terminating null byte.
fn gethostname(name: *mut u8, len: libc::size_t) -> i32;
}

let mut buffer = [0u8; 255 + 1];
let result = unsafe { gethostname(buffer.as_mut_ptr(), buffer.len() as libc::size_t) };
if result != 0 {
let lerr = io::Error::last_os_error();
tracing::error!("gethostname failed: {lerr}");
Err(lerr)
} else {
// Since the buffer *MAY* or *MAY NOT* be null-terminated, we need to either
// find the first null-byte or assume the entire buffer is the host name
match buffer.split(|b| *b == 0).next() {
Some(hostname) => Ok(String::from_utf8_lossy(hostname).into_owned()),
None => Ok(String::from_utf8_lossy(&buffer).into_owned()),
}
}
}

get_computer_name().map(Cow::Owned).unwrap_or_default()
}

#[cfg(any(all(unix, feature = "integrated-auth-gssapi"), windows))]
pub fn integrated_security(&mut self, bytes: Option<Vec<u8>>) {
if bytes.is_some() {
Expand Down
Loading