This is an automated email from the ASF dual-hosted git repository.

CritasWang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iotdb-client-rust.git

commit c7dcdc140e072b5b764aaf6e3cc2a4896fcd1e5e
Author: CritasWang <[email protected]>
AuthorDate: Mon Jul 13 14:16:59 2026 +0800

    V5: TLS (feature-gated, native-tls shared-stream channel) + RPC compression 
(compact protocol; server requires dn_rpc_thrift_compression_enable — no 
auto-detection, spec corrected)
---
 .github/workflows/ci.yml     |  10 ++
 Cargo.toml                   |   4 +
 README.md                    |  30 ++++
 README_ZH.md                 |  30 ++++
 src/client/pool.rs           |  11 +-
 src/client/session.rs        | 251 +++++++++++++++++++++++++--
 src/client/table_session.rs  |  57 +++++++
 src/connection/mod.rs        | 392 ++++++++++++++++++++++++++++++++++++++++---
 src/error.rs                 |   5 +
 src/lib.rs                   |   4 +-
 tests/fixtures/tls/README.md |  39 +++++
 tests/fixtures/tls/cert.pem  |  20 +++
 tests/fixtures/tls/key.pem   |  28 ++++
 13 files changed, 845 insertions(+), 36 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d7b001c..6e02e1b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,13 +47,23 @@ jobs:
       - name: Clippy
         run: cargo clippy --all-targets -- -D warnings
 
+      - name: Clippy (tls)
+        run: cargo clippy --all-targets --features tls -- -D warnings
+
       - name: Build
         run: cargo build
 
+      - name: Build (tls)
+        run: cargo build --features tls
+
       # Live-server tests skip themselves when nothing listens on 6667.
       - name: Unit tests
         run: cargo test
 
+      # Adds the loopback TLS handshake tests (self-signed fixture cert).
+      - name: Unit tests (tls)
+        run: cargo test --features tls
+
   integration:
     runs-on: ubuntu-latest
     services:
diff --git a/Cargo.toml b/Cargo.toml
index 23195dd..1399830 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,9 +31,13 @@ thrift = "0.23"
 byteorder = "1.5"
 chrono = "0.4"
 log = "0.4"
+native-tls = { version = "0.2", optional = true }
 
 [dev-dependencies]
 env_logger = "0.11"
 
 [features]
 default = []
+# TLS support via the platform-native TLS stack (SecureTransport /
+# SChannel / OpenSSL). Adds `use_ssl` & friends to SessionConfig.
+tls = ["dep:native-tls"]
diff --git a/README.md b/README.md
index a6bc084..823be0b 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,36 @@ cargo run --example table_session
 cargo run --example session_pool
 ```
 
+## TLS & RPC compression
+
+**RPC compression** (IoTDB's term for the Thrift *compact protocol*) is a 
plain config flag:
+
+```rust
+let config = SessionConfig { enable_rpc_compression: true, 
..Default::default() };
+// or: TableSession::builder().enable_rpc_compression(true)...
+```
+
+It must match the **server** setting `dn_rpc_thrift_compression_enable` 
(default `false`). The server speaks exactly one protocol — there is no 
per-connection negotiation, so a mismatch in either direction fails at the 
first RPC with a transport error.
+
+**TLS** is behind the `tls` cargo feature (platform-native TLS via 
[`native-tls`](https://crates.io/crates/native-tls)):
+
+```toml
+iotdb-client = { version = "0.1", features = ["tls"] }
+```
+
+```rust
+let config = SessionConfig {
+    use_ssl: true,
+    ca_cert_path: Some("ca.pem".into()),  // trust a private CA / self-signed 
cert
+    accept_invalid_certs: false,          // true skips verification (tests 
only!)
+    domain_override: None,                // SNI/validation hostname when 
connecting by IP
+    ..Default::default()
+};
+// or: TableSession::builder().use_ssl(true).ca_cert_path("ca.pem")...
+```
+
+The server needs Thrift SSL enabled (`enable_thrift_ssl=true` + key store). 
Pool configs pass both options through their embedded `session` config.
+
 ## Thrift codegen
 
 Generated stubs live in `src/protocol/` (`client.rs`, `common.rs`); never 
hand-edit them. The IDL sources in `thrift/` are synced from the IoTDB repo's 
`iotdb-protocol/` (`thrift-datanode/src/main/thrift/client.thrift`, 
`thrift-commons/src/main/thrift/common.thrift`).
diff --git a/README_ZH.md b/README_ZH.md
index 2a5cb36..6f40256 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -172,6 +172,36 @@ cargo run --example table_session
 cargo run --example session_pool
 ```
 
