diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index d476929923..bbe5187c30 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -30,7 +30,7 @@ jobs: - name: Install mdbook run: cargo install mdbook --no-default-features - name: Build the project - run: cargo build --verbose --examples + run: cargo build --verbose --features ssl --features cloud --examples - name: Build the book run: mdbook build docs - name: Build the book using the script diff --git a/.github/workflows/serverless.yaml b/.github/workflows/serverless.yaml index 477e737343..229d270686 100644 --- a/.github/workflows/serverless.yaml +++ b/.github/workflows/serverless.yaml @@ -3,12 +3,12 @@ name: Serverless on: push: branches: - - main - - 'branch-*' + - main + - "branch-*" pull_request: branches: - - main - - 'branch-*' + - main + - "branch-*" env: CARGO_TERM_COLOR: always @@ -33,9 +33,9 @@ jobs: - name: Check run: cargo check --verbose - name: Run cloud example - run: cargo run --example cloud -- $HOME/.ccm/serverless/config_data.yaml + run: cargo run --features cloud --example cloud -- $HOME/.ccm/serverless/config_data.yaml - name: Run cloud tests - run: CLOUD_CONFIG_PATH=$HOME/.ccm/serverless/config_data.yaml RUSTFLAGS="--cfg scylla_cloud_tests" cargo test --verbose + run: CLOUD_CONFIG_PATH=$HOME/.ccm/serverless/config_data.yaml RUSTFLAGS="--cfg scylla_cloud_tests" cargo test --features cloud --verbose - name: Remove serverless cluster - run: ccm remove serverless \ No newline at end of file + run: ccm remove serverless diff --git a/.github/workflows/tls.yml b/.github/workflows/tls.yml index 94d3b4926c..a860a81aa3 100644 --- a/.github/workflows/tls.yml +++ b/.github/workflows/tls.yml @@ -32,8 +32,13 @@ jobs: working-directory: ./scylla steps: - uses: actions/checkout@v3 - - name: Check + - name: Check OpenSSL run: cargo check --verbose --features "ssl" working-directory: ${{env.working-directory}} - - name: Run tls example - run: cargo run --example tls + - name: Check OpenSSL + run: cargo check --verbose --features "rustls" + working-directory: ${{env.working-directory}} + - name: Run OpenSSL example + run: cargo run --features "ssl" --example tls + - name: Run rustls example + run: cargo run --features "rustls" --example rustls diff --git a/docs/source/connecting/tls.md b/docs/source/connecting/tls.md index 22379b1fe5..4413d0709d 100644 --- a/docs/source/connecting/tls.md +++ b/docs/source/connecting/tls.md @@ -1,11 +1,12 @@ # TLS -Driver uses the [`openssl`](https://github.com/sfackler/rust-openssl) crate for TLS functionality.\ -It was chosen because [`rustls`](https://github.com/ctz/rustls) doesn't support certificates for ip addresses -(see [issue](https://github.com/briansmith/webpki/issues/54)), which is a common use case for Scylla. +Enabling TLS can be done with the [`openssl`](https://github.com/sfackler/rust-openssl) crate or +the [`rustls`](https://github.com/rustls/rustls) crate. +Using the `openssl` crate with the `ssl` feature easily supports most common use cases. + +### Enabling OpenSSL -### Enabling feature `openssl` is not a pure Rust library so you need enable a feature and install the proper package. To enable the `tls` feature add in `Cargo.toml`: @@ -37,7 +38,7 @@ Then install the package with `openssl`: pacman -S openssl pkg-config ``` -### Using TLS +### Using TLS with OpenSSL To use tls you will have to create an openssl [`SslContext`](https://docs.rs/openssl/0.10.33/openssl/ssl/struct.SslContext.html) and pass it to `SessionBuilder` @@ -67,3 +68,11 @@ let session: Session = SessionBuilder::new() ``` See the full [example](https://github.com/scylladb/scylla-rust-driver/blob/main/examples/tls.rs) for more details + +### Using TLS with rustls + +Rustls is a pure Rust crate and does not require installing and C packages. However, +Rustls is a more strict and requires more boilerplate for less secure setups, such as +certifcates with empty common names. + +See the full [example](https://github.com/scylladb/scylla-rust-driver/blob/main/examples/rustls.rs) for more details diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 88a7dbda62..6e3ad3eae6 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -4,14 +4,19 @@ name = "examples" publish = false version = "0.0.0" +[features] +rustls = ["scylla/rustls"] +cloud = ["scylla/cloud"] +ssl = ["scylla/ssl"] + [dev-dependencies] anyhow = "1.0.33" futures = "0.3.6" openssl = "0.10.32" rustyline = "9" rustyline-derive = "0.6" -scylla = {path = "../scylla", features = ["ssl", "cloud", "chrono", "time"]} -tokio = {version = "1.1.0", features = ["full"]} +scylla = { path = "../scylla", features = ["chrono", "time"] } +tokio = { version = "1.1.0", features = ["full"] } tracing = "0.1.25" tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } chrono = { version = "0.4", default-features = false } @@ -20,6 +25,8 @@ uuid = "1.0" tower = "0.4" stats_alloc = "0.1" clap = { version = "3.2.4", features = ["derive"] } +tokio-rustls = "0.25" +rustls-pemfile = "2" [[example]] name = "auth" @@ -36,6 +43,12 @@ path = "logging.rs" [[example]] name = "tls" path = "tls.rs" +required-features = ["ssl"] + +[[example]] +name = "rustls" +path = "rustls.rs" +required-features = ["rustls"] [[example]] name = "cqlsh-rs" @@ -108,6 +121,7 @@ path = "query_history.rs" [[example]] name = "cloud" path = "cloud.rs" +required-features = ["cloud"] [[example]] diff --git a/examples/rustls.rs b/examples/rustls.rs new file mode 100644 index 0000000000..5f3af4bd76 --- /dev/null +++ b/examples/rustls.rs @@ -0,0 +1,163 @@ +use anyhow::Result; +use scylla::transport::session::{IntoTypedRows, Session}; +use scylla::SessionBuilder; +use std::env; +use std::sync::Arc; + +use tokio_rustls::rustls::{ + self, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + pki_types::{CertificateDer, ServerName, UnixTime}, + ClientConfig, DigitallySignedStruct, RootCertStore, +}; + +// How to run scylla instance with TLS: +// +// Edit your scylla.yaml file and add paths to certificates +// ex: +// client_encryption_options: +// enabled: true +// certificate: /etc/scylla/db.crt +// keyfile: /etc/scylla/db.key +// +// If using docker mount your scylla.yaml file and your cert files with option +// --volume $(pwd)/tls.yaml:/etc/scylla/scylla.yaml +// +// If python returns permission error 13 use "Z" flag +// --volume $(pwd)/tls.yaml:/etc/scylla/scylla.yaml:Z +// +// In your Rust program connect to port 9142 if it wasn't changed +// Create new a ClientConfig with your certificate added to it's +// root store. +// +// If your server is using a certifcate that does not have it's IP address +// as a Common Name or Subject Alternate Name you will need to skip +// name verification as part of rustls's configuration. +// +// Build it and add to scylla-rust-driver's SessionBuilder + +#[tokio::main] +async fn main() -> Result<()> { + // Create connection + let uri = env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9142".to_string()); + + println!("Connecting to {} ...", uri); + let mut root_cert_store = RootCertStore::empty(); + let rustls_pemfile::Item::X509Certificate(cert) = rustls_pemfile::read_one_from_slice( + &tokio::fs::read("./test/tls/ca.crt") + .await + .expect("Failed to load cert"), + ) + .expect("Failed to parse pem") + .expect("No certificates in file") + .0 + else { + panic!("not a certificate") + }; + root_cert_store + .add(cert) + .expect("Failed to add cert to root cert"); + + let mut config = ClientConfig::builder() + .with_root_certificates(root_cert_store) + .with_no_client_auth(); + + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification::default())); + + let session: Session = SessionBuilder::new() + .known_node(uri) + .rustls_config(Some(Arc::new(config))) + .build() + .await?; + + session.query("CREATE KEYSPACE IF NOT EXISTS ks WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'replication_factor' : 1}", &[]).await?; + + session + .query( + "CREATE TABLE IF NOT EXISTS ks.t (a int, b int, c text, primary key (a, b))", + &[], + ) + .await?; + + session + .query("INSERT INTO ks.t (a, b, c) VALUES (?, ?, ?)", (3, 4, "def")) + .await?; + + session + .query("INSERT INTO ks.t (a, b, c) VALUES (1, 2, 'abc')", &[]) + .await?; + + let prepared = session + .prepare("INSERT INTO ks.t (a, b, c) VALUES (?, 7, ?)") + .await?; + session + .execute(&prepared, (42_i32, "I'm prepared!")) + .await?; + session + .execute(&prepared, (43_i32, "I'm prepared 2!")) + .await?; + session + .execute(&prepared, (44_i32, "I'm prepared 3!")) + .await?; + + // Rows can be parsed as tuples + if let Some(rows) = session.query("SELECT a, b, c FROM ks.t", &[]).await?.rows { + for row in rows.into_typed::<(i32, i32, String)>() { + let (a, b, c) = row?; + println!("a, b, c: {}, {}, {}", a, b, c); + } + } + println!("Ok."); + + Ok(()) +} + +#[derive(Debug)] +struct NoCertificateVerification { + supported: rustls::crypto::WebPkiSupportedAlgorithms, +} + +impl Default for NoCertificateVerification { + fn default() -> Self { + Self { + supported: rustls::crypto::ring::default_provider().signature_verification_algorithms, + } + } +} + +impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + return Ok(ServerCertVerified::assertion()); + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + return Ok(HandshakeSignatureValid::assertion()); + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + return Ok(HandshakeSignatureValid::assertion()); + } + + fn supported_verify_schemes(&self) -> Vec { + self.supported.supported_schemes() + } +} diff --git a/scylla/Cargo.toml b/scylla/Cargo.toml index adbb51f04a..68303a8bf2 100644 --- a/scylla/Cargo.toml +++ b/scylla/Cargo.toml @@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] ssl = ["dep:tokio-openssl", "dep:openssl"] +rustls = ["dep:tokio-rustls"] cloud = ["ssl", "scylla-cql/serde", "dep:serde_yaml", "dep:serde", "dep:url", "dep:base64"] secret = ["scylla-cql/secret"] chrono = ["scylla-cql/chrono"] @@ -42,6 +43,7 @@ tracing = "0.1.36" chrono = { version = "0.4.20", default-features = false, features = ["clock"] } openssl = { version = "0.10.32", optional = true } tokio-openssl = { version = "0.6.1", optional = true } +tokio-rustls = { version = "0.25", optional = true } arc-swap = "1.3.0" dashmap = "5.2" strum = "0.23" diff --git a/scylla/src/lib.rs b/scylla/src/lib.rs index 5bf9bc69e8..37edb08d65 100644 --- a/scylla/src/lib.rs +++ b/scylla/src/lib.rs @@ -142,3 +142,6 @@ pub use transport::retry_policy; pub use transport::speculative_execution; pub use transport::metrics::Metrics; + +#[cfg(all(feature = "ssl", feature = "rustls"))] +compile_error!("both rustls and ssl should not be enabled together."); diff --git a/scylla/src/transport/connection.rs b/scylla/src/transport/connection.rs index b6b91b69db..f0aa7f327b 100644 --- a/scylla/src/transport/connection.rs +++ b/scylla/src/transport/connection.rs @@ -23,8 +23,10 @@ use std::sync::atomic::AtomicU64; use std::time::Duration; #[cfg(feature = "ssl")] use tokio_openssl::SslStream; +#[cfg(feature = "rustls")] +use tokio_rustls::TlsConnector; -#[cfg(feature = "ssl")] +#[cfg(any(feature = "ssl", feature = "rustls"))] pub(crate) use ssl_config::SslConfig; use crate::authentication::AuthenticatorProvider; @@ -280,12 +282,17 @@ impl NonErrorQueryResponse { }) } } -#[cfg(feature = "ssl")] +#[cfg(any(feature = "ssl", feature = "rustls"))] mod ssl_config { + #[cfg(feature = "ssl")] use openssl::{ error::ErrorStack, ssl::{Ssl, SslContext}, }; + #[cfg(feature = "rustls")] + use std::{net::IpAddr, sync::Arc}; + #[cfg(feature = "rustls")] + use tokio_rustls::rustls::{pki_types::ServerName, ClientConfig}; #[cfg(feature = "cloud")] use uuid::Uuid; @@ -299,6 +306,7 @@ mod ssl_config { // NodeConnectionPool::new(). Inside that function, the field is mutated to contain SslConfig specific // for the particular node. (The SslConfig must be different, because SNIs differ for different nodes.) // Thenceforth, all connections to that node share the same SslConfig. + #[cfg(feature = "ssl")] #[derive(Clone)] pub struct SslConfig { context: SslContext, @@ -306,6 +314,7 @@ mod ssl_config { sni: Option, } + #[cfg(feature = "ssl")] impl SslConfig { // Used in case when the user provided their own SslContext to be used in all connections. pub fn new_with_global_context(context: SslContext) -> Self { @@ -345,6 +354,59 @@ mod ssl_config { Ok(ssl) } } + + #[cfg(feature = "rustls")] + #[derive(Clone)] + pub struct SslConfig { + config: Arc, + #[cfg(feature = "cloud")] + sni: Option>, + } + + #[cfg(feature = "rustls")] + impl SslConfig { + // Used in case when the user provided their own ClientConfig to be used in all connections. + pub fn new_with_global_config(config: &Arc) -> Self { + Self { + config: config.clone(), + #[cfg(feature = "cloud")] + sni: None, + } + } + + // Used in case of Serverless Cloud connections. + #[cfg(feature = "cloud")] + pub(crate) fn new_for_sni( + config: &Arc, + domain_name: &str, + host_id: Option, + ) -> Self { + Self { + config: config.clone(), + #[cfg(feature = "cloud")] + sni: Some(if let Some(host_id) = host_id { + ServerName::try_from(&format!("{}.{}", host_id, domain_name)) + .expect("invalid DNS name") + .to_owned() + } else { + ServerName::try_from(domain_name.into().expect("invalid DNS name")).to_owned() + }), + } + } + + pub(crate) fn server_name(&self, node_addr: IpAddr) -> ServerName<'static> { + #[cfg(feature = "cloud")] + if let Some(sni) = self.sni.as_ref() { + return sni.clone(); + } + ServerName::IpAddress(node_addr.into()) + } + + // A reference to the rustls Client Config to produce a TlsConnection + pub(crate) fn config(&self) -> &Arc { + &self.config + } + } } #[derive(Clone)] @@ -352,7 +414,7 @@ pub struct ConnectionConfig { pub compression: Option, pub tcp_nodelay: bool, pub tcp_keepalive_interval: Option, - #[cfg(feature = "ssl")] + #[cfg(any(feature = "ssl", feature = "rustls"))] pub ssl_config: Option, pub connect_timeout: std::time::Duration, // should be Some only in control connections, @@ -375,7 +437,7 @@ impl Default for ConnectionConfig { tcp_nodelay: true, tcp_keepalive_interval: None, event_sender: None, - #[cfg(feature = "ssl")] + #[cfg(any(feature = "ssl", feature = "rustls"))] ssl_config: None, connect_timeout: std::time::Duration::from_secs(5), default_consistency: Default::default(), @@ -393,7 +455,7 @@ impl Default for ConnectionConfig { } impl ConnectionConfig { - #[cfg(feature = "ssl")] + #[cfg(any(feature = "ssl", feature = "rustls"))] pub fn is_ssl(&self) -> bool { #[cfg(feature = "cloud")] if self.cloud_config.is_some() { @@ -402,7 +464,7 @@ impl ConnectionConfig { self.ssl_config.is_some() } - #[cfg(not(feature = "ssl"))] + #[cfg(not(any(feature = "ssl", feature = "rustls")))] pub fn is_ssl(&self) -> bool { false } @@ -1034,6 +1096,27 @@ impl Connection { return Ok(handle); } + #[cfg(feature = "rustls")] + if let Some(rustls_config) = &config.ssl_config { + let connector = TlsConnector::from(rustls_config.config().clone()); + let stream = connector + .connect(rustls_config.server_name(node_address), stream) + .await?; + + let (task, handle) = Self::router( + config, + stream, + receiver, + error_sender, + orphan_notification_receiver, + router_handle, + node_address, + ) + .remote_handle(); + tokio::task::spawn(task.with_current_subscriber()); + return Ok(handle); + } + let (task, handle) = Self::router( config, stream, diff --git a/scylla/src/transport/connection_pool.rs b/scylla/src/transport/connection_pool.rs index f26ea36ac2..c3599a4b3f 100644 --- a/scylla/src/transport/connection_pool.rs +++ b/scylla/src/transport/connection_pool.rs @@ -1282,7 +1282,7 @@ mod tests { let connection_config = ConnectionConfig { compression: None, tcp_nodelay: true, - #[cfg(feature = "ssl")] + #[cfg(any(feature = "ssl", feature = "rustls"))] ssl_config: None, ..Default::default() }; diff --git a/scylla/src/transport/session.rs b/scylla/src/transport/session.rs index f4f5ab2365..4085acd284 100644 --- a/scylla/src/transport/session.rs +++ b/scylla/src/transport/session.rs @@ -36,7 +36,7 @@ use uuid::Uuid; use super::connection::NonErrorQueryResponse; use super::connection::QueryResponse; -#[cfg(feature = "ssl")] +#[cfg(any(feature = "ssl", feature = "rustls"))] use super::connection::SslConfig; use super::errors::{NewSessionError, QueryError}; use super::execution_profile::{ExecutionProfile, ExecutionProfileHandle, ExecutionProfileInner}; @@ -77,6 +77,8 @@ use crate::authentication::AuthenticatorProvider; #[cfg(feature = "ssl")] use openssl::ssl::SslContext; use scylla_cql::errors::BadQuery; +#[cfg(feature = "rustls")] +use tokio_rustls::rustls::ClientConfig; /// Translates IP addresses received from ScyllaDB nodes into locally reachable addresses. /// @@ -196,6 +198,10 @@ pub struct SessionConfig { #[cfg(feature = "ssl")] pub ssl_context: Option, + /// Provide our Session with TLS + #[cfg(feature = "rustls")] + pub rustls_config: Option>, + pub authenticator: Option>, pub connect_timeout: Duration, @@ -312,6 +318,8 @@ impl SessionConfig { keyspace_case_sensitive: false, #[cfg(feature = "ssl")] ssl_context: None, + #[cfg(feature = "rustls")] + rustls_config: None, authenticator: None, connect_timeout: Duration::from_secs(5), connection_pool_size: Default::default(), @@ -499,6 +507,11 @@ impl Session { tcp_keepalive_interval: config.tcp_keepalive_interval, #[cfg(feature = "ssl")] ssl_config: config.ssl_context.map(SslConfig::new_with_global_context), + #[cfg(feature = "rustls")] + ssl_config: config + .rustls_config + .as_ref() + .map(SslConfig::new_with_global_config), authenticator: config.authenticator.clone(), connect_timeout: config.connect_timeout, event_sender: None, diff --git a/scylla/src/transport/session_builder.rs b/scylla/src/transport/session_builder.rs index 09ee03b961..da779520b3 100644 --- a/scylla/src/transport/session_builder.rs +++ b/scylla/src/transport/session_builder.rs @@ -25,6 +25,8 @@ use std::time::Duration; use crate::authentication::{AuthenticatorProvider, PlainTextAuthenticator}; #[cfg(feature = "ssl")] use openssl::ssl::SslContext; +#[cfg(feature = "rustls")] +use tokio_rustls::rustls::ClientConfig; use tracing::warn; mod sealed { @@ -334,6 +336,38 @@ impl GenericSessionBuilder { self.config.ssl_context = ssl_context; self } + + /// rustls feature + /// Provide SessionBuilder with ClientConfig from rustls crate that will be + /// used to create an ssl connection to the database. + /// If set to None SSL connection won't be used. + /// Default is None. + /// + /// # Example + /// ``` + /// # use std::fs; + /// # use std::path::PathBuf; + /// # use scylla::{Session, SessionBuilder}; + /// # use openssl::ssl::{SslContextBuilder, SslVerifyMode, SslMethod, SslFiletype}; + /// # async fn example() -> Result<(), Box> { + /// let certdir = fs::canonicalize(PathBuf::from("./examples/certs/scylla.crt"))?; + /// let mut context_builder = SslContextBuilder::new(SslMethod::tls())?; + /// context_builder.set_certificate_file(certdir.as_path(), SslFiletype::PEM)?; + /// context_builder.set_verify(SslVerifyMode::NONE); + /// + /// let session: Session = SessionBuilder::new() + /// .known_node("127.0.0.1:9042") + /// .ssl_context(Some(context_builder.build())) + /// .build() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "rustls")] + pub fn rustls_config(mut self, config: Option>) -> Self { + self.config.rustls_config = config; + self + } } // NOTE: this `impl` block contains configuration options specific for **Cloud** [`Session`].