From 75a83360802105dcc9e1f4141ff3f2a1221d9d6f Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 6 Aug 2024 09:06:20 -0300 Subject: [PATCH] feat: improving tls flow (#34) ## Motivation I'm writing an article on how easy it's suppose to be to setup TLS/SSL with ScyllaDB and one of the examples which I want to add is using JS. ATM the driver only support the Certificate without Keys/Truststore and this pull request adds this specific support. ```js // Before const cluster = new Cluster({ nodes, ssl: { caFilepath: "/your/path/to/certificates/client_truststore.pem", verifyMode: VerifyMode.Peer, } }); // After const cluster = new Cluster({ nodes, ssl: { enabled: true, // Feature Flag truststoreFilepath: "/your/path/to/certificates/client_cert.pem", // Added field privateKeyFilepath: "/your/path/to/certificates/client_key.pem", // Added field caFilepath: "/your/path/to/certificates/client_truststore.pem", verifyMode: VerifyMode.Peer, } }); ``` IMHO I don't know if this feature flag is useful, but at least for me seems more like a easy way to turn it on/off. So, please let me know your thoughts on that. > [!TIP] > You can test with [this sample](https://github.com/DanielHe4rt/scylladb-role-tls-auth) by running `make setup` and then pointing your keys **absolute path** at the SSL object. Also, don't forget to switch your port to `9142` at the connection string. ## Changes - [x] TLS/SSL with Keystore and Private Keys - [x] Feature flag to enable/disable SSL. - [x] Simple example on how to use it. --- examples/tls.mts | 41 ++++++++++++++++ index.d.ts | 21 ++++---- src/cluster/scylla_cluster.rs | 91 +++++++++++++++++++++++------------ 3 files changed, 112 insertions(+), 41 deletions(-) create mode 100644 examples/tls.mts diff --git a/examples/tls.mts b/examples/tls.mts new file mode 100644 index 0000000..27eb6dc --- /dev/null +++ b/examples/tls.mts @@ -0,0 +1,41 @@ +import {Cluster, VerifyMode} from "../index.js" + +const nodes = process.env.CLUSTER_NODES?.split(",") ?? ["localhost:9142"]; +console.log(`Connecting to ${nodes}`); + +const cluster = new Cluster({ + nodes, + ssl: { + enabled: true, + truststoreFilepath: "/your/path/to/certificates/client_cert.pem", + privateKeyFilepath: "/your/path/to/certificates/client_key.pem", + caFilepath: "/your/path/to/certificates/client_truststore.pem", + verifyMode: VerifyMode.Peer, + } +}); + +const session = await cluster.connect(); + +interface ConnectedClient { + address: String, + port: number, + username: String, + driver_name: String, + driver_version: String, +} + +// @ts-ignore +let result = await session.execute("SELECT address, port, username, driver_name, driver_version FROM system.clients"); + +console.log(result) +// [ +// { +// address: '127.0.0.1', +// driver_name: 'scylla-rust-driver', +// driver_version: '0.10.1', +// port: 58846, +// username: 'developer' +// } +// ] + + diff --git a/index.d.ts b/index.d.ts index 7755299..654479a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -52,7 +52,10 @@ export interface Auth { password: string } export interface Ssl { - caFilepath: string + enabled: boolean + caFilepath?: string + privateKeyFilepath?: string + truststoreFilepath?: string verifyMode?: VerifyMode } export const enum VerifyMode { @@ -89,7 +92,7 @@ export interface ScyllaMaterializedView { baseTableName: string } export type ScyllaCluster = Cluster -export class Cluster { +export declare class Cluster { /** * Object config is in the format: * { @@ -108,7 +111,7 @@ export type ScyllaBatchStatement = BatchStatement * These statements can be simple or prepared. * Only INSERT, UPDATE and DELETE statements are allowed. */ -export class BatchStatement { +export declare class BatchStatement { constructor() /** * Appends a statement to the batch. @@ -118,17 +121,17 @@ export class BatchStatement { */ appendStatement(statement: Query | PreparedStatement): void } -export class PreparedStatement { +export declare class PreparedStatement { setConsistency(consistency: Consistency): void setSerialConsistency(serialConsistency: SerialConsistency): void } -export class Query { +export declare class Query { constructor(query: string) setConsistency(consistency: Consistency): void setSerialConsistency(serialConsistency: SerialConsistency): void setPageSize(pageSize: number): void } -export class Metrics { +export declare class Metrics { /** Returns counter for nonpaged queries */ getQueriesNum(): bigint /** Returns counter for pages requested in paged queries */ @@ -148,7 +151,7 @@ export class Metrics { */ getLatencyPercentileMs(percentile: number): bigint } -export class ScyllaSession { +export declare class ScyllaSession { metrics(): Metrics getClusterData(): Promise execute(query: string | Query | PreparedStatement, parameters?: Array | undefined | null): Promise @@ -261,14 +264,14 @@ export class ScyllaSession { awaitSchemaAgreement(): Promise checkSchemaAgreement(): Promise } -export class ScyllaClusterData { +export declare class ScyllaClusterData { /** * Access keyspaces details collected by the driver Driver collects various schema details like * tables, partitioners, columns, types. They can be read using this method */ getKeyspaceInfo(): Record | null } -export class Uuid { +export declare class Uuid { /** Generates a random UUID v4. */ static randomV4(): Uuid /** Parses a UUID from a string. It may fail if the string is not a valid UUID. */ diff --git a/src/cluster/scylla_cluster.rs b/src/cluster/scylla_cluster.rs index 718f1c1..484995b 100644 --- a/src/cluster/scylla_cluster.rs +++ b/src/cluster/scylla_cluster.rs @@ -1,7 +1,7 @@ use std::time::Duration; use napi::Either; -use openssl::ssl::SslContextBuilder; +use openssl::ssl::{SslContextBuilder, SslFiletype}; use crate::{ cluster::{ @@ -40,8 +40,12 @@ pub struct Auth { #[napi(object)] #[derive(Clone)] pub struct Ssl { - pub ca_filepath: String, + pub enabled: bool, + pub ca_filepath: Option, + pub private_key_filepath: Option, + pub truststore_filepath: Option, pub verify_mode: Option, + // SSL Filetype: PEM / ASN1 } #[napi] @@ -192,43 +196,66 @@ impl ScyllaCluster { } if let Some(ssl) = ssl? { - let ssl_builder = SslContextBuilder::new(openssl::ssl::SslMethod::tls()); + if ssl.enabled { + let ssl_builder = SslContextBuilder::new(openssl::ssl::SslMethod::tls()); - if let Err(err) = ssl_builder { - return Err(napi::Error::new( - napi::Status::InvalidArg, - format!("Failed to create SSL context: {}", err), - )); - } + if let Err(err) = ssl_builder { + return Err(napi::Error::new( + napi::Status::InvalidArg, + format!("Failed to create SSL context: {}", err), + )); + } - // Safe to unwrap because we checked for Err above - let mut ssl_builder = ssl_builder.unwrap(); + // Safe to unwrap because we checked for Err above + let mut ssl_builder = ssl_builder.unwrap(); - if let Some(verify_mode) = ssl.verify_mode { - ssl_builder.set_verify(match verify_mode { - VerifyMode::None => openssl::ssl::SslVerifyMode::NONE, - VerifyMode::Peer => openssl::ssl::SslVerifyMode::PEER, - }); - } else { - ssl_builder.set_verify(openssl::ssl::SslVerifyMode::NONE); - } + if let Some(verify_mode) = ssl.verify_mode { + ssl_builder.set_verify(match verify_mode { + VerifyMode::None => openssl::ssl::SslVerifyMode::NONE, + VerifyMode::Peer => openssl::ssl::SslVerifyMode::PEER, + }); + } else { + ssl_builder.set_verify(openssl::ssl::SslVerifyMode::NONE); + } - if let Err(err) = ssl_builder.set_ca_file(ssl.ca_filepath) { - return Err(napi::Error::new( - napi::Status::InvalidArg, - format!("Failed to set CA file: {}", err), - )); - } + if let Some(private_key_filepath) = ssl.private_key_filepath { + if let Err(err) = ssl_builder.set_private_key_file(private_key_filepath, SslFiletype::PEM) + { + return Err(napi::Error::new( + napi::Status::InvalidArg, + format!("Failed to set private key file: {}", err), + )); + } + } - if let Some(auto_await_schema_agreement) = self.auto_await_schema_agreement { - builder = builder.auto_await_schema_agreement(auto_await_schema_agreement); - } + if let Some(truststore_filepath) = ssl.truststore_filepath { + if let Err(err) = ssl_builder.set_certificate_chain_file(truststore_filepath) { + return Err(napi::Error::new( + napi::Status::InvalidArg, + format!("Failed to set truststore file: {}", err), + )); + } + } - if let Some(schema_agreement_interval) = self.schema_agreement_interval { - builder = builder.schema_agreement_interval(schema_agreement_interval); - } + if let Some(ca_filepath) = ssl.ca_filepath { + if let Err(err) = ssl_builder.set_ca_file(ca_filepath) { + return Err(napi::Error::new( + napi::Status::InvalidArg, + format!("Failed to set CA file: {}", err), + )); + } + } + + if let Some(auto_await_schema_agreement) = self.auto_await_schema_agreement { + builder = builder.auto_await_schema_agreement(auto_await_schema_agreement); + } + + if let Some(schema_agreement_interval) = self.schema_agreement_interval { + builder = builder.schema_agreement_interval(schema_agreement_interval); + } - builder = builder.ssl_context(Some(ssl_builder.build())); + builder = builder.ssl_context(Some(ssl_builder.build())); + } } if let Some(default_execution_profile) = &self.default_execution_profile {