+## TLS 与 RPC 压缩
+
+**RPC 压缩**(IoTDB 术语,实为 Thrift *compact 协议*)只是一个配置开关:
+
+```rust
+let config = SessionConfig { enable_rpc_compression: true, 
..Default::default() };
+// 或:TableSession::builder().enable_rpc_compression(true)...
+```
+
+必须与**服务端**配置 `dn_rpc_thrift_compression_enable`(默认 
`false`)一致。服务端只讲一种协议——没有按连接协商的机制,任意方向的不匹配都会在第一个 RPC 上以传输错误失败。
+
+**TLS** 位于 `tls` cargo feature 之后(基于 
[`native-tls`](https://crates.io/crates/native-tls) 的平台原生 TLS):
+
+```toml
+iotdb-client = { version = "0.1", features = ["tls"] }
+```
+
+```rust
+let config = SessionConfig {
+    use_ssl: true,
+    ca_cert_path: Some("ca.pem".into()),  // 信任私有 CA / 自签名证书
+    accept_invalid_certs: false,          // true 跳过证书校验(仅限测试!)
+    domain_override: None,                // 按 IP 连接时用于 SNI/校验的主机名
+    ..Default::default()
+};
+// 或:TableSession::builder().use_ssl(true).ca_cert_path("ca.pem")...
+```
+
+服务端需开启 Thrift SSL(`enable_thrift_ssl=true` + 密钥库)。连接池配置通过其内嵌的 `session` 
配置透传这两组选项。
+
 ## Thrift 代码生成
 
 生成的桩代码位于 `src/protocol/`(`client.rs`、`common.rs`),请勿手动编辑。`thrift/` 中的 IDL 源文件从 
IoTDB 仓库的 `iotdb-protocol/` 
同步(`thrift-datanode/src/main/thrift/client.thrift`、`thrift-commons/src/main/thrift/common.thrift`)。
diff --git a/src/client/pool.rs b/src/client/pool.rs
index 64b3564..3df10aa 100644
--- a/src/client/pool.rs
+++ b/src/client/pool.rs
@@ -621,9 +621,14 @@ mod tests {
 
     fn injected_session(endpoint: &Endpoint) -> Session {
         let mut session = Session::new(dead_endpoint_config());
-        let connection =
-            crate::connection::Connection::open(endpoint.clone(), 
Duration::from_millis(500))
-                .expect("connect to test listener");
+        let connection = crate::connection::Connection::open(
+            endpoint.clone(),
+            &crate::connection::ConnectionOptions {
+                connect_timeout: Duration::from_millis(500),
+                ..Default::default()
+            },
+        )
+        .expect("connect to test listener");
         session.test_inject_connection(connection);
         session
     }
diff --git a/src/client/session.rs b/src/client/session.rs
index f98a308..5f4425c 100644
--- a/src/client/session.rs
+++ b/src/client/session.rs
@@ -23,7 +23,7 @@ use std::time::Duration;
 
 use crate::client::dataset::SessionDataSet;
 use crate::client::redirect::{self, RedirectCache, RedirectCacheStats};
-use crate::connection::{Connection, Endpoint};
+use crate::connection::{Connection, ConnectionOptions, Endpoint, RpcProtocol};
 use crate::data::record::serialize_record_values;
 use crate::data::{Tablet, Value};
 use crate::error::{Error, Result};
@@ -73,6 +73,28 @@ pub struct SessionConfig {
     /// Harvest status-400 `redirectNode` hints from insert responses into
     /// the per-session [`RedirectCache`]. Default `true`.
     pub enable_redirection: bool,
+    /// Speak TCompactProtocol instead of TBinaryProtocol ("RPC compression"
+    /// in IoTDB terms). Must match the **server** setting
+    /// `dn_rpc_thrift_compression_enable` (default `false`) — there is no
+    /// per-connection negotiation; a mismatch fails at the first RPC.
+    /// Default `false`.
+    pub enable_rpc_compression: bool,
+    /// Wrap connections in TLS (cargo feature `tls`). The server must have
+    /// `enable_thrift_ssl=true`. Default `false`.
+    #[cfg(feature = "tls")]
+    pub use_ssl: bool,
+    /// PEM certificate added as trusted root (private CA / self-signed
+    /// server cert). `None` → platform trust store only.
+    #[cfg(feature = "tls")]
+    pub ca_cert_path: Option<std::path::PathBuf>,
+    /// Skip certificate verification (self-signed test certs).
+    /// **Dangerous** outside tests. Default `false`.
+    #[cfg(feature = "tls")]
+    pub accept_invalid_certs: bool,
+    /// Hostname for SNI + certificate validation instead of the endpoint
+    /// host (e.g. when connecting by IP).
+    #[cfg(feature = "tls")]
+    pub domain_override: Option<String>,
 }
 
 impl Default for SessionConfig {
@@ -91,6 +113,15 @@ impl Default for SessionConfig {
             max_reconnect_attempts: 3,
             retry_interval: Duration::from_secs(1),
             enable_redirection: true,
+            enable_rpc_compression: false,
+            #[cfg(feature = "tls")]
+            use_ssl: false,
+            #[cfg(feature = "tls")]
+            ca_cert_path: None,
+            #[cfg(feature = "tls")]
+            accept_invalid_certs: false,
+            #[cfg(feature = "tls")]
+            domain_override: None,
         }
     }
 }
@@ -105,6 +136,25 @@ impl SessionConfig {
             .collect::<Result<Vec<_>>>()?;
         Ok(self)
     }
+
+    /// Resolve the connection-level options (timeout, wire protocol, TLS)
+    /// this config implies.
+    pub fn connection_options(&self) -> ConnectionOptions {
+        ConnectionOptions {
+            connect_timeout: self.connect_timeout,
+            protocol: if self.enable_rpc_compression {
+                RpcProtocol::Compact
+            } else {
+                RpcProtocol::Binary
+            },
+            #[cfg(feature = "tls")]
+            tls: self.use_ssl.then(|| crate::connection::TlsOptions {
+                ca_cert_path: self.ca_cert_path.clone(),
+                accept_invalid_certs: self.accept_invalid_certs,
+                domain_override: self.domain_override.clone(),
+            }),
+        }
+    }
 }
 
 /// Raw result of a query statement: response metadata plus undecoded TsBlocks.
@@ -167,11 +217,12 @@ impl Session {
         }
 
         let start = ENDPOINT_START_INDEX.fetch_add(1, Ordering::Relaxed) % n;
+        let options = self.config.connection_options();
         let mut connection = None;
         let mut last_err: Option<Error> = None;
         for i in 0..n {
             let endpoint = self.config.endpoints[(start + i) % n].clone();
-            match Connection::open(endpoint, self.config.connect_timeout) {
+            match Connection::open(endpoint, &options) {
                 Ok(c) => {
                     connection = Some(c);
                     break;
@@ -242,6 +293,7 @@ impl Session {
             .and_then(|ep| self.config.endpoints.iter().position(|e| e == ep))
             .unwrap_or(0);
         let attempts = self.config.max_reconnect_attempts.max(1);
+        let options = self.config.connection_options();
         let mut last_err: Option<Error> = None;
         for attempt in 0..attempts {
             if attempt > 0 {
@@ -249,12 +301,10 @@ impl Session {
             }
             for i in 0..n {
                 let endpoint = self.config.endpoints[(start + i) % n].clone();
-                let result = Connection::open(endpoint, 
self.config.connect_timeout).and_then(
-                    |mut connection| {
-                        let ids = self.authenticate(&mut connection)?;
-                        Ok((connection, ids))
-                    },
-                );
+                let result = Connection::open(endpoint, 
&options).and_then(|mut connection| {
+                    let ids = self.authenticate(&mut connection)?;
+                    Ok((connection, ids))
+                });
                 match result {
                     Ok((connection, (session_id, statement_id))) => {
                         log::info!("reconnected to {}", connection.endpoint());
@@ -970,6 +1020,49 @@ mod tests {
         assert_eq!(cfg.max_reconnect_attempts, 3);
         assert_eq!(cfg.retry_interval, Duration::from_secs(1));
         assert!(cfg.enable_redirection);
+        assert!(!cfg.enable_rpc_compression);
+        #[cfg(feature = "tls")]
+        {
+            assert!(!cfg.use_ssl);
+            assert!(cfg.ca_cert_path.is_none());
+            assert!(!cfg.accept_invalid_certs);
+            assert!(cfg.domain_override.is_none());
+        }
+    }
+
+    /// Config → connection options mapping: compression selects the compact
+    /// protocol, `use_ssl` (feature `tls`) carries the TLS fields through.
+    #[test]
+    fn connection_options_from_config() {
+        use crate::connection::RpcProtocol;
+
+        let cfg = SessionConfig::default();
+        let options = cfg.connection_options();
+        assert_eq!(options.connect_timeout, cfg.connect_timeout);
+        assert_eq!(options.protocol, RpcProtocol::Binary);
+        #[cfg(feature = "tls")]
+        assert!(options.tls.is_none());
+
+        let cfg = SessionConfig {
+            enable_rpc_compression: true,
+            ..Default::default()
+        };
+        assert_eq!(cfg.connection_options().protocol, RpcProtocol::Compact);
+
+        #[cfg(feature = "tls")]
+        {
+            let cfg = SessionConfig {
+                use_ssl: true,
+                ca_cert_path: Some("/certs/ca.pem".into()),
+                accept_invalid_certs: true,
+                domain_override: Some("iotdb.internal".into()),
+                ..Default::default()
+            };
+            let tls = cfg.connection_options().tls.expect("tls options");
+            assert_eq!(tls.ca_cert_path.as_deref(), 
Some("/certs/ca.pem".as_ref()));
+            assert!(tls.accept_invalid_certs);
+            assert_eq!(tls.domain_override.as_deref(), Some("iotdb.internal"));
+        }
     }
 
     #[test]
@@ -1302,8 +1395,14 @@ mod tests {
             retry_interval: Duration::from_millis(10),
             ..Default::default()
         });
-        let connection = Connection::open(endpoint.clone(), 
Duration::from_millis(500)) //
-            .expect("connect to listener");
+        let connection = Connection::open(
+            endpoint.clone(),
+            &ConnectionOptions {
+                connect_timeout: Duration::from_millis(500),
+                ..Default::default()
+            },
+        )
+        .expect("connect to listener");
         session.test_inject_connection(connection);
         assert_eq!(session.current_endpoint(), Some(&endpoint));
 
@@ -1328,8 +1427,14 @@ mod tests {
             enable_auto_reconnect: false,
             ..Default::default()
         });
-        let connection = Connection::open(endpoint, 
Duration::from_millis(500)) //
-            .expect("connect to listener");
+        let connection = Connection::open(
+            endpoint,
+            &ConnectionOptions {
+                connect_timeout: Duration::from_millis(500),
+                ..Default::default()
+            },
+        )
+        .expect("connect to listener");
         session.test_inject_connection(connection);
 
         let err = session.execute_non_query("SHOW DATABASES").unwrap_err();
@@ -1378,6 +1483,128 @@ mod tests {
         assert!(!session.is_open());
     }
 
+    /// RPC compression (TCompactProtocol) against a live server.
+    ///
+    /// Verified 2026-07-13 against apache/iotdb:2.0.6-standalone: the server
+    /// speaks exactly **one** protocol, fixed by its config
+    /// `dn_rpc_thrift_compression_enable` (default `false` → binary) — there
+    /// is no per-connection auto-detection (server source:
+    /// `AbstractThriftServiceThread.getProtocolFactory(compress)` picks a
+    /// single factory). Matrix observed live:
+    /// compact↔compression-enabled server: OK; compact↔default server: EOF
+    /// at openSession; binary↔compression-enabled server: EOF.
+    ///
+    /// So this test adapts: if the compact open succeeds the server has
+    /// compression enabled and we assert a full insert+query roundtrip;
+    /// if it fails at the transport level (the expected outcome against the
+    /// default docker image) we skip with a message — the clean transport
+    /// failure is itself the verified mismatch behavior.
+    #[test]
+    fn live_rpc_compression_roundtrip() {
+        use crate::data::{tablet::Tablet, TSDataType, Value};
+        use std::net::TcpStream;
+        // IOTDB_COMPACT_URL points at a compression-enabled server (e.g.
+        // docker run -e dn_rpc_thrift_compression_enable=true …) to force
+        // the positive roundtrip; default is the standard test server.
+        let url = std::env::var("IOTDB_COMPACT_URL").unwrap_or_else(|_| 
"127.0.0.1:6667".into());
+        let endpoint = Endpoint::parse(&url).expect("IOTDB_COMPACT_URL");
+        if TcpStream::connect_timeout(
+            &format!("{}:{}", endpoint.host, endpoint.port)
+                .parse()
+                .unwrap(),
+            Duration::from_millis(300),
+        )
+        .is_err()
+        {
+            eprintln!("skipping live_rpc_compression_roundtrip: no IoTDB 
server on {url}");
+            return;
+        }
+
+        const DB: &str = "root.rusttest_compact";
+        let _guard = LIVE_DB_LOCK.lock().unwrap_or_else(|p| p.into_inner());
+
+        let mut session = Session::new(SessionConfig {
+            endpoints: vec![endpoint],
+            enable_rpc_compression: true,
+            // Fail fast: a protocol mismatch dies at the first RPC, and
+            // reconnect passes cannot fix it.
+            enable_auto_reconnect: false,
+            max_reconnect_attempts: 1,
+            connect_timeout: Duration::from_secs(2),
+            ..Default::default()
+        });
+        match session.open() {
+            Err(Error::Thrift(e)) => {
+                eprintln!(
+                    "skipping live_rpc_compression_roundtrip: server rejected 
the compact \
+                     protocol ({e}) — expected when 
dn_rpc_thrift_compression_enable=false \
+                     (the default); IoTDB has no per-connection protocol 
auto-detection"
+                );
+                return;
+            }
+            other => other.expect("open compact session"),
+        }
+
+        // The server accepted the compact handshake ⇒ compression is
+        // enabled server-side; the whole roundtrip must work.
+        let _ = session.execute_non_query(&format!("DELETE DATABASE {DB}"));
+        session
+            .execute_non_query(&format!("CREATE DATABASE {DB}"))
+            .expect("create database");
+        let mut tablet = Tablet::new(
+            format!("{DB}.d1"),
+            vec!["v".into()],
+            vec![TSDataType::Int32],
+        )
+        .expect("tablet");
+        tablet
+            .add_row(1, vec![Some(Value::Int32(42))])
+            .expect("add_row");
+        session.insert_tablet(&tablet).expect("insert_tablet");
+        let mut rows = Vec::new();
+        {
+            let mut dataset = session
+                .execute_query(&format!("SELECT v FROM {DB}.d1"))
+                .expect("query");
+            while let Some(row) = dataset.next_row().expect("next_row") {
+                rows.push((row.timestamp.expect("timestamp"), 
row.values[0].clone()));
+            }
+        }
+        assert_eq!(rows, [(1, Value::Int32(42))]);
+        session
+            .execute_non_query(&format!("DELETE DATABASE {DB}"))
+            .expect("cleanup");
+        session.close().expect("close session");
+    }
+
+    /// Live TLS against a real IoTDB requires a server with
+    /// `enable_thrift_ssl=true` plus its certificate — not part of the
+    /// standard docker test topology. Opt in by pointing
+    /// `IOTDB_TLS_URL` (`host:port`) at such a server; otherwise this
+    /// skips. The TLS handshake/transport path itself is covered by the
+    /// loopback tests in `connection::tls_tests`.
+    #[cfg(feature = "tls")]
+    #[test]
+    fn live_tls_roundtrip() {
+        let Ok(url) = std::env::var("IOTDB_TLS_URL") else {
+            eprintln!(
+                "skipping live_tls_roundtrip: set IOTDB_TLS_URL=host:port to a 
TLS-enabled \
+                 IoTDB (enable_thrift_ssl=true); the standard test server is 
plain TCP"
+            );
+            return;
+        };
+        let mut session = Session::new(SessionConfig {
+            endpoints: vec![Endpoint::parse(&url).expect("IOTDB_TLS_URL")],
+            use_ssl: true,
+            ca_cert_path: std::env::var_os("IOTDB_TLS_CA").map(Into::into),
+            accept_invalid_certs: 
std::env::var_os("IOTDB_TLS_INSECURE").is_some(),
+            ..Default::default()
+        });
+        session.open().expect("open TLS session");
+        let mut dataset = session.execute_query("SHOW 
DATABASES").expect("query");
+        while dataset.next_row().expect("next_row").is_some() {}
+    }
+
     /// DATE wire-format adjudication test (goal V1). Inserts one DATE row via
     /// the tablet binary path (i32 yyyyMMdd) and one via a SQL date literal
     /// (parsed server-side), then reads both back: if the tablet encoding is
diff --git a/src/client/table_session.rs b/src/client/table_session.rs
index 10f740e..a37b02f 100644
--- a/src/client/table_session.rs
+++ b/src/client/table_session.rs
@@ -107,6 +107,43 @@ impl TableSessionBuilder {
         self
     }
 
+    /// Speak TCompactProtocol ("RPC compression"). Must match the server's
+    /// `dn_rpc_thrift_compression_enable` setting — see
+    /// [`SessionConfig::enable_rpc_compression`].
+    pub fn enable_rpc_compression(mut self, enable: bool) -> Self {
+        self.config.enable_rpc_compression = enable;
+        self
+    }
+
+    /// Wrap connections in TLS (cargo feature `tls`).
+    #[cfg(feature = "tls")]
+    pub fn use_ssl(mut self, use_ssl: bool) -> Self {
+        self.config.use_ssl = use_ssl;
+        self
+    }
+
+    /// PEM certificate added as trusted root (cargo feature `tls`).
+    #[cfg(feature = "tls")]
+    pub fn ca_cert_path(mut self, path: impl Into<std::path::PathBuf>) -> Self 
{
+        self.config.ca_cert_path = Some(path.into());
+        self
+    }
+
+    /// Skip certificate verification — **dangerous** outside tests
+    /// (cargo feature `tls`).
+    #[cfg(feature = "tls")]
+    pub fn accept_invalid_certs(mut self, accept: bool) -> Self {
+        self.config.accept_invalid_certs = accept;
+        self
+    }
+
+    /// Hostname for SNI + certificate validation (cargo feature `tls`).
+    #[cfg(feature = "tls")]
+    pub fn domain_override(mut self, domain: impl Into<String>) -> Self {
+        self.config.domain_override = Some(domain.into());
+        self
+    }
+
     /// The [`SessionConfig`] this builder resolves to (dialect always
     /// `"table"`).
     pub fn config(&self) -> &SessionConfig {
@@ -201,6 +238,26 @@ mod tests {
         assert_eq!(cfg.query_timeout_ms, 1_000);
     }
 
+    #[test]
+    fn builder_passes_transport_options_through() {
+        let b = TableSessionBuilder::new().enable_rpc_compression(true);
+        assert!(b.config().enable_rpc_compression);
+
+        #[cfg(feature = "tls")]
+        {
+            let b = TableSessionBuilder::new()
+                .use_ssl(true)
+                .ca_cert_path("/certs/ca.pem")
+                .accept_invalid_certs(true)
+                .domain_override("iotdb.internal");
+            let cfg = b.config();
+            assert!(cfg.use_ssl);
+            assert_eq!(cfg.ca_cert_path.as_deref(), 
Some("/certs/ca.pem".as_ref()));
+            assert!(cfg.accept_invalid_certs);
+            assert_eq!(cfg.domain_override.as_deref(), Some("iotdb.internal"));
+        }
+    }
+
     #[test]
     fn insert_rejects_tree_model_tablets() {
         let mut session = TableSession {
diff --git a/src/connection/mod.rs b/src/connection/mod.rs
index c6cdc89..5f77f6c 100644
--- a/src/connection/mod.rs
+++ b/src/connection/mod.rs
@@ -18,15 +18,19 @@
 //! Low-level Thrift connection to an IoTDB node.
 //!
 //! Mirrors `src/connection/Connection.ts` (Node.js) and the Thrift client 
setup
-//! in the C# SDK: TCP → TFramedTransport → TBinaryProtocol → 
IClientRPCService client.
+//! in the C# SDK: TCP (optionally TLS) → TFramedTransport → TBinaryProtocol
+//! (or TCompactProtocol when RPC compression is enabled) → IClientRPCService
+//! client.
 
+use std::io::{Read, Write};
 use std::net::{TcpStream, ToSocketAddrs};
 use std::time::Duration;
 
-use thrift::protocol::{TBinaryInputProtocol, TBinaryOutputProtocol};
-use thrift::transport::{
-    ReadHalf, TFramedReadTransport, TFramedWriteTransport, TIoChannel, 
TTcpChannel, WriteHalf,
+use thrift::protocol::{
+    TBinaryInputProtocol, TBinaryOutputProtocol, TCompactInputProtocol, 
TCompactOutputProtocol,
+    TInputProtocol, TOutputProtocol,
 };
+use thrift::transport::{TFramedReadTransport, TFramedWriteTransport, 
TIoChannel, TTcpChannel};
 
 use crate::error::{Error, Result};
 use crate::protocol::client::IClientRPCServiceSyncClient;
@@ -34,11 +38,73 @@ use crate::protocol::client::IClientRPCServiceSyncClient;
 /// Default IoTDB DataNode RPC port.
 pub const DEFAULT_PORT: u16 = 6667;
 
-/// The concrete generated RPC client over framed transport + strict binary 
protocol.
-pub type RpcClient = IClientRPCServiceSyncClient<
-    TBinaryInputProtocol<TFramedReadTransport<ReadHalf<TTcpChannel>>>,
-    TBinaryOutputProtocol<TFramedWriteTransport<WriteHalf<TTcpChannel>>>,
->;
+/// Wire protocol used on top of the framed transport.
+///
+/// IoTDB's Thrift server speaks **one** protocol per server instance,
+/// chosen by the server config `dn_rpc_thrift_compression_enable`
+/// (default `false` → binary). There is **no** per-connection
+/// auto-detection: a compact-protocol client against a binary-protocol
+/// server fails at the first RPC, and vice versa — pick the protocol that
+/// matches the server (the C# SDK's `enableRpcCompression` does the same
+/// blind switch).
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum RpcProtocol {
+    /// Strict TBinaryProtocol — matches the server default.
+    #[default]
+    Binary,
+    /// TCompactProtocol — matches `dn_rpc_thrift_compression_enable=true`
+    /// ("RPC compression" in IoTDB terms is the compact protocol, not a
+    /// compressed stream).
+    Compact,
+}
+
+/// TLS settings for a [`Connection`] (cargo feature `tls`).
+#[cfg(feature = "tls")]
+#[derive(Debug, Clone, Default)]
+pub struct TlsOptions {
+    /// PEM certificate added as a trusted root (e.g. a private CA or the
+    /// server's self-signed certificate).
+    pub ca_cert_path: Option<std::path::PathBuf>,
+    /// Skip certificate verification entirely (self-signed test certs).
+    /// **Dangerous** outside tests. Default `false`.
+    pub accept_invalid_certs: bool,
+    /// Hostname used for SNI + certificate validation instead of the
+    /// endpoint host (e.g. when connecting by IP).
+    pub domain_override: Option<String>,
+}
+
+/// How to open a [`Connection`]: timeout, wire protocol, optional TLS.
+#[derive(Debug, Clone)]
+pub struct ConnectionOptions {
+    /// TCP connect timeout per endpoint attempt. Default 10 s.
+    pub connect_timeout: Duration,
+    /// Wire protocol; must match the server (see [`RpcProtocol`]).
+    pub protocol: RpcProtocol,
+    /// Wrap the TCP stream in TLS before the Thrift transports.
+    #[cfg(feature = "tls")]
+    pub tls: Option<TlsOptions>,
+}
+
+impl Default for ConnectionOptions {
+    fn default() -> Self {
+        Self {
+            connect_timeout: Duration::from_secs(10),
+            protocol: RpcProtocol::Binary,
+            #[cfg(feature = "tls")]
+            tls: None,
+        }
+    }
+}
+
+/// Type-erased Thrift input protocol (binary or compact, plain or TLS).
+pub type BoxedInputProtocol = Box<dyn TInputProtocol + Send>;
+/// Type-erased Thrift output protocol (binary or compact, plain or TLS).
+pub type BoxedOutputProtocol = Box<dyn TOutputProtocol + Send>;
+
+/// The generated RPC client over framed transport and a type-erased
+/// protocol pair (the `thrift` crate forwards the protocol traits through
+/// `Box`, so the generated generic client works unchanged).
+pub type RpcClient = IClientRPCServiceSyncClient<BoxedInputProtocol, 
BoxedOutputProtocol>;
 
 /// A single endpoint `host:port` of an IoTDB DataNode (default port 6667).
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -93,25 +159,38 @@ impl std::fmt::Display for Endpoint {
 /// and the generated `IClientRPCService` client.
 pub struct Connection {
     endpoint: Endpoint,
+    protocol: RpcProtocol,
     client: RpcClient,
 }
 
 impl Connection {
-    /// Establish a TCP connection to `endpoint` (bounded by `connect_timeout`)
-    /// and wrap it in framed transport + strict binary protocol.
-    pub fn open(endpoint: Endpoint, connect_timeout: Duration) -> Result<Self> 
{
-        let stream = connect_stream(&endpoint, connect_timeout)?;
+    /// Establish a TCP connection to `endpoint` (bounded by
+    /// `options.connect_timeout`), optionally wrap it in TLS, and stack
+    /// framed transport + the selected protocol on top.
+    pub fn open(endpoint: Endpoint, options: &ConnectionOptions) -> 
Result<Self> {
+        let stream = connect_stream(&endpoint, options.connect_timeout)?;
         stream.set_nodelay(true).map_err(thrift::Error::from)?;
 
+        #[cfg(feature = "tls")]
+        if let Some(tls) = &options.tls {
+            let stream = tls_handshake(&endpoint, stream, tls)?;
+            let shared = SharedTlsStream::new(stream);
+            let (input, output) = build_protocols(shared.clone(), shared, 
options.protocol);
+            return Ok(Self {
+                endpoint,
+                protocol: options.protocol,
+                client: IClientRPCServiceSyncClient::new(input, output),
+            });
+        }
+
         let channel = TTcpChannel::with_stream(stream);
         let (read_half, write_half) = channel.split()?;
-        let read_transport = TFramedReadTransport::new(read_half);
-        let write_transport = TFramedWriteTransport::new(write_half);
-        let input_protocol = TBinaryInputProtocol::new(read_transport, true);
-        let output_protocol = TBinaryOutputProtocol::new(write_transport, 
true);
-        let client = IClientRPCServiceSyncClient::new(input_protocol, 
output_protocol);
-
-        Ok(Self { endpoint, client })
+        let (input, output) = build_protocols(read_half, write_half, 
options.protocol);
+        Ok(Self {
+            endpoint,
+            protocol: options.protocol,
+            client: IClientRPCServiceSyncClient::new(input, output),
+        })
     }
 
     /// Mutable access to the generated RPC client for issuing calls.
@@ -122,6 +201,36 @@ impl Connection {
     pub fn endpoint(&self) -> &Endpoint {
         &self.endpoint
     }
+
+    /// The wire protocol this connection was opened with.
+    pub fn protocol(&self) -> RpcProtocol {
+        self.protocol
+    }
+}
+
+/// Stack framed transport + the selected protocol over a read/write pair
+/// and type-erase the result (see [`RpcClient`]).
+fn build_protocols<R, W>(
+    read: R,
+    write: W,
+    protocol: RpcProtocol,
+) -> (BoxedInputProtocol, BoxedOutputProtocol)
+where
+    R: Read + Send + 'static,
+    W: Write + Send + 'static,
+{
+    let read_transport = TFramedReadTransport::new(read);
+    let write_transport = TFramedWriteTransport::new(write);
+    match protocol {
+        RpcProtocol::Binary => (
+            Box::new(TBinaryInputProtocol::new(read_transport, true)),
+            Box::new(TBinaryOutputProtocol::new(write_transport, true)),
+        ),
+        RpcProtocol::Compact => (
+            Box::new(TCompactInputProtocol::new(read_transport)),
+            Box::new(TCompactOutputProtocol::new(write_transport)),
+        ),
+    }
 }
 
 /// Resolve the endpoint and try each resolved address with the connect 
timeout.
@@ -142,6 +251,78 @@ fn connect_stream(endpoint: &Endpoint, connect_timeout: 
Duration) -> Result<TcpS
     })
 }
 
+/// Run the TLS handshake over an established TCP stream.
+#[cfg(feature = "tls")]
+fn tls_handshake(
+    endpoint: &Endpoint,
+    stream: TcpStream,
+    tls: &TlsOptions,
+) -> Result<native_tls::TlsStream<TcpStream>> {
+    let mut builder = native_tls::TlsConnector::builder();
+    if let Some(path) = &tls.ca_cert_path {
+        let pem = std::fs::read(path).map_err(|e| {
+            Error::Client(format!(
+                "cannot read CA certificate {}: {e}",
+                path.display()
+            ))
+        })?;
+        let cert = 
native_tls::Certificate::from_pem(&pem).map_err(Error::Tls)?;
+        builder.add_root_certificate(cert);
+    }
+    if tls.accept_invalid_certs {
+        builder.danger_accept_invalid_certs(true);
+    }
+    let connector = builder.build().map_err(Error::Tls)?;
+    let domain = tls.domain_override.as_deref().unwrap_or(&endpoint.host);
+    connector.connect(domain, stream).map_err(|e| match e {
+        native_tls::HandshakeError::Failure(e) => Error::Tls(e),
+        // Blocking sockets never yield the mid-handshake variant.
+        native_tls::HandshakeError::WouldBlock(_) => {
+            Error::Client("TLS handshake interrupted".into())
+        }
+    })
+}
+
+/// A `TlsStream` shared between the read and write transports.
+///
+/// `TTcpChannel::split` clones the underlying OS socket, but a TLS stream
+/// cannot be split that way (record layer state is shared), so both framed
+/// transports hold the same stream behind a mutex. The generated sync
+/// client fully writes + flushes a request before reading the response, so
+/// read and write never contend.
+#[cfg(feature = "tls")]
+#[derive(Clone)]
+struct 
SharedTlsStream(std::sync::Arc<std::sync::Mutex<native_tls::TlsStream<TcpStream>>>);
+
+#[cfg(feature = "tls")]
+impl SharedTlsStream {
+    fn new(stream: native_tls::TlsStream<TcpStream>) -> Self {
+        Self(std::sync::Arc::new(std::sync::Mutex::new(stream)))
+    }
+
+    fn lock(&self) -> std::sync::MutexGuard<'_, 
native_tls::TlsStream<TcpStream>> {
+        self.0.lock().unwrap_or_else(|p| p.into_inner())
+    }
+}
+
+#[cfg(feature = "tls")]
+impl Read for SharedTlsStream {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        self.lock().read(buf)
+    }
+}
+
+#[cfg(feature = "tls")]
+impl Write for SharedTlsStream {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.lock().write(buf)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        self.lock().flush()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -203,4 +384,175 @@ mod tests {
             Endpoint::new("::1", 6667)
         );
     }
+
+    #[test]
+    fn default_options_are_binary_no_tls() {
+        let options = ConnectionOptions::default();
+        assert_eq!(options.connect_timeout, Duration::from_secs(10));
+        assert_eq!(options.protocol, RpcProtocol::Binary);
+        #[cfg(feature = "tls")]
+        assert!(options.tls.is_none());
+    }
+
+    /// A local listener that accepts and immediately drops connections, so
+    /// `Connection::open` (which issues no RPC) succeeds for any protocol.
+    fn accept_then_drop_listener() -> Endpoint {
+        let listener = 
std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
+        let port = listener.local_addr().expect("local_addr").port();
+        std::thread::spawn(move || {
+            for stream in listener.incoming() {
+                match stream {
+                    Ok(s) => drop(s),
+                    Err(_) => break,
+                }
+            }
+        });
+        Endpoint::new("127.0.0.1", port)
+    }
+
+    /// Both protocol variants construct their transport/protocol stack and
+    /// report the choice back via `Connection::protocol`. (Wire-level
+    /// verification against a live server lives in the session tests.)
+    #[test]
+    fn open_with_each_protocol() {
+        let endpoint = accept_then_drop_listener();
+        for protocol in [RpcProtocol::Binary, RpcProtocol::Compact] {
+            // The struct update is only "needless" without the tls feature,
+            // which adds a field this literal doesn't name.
+            #[allow(clippy::needless_update)]
+            let options = ConnectionOptions {
+                connect_timeout: Duration::from_millis(500),
+                protocol,
+                ..Default::default()
+            };
+            let connection = Connection::open(endpoint.clone(), 
&options).expect("open");
+            assert_eq!(connection.protocol(), protocol);
+            assert_eq!(connection.endpoint(), &endpoint);
+        }
+    }
+}
+
+#[cfg(all(test, feature = "tls"))]
+mod tls_tests {
+    use super::*;
+    use crate::protocol::client::TIClientRPCServiceSyncClient;
+    use std::path::PathBuf;
+
+    fn fixture(name: &str) -> PathBuf {
+        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+            .join("tests/fixtures/tls")
+            .join(name)
+    }
+
+    /// Spawn a TLS acceptor on a loopback port that completes one handshake
+    /// and then drops the connection. Uses the checked-in self-signed cert
+    /// (CN=localhost, SAN DNS:localhost + IP:127.0.0.1, 100-year validity).
+    fn tls_acceptor_once() -> Endpoint {
+        let cert = std::fs::read(fixture("cert.pem")).expect("read cert 
fixture");
+        let key = std::fs::read(fixture("key.pem")).expect("read key fixture");
+        let identity = native_tls::Identity::from_pkcs8(&cert, 
&key).expect("identity");
+        let acceptor = 
native_tls::TlsAcceptor::new(identity).expect("acceptor");
+        let listener = 
std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
+        let port = listener.local_addr().expect("local_addr").port();
+        std::thread::spawn(move || {
+            for stream in listener.incoming() {
+                match stream {
+                    // Handshake (which may itself fail when the client
+                    // rejects our cert — fine, just move on), then drop.
+                    Ok(s) => drop(acceptor.accept(s)),
+                    Err(_) => break,
+                }
+            }
+        });
+        Endpoint::new("127.0.0.1", port)
+    }
+
+    /// Full client-side TLS path with the fixture cert as trusted root and
+    /// hostname pinned via `domain_override`: the handshake completes (so
+    /// `Connection::open` succeeds), and the first RPC then dies at the
+    /// Thrift layer because the acceptor closed the connection — proving
+    /// the failure is post-handshake.
+    #[test]
+    fn tls_handshake_with_trusted_root_then_thrift_failure() {
+        let endpoint = tls_acceptor_once();
+        let options = ConnectionOptions {
+            connect_timeout: Duration::from_millis(500),
+            protocol: RpcProtocol::Binary,
+            tls: Some(TlsOptions {
+                ca_cert_path: Some(fixture("cert.pem")),
+                accept_invalid_certs: false,
+                domain_override: Some("localhost".into()),
+            }),
+        };
+        let mut connection = Connection::open(endpoint, &options).expect("TLS 
handshake");
+        assert_eq!(connection.protocol(), RpcProtocol::Binary);
+        let err = connection
+            .client_mut()
+            .request_statement_id(1)
+            .expect_err("RPC on a closed TLS connection must fail");
+        // Post-handshake: the error is a Thrift transport error, not TLS.
+        let msg = err.to_string();
+        assert!(!msg.to_lowercase().contains("certificate"), "got: {msg}");
+    }
+
+    /// Without the fixture as trusted root, certificate validation rejects
+    /// the self-signed server during the handshake: `Connection::open`
+    /// itself fails with a TLS error.
+    #[test]
+    fn tls_untrusted_cert_fails_handshake() {
+        let endpoint = tls_acceptor_once();
+        let options = ConnectionOptions {
+            connect_timeout: Duration::from_millis(500),
+            protocol: RpcProtocol::Binary,
+            tls: Some(TlsOptions {
+                ca_cert_path: None,
+                accept_invalid_certs: false,
+                domain_override: Some("localhost".into()),
+            }),
+        };
+        let err = match Connection::open(endpoint, &options) {
+            Ok(_) => panic!("untrusted self-signed cert must fail the 
handshake"),
+            Err(e) => e,
+        };
+        assert!(matches!(err, Error::Tls(_)), "got {err:?}");
+    }
+
+    /// `accept_invalid_certs` bypasses validation for self-signed test
+    /// certs — handshake succeeds without any trusted root.
+    #[test]
+    fn tls_accept_invalid_certs_bypasses_validation() {
+        let endpoint = tls_acceptor_once();
+        let options = ConnectionOptions {
+            connect_timeout: Duration::from_millis(500),
+            protocol: RpcProtocol::Compact, // also exercise compact-over-TLS
+            tls: Some(TlsOptions {
+                accept_invalid_certs: true,
+                ..Default::default()
+            }),
+        };
+        let connection = Connection::open(endpoint, &options).expect("TLS 
handshake");
+        assert_eq!(connection.protocol(), RpcProtocol::Compact);
+    }
+
+    /// A missing CA file is a clear client error before any connect.
+    #[test]
+    fn tls_missing_ca_file_is_client_error() {
+        let endpoint = tls_acceptor_once();
+        let options = ConnectionOptions {
+            connect_timeout: Duration::from_millis(500),
+            protocol: RpcProtocol::Binary,
+            tls: Some(TlsOptions {
+                ca_cert_path: Some(fixture("does-not-exist.pem")),
+                ..Default::default()
+            }),
+        };
+        let err = match Connection::open(endpoint, &options) {
+            Ok(_) => panic!("missing CA file must fail"),
+            Err(e) => e,
+        };
+        assert!(
+            matches!(&err, Error::Client(m) if m.contains("cannot read CA 
certificate")),
+            "got {err:?}"
+        );
+    }
 }
diff --git a/src/error.rs b/src/error.rs
index d28dbc8..52b126c 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -31,6 +31,9 @@ pub enum Error {
     Client(String),
     /// Malformed binary payload received from the server (e.g. truncated 
TsBlock).
     Decode(String),
+    /// TLS setup or handshake failure (cargo feature `tls`).
+    #[cfg(feature = "tls")]
+    Tls(native_tls::Error),
 }
 
 impl fmt::Display for Error {
@@ -40,6 +43,8 @@ impl fmt::Display for Error {
             Error::Server { code, message } => write!(f, "server error {code}: 
{message}"),
             Error::Client(msg) => write!(f, "client error: {msg}"),
             Error::Decode(msg) => write!(f, "decode error: {msg}"),
+            #[cfg(feature = "tls")]
+            Error::Tls(e) => write!(f, "tls error: {e}"),
         }
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 35a71a2..bd26ccd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -35,6 +35,8 @@ pub use client::pool::{PooledSession, SessionPool, 
SessionPoolConfig, TableSessi
 pub use client::redirect::{RedirectCache, RedirectCacheStats};
 pub use client::session::{QueryHandle, Session, SessionConfig};
 pub use client::table_session::{TableSession, TableSessionBuilder};
-pub use connection::Endpoint;
+#[cfg(feature = "tls")]
+pub use connection::TlsOptions;
+pub use connection::{ConnectionOptions, Endpoint, RpcProtocol};
 pub use data::{ColumnCategory, TSDataType, Tablet, TsBlock, Value};
 pub use error::{Error, Result};
diff --git a/tests/fixtures/tls/README.md b/tests/fixtures/tls/README.md
new file mode 100644
index 0000000..4ff8a2a
--- /dev/null
+++ b/tests/fixtures/tls/README.md
@@ -0,0 +1,39 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# TLS test fixtures
+
+Self-signed certificate + PKCS#8 key used only by the loopback TLS tests
+(`connection::tls_tests`, cargo feature `tls`). Not a secret — the key
+never protects anything.
+
+macOS SecureTransport imposes extra requirements even on explicitly
+trusted roots: validity ≤ 825 days (error −67901) and an
+`extendedKeyUsage=serverAuth` extension (error −67609). Current cert
+expires **2028-10-10**; when the trusted-root test starts failing with a
+validity/expiry error, regenerate:
+
+```sh
+cd tests/fixtures/tls
+openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
+  -days 820 -nodes -subj "/CN=localhost" \
+  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
+  -addext "extendedKeyUsage=serverAuth" \
+  -addext "keyUsage=digitalSignature,keyEncipherment"
+```
diff --git a/tests/fixtures/tls/cert.pem b/tests/fixtures/tls/cert.pem
new file mode 100644
index 0000000..ca7e9d0
--- /dev/null
+++ b/tests/fixtures/tls/cert.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSTCCAjGgAwIBAgIUXWTGi5P/NlLO5jb4yB/faKrLSmEwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcxMzA2MDcyMFoXDTI4MTAx
+MDA2MDcyMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAihtCghtrZyQRS08IclOEhez3pB5DUfQzfo8sZE4uUIsp
+Rxb1gbbVpICi7k8rc3NGKmmIzOOPnLPMlpISunxfdzYWEO1bSJQKEk1DCSFYL60t
+ZCBJzJ5XdEQypQ8AqGsC4QGsqEhzzvEF6ttOGVIHOsTzoRnClyZTVJxqU62RfXuk
+MqPiXwSa+LTfr/Yz31DBossQyHgTgQvS2zHIwFPQfr/FncAjnAcRs9gWjDFnoIX7
+pxamN1BVa6ypSbPXO3W0PpNGsGTpnOzD+yJFeO2SHzHfVwgI4MC4eyZO1y5YHaF1
+73VGTCdEVCMzmT/o498xkK0Nsp9zLZfbGW82UHrWMQIDAQABo4GSMIGPMB0GA1Ud
+DgQWBBSf+ZeI7VU6K87FdqUuvKDXkUpT2jAfBgNVHSMEGDAWgBSf+ZeI7VU6K87F
+dqUuvKDXkUpT2jAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z
+dIcEfwAAATATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZI
+hvcNAQELBQADggEBAGAGhMfPVg6sVzIky9vy0rcve1gRre7SltiBzA7rexjqJZfY
+yL6DjxqbSHxbkM1HB02n118BawaVxATFdViYuSFv278AJMryMePKeQJx/rLVAvJn
++kmav9KuQRogxDsO0XwoE7cTa1er0oa9Y9uTLQ/5yoPSl/9dGyGObYQjbb1zavgz
+RQs3Px4jrmLvv1iqFXiWSAxqhtV2b3pHOIrDaB4yPtBSNmQhjO+ngHoQ4W7MSv4X
+n+5siHE0TSBKfmZx+ETVONRNnElmPGmog3l/A25AmZipq4Yr4VJZbxoBVUDYvVyu
+QieLpNS4Owx3y8pCUiNBqpeaFgks4hOtBE9bs5k=
+-----END CERTIFICATE-----
diff --git a/tests/fixtures/tls/key.pem b/tests/fixtures/tls/key.pem
new file mode 100644
index 0000000..88e8568
--- /dev/null
+++ b/tests/fixtures/tls/key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCKG0KCG2tnJBFL
+TwhyU4SF7PekHkNR9DN+jyxkTi5QiylHFvWBttWkgKLuTytzc0YqaYjM44+cs8yW
+khK6fF93NhYQ7VtIlAoSTUMJIVgvrS1kIEnMnld0RDKlDwCoawLhAayoSHPO8QXq
+204ZUgc6xPOhGcKXJlNUnGpTrZF9e6Qyo+JfBJr4tN+v9jPfUMGiyxDIeBOBC9Lb
+McjAU9B+v8WdwCOcBxGz2BaMMWeghfunFqY3UFVrrKlJs9c7dbQ+k0awZOmc7MP7
+IkV47ZIfMd9XCAjgwLh7Jk7XLlgdoXXvdUZMJ0RUIzOZP+jj3zGQrQ2yn3Mtl9sZ
+bzZQetYxAgMBAAECggEALpxFpZCofn5rQc3yLVenneWcrKy0DshKytd6ZX4HXpKh
+A0ep4zX4Q9VQZ/qbURMiJrVIgNH6iaobDeRN4teQBrVf3CWnjca84XFnAwFYeHrL
+m4PUNHVqUaikqRV3pN+88IC2q3MMdtbcpueOI7iODRUknoYJqSa7sA48SnKFbGJb
+xqMwE8xPoSVnatCWaUVWC+YPwRUsEDFiWZbCA14n9zgXRwvK7vs0yImoi23z3Td2
+70XwxEsPr2Tz1B7Atu4FatL44ZnzQzqzY69N6vDV5VVw9E2ZILAYAehe4s9J0CV9
+M6XWoMb/7qiCHnKY+4q45PSBTAqOp6GTnPUnzJcxMwKBgQDDAIdKQ3x+mJKuotna
+ukhOacgk0u5Rn8n+0WY69GZRwd/T2wnWojN05du4SvJ80HS5B4y96TZ6OVkKGNcW
+Hx8WE1l4QLy2RDaZp2gM18xBQxWiHNnqBs+HQze029GlA6Si1BITykH7K/1XoD0s
+9h6ljKmSCTRsOSlfweFx6sErywKBgQC1Tp5ySKeAG/81WTLDtVRKfbI50lqb31Sn
+JhMzoi2iaoladqEkw7pfTxywDCZk7yDXyVQBuwI9XLcEV1hSDNCaoQWNl4Od3a+Z
++6Qeks3qVFHqiKm54FKaTgc7v/lJ9r86vTt7z2TRRu3/9POUJymmv+bSps9QVPk9
+a6wPKCs+cwKBgBrQk8oOhawS5vjExBhjzVWbDj6iEst+oZQ8z0YEHZ3YfyLu1+d/
+3nuQsCojhDzNnX9kHmJvE2KzSB8vU0Rjey7Z8k+q90hJEQkIEDLT6e5/fxYPyZd4
+4EjFYX+pSqbaXWVWrDW0dmZHokrOahsorQYu1ZKTWXYYViwoyQuVMIMJAoGACkN/
+GL0gLlJnah+4jfev49+lvTw5QOWtpyCyqZOevbkGOYbJrIkf/dE+sICfk8stssmE
+5ewuPkcHXUmMiGiNTpOa+t7+5mrsS+1A5zIsUf2f/YTrBsi6JF4SbsF5XUSIosyf
+l2ywKmC3jGvTdimZZAUtDfO/fK6yxVVZEiqV+B0CgYB2adpXgm38TWgIz7xwpFed
+hQvNKn0SxMHfbk3TdXC8kVwl/49NWFBaEd6NHcxsubZICHCbo0BIUkkYCqe0r+Na
+DDDRbwrTgcffQwiR+RT9qpxnOLw1/ff8q0hbdmd7Vutq8wPDW2Cx6tEngegKMlpd
+5BsJT6Q5VbJ5nQHhz8tPMQ==
+-----END PRIVATE KEY-----


Reply via email to