From 5c9a4d80f95365f420563b50cbf2e9b5a50b97c5 Mon Sep 17 00:00:00 2001 From: HTHou Date: Wed, 12 Aug 2026 17:31:54 +0800 Subject: [PATCH 1/4] Use rustls for TLS --- .github/workflows/ci.yml | 33 +++++ Cargo.toml | 19 ++- README.md | 11 +- README_ZH.md | 10 +- src/connection/mod.rs | 265 +++++++++++++++++++++++++++-------- src/error.rs | 5 +- tests/fixtures/tls/README.md | 14 +- 7 files changed, 287 insertions(+), 70 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bec0ae..23b6d6f 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 each OS verifier and native root-store integration. + - 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..c025128 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,13 +36,24 @@ 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 } +# 0.7 raises MSRV to 1.85; the crate currently supports Rust 1.75. +rustls-platform-verifier = { version = "0.6.2", 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-platform-verifier 0.6 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 +# certificate verification. Adds `use_ssl` & friends to SessionConfig. +tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-platform-verifier", "dep:zeroize", "dep:security-framework"] diff --git a/README.md b/README.md index ef06b58..5691ab7 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,12 @@ 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 with [`rustls-platform-verifier`](https://crates.io/crates/rustls-platform-verifier), +so the platform trust store and verification policy are used where available; +on Linux/BSD, platform roots are loaded and verified with WebPKI. A CA supplied +through `ca_cert_path` is added to those platform roots. ```toml iotdb-client-rust = { version = "0.1", features = ["tls"] } @@ -200,6 +205,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..f3e454a 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -183,7 +183,12 @@ 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-platform-verifier`](https://crates.io/crates/rustls-platform-verifier) +校验:在支持的平台上使用系统信任库及校验策略;在 Linux/BSD 上加载系统根证书并通过 +WebPKI 校验。`ca_cert_path` 指定的 CA 会追加到系统根证书中。 ```toml iotdb-client-rust = { version = "0.1", features = ["tls"] } @@ -200,6 +205,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..f861d37 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -24,8 +24,19 @@ 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, SignatureScheme, StreamOwned}; + use thrift::protocol::{ TBinaryInputProtocol, TBinaryOutputProtocol, TCompactInputProtocol, TCompactOutputProtocol, TInputProtocol, TOutputProtocol, @@ -63,9 +74,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, - /// 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 +276,155 @@ fn connect_stream(endpoint: &Endpoint, connect_timeout: Duration) -> Result Result> { - 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> { + 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> { + 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 = if tls.accept_invalid_certs { + Arc::new(NoCertificateVerification::new( + provider.signature_verification_algorithms, + )) + } else { + Arc::new( + rustls_platform_verifier::Verifier::new_with_extra_roots( + extra_roots, + Arc::clone(&provider), + ) + .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>> { + 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::>>() + .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 { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> std::result::Result { + 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 { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.supported_algorithms.supported_schemes() + } } /// A `TlsStream` shared between the read and write transports. @@ -325,15 +436,15 @@ fn tls_handshake( /// read and write never contend. #[cfg(feature = "tls")] #[derive(Clone)] -struct SharedTlsStream(std::sync::Arc>>); +struct SharedTlsStream(std::sync::Arc>>); #[cfg(feature = "tls")] impl SharedTlsStream { - fn new(stream: native_tls::TlsStream) -> Self { + fn new(stream: StreamOwned) -> Self { Self(std::sync::Arc::new(std::sync::Mutex::new(stream))) } - fn lock(&self) -> std::sync::MutexGuard<'_, native_tls::TlsStream> { + fn lock(&self) -> std::sync::MutexGuard<'_, StreamOwned> { self.0.lock().unwrap_or_else(|p| p.into_inner()) } } @@ -515,6 +626,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,14 +634,56 @@ 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 key = rustls_pemfile::pkcs8_private_keys(&mut reader) + .next() + .expect("PKCS#8 key item") + .expect("parse PKCS#8 key") + .into(); + key + } + + fn server_config(require_client_auth: bool) -> Arc { + 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) + } + + fn tls_acceptor(require_client_auth: bool) -> Endpoint { + 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(); std::thread::spawn(move || { @@ -537,7 +691,11 @@ mod tls_tests { 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)), + Ok(mut stream) => { + let mut connection = rustls::ServerConnection::new(Arc::clone(&config)) + .expect("server connection"); + let _ = connection.complete_io(&mut stream); + } Err(_) => break, } } @@ -674,15 +832,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 = tls_acceptor(true); 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..fb7eb26 100644 --- a/tests/fixtures/tls/README.md +++ b/tests/fixtures/tls/README.md @@ -25,11 +25,12 @@ 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 +The macOS verifier used by `rustls-platform-verifier` 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: @@ -43,10 +44,9 @@ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \ -addext "keyUsage=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-10-10** as well) is a standalone +self-signed test CA and client identity. The rustls loopback server trusts it +as a root and requires it during the mutual-TLS test: ```sh cd tests/fixtures/tls From 37c75ac9fbe13abd6067b377af5593f653f1753c Mon Sep 17 00:00:00 2001 From: HTHou Date: Wed, 12 Aug 2026 17:45:23 +0800 Subject: [PATCH 2/4] Use WebPKI for TLS verification --- Cargo.toml | 9 ++++----- README.md | 7 +++---- README_ZH.md | 7 +++---- src/connection/mod.rs | 32 +++++++++++++++++++++++------- tests/fixtures/tls/README.md | 20 +++++++++---------- tests/fixtures/tls/cert.pem | 22 ++++++++++---------- tests/fixtures/tls/client-cert.pem | 18 ++++++++--------- 7 files changed, 64 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c025128..5b58379 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,13 +40,12 @@ log = "0.4" uuid = "=1.20.0" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"], optional = true } rustls-pemfile = { version = "2.2", optional = true } -# 0.7 raises MSRV to 1.85; the crate currently supports Rust 1.75. -rustls-platform-verifier = { version = "0.6.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-platform-verifier 0.6 allows 3.x; 3.7 raises MSRV to Rust 1.85. +# 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] @@ -55,5 +54,5 @@ env_logger = "0.11" [features] default = [] # TLS 1.2/1.3 via rustls, using the ring crypto provider and platform -# certificate verification. Adds `use_ssl` & friends to SessionConfig. -tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-platform-verifier", "dep:zeroize", "dep:security-framework"] +# 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 5691ab7..93e607b 100644 --- a/README.md +++ b/README.md @@ -185,10 +185,9 @@ It must match the **server** setting `dn_rpc_thrift_compression_enable` (default **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 with [`rustls-platform-verifier`](https://crates.io/crates/rustls-platform-verifier), -so the platform trust store and verification policy are used where available; -on Linux/BSD, platform roots are loaded and verified with WebPKI. A CA supplied -through `ca_cert_path` is added to those platform roots. +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"] } diff --git a/README_ZH.md b/README_ZH.md index f3e454a..3da4c98 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -185,10 +185,9 @@ let config = SessionConfig { enable_rpc_compression: true, ..Default::default() **TLS** 位于 `tls` cargo feature 之后。其底层使用 [`rustls`](https://crates.io/crates/rustls) 和 `ring` 密码学 provider,支持 -TLS 1.2/1.3。服务端证书通过 -[`rustls-platform-verifier`](https://crates.io/crates/rustls-platform-verifier) -校验:在支持的平台上使用系统信任库及校验策略;在 Linux/BSD 上加载系统根证书并通过 -WebPKI 校验。`ca_cert_path` 指定的 CA 会追加到系统根证书中。 +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"] } diff --git a/src/connection/mod.rs b/src/connection/mod.rs index f861d37..21aaff1 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -35,7 +35,10 @@ use rustls::crypto::WebPkiSupportedAlgorithms; #[cfg(feature = "tls")] use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; #[cfg(feature = "tls")] -use rustls::{ClientConfig, ClientConnection, DigitallySignedStruct, SignatureScheme, StreamOwned}; +use rustls::{ + ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, SignatureScheme, + StreamOwned, +}; use thrift::protocol::{ TBinaryInputProtocol, TBinaryOutputProtocol, TCompactInputProtocol, TCompactOutputProtocol, @@ -309,13 +312,28 @@ fn tls_client_config(tls: &TlsOptions) -> Result> { provider.signature_verification_algorithms, )) } else { - Arc::new( - rustls_platform_verifier::Verifier::new_with_extra_roots( - extra_roots, - Arc::clone(&provider), - ) - .map_err(|e| Error::Tls(e.to_string()))?, + 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) diff --git a/tests/fixtures/tls/README.md b/tests/fixtures/tls/README.md index fb7eb26..4b15ed4 100644 --- a/tests/fixtures/tls/README.md +++ b/tests/fixtures/tls/README.md @@ -28,32 +28,30 @@ Not secrets — the keys never protect anything. | `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 | -The macOS verifier used by `rustls-platform-verifier` 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 a standalone -self-signed test CA and client identity. The rustls loopback server trusts it -as a root and requires it during the mutual-TLS test: +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----- From 0022b792a7c2fcd028f8033683b9efb585c13dcc Mon Sep 17 00:00:00 2001 From: HTHou Date: Wed, 12 Aug 2026 17:53:45 +0800 Subject: [PATCH 3/4] Clarify TLS platform CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23b6d6f..cad6d3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - # Exercises each OS verifier and native root-store integration. + # Exercises native root loading plus the shared WebPKI verifier on each OS. - name: Unit tests (tls) run: cargo test --features tls From 4c38328480a7865bee62d432759673c2b8df2932 Mon Sep 17 00:00:00 2001 From: HTHou Date: Wed, 12 Aug 2026 18:44:10 +0800 Subject: [PATCH 4/4] Verify server-side mTLS handshake --- src/connection/mod.rs | 52 ++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/connection/mod.rs b/src/connection/mod.rs index 21aaff1..7bcdf81 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -655,12 +655,11 @@ mod tls_tests { 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 key = rustls_pemfile::pkcs8_private_keys(&mut reader) - .next() + let mut keys = rustls_pemfile::pkcs8_private_keys(&mut reader); + keys.next() .expect("PKCS#8 key item") .expect("parse PKCS#8 key") - .into(); - key + .into() } fn server_config(require_client_auth: bool) -> Arc { @@ -697,28 +696,35 @@ mod tls_tests { /// 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 { - tls_acceptor(false) + tls_acceptor(false).0 } - fn tls_acceptor(require_client_auth: bool) -> Endpoint { + fn tls_acceptor( + require_client_auth: bool, + ) -> ( + Endpoint, + std::sync::mpsc::Receiver>, + ) { 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(mut stream) => { - let mut connection = rustls::ServerConnection::new(Arc::clone(&config)) - .expect("server connection"); - let _ = connection.complete_io(&mut stream); - } - 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 @@ -855,7 +861,7 @@ mod tls_tests { /// certificate completes the handshake. #[test] fn tls_client_identity_handshake_succeeds() { - let endpoint = tls_acceptor(true); + let (endpoint, server_handshake) = tls_acceptor(true); let options = ConnectionOptions { connect_timeout: Duration::from_millis(500), protocol: RpcProtocol::Binary, @@ -869,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,