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
The following commit(s) were added to refs/heads/main by this push:
new 8046bd3 Use rustls for TLS (#2)
8046bd3 is described below
commit 8046bd3adb29648dfae178523f0a72a1acf527aa
Author: Haonan <[email protected]>
AuthorDate: Wed Aug 12 19:11:16 2026 +0800
Use rustls for TLS (#2)
* Use rustls for TLS
* Use WebPKI for TLS verification
* Clarify TLS platform CI
* Verify server-side mTLS handshake
---
.github/workflows/ci.yml | 33 ++++
Cargo.toml | 18 ++-
README.md | 10 +-
README_ZH.md | 9 +-
src/connection/mod.rs | 311 +++++++++++++++++++++++++++++--------
src/error.rs | 5 +-
tests/fixtures/tls/README.md | 22 ++-
tests/fixtures/tls/cert.pem | 22 +--
tests/fixtures/tls/client-cert.pem | 18 +--
9 files changed, 344 insertions(+), 104 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6bec0ae..cad6d3a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -65,6 +65,39 @@ jobs:
- name: Unit tests (tls)
run: cargo test --features tls
+ tls-platforms:
+ name: tls (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [macos-latest, windows-latest]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
# stable
+
+ - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
+
+ # Exercises native root loading plus the shared WebPKI verifier on each
OS.
+ - name: Unit tests (tls)
+ run: cargo test --features tls
+
+ msrv:
+ name: msrv (rust 1.75)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
+ with:
+ toolchain: 1.75.0
+
+ - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
+
+ - name: Check (tls)
+ run: cargo check --features tls
+
integration:
runs-on: ubuntu-latest
strategy:
diff --git a/Cargo.toml b/Cargo.toml
index 8d9b168..5b58379 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,13 +36,23 @@ thrift = "0.23"
byteorder = "1.5"
chrono = "0.4"
log = "0.4"
-native-tls = { version = "0.2", optional = true }
+# thrift 0.23 allows any uuid 1.x; 1.21+ raises MSRV to Rust 1.85.
+uuid = "=1.20.0"
+rustls = { version = "0.23", default-features = false, features = ["ring",
"std", "tls12"], optional = true }
+rustls-pemfile = { version = "2.2", optional = true }
+rustls-native-certs = { version = "0.8.4", optional = true }
+# Keep Cargo 1.75 from resolving zeroize 1.9, whose manifest uses edition 2024.
+zeroize = { version = "=1.8.2", optional = true }
+
+[target.'cfg(any(target_vendor = "apple"))'.dependencies]
+# rustls-native-certs 0.8 allows 3.x; 3.7 raises MSRV to Rust 1.85.
+security-framework = { version = "=3.5.1", 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"]
+# TLS 1.2/1.3 via rustls, using the ring crypto provider and platform
+# trust roots with consistent WebPKI verification. Adds `use_ssl` & friends to
SessionConfig.
+tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-native-certs",
"dep:zeroize", "dep:security-framework"]
diff --git a/README.md b/README.md
index ef06b58..93e607b 100644
--- a/README.md
+++ b/README.md
@@ -183,7 +183,11 @@ let config = SessionConfig { enable_rpc_compression: true,
..Default::default()
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)):
+**TLS** is behind the `tls` cargo feature. It uses
[`rustls`](https://crates.io/crates/rustls)
+with the `ring` crypto provider and supports TLS 1.2/1.3. Server certificates
are
+verified consistently with rustls/WebPKI on every platform. The native trust
roots
+are loaded with
[`rustls-native-certs`](https://crates.io/crates/rustls-native-certs),
+and a CA supplied through `ca_cert_path` is added to those roots.
```toml
iotdb-client-rust = { version = "0.1", features = ["tls"] }
@@ -200,6 +204,10 @@ let config = SessionConfig {
// or: TableSession::builder().use_ssl(true).ca_cert_path("ca.pem")...
```
+`accept_invalid_certs` disables certificate-chain and hostname verification,
+but TLS handshake signatures are still cryptographically verified. It should
+only be used with controlled test servers.
+
For **mutual TLS** (server has `thrift_ssl_client_auth=true`), add a PEM
client certificate and its PKCS#8 key — the analogue of the Node.js
`sslOptions.cert`/`sslOptions.key`:
```rust
diff --git a/README_ZH.md b/README_ZH.md
index 8161bf1..3da4c98 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -183,7 +183,11 @@ let config = SessionConfig { enable_rpc_compression: true,
..Default::default()
必须与**服务端**配置 `dn_rpc_thrift_compression_enable`(默认
`false`)一致。服务端只讲一种协议——没有按连接协商的机制,任意方向的不匹配都会在第一个 RPC 上以传输错误失败。
-**TLS** 位于 `tls` cargo feature 之后(基于
[`native-tls`](https://crates.io/crates/native-tls) 的平台原生 TLS):
+**TLS** 位于 `tls` cargo feature 之后。其底层使用
+[`rustls`](https://crates.io/crates/rustls) 和 `ring` 密码学 provider,支持
+TLS 1.2/1.3。所有平台均使用 rustls/WebPKI 执行一致的服务端证书校验;系统信任根通过
+[`rustls-native-certs`](https://crates.io/crates/rustls-native-certs) 加载,
+`ca_cert_path` 指定的 CA 会追加到这些信任根中。
```toml
iotdb-client-rust = { version = "0.1", features = ["tls"] }
@@ -200,6 +204,9 @@ let config = SessionConfig {
// 或:TableSession::builder().use_ssl(true).ca_cert_path("ca.pem")...
```
+`accept_invalid_certs` 会关闭证书链和主机名校验,但 TLS 握手签名仍会经过密码学校验。
+该选项只应对受控的测试服务使用。
+
**双向 TLS**(服务端 `thrift_ssl_client_auth=true`)需额外提供 PEM 客户端证书及其 PKCS#8 私钥 —— 对应
Node.js 的 `sslOptions.cert`/`sslOptions.key`:
```rust
diff --git a/src/connection/mod.rs b/src/connection/mod.rs
index 69dc3c7..7bcdf81 100644
--- a/src/connection/mod.rs
+++ b/src/connection/mod.rs
@@ -24,8 +24,22 @@
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
+#[cfg(feature = "tls")]
+use std::sync::Arc;
use std::time::Duration;
+#[cfg(feature = "tls")]
+use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified,
ServerCertVerifier};
+#[cfg(feature = "tls")]
+use rustls::crypto::WebPkiSupportedAlgorithms;
+#[cfg(feature = "tls")]
+use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
+#[cfg(feature = "tls")]
+use rustls::{
+ ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore,
SignatureScheme,
+ StreamOwned,
+};
+
use thrift::protocol::{
TBinaryInputProtocol, TBinaryOutputProtocol, TCompactInputProtocol,
TCompactOutputProtocol,
TInputProtocol, TOutputProtocol,
@@ -63,9 +77,10 @@ pub enum RpcProtocol {
#[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).
+ /// server's self-signed certificate), in addition to the platform roots.
pub ca_cert_path: Option<std::path::PathBuf>,
- /// Skip certificate verification entirely (self-signed test certs).
+ /// Skip certificate-chain and hostname verification (self-signed test
+ /// certs). TLS handshake signatures are still verified.
/// **Dangerous** outside tests. Default `false`.
pub accept_invalid_certs: bool,
/// Hostname used for SNI + certificate validation instead of the
@@ -264,56 +279,170 @@ fn connect_stream(endpoint: &Endpoint, connect_timeout:
Duration) -> Result<TcpS
#[cfg(feature = "tls")]
fn tls_handshake(
endpoint: &Endpoint,
- stream: TcpStream,
+ mut 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);
+) -> Result<StreamOwned<ClientConnection, TcpStream>> {
+ let config = tls_client_config(tls)?;
+ let domain = tls.domain_override.as_deref().unwrap_or(&endpoint.host);
+ let server_name = ServerName::try_from(domain.to_owned())
+ .map_err(|e| Error::Client(format!("invalid TLS server name
'{domain}': {e}")))?;
+ let mut connection =
+ ClientConnection::new(config, server_name).map_err(|e|
Error::Tls(e.to_string()))?;
+
+ connection
+ .complete_io(&mut stream)
+ .map_err(|e| Error::Tls(e.to_string()))?;
+ if connection.is_handshaking() {
+ return Err(Error::Tls("TLS handshake did not complete".into()));
}
- match (&tls.client_cert_path, &tls.client_key_path) {
+
+ Ok(StreamOwned::new(connection, stream))
+}
+
+#[cfg(feature = "tls")]
+fn tls_client_config(tls: &TlsOptions) -> Result<Arc<ClientConfig>> {
+ let provider = Arc::new(rustls::crypto::ring::default_provider());
+ let extra_roots = match &tls.ca_cert_path {
+ Some(path) => load_certificates(path, "CA certificate")?,
+ None => Vec::new(),
+ };
+
+ let verifier: Arc<dyn ServerCertVerifier> = if tls.accept_invalid_certs {
+ Arc::new(NoCertificateVerification::new(
+ provider.signature_verification_algorithms,
+ ))
+ } else {
+ let native_roots = rustls_native_certs::load_native_certs();
+ for error in native_roots.errors {
+ log::warn!("cannot load a native CA certificate: {error}");
+ }
+
+ let mut roots = RootCertStore::empty();
+ let (_, ignored) = roots.add_parsable_certificates(native_roots.certs);
+ if ignored != 0 {
+ log::warn!("ignored {ignored} native CA certificate(s) that WebPKI
cannot parse");
+ }
+ for certificate in extra_roots {
+ roots
+ .add(certificate)
+ .map_err(|e| Error::Tls(format!("cannot add CA certificate:
{e}")))?;
+ }
+
+ rustls::client::WebPkiServerVerifier::builder_with_provider(
+ Arc::new(roots),
+ Arc::clone(&provider),
+ )
+ .build()
+ .map_err(|e| Error::Tls(e.to_string()))?
+ };
+
+ let builder = ClientConfig::builder_with_provider(provider)
+ .with_safe_default_protocol_versions()
+ .map_err(|e| Error::Tls(e.to_string()))?
+ .dangerous()
+ .with_custom_certificate_verifier(verifier);
+
+ let config = match (&tls.client_cert_path, &tls.client_key_path) {
(Some(cert_path), Some(key_path)) => {
- let cert = std::fs::read(cert_path).map_err(|e| {
- Error::Client(format!(
- "cannot read client certificate {}: {e}",
- cert_path.display()
- ))
- })?;
- let key = std::fs::read(key_path).map_err(|e| {
+ let certificates = load_certificates(cert_path, "client
certificate")?;
+ let key_file = std::fs::File::open(key_path).map_err(|e| {
Error::Client(format!(
"cannot read client key {}: {e}",
key_path.display()
))
})?;
- let identity = native_tls::Identity::from_pkcs8(&cert,
&key).map_err(Error::Tls)?;
- builder.identity(identity);
+ let mut key_reader = std::io::BufReader::new(key_file);
+ let key = rustls_pemfile::pkcs8_private_keys(&mut key_reader)
+ .next()
+ .transpose()
+ .map_err(|e| Error::Tls(format!("cannot parse client key:
{e}")))?
+ .ok_or_else(|| Error::Tls("client key is not a PEM PKCS#8
private key".into()))?;
+ builder
+ .with_client_auth_cert(certificates, key.into())
+ .map_err(|e| Error::Tls(e.to_string()))?
}
- (None, None) => {}
+ (None, None) => builder.with_no_client_auth(),
_ => {
return Err(Error::Client(
"mutual TLS requires both client_cert_path and
client_key_path".into(),
))
}
- }
- 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())
+ };
+
+ Ok(Arc::new(config))
+}
+
+#[cfg(feature = "tls")]
+fn load_certificates(
+ path: &std::path::Path,
+ description: &str,
+) -> Result<Vec<CertificateDer<'static>>> {
+ let file = std::fs::File::open(path)
+ .map_err(|e| Error::Client(format!("cannot read {description} {}:
{e}", path.display())))?;
+ let mut reader = std::io::BufReader::new(file);
+ let certificates = rustls_pemfile::certs(&mut reader)
+ .collect::<std::io::Result<Vec<_>>>()
+ .map_err(|e| Error::Tls(format!("cannot parse {description}: {e}")))?;
+ if certificates.is_empty() {
+ return Err(Error::Tls(format!(
+ "{description} {} contains no PEM certificates",
+ path.display()
+ )));
+ }
+ Ok(certificates)
+}
+
+/// Disables certificate-chain and hostname validation while retaining
+/// cryptographic verification of the TLS handshake signatures.
+#[cfg(feature = "tls")]
+#[derive(Debug)]
+struct NoCertificateVerification {
+ supported_algorithms: WebPkiSupportedAlgorithms,
+}
+
+#[cfg(feature = "tls")]
+impl NoCertificateVerification {
+ fn new(supported_algorithms: WebPkiSupportedAlgorithms) -> Self {
+ Self {
+ supported_algorithms,
}
- })
+ }
+}
+
+#[cfg(feature = "tls")]
+impl ServerCertVerifier for NoCertificateVerification {
+ fn verify_server_cert(
+ &self,
+ _end_entity: &CertificateDer<'_>,
+ _intermediates: &[CertificateDer<'_>],
+ _server_name: &ServerName<'_>,
+ _ocsp_response: &[u8],
+ _now: UnixTime,
+ ) -> std::result::Result<ServerCertVerified, rustls::Error> {
+ Ok(ServerCertVerified::assertion())
+ }
+
+ fn verify_tls12_signature(
+ &self,
+ message: &[u8],
+ cert: &CertificateDer<'_>,
+ dss: &DigitallySignedStruct,
+ ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
+ rustls::crypto::verify_tls12_signature(message, cert, dss,
&self.supported_algorithms)
+ }
+
+ fn verify_tls13_signature(
+ &self,
+ message: &[u8],
+ cert: &CertificateDer<'_>,
+ dss: &DigitallySignedStruct,
+ ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
+ rustls::crypto::verify_tls13_signature(message, cert, dss,
&self.supported_algorithms)
+ }
+
+ fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
+ self.supported_algorithms.supported_schemes()
+ }
}
/// A `TlsStream` shared between the read and write transports.
@@ -325,15 +454,15 @@ fn tls_handshake(
/// read and write never contend.
#[cfg(feature = "tls")]
#[derive(Clone)]
-struct
SharedTlsStream(std::sync::Arc<std::sync::Mutex<native_tls::TlsStream<TcpStream>>>);
+struct
SharedTlsStream(std::sync::Arc<std::sync::Mutex<StreamOwned<ClientConnection,
TcpStream>>>);
#[cfg(feature = "tls")]
impl SharedTlsStream {
- fn new(stream: native_tls::TlsStream<TcpStream>) -> Self {
+ fn new(stream: StreamOwned<ClientConnection, TcpStream>) -> Self {
Self(std::sync::Arc::new(std::sync::Mutex::new(stream)))
}
- fn lock(&self) -> std::sync::MutexGuard<'_,
native_tls::TlsStream<TcpStream>> {
+ fn lock(&self) -> std::sync::MutexGuard<'_, StreamOwned<ClientConnection,
TcpStream>> {
self.0.lock().unwrap_or_else(|p| p.into_inner())
}
}
@@ -515,6 +644,7 @@ mod tls_tests {
use super::*;
use crate::protocol::client::TIClientRPCServiceSyncClient;
use std::path::PathBuf;
+ use std::sync::Arc;
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -522,27 +652,79 @@ mod tls_tests {
.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 pkcs8_key(name: &str) -> rustls::pki_types::PrivateKeyDer<'static> {
+ let file = std::fs::File::open(fixture(name)).expect("read key
fixture");
+ let mut reader = std::io::BufReader::new(file);
+ let mut keys = rustls_pemfile::pkcs8_private_keys(&mut reader);
+ keys.next()
+ .expect("PKCS#8 key item")
+ .expect("parse PKCS#8 key")
+ .into()
+ }
+
+ fn server_config(require_client_auth: bool) -> Arc<rustls::ServerConfig> {
+ let provider = Arc::new(rustls::crypto::ring::default_provider());
+ let builder =
rustls::ServerConfig::builder_with_provider(Arc::clone(&provider))
+ .with_safe_default_protocol_versions()
+ .expect("protocol versions");
+ let builder = if require_client_auth {
+ let mut roots = rustls::RootCertStore::empty();
+ for certificate in
+ load_certificates(&fixture("client-cert.pem"), "client
root").expect("client root")
+ {
+ roots.add(certificate).expect("add client root");
+ }
+ let verifier =
+
rustls::server::WebPkiClientVerifier::builder_with_provider(roots.into(),
provider)
+ .build()
+ .expect("client verifier");
+ builder.with_client_cert_verifier(verifier)
+ } else {
+ builder.with_no_client_auth()
+ };
+ let config = builder
+ .with_single_cert(
+ load_certificates(&fixture("cert.pem"), "server certificate")
+ .expect("server certificate"),
+ pkcs8_key("key.pem"),
+ )
+ .expect("server config");
+ Arc::new(config)
+ }
+
+ /// Spawn a rustls acceptor on a loopback port that completes handshakes
+ /// and then drops each connection. Uses the checked-in self-signed cert
+ /// (CN=localhost, SAN DNS:localhost + IP:127.0.0.1).
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");
+ tls_acceptor(false).0
+ }
+
+ fn tls_acceptor(
+ require_client_auth: bool,
+ ) -> (
+ Endpoint,
+ std::sync::mpsc::Receiver<std::result::Result<(), String>>,
+ ) {
+ let config = server_config(require_client_auth);
let listener =
std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().expect("local_addr").port();
+ let (handshake_result_tx, handshake_result_rx) =
std::sync::mpsc::channel();
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,
- }
- }
+ let result = match listener.accept() {
+ Ok((mut stream, _)) => match
rustls::ServerConnection::new(config) {
+ Ok(mut connection) => connection
+ .complete_io(&mut stream)
+ .map(|_| ())
+ .map_err(|error| error.to_string()),
+ Err(error) => Err(error.to_string()),
+ },
+ Err(error) => Err(error.to_string()),
+ };
+ // Most tests only need the client-side result and deliberately
+ // discard this receiver. The mutual-TLS test asserts it below.
+ let _ = handshake_result_tx.send(result);
});
- Endpoint::new("127.0.0.1", port)
+ (Endpoint::new("127.0.0.1", port), handshake_result_rx)
}
/// Full client-side TLS path with the fixture cert as trusted root and
@@ -674,15 +856,12 @@ mod tls_tests {
assert!(matches!(err, Error::Tls(_)), "got {err:?}");
}
- /// Mutual TLS: a PEM client certificate + PKCS#8 key load into an
- /// identity and the handshake completes with the identity configured.
- /// `native-tls`'s `TlsAcceptor` has no API to *request* a client
- /// certificate, so the loopback server cannot verify it — acceptor-side
- /// client-auth is exercised only by the live `IOTDB_TLS_URL` test
- /// against a real server.
+ /// Mutual TLS: a PEM client certificate + PKCS#8 key load into the
+ /// client config, and a rustls server that requires the fixture client
+ /// certificate completes the handshake.
#[test]
fn tls_client_identity_handshake_succeeds() {
- let endpoint = tls_acceptor_once();
+ let (endpoint, server_handshake) = tls_acceptor(true);
let options = ConnectionOptions {
connect_timeout: Duration::from_millis(500),
protocol: RpcProtocol::Binary,
@@ -696,17 +875,21 @@ mod tls_tests {
};
let connection = Connection::open(endpoint, &options).expect("TLS
handshake with identity");
assert_eq!(connection.protocol(), RpcProtocol::Binary);
+ server_handshake
+ .recv_timeout(Duration::from_secs(5))
+ .expect("server handshake result")
+ .expect("server accepted client identity");
}
/// Setting only one of the client cert/key pair is a config error
/// caught before any I/O.
#[test]
fn tls_client_identity_requires_both_paths() {
- let endpoint = tls_acceptor_once();
for (cert, key) in [
(Some(fixture("client-cert.pem")), None),
(None, Some(fixture("client-key.pem"))),
] {
+ let endpoint = tls_acceptor_once();
let options = ConnectionOptions {
connect_timeout: Duration::from_millis(500),
protocol: RpcProtocol::Binary,
diff --git a/src/error.rs b/src/error.rs
index 52b126c..5cb372f 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -31,9 +31,10 @@ 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`).
+ /// TLS configuration, certificate parsing, or handshake failure (cargo
+ /// feature `tls`).
#[cfg(feature = "tls")]
- Tls(native_tls::Error),
+ Tls(String),
}
impl fmt::Display for Error {
diff --git a/tests/fixtures/tls/README.md b/tests/fixtures/tls/README.md
index 7b5a9e5..4b15ed4 100644
--- a/tests/fixtures/tls/README.md
+++ b/tests/fixtures/tls/README.md
@@ -25,35 +25,33 @@ Not secrets — the keys never protect anything.
| File | Role |
|---|---|
-| `cert.pem` + `key.pem` | server identity (loopback `TlsAcceptor`,
live-server keystore) |
+| `cert.pem` + `key.pem` | server identity (loopback rustls server,
live-server keystore) |
| `client-cert.pem` + `client-key.pem` | client identity for the mutual-TLS
tests |
-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:
+The current server certificate expires **2028-11-09**. Regenerate it before
+then with the extensions required by the WebPKI verifier:
```sh
cd tests/fixtures/tls
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
-days 820 -nodes -subj "/CN=localhost" \
+ -addext "basicConstraints=critical,CA:FALSE" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
-addext "extendedKeyUsage=serverAuth" \
- -addext "keyUsage=digitalSignature,keyEncipherment"
+ -addext "keyUsage=critical,digitalSignature,keyEncipherment"
```
-The client certificate (expires **2028-10-10** as well) is standalone
-self-signed — `cert.pem` cannot act as its issuer because its `keyUsage`
-lacks `keyCertSign`; nothing in the tests validates the client chain
-anyway (`native-tls`'s `TlsAcceptor` cannot request client certs):
+The client certificate (expires **2028-11-09** as well) is a standalone
+self-signed client identity. The rustls loopback server explicitly trusts it
+and requires it during the mutual-TLS test:
```sh
cd tests/fixtures/tls
openssl req -x509 -newkey rsa:2048 -keyout client-key.pem -out client-cert.pem
\
-days 820 -nodes -subj "/CN=iotdb-client-rust-test-client" \
+ -addext "basicConstraints=critical,CA:FALSE" \
-addext "extendedKeyUsage=clientAuth" \
- -addext "keyUsage=digitalSignature,keyEncipherment"
+ -addext "keyUsage=critical,digitalSignature,keyEncipherment"
```
## Live TLS server keystore
diff --git a/tests/fixtures/tls/cert.pem b/tests/fixtures/tls/cert.pem
index ca7e9d0..15aca49 100644
--- a/tests/fixtures/tls/cert.pem
+++ b/tests/fixtures/tls/cert.pem
@@ -1,7 +1,7 @@
-----BEGIN CERTIFICATE-----
-MIIDSTCCAjGgAwIBAgIUXWTGi5P/NlLO5jb4yB/faKrLSmEwDQYJKoZIhvcNAQEL
-BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDcxMzA2MDcyMFoXDTI4MTAx
-MDA2MDcyMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+MIIDSTCCAjGgAwIBAgIUfdH0iMiE5zWJ86sySDOXC7J6YNowDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDgxMjA5NDI0NVoXDTI4MTEw
+OTA5NDI0NVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAihtCghtrZyQRS08IclOEhez3pB5DUfQzfo8sZE4uUIsp
Rxb1gbbVpICi7k8rc3NGKmmIzOOPnLPMlpISunxfdzYWEO1bSJQKEk1DCSFYL60t
ZCBJzJ5XdEQypQ8AqGsC4QGsqEhzzvEF6ttOGVIHOsTzoRnClyZTVJxqU62RfXuk
@@ -9,12 +9,12 @@
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=
+dqUuvKDXkUpT2jAMBgNVHRMBAf8EAjAAMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcE
+fwAAATATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZI
+hvcNAQELBQADggEBAGBSggPkAfWDByrVomrJsX1tfASasP8tTwRAgy/t1h8X2No4
+0JHqS4YtyoidRdXMtoSSgLFKIbMI/axg1s6D/G8IaF7Nho0tJw2g4swe3Xmz8ou0
+fZzxOtPkjxJ7Z24OH/WlNTje25St3v8lQyWdvz3vMFDPM8Yk5RVG0bXypAgp0Gs5
+VKACu2WALLg6JDpqi+4bfvicvDyorLCcW1R+ZqIRdv5PqGE559TX3Lxz/lbWKBxy
+0SzQdlZ+T7D/2NSLduo7G+kpAMIqwDvp88hk/vb5a7meV6lQA/L65h5ZTa13dnqL
+9YvkQrAZkx0m2QuLZMTtL00lW3XcYf1hIJ4FhYk=
-----END CERTIFICATE-----
diff --git a/tests/fixtures/tls/client-cert.pem
b/tests/fixtures/tls/client-cert.pem
index 7fd9ba9..43f4f35 100644
--- a/tests/fixtures/tls/client-cert.pem
+++ b/tests/fixtures/tls/client-cert.pem
@@ -1,7 +1,7 @@
-----BEGIN CERTIFICATE-----
-MIIDUzCCAjugAwIBAgIUPEd19SXvrayngu1MttuvSr5zI9owDQYJKoZIhvcNAQEL
+MIIDUzCCAjugAwIBAgIUDafY7BR5FrS+A8HUZMwOxNblaccwDQYJKoZIhvcNAQEL
BQAwKDEmMCQGA1UEAwwdaW90ZGItY2xpZW50LXJ1c3QtdGVzdC1jbGllbnQwHhcN
-MjYwNzEzMDY0MDUxWhcNMjgxMDEwMDY0MDUxWjAoMSYwJAYDVQQDDB1pb3RkYi1j
+MjYwODEyMDk0MjQ1WhcNMjgxMTA5MDk0MjQ1WjAoMSYwJAYDVQQDDB1pb3RkYi1j
bGllbnQtcnVzdC10ZXN0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAN9vhv0UheTWaJh44F0/0Hn0gvek+YQQkhP5AU7m5XRR10ahG9m/3XLv
5AVxvowaVYtmlTBigyLOZH8AX7UrUg4RMMI/lahUmthauXFklfW8cnCj+1BEsYiD
@@ -10,11 +10,11 @@
Nhtix/a9aerFbxuK3XERl2skzJZRLib0aUKXGLrEGFIrlTea95zZ7Q9R0NbU+6XJ
/KHHAjdWH/jP1vk+a+G1ov/jRsbhuAjIPSyuFTS+nBsHjTvccA4Jb2iALu5ImzUT
GEvB+CBeYe+hzxW4WE7kycf3+C7GhZkCAwEAAaN1MHMwHQYDVR0OBBYEFLjH7RNa
2AHWqxjUag9W4M3maHwFMB8GA1UdIwQYMBaAFLjH7RNa2AHWqxjUag9W4M3maHwF
-MA8GA1UdEwEB/wQFMAMBAf8wEwYDVR0lBAwwCgYIKwYBBQUHAwIwCwYDVR0PBAQD
-AgWgMA0GCSqGSIb3DQEBCwUAA4IBAQC4sjGOurJLqDYRGneoWfleQdQXI3uelmMJ
-+cHMjwU/CoCC1Ri3WYwQR4FTQYSI6ONhneVNTFp4fZSH5qYOzH6U1FudpaarY8kD
-bYRi/01MnmJ+rJW7K5ni9EVMN3qH4w3GK+pTOx1SkEUFQYzRHNCtFSWvYATk8eFQ
-JdBghxHPe7WQrgOre4Z7WgUUU4IwA9nfp+rLArXaQFfzX0qpm/9LzluHyTS895Ph
-E0Jn2hdUzLQGwfW7e5W6WRbtzPAuDoXz87pvYcnX020S+//wYoEE6zRnq/mvSgMC
-GGXBAsp4ihTz1zcwYTUlUabTSYY0R8MRP7jSvAtUf1JFG4BtcMUf
+MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDgYDVR0PAQH/BAQD
+AgWgMA0GCSqGSIb3DQEBCwUAA4IBAQA2TUAZWTdjG7PjJw6ySDfMfFAivPqT/1L/
+jSqxH1KkXOhmK8IASVBTE2WU2I0v61VXlmf1Ob3FXxOsizYwQN1hggO/RnpBJBo3
+Ip/LyP1i0lfWzCujnZzsoRWLk0YqCwmUYkW/4TWLeMwa/zer3q0SPpy7UGlgFGyR
+FDIXJ9rpN4XTKBHvJR+Flu8CAMG96GRai7oaznI49BRPmWfxcd36bvr2aB3MfgFW
+VffAktHDIGNBjbXWnAOzyqKggkdMR2DKLDkjonx2TP7VpRYGWVK59nJiKYNbYXaN
+g4aMa5un50+i2Um1z8wMniZstEsyaVGEbWUnhXxRgjpxmiAaOv8s
-----END CERTIFICATE-----