From 41f9d1a682543cc4c49f210c9d2dd18a00d5b44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Thu, 6 Aug 2026 10:01:37 +0200 Subject: [PATCH 1/8] ssl: use CassSslVerifyFlags from cassandra.h CassSslVerifyFlags were duplicated and re-defined in ssl.rs manually. This commit sets up bindgen to generate the corresponding Rust definition automatically based on cassandra.h definition. --- scylla-rust-wrapper/build.rs | 1 + scylla-rust-wrapper/src/lib.rs | 9 +++++++++ scylla-rust-wrapper/src/ssl.rs | 22 +++++++++++----------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/scylla-rust-wrapper/build.rs b/scylla-rust-wrapper/build.rs index 1645cfe56..2bc5ae005 100644 --- a/scylla-rust-wrapper/build.rs +++ b/scylla-rust-wrapper/build.rs @@ -172,6 +172,7 @@ fn main() { ], &out_path, ); + prepare_cppdriver_data("cppdriver_ssl_types.rs", &["CassSslVerifyFlags"], &out_path); prepare_cppdriver_data( "cppdriver_host_listener_types.rs", &["CassHostListenerEvent", "CassHostListenerCallback"], diff --git a/scylla-rust-wrapper/src/lib.rs b/scylla-rust-wrapper/src/lib.rs index e604e35f5..1b208173c 100644 --- a/scylla-rust-wrapper/src/lib.rs +++ b/scylla-rust-wrapper/src/lib.rs @@ -159,6 +159,15 @@ pub(crate) mod cass_authenticator_types { include_bindgen_generated!("cppdriver_authenticator_types.rs"); } +/// CassSsl +pub(crate) mod cass_ssl_types { + #![allow(unused)] + #![allow(non_camel_case_types, non_snake_case)] + #![allow(unreachable_pub, unnameable_types)] + + include_bindgen_generated!("cppdriver_ssl_types.rs"); +} + /// CassHostListenerEvent, CassHostListenerCallback pub(crate) mod cass_host_listener_types { #![allow(unused)] diff --git a/scylla-rust-wrapper/src/ssl.rs b/scylla-rust-wrapper/src/ssl.rs index c964f779d..620df2585 100644 --- a/scylla-rust-wrapper/src/ssl.rs +++ b/scylla-rust-wrapper/src/ssl.rs @@ -3,6 +3,7 @@ use crate::argconv::{ CassStrNulTerminated, FFI, FromArc, }; use crate::cass_error::CassError; +use crate::cass_ssl_types::CassSslVerifyFlags; use crate::types::size_t; use libc::{c_int, strlen}; use openssl::ssl::SslVerifyMode; @@ -26,11 +27,6 @@ impl FFI for CassSsl { type Origin = FromArc; } -pub(crate) const CASS_SSL_VERIFY_NONE: i32 = 0x00; -pub(crate) const CASS_SSL_VERIFY_PEER_CERT: i32 = 0x01; -pub(crate) const CASS_SSL_VERIFY_PEER_IDENTITY: i32 = 0x02; -pub(crate) const CASS_SSL_VERIFY_PEER_IDENTITY_DNS: i32 = 0x04; - #[unsafe(no_mangle)] pub unsafe extern "C" fn cass_ssl_new() -> CassOwnedSharedPtr { openssl_sys::init(); @@ -44,7 +40,11 @@ pub unsafe extern "C" fn cass_ssl_new_no_lib_init() -> CassOwnedSharedPtr unsafe { + match CassSslVerifyFlags(flags as u32) { + CassSslVerifyFlags::CASS_SSL_VERIFY_NONE => unsafe { SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::NONE.bits(), None) }, - CASS_SSL_VERIFY_PEER_CERT => unsafe { + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT => unsafe { SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) }, _ => { - if flags & CASS_SSL_VERIFY_PEER_IDENTITY != 0 { + if flags & CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY.0 as i32 != 0 { eprintln!( "The CASS_SSL_VERIFY_PEER_CERT_IDENTITY is not supported, CASS_SSL_VERIFY_PEER_CERT is set in SSL context." ); } - if flags & CASS_SSL_VERIFY_PEER_IDENTITY_DNS != 0 { + if flags & CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS.0 as i32 != 0 { eprintln!( "The CASS_SSL_VERIFY_PEER_CERT_IDENTITY_DNS is not supported, CASS_SSL_VERIFY_PEER_CERT is set in SSL context." ); From fff073ea9abbd786e46526aa2a1a797341e494c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Thu, 6 Aug 2026 10:11:10 +0200 Subject: [PATCH 2/8] ssl: recognize CASS_SSL_VERIFY_PEER_IDENTITY as supported CASS_SSL_VERIFY_PEER_IDENTITY is now recognized as supported, because the Rust Driver since Dec '25 (https://github.com/scylladb/scylla-rust- driver/pull/1491) pins the expected identity to the node's IP on every connection, so peer verification already implies identity verification. Note that CPP Driver supported both CN and SAN field-based verification. Current implementation supports only SAN, which is OK due to CN being considered obsolete. The same Rust Driver's change made CASS_SSL_VERIFY_PEER_CERT incorrectly supported in CPP RS Driver. We add a comment and we'll come back to it in a next commit. --- include/cassandra.h | 12 +++++++++--- scylla-rust-wrapper/src/ssl.rs | 13 +++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/include/cassandra.h b/include/cassandra.h index d2ae07239..994d79ebb 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -4302,9 +4302,15 @@ cass_ssl_add_trusted_cert_n(CassSsl* ssl, * * CASS_SSL_VERIFY_NONE - No verification is performed * CASS_SSL_VERIFY_PEER_CERT - Certificate is present and valid - * CASS_SSL_VERIFY_PEER_IDENTITY - IP address matches the certificate's - * common name or one of its subject alternative names. This implies the - * certificate is also present. + * CASS_SSL_VERIFY_PEER_IDENTITY - IP address matches one of the certificate's + * subject alternative names of type iPAddress. This implies the certificate + * is also present. + * NOTE: unlike the C/C++ driver, the subject common name (CN) is NOT + * consulted. A certificate that identifies the node only by CN, with no + * iPAddress subject alternative name (SAN), is rejected. This follows from + * OpenSSL's X509_VERIFY_PARAM_set1_ip() (called by Rust Driver), which + * never falls back to the subject for IP address checks. Note that modern + * practices advocate for using SAN, and consider CN obsolete. * CASS_SSL_VERIFY_PEER_IDENTITY_DNS - Hostname matches the certificate's * common name or one of its subject alternative names. This implies the * certificate is also present. Hostname resolution must also be enabled. diff --git a/scylla-rust-wrapper/src/ssl.rs b/scylla-rust-wrapper/src/ssl.rs index 620df2585..0be996468 100644 --- a/scylla-rust-wrapper/src/ssl.rs +++ b/scylla-rust-wrapper/src/ssl.rs @@ -183,15 +183,16 @@ pub unsafe extern "C" fn cass_ssl_set_verify_flags( SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::NONE.bits(), None) }, CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT => unsafe { + // FIXME: work around Rust Driver's obligatory identity verification. + SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) + }, + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY => unsafe { + // Rust Driver verifies identity by default (and provides no lever to turn this verification off) + // by expecting particular IP address to be present in the SAN field. + // This means that once we enable SslVerifyMode::PEER, we get certificate + identity verification. SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) }, _ => { - if flags & CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY.0 as i32 != 0 { - eprintln!( - "The CASS_SSL_VERIFY_PEER_CERT_IDENTITY is not supported, CASS_SSL_VERIFY_PEER_CERT is set in SSL context." - ); - } - if flags & CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS.0 as i32 != 0 { eprintln!( "The CASS_SSL_VERIFY_PEER_CERT_IDENTITY_DNS is not supported, CASS_SSL_VERIFY_PEER_CERT is set in SSL context." From 5f7f89fcfa2046944811c91063bbe3ac9c5450d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Mon, 3 Aug 2026 14:53:03 +0200 Subject: [PATCH 3/8] ssl: map *_VERIFY_PEER_IDENTITY_DNS to *_VERIFY_IDENTITY Rust Driver assumes that hostname verification is always done by IP address in the SAN field, so the *_VERIFY_PEER_IDENTITY_DNS flag is not supported. This change maps it to *_VERIFY_PEER_IDENTITY and adds a warning message when it is used. --- include/cassandra.h | 1 + scylla-rust-wrapper/src/ssl.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/include/cassandra.h b/include/cassandra.h index 994d79ebb..4c0762bac 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -4314,6 +4314,7 @@ cass_ssl_add_trusted_cert_n(CassSsl* ssl, * CASS_SSL_VERIFY_PEER_IDENTITY_DNS - Hostname matches the certificate's * common name or one of its subject alternative names. This implies the * certificate is also present. Hostname resolution must also be enabled. + * NOTE: not supported; treated as CASS_SSL_VERIFY_PEER_IDENTITY. * * Default: CASS_SSL_VERIFY_NONE * diff --git a/scylla-rust-wrapper/src/ssl.rs b/scylla-rust-wrapper/src/ssl.rs index 0be996468..378763c0a 100644 --- a/scylla-rust-wrapper/src/ssl.rs +++ b/scylla-rust-wrapper/src/ssl.rs @@ -192,13 +192,14 @@ pub unsafe extern "C" fn cass_ssl_set_verify_flags( // This means that once we enable SslVerifyMode::PEER, we get certificate + identity verification. SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) }, + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS => unsafe { + // Rust Driver always verifies by IP only, so DNS verification is unsupported. + tracing::warn!( + "The CASS_SSL_VERIFY_PEER_CERT_IDENTITY_DNS is not supported, CASS_SSL_VERIFY_PEER_IDENTITY is set in SSL context." + ); + SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) + }, _ => { - if flags & CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS.0 as i32 != 0 { - eprintln!( - "The CASS_SSL_VERIFY_PEER_CERT_IDENTITY_DNS is not supported, CASS_SSL_VERIFY_PEER_CERT is set in SSL context." - ); - } - unsafe { SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) }; } } From 9b905fa054085dc45a1f5cde6b5d4b6e9d5b5f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Tue, 4 Aug 2026 14:40:14 +0200 Subject: [PATCH 4/8] ssl: use strictest option on unknown verify flags Previously any flags value we did not recognise fell through to a catch- all that enabled SSL_VERIFY_PEER, silently and without a trace. Passing garbage - or a flag from a newer header - therefore changed the security settings with no indication that the request was not honoured. An unrecognised value cannot be honoured, and this API returns void, so there is no way to report that back to the caller. Leaving the SSL_CTX untouched would be the wrong direction to fail in: a previously requested CASS_SSL_VERIFY_NONE would silently stay in effect. The only thing we control is the resulting security posture, so keep enforcing the strictest verification we support and log an error saying so. --- scylla-rust-wrapper/src/ssl.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scylla-rust-wrapper/src/ssl.rs b/scylla-rust-wrapper/src/ssl.rs index 378763c0a..6f6de8c12 100644 --- a/scylla-rust-wrapper/src/ssl.rs +++ b/scylla-rust-wrapper/src/ssl.rs @@ -199,9 +199,17 @@ pub unsafe extern "C" fn cass_ssl_set_verify_flags( ); SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) }, - _ => { - unsafe { SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) }; - } + _ => unsafe { + // An unrecognised value cannot be honoured, and this API has no way + // of reporting that back to the caller. The only thing we control is + // the resulting security posture, so fail closed: enforce the + // strictest verification we support. + tracing::error!( + "Provided unknown CASS_SSL_VERIFY flags: {flags:#x}. \ + Enforcing the strictest verification (peer certificate and peer identity) instead." + ); + SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) + }, } } From 40faa5eaed0f39580d82c5c45c93ced17836970d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Tue, 4 Aug 2026 14:44:43 +0200 Subject: [PATCH 5/8] ssl: support chain-only CASS_SSL_VERIFY_PEER_CERT The C API defines CASS_SSL_VERIFY_PEER_CERT as "certificate is present and valid", i.e. chain validation without an identity check, in contrast to CASS_SSL_VERIFY_PEER_IDENTITY which additionally requires the node's IP to match the certificate. We could not express that so far, because the Rust driver pins the expected identity itself, calling `ssl.param_mut().set_ip()` on every connection right after `Ssl::new()`. Nothing we put in the SSL_CTX can undo that, so PEER_CERT ended up behaving exactly like PEER_IDENTITY and we only logged a warning saying so. Install a verification callback for PEER_CERT instead. OpenSSL reports a failed identity check to the callback as X509_V_ERR_{HOSTNAME,EMAIL, IP_ADDRESS}_MISMATCH; accepting precisely those, and resetting the stored error so that SSL_get_verify_result() reports success too, yields chain-only verification. Every other failure - an untrusted issuer, an expired certificate, a broken chain - remains fatal. The callback is installed only when certificate verification was asked for without any of the identity bits, which is exactly when the two differ. This un-ignores tls_peer_cert_verifies_chain_only, which asserts both halves: a mismatched IP SAN is tolerated, while an untrusted CA is not. --- scylla-rust-wrapper/src/ssl.rs | 52 +++++++++++++++++-- .../tests/integration/ccm/tls.rs | 14 +++-- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/scylla-rust-wrapper/src/ssl.rs b/scylla-rust-wrapper/src/ssl.rs index 6f6de8c12..152a98fc4 100644 --- a/scylla-rust-wrapper/src/ssl.rs +++ b/scylla-rust-wrapper/src/ssl.rs @@ -11,7 +11,9 @@ use openssl_sys::{ BIO, BIO_free_all, BIO_new_mem_buf, EVP_PKEY_free, PEM_read_bio_PrivateKey, PEM_read_bio_X509, SSL_CTX, SSL_CTX_add_extra_chain_cert, SSL_CTX_free, SSL_CTX_new, SSL_CTX_set_cert_store, SSL_CTX_set_verify, SSL_CTX_use_PrivateKey, SSL_CTX_use_certificate, TLS_method, X509_STORE, - X509_STORE_add_cert, X509_STORE_new, X509_free, + X509_STORE_CTX, X509_STORE_CTX_get_error, X509_STORE_CTX_set_error, X509_STORE_add_cert, + X509_STORE_new, X509_V_ERR_EMAIL_MISMATCH, X509_V_ERR_HOSTNAME_MISMATCH, + X509_V_ERR_IP_ADDRESS_MISMATCH, X509_V_OK, X509_free, }; use std::convert::TryInto; use std::os::raw::c_char; @@ -75,6 +77,45 @@ pub unsafe extern "C" fn cass_ssl_free(ssl: CassOwnedSharedPtr) { ArcFFI::free(ssl); } +/// Verification callback used to implement [`CASS_SSL_VERIFY_PEER_CERT`], +/// i.e. chain-only verification. +/// +/// The Rust driver unconditionally pins the expected peer identity to the +/// node's IP address (`ssl.param_mut().set_ip(node_address.ip())`) before every +/// handshake, so OpenSSL always checks the peer identity on top of the chain. +/// There is no way to undo that from the `SSL_CTX` we hand over. Instead, we +/// install this callback, which lets the chain validation proceed as usual but +/// tolerates the identity mismatch errors, leaving `CASS_SSL_VERIFY_PEER_CERT` +/// with exactly the semantics the C API promises: "certificate is present and +/// valid", without requiring it to match the peer's address. +/// +/// Declared as a safe `extern "C" fn`, because that is the callback type +/// `SSL_CTX_set_verify()` expects; `x509_ctx` is only ever supplied by OpenSSL. +extern "C" fn verify_peer_cert_callback( + preverify_ok: c_int, + x509_ctx: *mut X509_STORE_CTX, +) -> c_int { + // Anything that already passed is accepted as-is. + if preverify_ok == 1 { + return 1; + } + + let error = unsafe { X509_STORE_CTX_get_error(x509_ctx) }; + match error { + X509_V_ERR_HOSTNAME_MISMATCH + | X509_V_ERR_EMAIL_MISMATCH + | X509_V_ERR_IP_ADDRESS_MISMATCH => { + // Reset the error, so that the handshake does not merely continue + // but `SSL_get_verify_result()` also reports success afterwards. + unsafe { X509_STORE_CTX_set_error(x509_ctx, X509_V_OK) }; + 1 + } + // Every other failure - an untrusted issuer, an expired certificate, + // a broken chain - is still fatal. + _ => 0, + } +} + unsafe extern "C" fn pem_password_callback( buf: *mut c_char, size: c_int, @@ -183,8 +224,13 @@ pub unsafe extern "C" fn cass_ssl_set_verify_flags( SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::NONE.bits(), None) }, CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT => unsafe { - // FIXME: work around Rust Driver's obligatory identity verification. - SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) + // Chain-only verification. The driver pins the peer's identity for us, + // so the mismatches that pinning produces have to be tolerated. + SSL_CTX_set_verify( + ssl.ssl_context, + SslVerifyMode::PEER.bits(), + Some(verify_peer_cert_callback), + ) }, CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY => unsafe { // Rust Driver verifies identity by default (and provides no lever to turn this verification off) diff --git a/scylla-rust-wrapper/tests/integration/ccm/tls.rs b/scylla-rust-wrapper/tests/integration/ccm/tls.rs index ca0c41726..36eb65dab 100644 --- a/scylla-rust-wrapper/tests/integration/ccm/tls.rs +++ b/scylla-rust-wrapper/tests/integration/ccm/tls.rs @@ -480,15 +480,13 @@ async fn connect_tls_with_client_auth() { run_ccm_tls_test(prepare_cert, require_client_auth, test).await } -/// Documents the *desired* semantics of `CASS_SSL_VERIFY_PEER_CERT`: it should -/// validate the certificate chain only, without checking the peer identity -/// (IP SAN). This is not implemented yet — currently `PEER_CERT` is equivalent -/// to `PEER_IDENTITY` because the underlying Rust driver always verifies the -/// node IP. The test is therefore `#[ignore]`d; un-ignore it once chain-only -/// `PEER_CERT` support lands. +/// Checks the semantics of `CASS_SSL_VERIFY_PEER_CERT`: it validates the +/// certificate chain only, without checking the peer identity (IP SAN). +/// +/// The Rust driver always pins the expected identity to the node's IP, so this +/// relies on the wrapper installing a verification callback that tolerates the +/// identity mismatch while keeping the rest of the chain validation intact. #[tokio::test] -#[ignore = "PEER_CERT currently behaves like PEER_IDENTITY (verifies the IP SAN); \ - chain-only PEER_CERT support is not implemented yet"] async fn tls_peer_cert_verifies_chain_only() { setup_tracing(); From d0d9e848ef6799dafd93949a2ca9779d3a45d7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Mon, 3 Aug 2026 16:20:54 +0200 Subject: [PATCH 6/8] ssl: make CASS_SSL_VERIFY_PEER_CERT default again cass_ssl_new() configured SSL_VERIFY_NONE, so a caller who set up TLS but never called cass_ssl_set_verify_flags() got an encrypted connection to an entirely unauthenticated peer. Verify the peer by default instead. This is in line with the CPP Driver, which documents PEER_CERT as its default. We have also documented such default, but due to a bug didn't respect it. Now the bug fix is completed, i.e., the default is actually set to the declared variant **and** PEER_CERT is now correctly supported (not accidentally requiring IP verification as before). --- include/cassandra.h | 2 +- scylla-rust-wrapper/src/ssl.rs | 4 +-- .../tests/integration/ccm/tls.rs | 28 +++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/include/cassandra.h b/include/cassandra.h index 4c0762bac..7856a405e 100644 --- a/include/cassandra.h +++ b/include/cassandra.h @@ -4316,7 +4316,7 @@ cass_ssl_add_trusted_cert_n(CassSsl* ssl, * certificate is also present. Hostname resolution must also be enabled. * NOTE: not supported; treated as CASS_SSL_VERIFY_PEER_IDENTITY. * - * Default: CASS_SSL_VERIFY_NONE + * Default: CASS_SSL_VERIFY_PEER_CERT * * @public @memberof CassSsl * diff --git a/scylla-rust-wrapper/src/ssl.rs b/scylla-rust-wrapper/src/ssl.rs index 152a98fc4..760b5cec6 100644 --- a/scylla-rust-wrapper/src/ssl.rs +++ b/scylla-rust-wrapper/src/ssl.rs @@ -44,8 +44,8 @@ pub unsafe extern "C" fn cass_ssl_new_no_lib_init() -> CassOwnedSharedPtr connects even without a trusted CA. - assert_cass_error_eq( - CassError::CASS_OK, - try_tls_connect(cluster, None, None, None).await, - ); + let assert_conn_fails = async |verify_flags| { + let err = try_tls_connect(cluster, verify_flags, None, None).await; + assert_ne!( + err, + CassError::CASS_OK, + "expected connection to fail when the server CA is not trusted" + ); + }; + + // default: PEER_CERT without a trusted CA -> certificate chain validation + // fails, so the connection is rejected. + assert_conn_fails(None).await; + + // PEER_CERT without a trusted CA -> certificate chain validation + // fails, so the connection is rejected. + assert_conn_fails(Some(CASS_SSL_VERIFY_PEER_CERT)).await; // PEER_IDENTITY without a trusted CA -> certificate chain validation // fails, so the connection is rejected. - let err = try_tls_connect(cluster, Some(CASS_SSL_VERIFY_PEER_IDENTITY), None, None).await; - assert_ne!( - err, - CassError::CASS_OK, - "expected connection to fail when the server CA is not trusted" - ); + assert_conn_fails(Some(CASS_SSL_VERIFY_PEER_IDENTITY)).await; } run_ccm_tls_test(prepare_cert, async |c| c, test).await From a4aa5cdc738730dea5ed01b22ea31173f0187116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Mon, 3 Aug 2026 14:47:36 +0200 Subject: [PATCH 7/8] ssl: handle CASS_SSL_VERIFY flags as a bitmask `CassSslVerifyFlags` is a bitmask: CASS_SSL_VERIFY_PEER_CERT (0x01), _PEER_IDENTITY (0x02) and _PEER_IDENTITY_DNS (0x04) are disjoint bits meant to be combined, and CASS_SSL_VERIFY_NONE (0x00) is the absence of all of them. The C/C++ driver tests them with `&`, and our own TLS guide tells users to write cass_ssl_set_verify_flags(ssl, CASS_SSL_VERIFY_PEER_CERT | CASS_SSL_VERIFY_PEER_IDENTITY); Matching on the flags value as a whole cannot express that: every combination lands in the catch-all arm. Test the bits instead, and cover the documented combination in `tls_verifies_hostname` test. --- scylla-rust-wrapper/src/ssl.rs | 183 ++++++++++++++---- .../tests/integration/ccm/tls.rs | 16 ++ 2 files changed, 164 insertions(+), 35 deletions(-) diff --git a/scylla-rust-wrapper/src/ssl.rs b/scylla-rust-wrapper/src/ssl.rs index 760b5cec6..2f370563d 100644 --- a/scylla-rust-wrapper/src/ssl.rs +++ b/scylla-rust-wrapper/src/ssl.rs @@ -29,6 +29,15 @@ impl FFI for CassSsl { type Origin = FromArc; } +/// Type of the verification callback accepted by `SSL_CTX_set_verify()`. +type VerifyCallback = Option c_int>; + +/// All bits recognised by [`cass_ssl_set_verify_flags`]. +const CASS_SSL_VERIFY_KNOWN_FLAGS: i32 = (CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT.0 + | CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY.0 + | CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS.0) + as i32; + #[unsafe(no_mangle)] pub unsafe extern "C" fn cass_ssl_new() -> CassOwnedSharedPtr { openssl_sys::init(); @@ -219,44 +228,55 @@ pub unsafe extern "C" fn cass_ssl_set_verify_flags( return; }; - match CassSslVerifyFlags(flags as u32) { - CassSslVerifyFlags::CASS_SSL_VERIFY_NONE => unsafe { - SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::NONE.bits(), None) - }, - CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT => unsafe { - // Chain-only verification. The driver pins the peer's identity for us, - // so the mismatches that pinning produces have to be tolerated. - SSL_CTX_set_verify( - ssl.ssl_context, - SslVerifyMode::PEER.bits(), - Some(verify_peer_cert_callback), - ) - }, - CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY => unsafe { + // `CassSslVerifyFlags` is a bitmask: the values are disjoint bits, meant to + // be combined, e.g. `CASS_SSL_VERIFY_PEER_CERT | CASS_SSL_VERIFY_PEER_IDENTITY`. + // Matching on the value as a whole would reject every such combination. + // + // Bits outside the mask are still unrecognised, so - as before - they make + // the whole value unhonourable and we fall back to the strictest setting. + let flags = if flags & !CASS_SSL_VERIFY_KNOWN_FLAGS != 0 { + tracing::error!( + "Provided unknown CASS_SSL_VERIFY flags: {flags:#x}. \ + Enforcing the strictest verification (peer certificate and peer identity) instead." + ); + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY.0 as i32 + } else { + flags + }; + + if flags & CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS.0 as i32 != 0 { + tracing::warn!( + "The CASS_SSL_VERIFY_PEER_IDENTITY_DNS is not supported, CASS_SSL_VERIFY_PEER_IDENTITY is set in SSL context instead." + ); + } + + // Verifying the peer's identity implies verifying its certificate, so any + // recognised bit turns peer verification on; `CASS_SSL_VERIFY_NONE` is + // their absence. + let verify_identity = flags + & (CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY.0 as i32 + | CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS.0 as i32) + != 0; + let verify_peer = + flags & CASS_SSL_VERIFY_KNOWN_FLAGS != CassSslVerifyFlags::CASS_SSL_VERIFY_NONE.0 as i32; + + let (mode, callback): (SslVerifyMode, VerifyCallback) = match (verify_peer, verify_identity) { + // Verifying the peer's identity implies verifying its certificate. + (_, true) => { // Rust Driver verifies identity by default (and provides no lever to turn this verification off) // by expecting particular IP address to be present in the SAN field. // This means that once we enable SslVerifyMode::PEER, we get certificate + identity verification. - SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) - }, - CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS => unsafe { - // Rust Driver always verifies by IP only, so DNS verification is unsupported. - tracing::warn!( - "The CASS_SSL_VERIFY_PEER_CERT_IDENTITY_DNS is not supported, CASS_SSL_VERIFY_PEER_IDENTITY is set in SSL context." - ); - SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) - }, - _ => unsafe { - // An unrecognised value cannot be honoured, and this API has no way - // of reporting that back to the caller. The only thing we control is - // the resulting security posture, so fail closed: enforce the - // strictest verification we support. - tracing::error!( - "Provided unknown CASS_SSL_VERIFY flags: {flags:#x}. \ - Enforcing the strictest verification (peer certificate and peer identity) instead." - ); - SSL_CTX_set_verify(ssl.ssl_context, SslVerifyMode::PEER.bits(), None) - }, - } + (SslVerifyMode::PEER, None) + } + (true, false) => { + // Chain-only verification. The driver pins the peer's identity for us, + // so the mismatches that pinning produces have to be tolerated. + (SslVerifyMode::PEER, Some(verify_peer_cert_callback)) + } + (false, false) => (SslVerifyMode::NONE, None), + }; + + unsafe { SSL_CTX_set_verify(ssl.ssl_context, mode.bits(), callback) }; } #[unsafe(no_mangle)] @@ -431,3 +451,96 @@ pub unsafe extern "C" fn cass_ssl_set_private_key_n( CassError::CASS_OK } + +#[cfg(test)] +mod tests { + use openssl_sys::SSL_CTX_get_verify_mode; + + use super::*; + + /// Reads the verification mode currently configured in the `SSL_CTX`. + fn verify_mode(ssl: &CassBorrowedSharedPtr<'_, CassSsl, CMut>) -> c_int { + let ssl = ArcFFI::as_ref(ssl.borrow()).unwrap(); + unsafe { SSL_CTX_get_verify_mode(ssl.ssl_context) } + } + + #[test] + fn verify_flags_are_a_bitmask() { + unsafe { + let ssl = cass_ssl_new(); + + cass_ssl_set_verify_flags( + ssl.borrow(), + CassSslVerifyFlags::CASS_SSL_VERIFY_NONE.0 as i32, + ); + assert_eq!(verify_mode(&ssl.borrow()), SslVerifyMode::NONE.bits()); + + // Each flag on its own enables peer verification... + for flags in [ + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT, + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY, + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS, + ] + .map(|flag| flag.0 as i32) + { + cass_ssl_set_verify_flags( + ssl.borrow(), + CassSslVerifyFlags::CASS_SSL_VERIFY_NONE.0 as i32, + ); + cass_ssl_set_verify_flags(ssl.borrow(), flags); + assert_eq!(verify_mode(&ssl.borrow()), SslVerifyMode::PEER.bits()); + } + + // ...and so does any combination of them, as documented in the + // TLS guide and accepted by the C/C++ driver. + for flags in [ + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT.0 as i32 + | CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY.0 as i32, + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_CERT.0 as i32 + | CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY_DNS.0 as i32, + CASS_SSL_VERIFY_KNOWN_FLAGS, + ] { + cass_ssl_set_verify_flags( + ssl.borrow(), + CassSslVerifyFlags::CASS_SSL_VERIFY_NONE.0 as i32, + ); + cass_ssl_set_verify_flags(ssl.borrow(), flags); + assert_eq!(verify_mode(&ssl.borrow()), SslVerifyMode::PEER.bits()); + } + + cass_ssl_free(ssl); + } + } + + #[test] + fn unknown_verify_flags_enforce_strictest_verification() { + unsafe { + let ssl = cass_ssl_new(); + + // An unrecognised bit makes the whole value unhonourable, whether it + // comes on its own or smuggled in next to known flags. Verification + // must then be turned on, and in particular a previously requested + // `CASS_SSL_VERIFY_NONE` must not be left in effect. + for flags in [ + 0x08, + -1, + CassSslVerifyFlags::CASS_SSL_VERIFY_PEER_IDENTITY.0 as i32 | 0x10, + ] { + cass_ssl_set_verify_flags( + ssl.borrow(), + CassSslVerifyFlags::CASS_SSL_VERIFY_NONE.0 as i32, + ); + assert_eq!(verify_mode(&ssl.borrow()), SslVerifyMode::NONE.bits()); + + cass_ssl_set_verify_flags(ssl.borrow(), flags); + assert_eq!( + verify_mode(&ssl.borrow()), + SslVerifyMode::PEER.bits(), + "unknown flags {flags:#x} did not enable peer verification" + ); + } + + cass_ssl_free(ssl); + } + } +} diff --git a/scylla-rust-wrapper/tests/integration/ccm/tls.rs b/scylla-rust-wrapper/tests/integration/ccm/tls.rs index e6335db17..9ef3ab753 100644 --- a/scylla-rust-wrapper/tests/integration/ccm/tls.rs +++ b/scylla-rust-wrapper/tests/integration/ccm/tls.rs @@ -409,6 +409,22 @@ async fn tls_verifies_hostname() { "expected connection to fail: certificate SAN does not match the node IP" ); + // The flags are a bitmask, so the combination the TLS guide documents + // must behave exactly like PEER_IDENTITY on its own, rather than + // falling through to some default. + let err = try_tls_connect( + cluster, + Some(CASS_SSL_VERIFY_PEER_CERT | CASS_SSL_VERIFY_PEER_IDENTITY), + Some(ca_pem.clone()), + None, + ) + .await; + assert_ne!( + err, + CassError::CASS_OK, + "expected PEER_CERT | PEER_IDENTITY to verify the identity too" + ); + // NONE: verification disabled -> the SAN mismatch is ignored and the // connection succeeds. assert_cass_error_eq( From fcc6af6fe3020950adc626db5c868fa44e0625b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Przytu=C5=82a?= Date: Thu, 6 Aug 2026 11:10:10 +0200 Subject: [PATCH 8/8] docs: update TLS guide The guide was outdated. While rewriting the section: - Say that identity is matched against iPAddress subject alternative names (SANs) only, and that the common name (CN) is not consulted, both where certificates are generated and where verification is configured. The guide previously presented CN and SAN as interchangeable. - Document CASS_SSL_VERIFY_PEER_CERT as the way to validate the chain without checking the peer's identity, which it now genuinely does. - Say plainly that CASS_SSL_VERIFY_PEER_IDENTITY_DNS is not supported and is treated as CASS_SSL_VERIFY_PEER_IDENTITY, instead of implying that enabling reverse DNS makes it work. --- docs/source/topics/security/tls.md | 55 +++++++++++++----------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/docs/source/topics/security/tls.md b/docs/source/topics/security/tls.md index a81c5dd63..c1c5dc4b2 100644 --- a/docs/source/topics/security/tls.md +++ b/docs/source/topics/security/tls.md @@ -12,20 +12,11 @@ Some notes on this guide: ### Generating the ScyllaDB/Cassandra Public and Private Keys -The most secure method of setting up TLS is to verify that DNS or IP address used to connect to the server matches identity information found in the TLS certificate. This helps to prevent man-in-the-middle attacks. ScyllaDB/Cassandra uses IP addresses internally so those can be used directly for verification or a domain name can be used via reverse DNS (PTR record). That means that the IP address or domain name of the ScyllaDB/Cassandra server where the certficate is installed needs to be present in either the certficate's common name (CN) or one of its subject alternative names (SANs). It's possible to create the certficate without either, but then it will not be possible to verify the server's identity. Although this is not as secure, it eases the deployment of TLS by allowing the same certficate to be deployed across the entire ScyllaDB/Cassandra cluster. +The most secure method of setting up TLS is to verify that DNS or IP address used to connect to the server matches identity information found in the TLS certificate. This helps to prevent man-in-the-middle attacks. ScyllaDB/Cassandra uses IP addresses internally so those can be used directly for verification (a domain name currently cannot be used via reverse DNS - PTR record). That means that the IP address of the ScyllaDB/Cassandra server where the certificate is installed needs to be present in one of the certificate's subject alternative names (SANs). It's possible to create the certificate without them, but then it will not be possible to verify the server's identity. Although this is not as secure, it eases the deployment of TLS by allowing the same certificate to be deployed across the entire ScyllaDB/Cassandra cluster. -To generate a public/private key pair with the IP address in the CN field use the following: +**NOTE:** this driver verifies the identity against subject alternative names of type `iPAddress` only; unlike the CPP driver, it does not fall back to the common name (CN). Prefer the SAN recipe below. A CN-only certificate can still be used, but only with identity verification relaxed to `CASS_SSL_VERIFY_PEER_CERT` or disabled with `CASS_SSL_VERIFY_NONE`. -```bash -keytool -genkeypair -noprompt -keyalg RSA -validity 36500 \ - -alias node \ - -keystore keystore.jks \ - -storepass \ - -keypass \ - -dname "CN=, OU=Drivers and Tools, O=DataStax Inc., L=Santa Clara, ST=California, C=US" -``` - -If SAN is preferred use this command: +To generate a public/private key pair with the IP address in the SAN field use the following: ```bash keytool -genkeypair -noprompt -keyalg RSA -validity 36500 \ @@ -37,8 +28,6 @@ keytool -genkeypair -noprompt -keyalg RSA -validity 36500 \ -dname "CN=node1.datastax.com, OU=Drivers and Tools, O=DataStax Inc., L=Santa Clara, ST=California, C=US" ``` -**NOTE:** If an IP address SAN is present then it overrides checking the CN. - ### Enabling `client-to-node` Encryption on ScyllaDB/Cassandra The generated keystore from the previous step will need to be copied to all ScyllaDB/Cassandra node(s) and an update of the `cassandra.yaml` configuration file will need to be performed. @@ -163,38 +152,40 @@ cass_ssl_set_verify_flags(ssl, CASS_SSL_VERIFY_NONE); cass_ssl_free(ssl); ``` -#### Enabling ScyllaDB/Cassandra identity verification +#### ScyllaDB/Cassandra identity verification -If a unique certificate has been generated for each ScyllaDB/Cassandra node with the IP address or domain name in the CN or SAN fields, you also need to enable identity verification. +If a unique certificate has been generated for each ScyllaDB/Cassandra node with +the IP address in the SAN field, the driver verifies that the node it connected +to is the one the certificate was issued for. -**NOTE:** This is disabled by default. +**NOTE:** This is disabled by default. This is part of `CASS_SSL_VERIFY_PEER_IDENTITY`. +The flags form a bitmask, so it can be requested explicitly on its own or combined with `CASS_SSL_VERIFY_PEER_CERT`: ```c CassSsl* ssl = cass_ssl_new(); -// Add identity verification flag: CASS_SSL_VERIFY_PEER_IDENTITY (IP address) +// Verify the certificate chain and the peer's identity (IP address). cass_ssl_set_verify_flags(ssl, CASS_SSL_VERIFY_PEER_CERT | CASS_SSL_VERIFY_PEER_IDENTITY); - -// Or use: CASS_SSL_VERIFY_PEER_IDENTITY_DNS (domain name) -cass_ssl_set_verify_flags(ssl, CASS_SSL_VERIFY_PEER_CERT | CASS_SSL_VERIFY_PEER_IDENTITY_DNS); ``` -If using a domain name to verify the peer's identity then hostname resolution -(reverse DNS) needs to be enabled: +**NOTE:** the identity is matched against the certificate's subject alternative +names of type `iPAddress` only. Unlike the C/C++ driver, this driver does not +fall back to the subject common name (CN), so a certificate that identifies a +node only by CN is rejected. -**NOTE:** This is also disabled by default. +To validate the certificate chain without checking who the peer claims to be — +useful with a single certificate shared by all nodes — ask for +`CASS_SSL_VERIFY_PEER_CERT` alone: ```c -CassCluster* cluster = cass_cluster_new(); - -// Enable reverse DNS -cass_cluster_set_use_hostname_resolution(cluster, cass_true); - -/* ... */ - -cass_cluster_free(cluster); +// Verify the certificate chain only; the peer's identity is not checked. +cass_ssl_set_verify_flags(ssl, CASS_SSL_VERIFY_PEER_CERT); ``` +Verifying the identity against a domain name rather than an IP address +(`CASS_SSL_VERIFY_PEER_IDENTITY_DNS`) is **not supported**; it is accepted, but +treated as `CASS_SSL_VERIFY_PEER_IDENTITY`. + ### Using ScyllaDB/Cassandra and the C/C++ driver with client-side certificates Client-side certificates allow ScyllaDB/Cassandra to authenticate the client using public key cryptography and chains of trust. This is same process as above but in reverse. The client has a public and private key and the ScyllaDB/Cassandra node has a copy of the private key or the CA chain used to generate the pair.