This is an automated email from the ASF dual-hosted git repository.
HTHou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new e9fe467592f Support Thrift client mutual TLS (#18026)
e9fe467592f is described below
commit e9fe467592fa60dcce20d35bfd5bcabecf0961ae
Author: Haonan <[email protected]>
AuthorDate: Wed Jul 1 11:11:01 2026 +0800
Support Thrift client mutual TLS (#18026)
---
.../it/env/cluster/config/MppCommonConfig.java | 6 +
.../env/cluster/config/MppSharedCommonConfig.java | 7 +
.../iotdb/it/env/cluster/env/AbstractEnv.java | 29 +++
.../it/env/remote/config/RemoteCommonConfig.java | 5 +
.../org/apache/iotdb/itbase/env/CommonConfig.java | 2 +
.../iotdb/session/it/IoTDBClientMutualSSLIT.java | 284 +++++++++++++++++++++
.../java/org/apache/iotdb/cli/AbstractCli.java | 44 ++++
.../src/main/java/org/apache/iotdb/cli/Cli.java | 23 +-
.../org/apache/iotdb/tool/common/Constants.java | 8 +
.../org/apache/iotdb/tool/common/OptionsUtil.java | 20 ++
.../apache/iotdb/tool/data/AbstractDataTool.java | 21 ++
.../iotdb/tool/schema/AbstractSchemaTool.java | 18 ++
iotdb-client/client-py/README.md | 36 ++-
iotdb-client/client-py/iotdb/Session.py | 28 +-
iotdb-client/client-py/iotdb/SessionPool.py | 8 +
iotdb-client/client-py/iotdb/dbapi/Connection.py | 35 ++-
iotdb-client/client-py/iotdb/table_session.py | 6 +
iotdb-client/client-py/iotdb/table_session_pool.py | 4 +
iotdb-client/client-py/session_ssl_example.py | 16 +-
.../client-py/tests/unit/test_session_ssl.py | 152 +++++++++++
.../main/java/org/apache/iotdb/jdbc/Config.java | 4 +
.../org/apache/iotdb/jdbc/IoTDBConnection.java | 4 +-
.../apache/iotdb/jdbc/IoTDBConnectionParams.java | 18 ++
.../src/main/java/org/apache/iotdb/jdbc/Utils.java | 8 +
.../test/java/org/apache/iotdb/jdbc/UtilsTest.java | 28 ++
.../apache/iotdb/rpc/BaseRpcTransportFactory.java | 24 +-
.../java/org/apache/iotdb/rpc/RpcSslUtils.java | 75 +++++-
.../java/org/apache/iotdb/rpc/RpcUtilsTest.java | 14 +
.../iotdb/session/AbstractSessionBuilder.java | 2 +
.../org/apache/iotdb/session/NodesSupplier.java | 12 +
.../java/org/apache/iotdb/session/Session.java | 16 ++
.../apache/iotdb/session/SessionConnection.java | 18 +-
.../apache/iotdb/session/TableSessionBuilder.java | 24 ++
.../org/apache/iotdb/session/ThriftConnection.java | 6 +-
.../org/apache/iotdb/session/pool/SessionPool.java | 22 ++
.../session/pool/TableSessionPoolBuilder.java | 24 ++
.../iotdb/db/protocol/client/ConfigNodeClient.java | 3 +-
.../iotdb/db/service/ExternalRPCService.java | 84 ++++--
.../conf/iotdb-system.properties.template | 7 +
.../apache/iotdb/commons/conf/CommonConfig.java | 11 +
.../iotdb/commons/conf/CommonDescriptor.java | 4 +
41 files changed, 1099 insertions(+), 61 deletions(-)
diff --git
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
index 4dd7f857162..bd2fb84e0b6 100644
---
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
+++
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppCommonConfig.java
@@ -632,6 +632,12 @@ public class MppCommonConfig extends MppBaseConfig
implements CommonConfig {
return this;
}
+ @Override
+ public CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+ setProperty("thrift_ssl_client_auth", String.valueOf(thriftSSLClientAuth));
+ return this;
+ }
+
@Override
public CommonConfig setEnableInternalSSL(boolean enableInternalSSL) {
setProperty("enable_internal_ssl", String.valueOf(enableInternalSSL));
diff --git
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
index 8e0398de309..b1c2a4f8d6b 100644
---
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
+++
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppSharedCommonConfig.java
@@ -658,6 +658,13 @@ public class MppSharedCommonConfig implements CommonConfig
{
return this;
}
+ @Override
+ public CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+ cnConfig.setThriftSSLClientAuth(thriftSSLClientAuth);
+ dnConfig.setThriftSSLClientAuth(thriftSSLClientAuth);
+ return this;
+ }
+
@Override
public CommonConfig setEnableInternalSSL(boolean enableInternalSSL) {
cnConfig.setEnableInternalSSL(enableInternalSSL);
diff --git
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
index 342b4aba26d..e5d4e19834d 100644
---
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
+++
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/env/AbstractEnv.java
@@ -692,6 +692,10 @@ public abstract class AbstractEnv implements BaseEnv {
return
Boolean.parseBoolean(getDataNodeCommonConfigProperty("enable_thrift_ssl",
"false"));
}
+ private boolean isThriftSSLClientAuthEnabled() {
+ return
Boolean.parseBoolean(getDataNodeCommonConfigProperty("thrift_ssl_client_auth",
"false"));
+ }
+
private String getDataNodeCommonConfigProperty(final String key, final
String defaultValue) {
return ((MppCommonConfig) clusterConfig.getDataNodeCommonConfig())
.getProperty(key, defaultValue);
@@ -711,6 +715,11 @@ public abstract class AbstractEnv implements BaseEnv {
putIfPresent(
info, Config.TRUST_STORE_PWD,
getDataNodeCommonConfigProperty("trust_store_pwd", ""));
putIfPresent(info, Config.SSL_PROTOCOL, getClientSSLProtocol());
+ if (isThriftSSLClientAuthEnabled()) {
+ putIfPresent(info, Config.KEY_STORE,
getDataNodeCommonConfigProperty("key_store_path", ""));
+ putIfPresent(
+ info, Config.KEY_STORE_PWD,
getDataNodeCommonConfigProperty("key_store_pwd", ""));
+ }
}
return info;
}
@@ -728,6 +737,11 @@ public abstract class AbstractEnv implements BaseEnv {
.trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
.trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd",
""))
.sslProtocol(getClientSSLProtocol());
+ if (isThriftSSLClientAuthEnabled()) {
+ builder
+ .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+ .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+ }
}
return builder;
}
@@ -739,6 +753,11 @@ public abstract class AbstractEnv implements BaseEnv {
.trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
.trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd",
""))
.sslProtocol(getClientSSLProtocol());
+ if (isThriftSSLClientAuthEnabled()) {
+ builder
+ .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+ .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+ }
}
return builder;
}
@@ -750,6 +769,11 @@ public abstract class AbstractEnv implements BaseEnv {
.trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
.trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd",
""))
.sslProtocol(getClientSSLProtocol());
+ if (isThriftSSLClientAuthEnabled()) {
+ builder
+ .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+ .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+ }
}
return builder;
}
@@ -761,6 +785,11 @@ public abstract class AbstractEnv implements BaseEnv {
.trustStore(getDataNodeCommonConfigProperty("trust_store_path", ""))
.trustStorePwd(getDataNodeCommonConfigProperty("trust_store_pwd",
""))
.sslProtocol(getClientSSLProtocol());
+ if (isThriftSSLClientAuthEnabled()) {
+ builder
+ .keyStore(getDataNodeCommonConfigProperty("key_store_path", ""))
+ .keyStorePwd(getDataNodeCommonConfigProperty("key_store_pwd", ""));
+ }
}
return builder;
}
diff --git
a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
index a2d10c01009..ba1f7106dd6 100644
---
a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
+++
b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteCommonConfig.java
@@ -447,6 +447,11 @@ public class RemoteCommonConfig implements CommonConfig {
return this;
}
+ @Override
+ public CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+ return this;
+ }
+
@Override
public CommonConfig setSubscriptionPrefetchTsFileBatchMaxDelayInMs(
int subscriptionPrefetchTsFileBatchMaxDelayInMs) {
diff --git
a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java
b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java
index 5b51a6a8cf9..5a7a004fa88 100644
---
a/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java
+++
b/integration-test/src/main/java/org/apache/iotdb/itbase/env/CommonConfig.java
@@ -203,6 +203,8 @@ public interface CommonConfig {
CommonConfig setEnableThriftClientSSL(boolean enableThriftClientSSL);
+ CommonConfig setThriftSSLClientAuth(boolean thriftSSLClientAuth);
+
CommonConfig setEnableInternalSSL(boolean enableInternalSSL);
CommonConfig setKeyStorePath(String keyStorePath);
diff --git
a/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBClientMutualSSLIT.java
b/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBClientMutualSSLIT.java
new file mode 100644
index 00000000000..0feb9ba6f30
--- /dev/null
+++
b/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBClientMutualSSLIT.java
@@ -0,0 +1,284 @@
+/*
+ * 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.
+ */
+package org.apache.iotdb.session.it;
+
+import org.apache.iotdb.isession.ISession;
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.isession.pool.ISessionPool;
+import org.apache.iotdb.isession.pool.SessionDataSetWrapper;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
+import org.apache.iotdb.jdbc.Config;
+import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.session.Session;
+import org.apache.iotdb.session.TableSessionBuilder;
+import org.apache.iotdb.session.pool.SessionPool;
+
+import org.apache.tsfile.read.common.RowRecord;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.Collections;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({
+ LocalStandaloneIT.class,
+ ClusterIT.class,
+ TableLocalStandaloneIT.class,
+ TableClusterIT.class
+})
+public class IoTDBClientMutualSSLIT {
+
+ private static final String STORE_PASSWORD = "thrift";
+ private static String keyDir;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ keyDir =
+ System.getProperty("user.dir")
+ + File.separator
+ + "target"
+ + File.separator
+ + "test-classes"
+ + File.separator;
+
+ EnvFactory.getEnv()
+ .getConfig()
+ .getCommonConfig()
+ .setEnableThriftClientSSL(true)
+ .setThriftSSLClientAuth(true)
+ .setKeyStorePath(keyStorePath())
+ .setKeyStorePwd(STORE_PASSWORD)
+ .setTrustStorePath(trustStorePath())
+ .setTrustStorePwd(STORE_PASSWORD)
+ .setSslProtocol(SessionConfig.DEFAULT_SSL_PROTOCOL);
+ EnvFactory.getEnv().initClusterEnvironment();
+ }
+
+ @After
+ public void tearDown() {
+ try (ISession session = newMutualSSLSession()) {
+ deleteTreeDatabase(session, "root.client_mtls_tree");
+ deleteTreeDatabase(session, "root.client_mtls_pool");
+ deleteTreeDatabase(session, "root.client_mtls_jdbc");
+ } catch (Exception ignored) {
+ // ignored
+ }
+ try (ITableSession session = newMutualSSLTableSession()) {
+ session.executeNonQueryStatement("DROP DATABASE IF EXISTS
client_mtls_table");
+ } catch (Exception ignored) {
+ // ignored
+ }
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ EnvFactory.getEnv().cleanClusterEnvironment();
+ }
+
+ @Test
+ public void sslClientWithoutKeyStoreCanNotConnectWhenClientAuthRequired() {
+ final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+ final Session session =
+ new Session.Builder()
+ .host(dataNode.getIp())
+ .port(dataNode.getPort())
+ .useSSL(true)
+ .trustStore(trustStorePath())
+ .trustStorePwd(STORE_PASSWORD)
+ .build();
+
+ assertThrows(IoTDBConnectionException.class, session::open);
+ }
+
+ @Test
+ public void treeSessionCanConnectWithMutualSSL() throws Exception {
+ try (ISession session = newMutualSSLSession()) {
+ session.executeNonQueryStatement("CREATE DATABASE
root.client_mtls_tree");
+ session.executeNonQueryStatement(
+ "CREATE TIMESERIES root.client_mtls_tree.d1.s1 WITH DATATYPE=INT32,
ENCODING=PLAIN");
+ session.executeNonQueryStatement(
+ "INSERT INTO root.client_mtls_tree.d1(time, s1) VALUES (1, 11)");
+
+ try (SessionDataSet dataSet =
+ session.executeQueryStatement("SELECT s1 FROM
root.client_mtls_tree.d1")) {
+ assertTrue(dataSet.hasNext());
+ final RowRecord record = dataSet.next();
+ assertEquals(1L, record.getTimestamp());
+ assertEquals(11, record.getFields().get(0).getIntV());
+ assertFalse(dataSet.hasNext());
+ }
+ }
+ }
+
+ @Test
+ public void sessionPoolCanConnectWithMutualSSL() throws Exception {
+ final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+ final ISessionPool pool =
+ new SessionPool.Builder()
+ .nodeUrls(Collections.singletonList(dataNode.getIpAndPortString()))
+ .maxSize(1)
+ .useSSL(true)
+ .trustStore(trustStorePath())
+ .trustStorePwd(STORE_PASSWORD)
+ .keyStore(keyStorePath())
+ .keyStorePwd(STORE_PASSWORD)
+ .build();
+ try {
+ pool.executeNonQueryStatement("CREATE DATABASE root.client_mtls_pool");
+ pool.executeNonQueryStatement(
+ "CREATE TIMESERIES root.client_mtls_pool.d1.s1 WITH DATATYPE=INT32,
ENCODING=PLAIN");
+ pool.executeNonQueryStatement(
+ "INSERT INTO root.client_mtls_pool.d1(time, s1) VALUES (1, 22)");
+
+ try (SessionDataSetWrapper dataSet =
+ pool.executeQueryStatement("SELECT s1 FROM
root.client_mtls_pool.d1")) {
+ assertTrue(dataSet.hasNext());
+ final RowRecord record = dataSet.next();
+ assertEquals(1L, record.getTimestamp());
+ assertEquals(22, record.getFields().get(0).getIntV());
+ assertFalse(dataSet.hasNext());
+ }
+ } finally {
+ pool.close();
+ }
+ }
+
+ @Test
+ public void tableSessionCanConnectWithMutualSSL() throws Exception {
+ try (ITableSession session = newMutualSSLTableSession()) {
+ session.executeNonQueryStatement("CREATE DATABASE IF NOT EXISTS
client_mtls_table");
+ session.executeNonQueryStatement("USE client_mtls_table");
+ session.executeNonQueryStatement(
+ "CREATE TABLE IF NOT EXISTS mtls_table (tag1 STRING TAG, value INT32
FIELD)");
+ session.executeNonQueryStatement(
+ "INSERT INTO mtls_table(time, tag1, value) VALUES (1, 'tag1', 33)");
+
+ try (SessionDataSet dataSet =
+ session.executeQueryStatement("SELECT time, value FROM mtls_table
WHERE tag1 = 'tag1'")) {
+ assertTrue(dataSet.hasNext());
+ final RowRecord record = dataSet.next();
+ assertEquals(1L, record.getFields().get(0).getLongV());
+ assertEquals(33, record.getFields().get(1).getIntV());
+ assertFalse(dataSet.hasNext());
+ }
+ }
+ }
+
+ @Test
+ public void jdbcCanConnectWithMutualSSL() throws Exception {
+ final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+
+ try (Connection connection =
+ DriverManager.getConnection(
+ Config.IOTDB_URL_PREFIX + dataNode.getIpAndPortString(),
mutualSSLProperties());
+ Statement statement = connection.createStatement()) {
+ statement.execute("CREATE DATABASE root.client_mtls_jdbc");
+ statement.execute(
+ "CREATE TIMESERIES root.client_mtls_jdbc.d1.s1 WITH DATATYPE=INT32,
ENCODING=PLAIN");
+ statement.execute("INSERT INTO root.client_mtls_jdbc.d1(time, s1) VALUES
(1, 44)");
+
+ try (ResultSet resultSet =
+ statement.executeQuery("SELECT s1 FROM root.client_mtls_jdbc.d1")) {
+ assertTrue(resultSet.next());
+ assertEquals(1L, resultSet.getLong(1));
+ assertEquals(44, resultSet.getInt(2));
+ assertFalse(resultSet.next());
+ }
+ }
+ }
+
+ private static ISession newMutualSSLSession() throws
IoTDBConnectionException {
+ final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+ final Session session =
+ new Session.Builder()
+ .host(dataNode.getIp())
+ .port(dataNode.getPort())
+ .useSSL(true)
+ .trustStore(trustStorePath())
+ .trustStorePwd(STORE_PASSWORD)
+ .keyStore(keyStorePath())
+ .keyStorePwd(STORE_PASSWORD)
+ .build();
+ session.open();
+ return session;
+ }
+
+ private static ITableSession newMutualSSLTableSession() throws
IoTDBConnectionException {
+ final DataNodeWrapper dataNode = EnvFactory.getEnv().getDataNodeWrapper(0);
+ return new TableSessionBuilder()
+ .nodeUrls(Collections.singletonList(dataNode.getIpAndPortString()))
+ .useSSL(true)
+ .trustStore(trustStorePath())
+ .trustStorePwd(STORE_PASSWORD)
+ .keyStore(keyStorePath())
+ .keyStorePwd(STORE_PASSWORD)
+ .build();
+ }
+
+ private static Properties mutualSSLProperties() {
+ final Properties properties = new Properties();
+ properties.put("user", SessionConfig.DEFAULT_USER);
+ properties.put("password", SessionConfig.DEFAULT_PASSWORD);
+ properties.put(Config.USE_SSL, Boolean.TRUE.toString());
+ properties.put(Config.TRUST_STORE, trustStorePath());
+ properties.put(Config.TRUST_STORE_PWD, STORE_PASSWORD);
+ properties.put(Config.KEY_STORE, keyStorePath());
+ properties.put(Config.KEY_STORE_PWD, STORE_PASSWORD);
+ return properties;
+ }
+
+ private void deleteTreeDatabase(final ISession session, final String
database) {
+ try {
+ session.executeNonQueryStatement("DELETE DATABASE " + database);
+ } catch (Exception ignored) {
+ // ignored
+ }
+ }
+
+ private static String keyStorePath() {
+ return keyDir + "test-keystore";
+ }
+
+ private static String trustStorePath() {
+ return keyDir + "test-truststore";
+ }
+}
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
index 017f0a1a6ca..088131c2570 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
@@ -74,8 +74,10 @@ public abstract class AbstractCli {
static final String USE_SSL_ARGS = "usessl";
static final String TRUST_STORE_ARGS = "ts";
+ static final String KEY_STORE_ARGS = "ks";
static final String TRUST_STORE_PWD_ARGS = "tpw";
+ static final String KEY_STORE_PWD_ARGS = "kpw";
static final String SSL_PROTOCOL_ARGS = "ssl_protocol";
@@ -83,8 +85,10 @@ public abstract class AbstractCli {
private static final String USE_SSL = "use_ssl";
private static final String TRUST_STORE = "trust_store";
+ private static final String KEY_STORE = "key_store";
private static final String TRUST_STORE_PWD = "trust_store_pwd";
+ private static final String KEY_STORE_PWD = "key_store_pwd";
private static final String SSL_PROTOCOL = "ssl_protocol";
private static final String NULL = "null";
@@ -135,6 +139,8 @@ public abstract class AbstractCli {
static String trustStore;
// TODO: Make non-static
static String trustStorePwd;
+ static String keyStore;
+ static String keyStorePwd;
static String sslProtocol;
static String execute;
@@ -160,6 +166,8 @@ public abstract class AbstractCli {
keywordSet.add("-" + USE_SSL_ARGS);
keywordSet.add("-" + TRUST_STORE_ARGS);
keywordSet.add("-" + TRUST_STORE_PWD_ARGS);
+ keywordSet.add("-" + KEY_STORE_ARGS);
+ keywordSet.add("-" + KEY_STORE_PWD_ARGS);
keywordSet.add("-" + SSL_PROTOCOL_ARGS);
keywordSet.add("-" + EXECUTE_ARGS);
keywordSet.add("-" + ISO8601_ARGS);
@@ -219,6 +227,42 @@ public abstract class AbstractCli {
.build();
options.addOption(useSSL);
+ Option trustStore =
+ Option.builder(TRUST_STORE_ARGS)
+ .longOpt(TRUST_STORE)
+ .argName(TRUST_STORE)
+ .hasArg()
+ .desc("Trust store. (optional)")
+ .build();
+ options.addOption(trustStore);
+
+ Option trustStorePwd =
+ Option.builder(TRUST_STORE_PWD_ARGS)
+ .longOpt(TRUST_STORE_PWD)
+ .argName(TRUST_STORE_PWD)
+ .hasArg()
+ .desc("Trust store password. (optional)")
+ .build();
+ options.addOption(trustStorePwd);
+
+ Option keyStore =
+ Option.builder(KEY_STORE_ARGS)
+ .longOpt(KEY_STORE)
+ .argName(KEY_STORE)
+ .hasArg()
+ .desc("Key store for mutual SSL. (optional)")
+ .build();
+ options.addOption(keyStore);
+
+ Option keyStorePwd =
+ Option.builder(KEY_STORE_PWD_ARGS)
+ .longOpt(KEY_STORE_PWD)
+ .argName(KEY_STORE_PWD)
+ .hasArg()
+ .desc("Key store password for mutual SSL. (optional)")
+ .build();
+ options.addOption(keyStorePwd);
+
Option sslProtocol =
Option.builder(SSL_PROTOCOL_ARGS)
.longOpt(SSL_PROTOCOL)
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
index a9e911be6ed..63c3c5d1153 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
@@ -111,6 +111,12 @@ public class Cli extends AbstractCli {
info.setProperty("use_ssl", useSsl);
info.setProperty("trust_store", trustStore);
info.setProperty("trust_store_pwd", trustStorePwd);
+ if (keyStore != null) {
+ info.setProperty(Config.KEY_STORE, keyStore);
+ }
+ if (keyStorePwd != null) {
+ info.setProperty(Config.KEY_STORE_PWD, keyStorePwd);
+ }
if (sslProtocol != null) {
info.setProperty(Config.SSL_PROTOCOL, sslProtocol);
}
@@ -164,8 +170,21 @@ public class Cli extends AbstractCli {
useSsl = commandLine.getOptionValue(USE_SSL_ARGS);
sslProtocol = commandLine.getOptionValue(SSL_PROTOCOL_ARGS);
if (Boolean.parseBoolean(useSsl)) {
- trustStore = ctx.getLineReader().readLine("please input your
trust_store:", '\0');
- trustStorePwd = ctx.getLineReader().readLine("please input your
trust_store_pwd:", '\0');
+ trustStore = commandLine.getOptionValue(TRUST_STORE_ARGS);
+ if (trustStore == null) {
+ trustStore = ctx.getLineReader().readLine("please input your
trust_store:", '\0');
+ }
+ trustStorePwd = commandLine.getOptionValue(TRUST_STORE_PWD_ARGS);
+ if (trustStorePwd == null) {
+ trustStorePwd = ctx.getLineReader().readLine("please input your
trust_store_pwd:", '\0');
+ }
+ keyStore = commandLine.getOptionValue(KEY_STORE_ARGS);
+ keyStorePwd = commandLine.getOptionValue(KEY_STORE_PWD_ARGS);
+ if (keyStore != null && keyStorePwd == null) {
+ keyStorePwd = ctx.getLineReader().readLine("please input your
key_store_pwd:", '\0');
+ } else if (keyStore == null && keyStorePwd != null) {
+ keyStore = ctx.getLineReader().readLine("please input your
key_store:", '\0');
+ }
}
password = commandLine.getOptionValue(PW_ARGS);
if (password == null) {
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
index a1063260957..ed2d96dc860 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
@@ -68,6 +68,14 @@ public class Constants {
public static final String TRUST_STORE_PWD_NAME = "trust_store_password";
public static final String TRUST_STORE_PWD_DESC = "Trust store password.
(optional)";
+ public static final String KEY_STORE_ARGS = "ks";
+ public static final String KEY_STORE_NAME = "key_store";
+ public static final String KEY_STORE_DESC = "Key store for mutual SSL.
(optional)";
+
+ public static final String KEY_STORE_PWD_ARGS = "kpw";
+ public static final String KEY_STORE_PWD_NAME = "key_store_password";
+ public static final String KEY_STORE_PWD_DESC = "Key store password for
mutual SSL. (optional)";
+
public static final String SSL_PROTOCOL_ARGS = "ssl_protocol";
public static final String SSL_PROTOCOL_NAME = "ssl_protocol";
public static final String SSL_PROTOCOL_DESC = "SSL protocol. (optional)";
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
index 1f79b303f6e..e7ca2317be6 100644
---
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
@@ -133,6 +133,26 @@ public class OptionsUtil extends Constants {
.build();
options.addOption(opTrustStorePwd);
+ Option opKeyStore =
+ Option.builder(KEY_STORE_ARGS)
+ .longOpt(KEY_STORE_NAME)
+ .optionalArg(true)
+ .argName(KEY_STORE_NAME)
+ .hasArg()
+ .desc(KEY_STORE_DESC)
+ .build();
+ options.addOption(opKeyStore);
+
+ Option opKeyStorePwd =
+ Option.builder(KEY_STORE_PWD_ARGS)
+ .longOpt(KEY_STORE_PWD_NAME)
+ .optionalArg(true)
+ .argName(KEY_STORE_PWD_NAME)
+ .hasArg()
+ .desc(KEY_STORE_PWD_DESC)
+ .build();
+ options.addOption(opKeyStorePwd);
+
Option opSslProtocol =
Option.builder(SSL_PROTOCOL_ARGS)
.longOpt(SSL_PROTOCOL_NAME)
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
index 6ea79a5b582..07a21109c20 100644
---
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractDataTool.java
@@ -97,6 +97,8 @@ public abstract class AbstractDataTool {
protected static Boolean useSsl;
protected static String trustStore;
protected static String trustStorePwd;
+ protected static String keyStore;
+ protected static String keyStorePwd;
protected static String sslProtocol;
protected static Boolean aligned;
protected static String database;
@@ -140,6 +142,9 @@ public abstract class AbstractDataTool {
protected static Session.Builder configureSsl(Session.Builder builder) {
builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+ }
if (sslProtocol != null) {
builder.sslProtocol(sslProtocol);
}
@@ -148,6 +153,9 @@ public abstract class AbstractDataTool {
protected static SessionPool.Builder configureSsl(SessionPool.Builder
builder) {
builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+ }
if (sslProtocol != null) {
builder.sslProtocol(sslProtocol);
}
@@ -156,6 +164,9 @@ public abstract class AbstractDataTool {
protected static TableSessionBuilder configureSsl(TableSessionBuilder
builder) {
builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+ }
if (sslProtocol != null) {
builder.sslProtocol(sslProtocol);
}
@@ -164,6 +175,9 @@ public abstract class AbstractDataTool {
protected static TableSessionPoolBuilder
configureSsl(TableSessionPoolBuilder builder) {
builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+ }
if (sslProtocol != null) {
builder.sslProtocol(sslProtocol);
}
@@ -219,6 +233,13 @@ public abstract class AbstractDataTool {
} else {
trustStorePwd = cliCtx.getLineReader().readLine("please input your
trust_store_pwd:", '\0');
}
+ keyStore = commandLine.getOptionValue(Constants.KEY_STORE_ARGS);
+ keyStorePwd = commandLine.getOptionValue(Constants.KEY_STORE_PWD_ARGS);
+ if (keyStore != null && keyStorePwd == null) {
+ keyStorePwd = cliCtx.getLineReader().readLine("please input your
key_store_pwd:", '\0');
+ } else if (keyStore == null && keyStorePwd != null) {
+ keyStore = cliCtx.getLineReader().readLine("please input your
key_store:", '\0');
+ }
}
boolean hasPw = commandLine.hasOption(Constants.PW_ARGS);
if (hasPw) {
diff --git
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
index 2bee5f5d331..6f0e533065f 100644
---
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
+++
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/schema/AbstractSchemaTool.java
@@ -55,6 +55,8 @@ public abstract class AbstractSchemaTool {
protected static Boolean useSsl;
protected static String trustStore;
protected static String trustStorePwd;
+ protected static String keyStore;
+ protected static String keyStorePwd;
protected static String sslProtocol;
protected static Session session;
protected static String queryPath;
@@ -78,6 +80,9 @@ public abstract class AbstractSchemaTool {
protected static Session.Builder configureSsl(Session.Builder builder) {
builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+ }
if (sslProtocol != null) {
builder.sslProtocol(sslProtocol);
}
@@ -86,6 +91,9 @@ public abstract class AbstractSchemaTool {
protected static SessionPool.Builder configureSsl(SessionPool.Builder
builder) {
builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+ }
if (sslProtocol != null) {
builder.sslProtocol(sslProtocol);
}
@@ -94,6 +102,9 @@ public abstract class AbstractSchemaTool {
protected static TableSessionPoolBuilder
configureSsl(TableSessionPoolBuilder builder) {
builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePwd);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePwd);
+ }
if (sslProtocol != null) {
builder.sslProtocol(sslProtocol);
}
@@ -147,6 +158,13 @@ public abstract class AbstractSchemaTool {
} else {
trustStorePwd = cliCtx.getLineReader().readLine("please input your
trust_store_pwd:", '\0');
}
+ keyStore = commandLine.getOptionValue(Constants.KEY_STORE_ARGS);
+ keyStorePwd = commandLine.getOptionValue(Constants.KEY_STORE_PWD_ARGS);
+ if (keyStore != null && keyStorePwd == null) {
+ keyStorePwd = cliCtx.getLineReader().readLine("please input your
key_store_pwd:", '\0');
+ } else if (keyStore == null && keyStorePwd != null) {
+ keyStore = cliCtx.getLineReader().readLine("please input your
key_store:", '\0');
+ }
}
boolean hasPw = commandLine.hasOption(Constants.PW_ARGS);
if (hasPw) {
diff --git a/iotdb-client/client-py/README.md b/iotdb-client/client-py/README.md
index c7dc1b33cc9..2441dfb8661 100644
--- a/iotdb-client/client-py/README.md
+++ b/iotdb-client/client-py/README.md
@@ -90,6 +90,40 @@ Notice: this RPC compression status of client must comply
with that of IoTDB ser
session.close()
```
+### SSL and Mutual TLS
+
+Use `use_ssl=True` and `ca_certs` to enable SSL and verify the server
certificate.
+
+```python
+session = Session(
+ ip,
+ port_,
+ username_,
+ password_,
+ use_ssl=True,
+ ca_certs="/path/ca.crt",
+)
+```
+
+When the server enables Thrift mutual TLS, also configure a client certificate
and an unencrypted PEM private key.
+
+```python
+session = Session(
+ ip,
+ port_,
+ username_,
+ password_,
+ use_ssl=True,
+ ca_certs="/path/ca.crt",
+ client_cert="/path/client.crt",
+ client_key="/path/client.key",
+)
+```
+
+`client_cert` and `client_key` must be set together. `client_key` should be
protected by file permissions because encrypted private keys are not supported
by this client API.
+
+The same SSL parameters are available in `PoolConfig`, `TableSessionConfig`,
`TableSessionPoolConfig`, and DBAPI `connect`.
+
### Data Definition Interface (DDL Interface)
#### DATABASE Management
@@ -606,5 +640,3 @@ Namely, these are
* Release to pypi
-
-
diff --git a/iotdb-client/client-py/iotdb/Session.py
b/iotdb-client/client-py/iotdb/Session.py
index a3d89d71746..0a5cb43fd54 100644
--- a/iotdb-client/client-py/iotdb/Session.py
+++ b/iotdb-client/client-py/iotdb/Session.py
@@ -87,6 +87,8 @@ class Session(object):
use_ssl=False,
ca_certs=None,
connection_timeout_in_ms=None,
+ client_cert=None,
+ client_key=None,
):
self.__host = host
self.__port = port
@@ -117,6 +119,8 @@ class Session(object):
self.database = None
self.__use_ssl = use_ssl
self.__ca_certs = ca_certs
+ self.__client_cert = client_cert
+ self.__client_key = client_key
self.__connection_timeout_in_ms = connection_timeout_in_ms
self.__time_precision = "ms"
@@ -132,6 +136,8 @@ class Session(object):
use_ssl=False,
ca_certs=None,
connection_timeout_in_ms=None,
+ client_cert=None,
+ client_key=None,
):
if node_urls is None:
raise RuntimeError("node urls is empty")
@@ -145,6 +151,8 @@ class Session(object):
enable_redirection,
use_ssl=use_ssl,
ca_certs=ca_certs,
+ client_cert=client_cert,
+ client_key=client_key,
connection_timeout_in_ms=connection_timeout_in_ms,
)
session.__hosts = []
@@ -259,7 +267,21 @@ class Session(object):
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
- context.load_verify_locations(cafile=self.__ca_certs)
+ context.load_default_certs(ssl.Purpose.SERVER_AUTH)
+ if self.__has_text(self.__ca_certs):
+ context.load_verify_locations(cafile=self.__ca_certs)
+ if self.__has_text(self.__client_cert) or self.__has_text(
+ self.__client_key
+ ):
+ if not self.__has_text(self.__client_cert) or not
self.__has_text(
+ self.__client_key
+ ):
+ raise TTransport.TTransportException(
+ message="client_cert and client_key must be set
together."
+ )
+ context.load_cert_chain(
+ certfile=self.__client_cert, keyfile=self.__client_key
+ )
socket = TSSLSocket.TSSLSocket(
host=endpoint.ip, port=endpoint.port, ssl_context=context
)
@@ -275,6 +297,10 @@ class Session(object):
raise IoTDBConnectionException(e) from None
return transport
+ @staticmethod
+ def __has_text(value):
+ return value is not None and str(value).strip() != ""
+
def is_open(self):
return not self.__is_close
diff --git a/iotdb-client/client-py/iotdb/SessionPool.py
b/iotdb-client/client-py/iotdb/SessionPool.py
index 1e34186b2e0..f52f4765395 100644
--- a/iotdb-client/client-py/iotdb/SessionPool.py
+++ b/iotdb-client/client-py/iotdb/SessionPool.py
@@ -49,6 +49,8 @@ class PoolConfig(object):
use_ssl: bool = False,
ca_certs: str = None,
connection_timeout_in_ms: int = None,
+ client_cert: str = None,
+ client_key: str = None,
):
self.host = host
self.port = port
@@ -68,6 +70,8 @@ class PoolConfig(object):
self.enable_redirection = enable_redirection
self.use_ssl = use_ssl
self.ca_certs = ca_certs
+ self.client_cert = client_cert
+ self.client_key = client_key
self.connection_timeout_in_ms = connection_timeout_in_ms
@@ -96,6 +100,8 @@ class SessionPool(object):
enable_redirection=self.__pool_config.enable_redirection,
use_ssl=self.__pool_config.use_ssl,
ca_certs=self.__pool_config.ca_certs,
+ client_cert=self.__pool_config.client_cert,
+ client_key=self.__pool_config.client_key,
connection_timeout_in_ms=self.__pool_config.connection_timeout_in_ms,
)
session.sql_dialect = self.sql_dialect
@@ -112,6 +118,8 @@ class SessionPool(object):
enable_redirection=self.__pool_config.enable_redirection,
use_ssl=self.__pool_config.use_ssl,
ca_certs=self.__pool_config.ca_certs,
+ client_cert=self.__pool_config.client_cert,
+ client_key=self.__pool_config.client_key,
connection_timeout_in_ms=self.__pool_config.connection_timeout_in_ms,
)
session.sql_dialect = self.sql_dialect
diff --git a/iotdb-client/client-py/iotdb/dbapi/Connection.py
b/iotdb-client/client-py/iotdb/dbapi/Connection.py
index aee5520e9af..4bbb4e288cf 100644
--- a/iotdb-client/client-py/iotdb/dbapi/Connection.py
+++ b/iotdb-client/client-py/iotdb/dbapi/Connection.py
@@ -37,12 +37,29 @@ class Connection(object):
zone_id=Session.DEFAULT_ZONE_ID,
enable_rpc_compression=False,
sqlalchemy_mode=False,
+ use_ssl=False,
+ ca_certs=None,
+ connection_timeout_in_ms=None,
+ client_cert=None,
+ client_key=None,
):
- self.__session = Session(host, port, username, password, fetch_size,
zone_id)
+ self.__session = Session(
+ host,
+ port,
+ username,
+ password,
+ fetch_size,
+ zone_id,
+ use_ssl=self.__to_bool(use_ssl),
+ ca_certs=ca_certs,
+
connection_timeout_in_ms=self.__to_optional_int(connection_timeout_in_ms),
+ client_cert=client_cert,
+ client_key=client_key,
+ )
self.__sqlalchemy_mode = sqlalchemy_mode
self.__is_close = True
try:
- self.__session.open(enable_rpc_compression)
+ self.__session.open(self.__to_bool(enable_rpc_compression))
self.__is_close = False
except Exception as e:
raise ConnectionError(e)
@@ -89,3 +106,17 @@ class Connection(object):
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
+
+ @staticmethod
+ def __to_bool(value):
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ return value.strip().lower() in ("true", "1", "yes", "y")
+ return bool(value)
+
+ @staticmethod
+ def __to_optional_int(value):
+ if value is None or value == "":
+ return None
+ return int(value)
diff --git a/iotdb-client/client-py/iotdb/table_session.py
b/iotdb-client/client-py/iotdb/table_session.py
index fdf20e7cfa5..47a81ea5c23 100644
--- a/iotdb-client/client-py/iotdb/table_session.py
+++ b/iotdb-client/client-py/iotdb/table_session.py
@@ -38,6 +38,8 @@ class TableSessionConfig(object):
use_ssl: bool = False,
ca_certs: str = None,
connection_timeout_in_ms: int = None,
+ client_cert: str = None,
+ client_key: str = None,
):
"""
Initialize a TableSessionConfig object with the provided parameters.
@@ -71,6 +73,8 @@ class TableSessionConfig(object):
self.enable_compression = enable_compression
self.use_ssl = use_ssl
self.ca_certs = ca_certs
+ self.client_cert = client_cert
+ self.client_key = client_key
self.connection_timeout_in_ms = connection_timeout_in_ms
@@ -91,6 +95,8 @@ class TableSession(object):
table_session_config.use_ssl,
table_session_config.ca_certs,
table_session_config.connection_timeout_in_ms,
+ client_cert=table_session_config.client_cert,
+ client_key=table_session_config.client_key,
)
self.__session.sql_dialect = "table"
self.__session.database = table_session_config.database
diff --git a/iotdb-client/client-py/iotdb/table_session_pool.py
b/iotdb-client/client-py/iotdb/table_session_pool.py
index f44df3f249b..08bfd2fc229 100644
--- a/iotdb-client/client-py/iotdb/table_session_pool.py
+++ b/iotdb-client/client-py/iotdb/table_session_pool.py
@@ -37,6 +37,8 @@ class TableSessionPoolConfig(object):
use_ssl: bool = False,
ca_certs: str = None,
connection_timeout_in_ms: int = None,
+ client_cert: str = None,
+ client_key: str = None,
):
"""
Initialize a TableSessionPoolConfig object with the provided
parameters.
@@ -77,6 +79,8 @@ class TableSessionPoolConfig(object):
use_ssl=use_ssl,
ca_certs=ca_certs,
connection_timeout_in_ms=connection_timeout_in_ms,
+ client_cert=client_cert,
+ client_key=client_key,
)
self.max_pool_size = max_pool_size
self.wait_timeout_in_ms = wait_timeout_in_ms
diff --git a/iotdb-client/client-py/session_ssl_example.py
b/iotdb-client/client-py/session_ssl_example.py
index 2d5a557afc4..afded04eb61 100644
--- a/iotdb-client/client-py/session_ssl_example.py
+++ b/iotdb-client/client-py/session_ssl_example.py
@@ -28,11 +28,21 @@ password_ = "root"
use_ssl = True
# Configure certificate path
ca_certs = "/path/server.crt"
+# Configure client certificate and private key when the server enables mTLS.
+client_cert = "/path/client.crt"
+client_key = "/path/client.key"
def get_data():
session = Session(
- ip, port_, username_, password_, use_ssl=use_ssl, ca_certs=ca_certs
+ ip,
+ port_,
+ username_,
+ password_,
+ use_ssl=use_ssl,
+ ca_certs=ca_certs,
+ client_cert=client_cert,
+ client_key=client_key,
)
session.open(False)
result = session.execute_query_statement("select * from root.eg.etth")
@@ -53,6 +63,8 @@ def get_data2():
max_retry=3,
use_ssl=use_ssl,
ca_certs=ca_certs,
+ client_cert=client_cert,
+ client_key=client_key,
)
max_pool_size = 5
wait_timeout_in_ms = 3000
@@ -74,6 +86,8 @@ def get_table_data():
time_zone="Asia/Shanghai",
use_ssl=use_ssl,
ca_certs=ca_certs,
+ client_cert=client_cert,
+ client_key=client_key,
)
session = TableSession(pool_config)
result = session.execute_query_statement("select * from test")
diff --git a/iotdb-client/client-py/tests/unit/test_session_ssl.py
b/iotdb-client/client-py/tests/unit/test_session_ssl.py
new file mode 100644
index 00000000000..11541223e1b
--- /dev/null
+++ b/iotdb-client/client-py/tests/unit/test_session_ssl.py
@@ -0,0 +1,152 @@
+# 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.
+#
+
+import importlib
+import ssl
+from types import SimpleNamespace
+
+import pytest
+from thrift.transport import TTransport
+from thrift.transport import TSSLSocket
+
+
+class FakeSslContext:
+ def __init__(self):
+ self.verify_mode = None
+ self.check_hostname = None
+ self.verify_locations = []
+ self.default_cert_purposes = []
+ self.cert_chain = None
+
+ def load_default_certs(self, purpose):
+ self.default_cert_purposes.append(purpose)
+
+ def load_verify_locations(self, cafile=None):
+ self.verify_locations.append(cafile)
+
+ def load_cert_chain(self, certfile=None, keyfile=None):
+ self.cert_chain = (certfile, keyfile)
+
+
+class FakeSocket:
+ def __init__(self, host, port, ssl_context):
+ self.host = host
+ self.port = port
+ self.ssl_context = ssl_context
+ self.timeout = None
+
+ def setTimeout(self, timeout):
+ self.timeout = timeout
+
+
+class FakeTransport:
+ def __init__(self, socket):
+ self.socket = socket
+ self.opened = False
+
+ def isOpen(self):
+ return self.opened
+
+ def open(self):
+ self.opened = True
+
+
+def patch_ssl_transport(monkeypatch, context):
+ monkeypatch.setattr(ssl, "create_default_context", lambda purpose: context)
+ monkeypatch.setattr(ssl, "SSLContext", lambda protocol: context)
+ monkeypatch.setattr(
+ TSSLSocket,
+ "TSSLSocket",
+ lambda host, port, ssl_context: FakeSocket(host, port, ssl_context),
+ )
+ monkeypatch.setattr(TTransport, "TFramedTransport", FakeTransport)
+
+
+def test_session_ssl_loads_client_cert_chain(monkeypatch):
+ session_module = importlib.import_module("iotdb.Session")
+ context = FakeSslContext()
+ patch_ssl_transport(monkeypatch, context)
+
+ session = session_module.Session(
+ "127.0.0.1",
+ "6667",
+ use_ssl=True,
+ ca_certs="ca.crt",
+ client_cert="client.crt",
+ client_key="client.key",
+ )
+
+ transport = session._Session__get_transport(
+ SimpleNamespace(ip="127.0.0.1", port=6667)
+ )
+
+ assert context.verify_locations == ["ca.crt"]
+ assert context.cert_chain == ("client.crt", "client.key")
+ assert transport.socket.ssl_context is context
+ assert transport.opened
+
+
+def test_session_ssl_requires_client_cert_and_key_together(monkeypatch):
+ session_module = importlib.import_module("iotdb.Session")
+ patch_ssl_transport(monkeypatch, FakeSslContext())
+
+ session = session_module.Session(
+ "127.0.0.1", "6667", use_ssl=True, client_cert="client.crt"
+ )
+
+ with pytest.raises(TTransport.TTransportException) as exc_info:
+ session._Session__get_transport(SimpleNamespace(ip="127.0.0.1",
port=6667))
+
+ assert "client_cert and client_key must be set together" in
str(exc_info.value)
+
+
+def test_dbapi_connection_passes_ssl_options(monkeypatch):
+ connection_module = importlib.import_module("iotdb.dbapi.Connection")
+
+ class FakeSession:
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ self.open_compression = None
+
+ def open(self, enable_rpc_compression):
+ self.open_compression = enable_rpc_compression
+
+ def close(self):
+ pass
+
+ monkeypatch.setattr(connection_module, "Session", FakeSession)
+
+ connection = connection_module.Connection(
+ "127.0.0.1",
+ "6667",
+ enable_rpc_compression="true",
+ use_ssl="true",
+ ca_certs="ca.crt",
+ connection_timeout_in_ms="1000",
+ client_cert="client.crt",
+ client_key="client.key",
+ )
+
+ session = connection._Connection__session
+ assert session.kwargs["use_ssl"] is True
+ assert session.kwargs["ca_certs"] == "ca.crt"
+ assert session.kwargs["connection_timeout_in_ms"] == 1000
+ assert session.kwargs["client_cert"] == "client.crt"
+ assert session.kwargs["client_key"] == "client.key"
+ assert session.open_compression is True
diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java
index d79656cc8d2..54aca784d15 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Config.java
@@ -82,6 +82,10 @@ public class Config {
public static final String TRUST_STORE_PWD = "trust_store_pwd";
+ public static final String KEY_STORE = "key_store";
+
+ public static final String KEY_STORE_PWD = "key_store_pwd";
+
public static final String SSL_PROTOCOL = "ssl_protocol";
static final String DEFAULT_SSL_PROTOCOL = "TLS";
diff --git
a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
index 0dc81115dd0..9a6dc91ceb4 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
@@ -544,12 +544,14 @@ public class IoTDBConnection implements Connection {
if (params.isUseSSL()) {
transport =
- DeepCopyRpcTransportFactory.INSTANCE.getTransportWithSSLConfig(
+ DeepCopyRpcTransportFactory.INSTANCE.getTransport(
params.getHost(),
params.getPort(),
getNetworkTimeout(),
params.getTrustStore(),
params.getTrustStorePwd(),
+ params.getKeyStore(),
+ params.getKeyStorePwd(),
params.getSslProtocol());
} else {
transport =
diff --git
a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
index 8bf51379c5c..6c9a148517c 100644
---
a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
+++
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnectionParams.java
@@ -51,6 +51,8 @@ public class IoTDBConnectionParams {
private boolean useSSL = false;
private String trustStore;
private String trustStorePwd;
+ private String keyStore;
+ private String keyStorePwd;
private String sslProtocol = Config.DEFAULT_SSL_PROTOCOL;
private String sqlDialect = TREE;
@@ -185,6 +187,22 @@ public class IoTDBConnectionParams {
this.trustStorePwd = trustStorePwd;
}
+ public String getKeyStore() {
+ return keyStore;
+ }
+
+ public void setKeyStore(String keyStore) {
+ this.keyStore = keyStore;
+ }
+
+ public String getKeyStorePwd() {
+ return keyStorePwd;
+ }
+
+ public void setKeyStorePwd(String keyStorePwd) {
+ this.keyStorePwd = keyStorePwd;
+ }
+
public String getSslProtocol() {
return sslProtocol;
}
diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java
index ea205ef7267..81d9347c688 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/Utils.java
@@ -138,6 +138,12 @@ public class Utils {
if (info.containsKey(Config.TRUST_STORE_PWD)) {
params.setTrustStorePwd(info.getProperty(Config.TRUST_STORE_PWD));
}
+ if (info.containsKey(Config.KEY_STORE)) {
+ params.setKeyStore(info.getProperty(Config.KEY_STORE));
+ }
+ if (info.containsKey(Config.KEY_STORE_PWD)) {
+ params.setKeyStorePwd(info.getProperty(Config.KEY_STORE_PWD));
+ }
if (info.containsKey(Config.SSL_PROTOCOL)) {
params.setSslProtocol(RpcSslUtils.normalizeProtocol(info.getProperty(Config.SSL_PROTOCOL)));
}
@@ -180,6 +186,8 @@ public class Utils {
case Config.USE_SSL:
case Config.TRUST_STORE:
case Config.TRUST_STORE_PWD:
+ case Config.KEY_STORE:
+ case Config.KEY_STORE_PWD:
case Config.SSL_PROTOCOL:
case Config.VERSION:
case Config.NETWORK_TIMEOUT:
diff --git
a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java
b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java
index d201a9b2d45..a84010ad4ec 100644
--- a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java
+++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/UtilsTest.java
@@ -163,11 +163,39 @@ public class UtilsTest {
@Test
public void testParseSslConfig() throws IoTDBURLException {
Properties properties = new Properties();
+ properties.setProperty(Config.TRUST_STORE, "/tmp/truststore.p12");
+ properties.setProperty(Config.TRUST_STORE_PWD, "trust_pass");
+ properties.setProperty(Config.KEY_STORE, "/tmp/keystore.p12");
+ properties.setProperty(Config.KEY_STORE_PWD, "key_pass");
IoTDBConnectionParams params =
Utils.parseUrl(
"jdbc:iotdb://127.0.0.1:6667?use_ssl=true&ssl_protocol=ProviderProtocol",
properties);
assertTrue(params.isUseSSL());
assertEquals("ProviderProtocol", params.getSslProtocol());
+ assertEquals("/tmp/truststore.p12", params.getTrustStore());
+ assertEquals("trust_pass", params.getTrustStorePwd());
+ assertEquals("/tmp/keystore.p12", params.getKeyStore());
+ assertEquals("key_pass", params.getKeyStorePwd());
+ }
+
+ @Test
+ public void testParseSslConfigFromUrl() throws IoTDBURLException {
+ IoTDBConnectionParams params =
+ Utils.parseUrl(
+ "jdbc:iotdb://127.0.0.1:6667?use_ssl=true"
+ + "&trust_store=/tmp/url-truststore.p12"
+ + "&trust_store_pwd=url_trust_pass"
+ + "&key_store=/tmp/url-keystore.p12"
+ + "&key_store_pwd=url_key_pass"
+ + "&ssl_protocol=ProviderProtocol",
+ new Properties());
+
+ assertTrue(params.isUseSSL());
+ assertEquals("ProviderProtocol", params.getSslProtocol());
+ assertEquals("/tmp/url-truststore.p12", params.getTrustStore());
+ assertEquals("url_trust_pass", params.getTrustStorePwd());
+ assertEquals("/tmp/url-keystore.p12", params.getKeyStore());
+ assertEquals("url_key_pass", params.getKeyStorePwd());
}
}
diff --git
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
index 03bf2070bca..4a78526210f 100644
---
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
+++
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/BaseRpcTransportFactory.java
@@ -19,8 +19,6 @@
package org.apache.iotdb.rpc;
-import org.apache.iotdb.rpc.i18n.RpcMessages;
-
import org.apache.thrift.transport.TMemoryInputTransport;
import org.apache.thrift.transport.TSSLTransportFactory;
import org.apache.thrift.transport.TSocket;
@@ -28,10 +26,6 @@ import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.apache.thrift.transport.TTransportFactory;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-
@SuppressWarnings("java:S1135") // ignore todos
public class BaseRpcTransportFactory extends TTransportFactory {
@@ -91,7 +85,9 @@ public class BaseRpcTransportFactory extends
TTransportFactory {
throws TTransportException {
TSSLTransportFactory.TSSLTransportParameters params =
RpcSslUtils.createTSSLTransportParameters(sslProtocol);
- RpcSslUtils.setTrustStore(params, trustStore, trustStorePwd);
+ if (hasText(trustStore)) {
+ RpcSslUtils.setTrustStore(params, trustStore, trustStorePwd,
sslProtocol);
+ }
TTransport transport = TSSLTransportFactory.getClientSocket(ip, port,
timeout, params);
return inner.getTransport(transport);
}
@@ -120,11 +116,11 @@ public class BaseRpcTransportFactory extends
TTransportFactory {
throws TTransportException {
TSSLTransportFactory.TSSLTransportParameters params =
RpcSslUtils.createTSSLTransportParameters(sslProtocol);
- if (Files.exists(Paths.get(trustStore)) &&
Files.exists(Paths.get(keyStore))) {
- RpcSslUtils.setTrustStore(params, trustStore, trustStorePwd);
- RpcSslUtils.setKeyStore(params, keyStore, keyStorePwd);
- } else {
- throw new TTransportException(new
IOException(RpcMessages.COULD_NOT_LOAD_KEYSTORE));
+ if (hasText(trustStore)) {
+ RpcSslUtils.setTrustStore(params, trustStore, trustStorePwd,
sslProtocol);
+ }
+ if (hasText(keyStore)) {
+ RpcSslUtils.setKeyStore(params, keyStore, keyStorePwd, sslProtocol);
}
TTransport transport = TSSLTransportFactory.getClientSocket(ip, port,
timeout, params);
return inner.getTransport(transport);
@@ -150,4 +146,8 @@ public class BaseRpcTransportFactory extends
TTransportFactory {
public static void setThriftMaxFrameSize(int thriftMaxFrameSize) {
BaseRpcTransportFactory.thriftMaxFrameSize = thriftMaxFrameSize;
}
+
+ private static boolean hasText(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
}
diff --git
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcSslUtils.java
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcSslUtils.java
index c6ceb5b4c5d..d2f2c03e5fb 100644
---
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcSslUtils.java
+++
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcSslUtils.java
@@ -48,6 +48,9 @@ public final class RpcSslUtils {
private static final String PKCS12_STORE_TYPE = "PKCS12";
private static final String JKS_STORE_TYPE = "JKS";
+ private static final String TLCP_KEY_MANAGER_TYPE = "NewSunX509";
+ private static final String TLCP_TRUST_MANAGER_TYPE = "PKIX";
+
private static volatile String protocol = DEFAULT_PROTOCOL;
private RpcSslUtils() {}
@@ -62,17 +65,27 @@ public final class RpcSslUtils {
public static TSSLTransportFactory.TSSLTransportParameters
createTSSLTransportParameters(
String sslProtocol) {
- return new
TSSLTransportFactory.TSSLTransportParameters(normalizeProtocol(sslProtocol),
null);
+ return new
TSSLTransportFactory.TSSLTransportParameters(resolveProtocol(sslProtocol),
null);
}
public static void setKeyStore(
TSSLTransportFactory.TSSLTransportParameters params, String
keyStorePath, String keyStorePwd)
throws TTransportException {
+ setKeyStore(params, keyStorePath, keyStorePwd, protocol);
+ }
+
+ public static void setKeyStore(
+ TSSLTransportFactory.TSSLTransportParameters params,
+ String keyStorePath,
+ String keyStorePwd,
+ String sslProtocol)
+ throws TTransportException {
try {
+ String protocol = resolveProtocol(sslProtocol);
params.setKeyStore(
keyStorePath,
keyStorePwd,
- KeyManagerFactory.getDefaultAlgorithm(),
+ keyManagerType(protocol),
detectStoreType(keyStorePath, keyStorePwd));
} catch (GeneralSecurityException | IOException e) {
throw new TTransportException(e);
@@ -84,11 +97,21 @@ public final class RpcSslUtils {
String trustStorePath,
String trustStorePwd)
throws TTransportException {
+ setTrustStore(params, trustStorePath, trustStorePwd, protocol);
+ }
+
+ public static void setTrustStore(
+ TSSLTransportFactory.TSSLTransportParameters params,
+ String trustStorePath,
+ String trustStorePwd,
+ String sslProtocol)
+ throws TTransportException {
try {
+ String protocol = resolveProtocol(sslProtocol);
params.setTrustStore(
trustStorePath,
trustStorePwd,
- TrustManagerFactory.getDefaultAlgorithm(),
+ trustManagerType(protocol),
detectStoreType(trustStorePath, trustStorePwd));
} catch (GeneralSecurityException | IOException e) {
throw new TTransportException(e);
@@ -112,11 +135,13 @@ public final class RpcSslUtils {
String trustStorePassword,
String sslProtocol)
throws GeneralSecurityException, IOException {
- SSLContext context =
SSLContext.getInstance(normalizeProtocol(sslProtocol));
+ SSLContext context = SSLContext.getInstance(resolveProtocol(sslProtocol));
KeyManager[] keyManagers =
- hasText(keyStorePath) ? loadKeyManagers(keyStorePath,
keyStorePassword) : null;
+ hasText(keyStorePath) ? loadKeyManagers(keyStorePath,
keyStorePassword, sslProtocol) : null;
TrustManager[] trustManagers =
- hasText(trustStorePath) ? loadTrustManagers(trustStorePath,
trustStorePassword) : null;
+ hasText(trustStorePath)
+ ? loadTrustManagers(trustStorePath, trustStorePassword,
sslProtocol)
+ : null;
context.init(keyManagers, trustManagers, null);
return context;
}
@@ -147,17 +172,30 @@ public final class RpcSslUtils {
private static KeyManager[] loadKeyManagers(String keyStorePath, String
keyStorePassword)
throws GeneralSecurityException, IOException {
+ return loadKeyManagers(keyStorePath, keyStorePassword, protocol);
+ }
+
+ private static KeyManager[] loadKeyManagers(
+ String keyStorePath, String keyStorePassword, String sslProtocol)
+ throws GeneralSecurityException, IOException {
KeyStore keyStore = loadStore(keyStorePath, keyStorePassword);
- KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ KeyManagerFactory kmf =
+
KeyManagerFactory.getInstance(keyManagerType(resolveProtocol(sslProtocol)));
kmf.init(keyStore, toPassword(keyStorePassword));
return kmf.getKeyManagers();
}
private static TrustManager[] loadTrustManagers(String trustStorePath,
String trustStorePassword)
throws GeneralSecurityException, IOException {
+ return loadTrustManagers(trustStorePath, trustStorePassword, protocol);
+ }
+
+ private static TrustManager[] loadTrustManagers(
+ String trustStorePath, String trustStorePassword, String sslProtocol)
+ throws GeneralSecurityException, IOException {
KeyStore trustStore = loadStore(trustStorePath, trustStorePassword);
TrustManagerFactory tmf =
-
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
TrustManagerFactory.getInstance(trustManagerType(resolveProtocol(sslProtocol)));
tmf.init(trustStore);
return tmf.getTrustManagers();
}
@@ -226,6 +264,11 @@ public final class RpcSslUtils {
return trimmed.isEmpty() ? DEFAULT_PROTOCOL : trimmed;
}
+ public static String resolveProtocol(String value) {
+ String trimmed = trimToEmpty(value);
+ return trimmed.isEmpty() ? protocol : trimmed;
+ }
+
private static String trimToEmpty(String value) {
return value == null ? "" : value.trim();
}
@@ -242,4 +285,20 @@ public final class RpcSslUtils {
.distinct()
.toArray(String[]::new);
}
+
+ private static String keyManagerType(String sslProtocol) {
+ return isTlcpProtocol(sslProtocol)
+ ? TLCP_KEY_MANAGER_TYPE
+ : KeyManagerFactory.getDefaultAlgorithm();
+ }
+
+ private static String trustManagerType(String sslProtocol) {
+ return isTlcpProtocol(sslProtocol)
+ ? TLCP_TRUST_MANAGER_TYPE
+ : TrustManagerFactory.getDefaultAlgorithm();
+ }
+
+ private static boolean isTlcpProtocol(String protocol) {
+ return protocol != null &&
protocol.trim().toUpperCase(Locale.ROOT).startsWith("TLCP");
+ }
}
diff --git
a/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
index 143de1d9ab7..54da8cb1e70 100644
---
a/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
+++
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
@@ -106,4 +106,18 @@ public class RpcUtilsTest {
Assert.assertEquals("TLSv1.3", RpcSslUtils.normalizeProtocol(" TLSv1.3 "));
Assert.assertEquals("ProviderProtocol", RpcSslUtils.normalizeProtocol("
ProviderProtocol "));
}
+
+ @Test
+ public void testSslProtocolResolution() {
+ String originProtocol = RpcSslUtils.getProtocol();
+ try {
+ RpcSslUtils.configure("ConfiguredProtocol");
+
+ Assert.assertEquals("ConfiguredProtocol",
RpcSslUtils.resolveProtocol(null));
+ Assert.assertEquals("ConfiguredProtocol", RpcSslUtils.resolveProtocol("
"));
+ Assert.assertEquals("ExplicitProtocol", RpcSslUtils.resolveProtocol("
ExplicitProtocol "));
+ } finally {
+ RpcSslUtils.configure(originProtocol);
+ }
+ }
}
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
index 873e6dd248d..44500ed3f10 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/AbstractSessionBuilder.java
@@ -63,6 +63,8 @@ public abstract class AbstractSessionBuilder {
public boolean useSSL = false;
public String trustStore;
public String trustStorePwd;
+ public String keyStore;
+ public String keyStorePwd;
public String sslProtocol = SessionConfig.DEFAULT_SSL_PROTOCOL;
// max retry count, if set to 0, means that we won't do any retry
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
index a41db581e9c..c1f43fa30d4 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
@@ -61,6 +61,8 @@ public class NodesSupplier implements INodeSupplier, Runnable
{
private final boolean useSSL;
private final String trustStore;
private final String trustStorePwd;
+ private final String keyStore;
+ private final String keyStorePwd;
private final String sslProtocol;
private final boolean enableRPCCompression;
private final String userName;
@@ -96,6 +98,8 @@ public class NodesSupplier implements INodeSupplier, Runnable
{
boolean useSSL,
String trustStore,
String trustStorePwd,
+ String keyStore,
+ String keyStorePwd,
String sslProtocol,
boolean enableRPCCompression,
String version) {
@@ -112,6 +116,8 @@ public class NodesSupplier implements INodeSupplier,
Runnable {
useSSL,
trustStore,
trustStorePwd,
+ keyStore,
+ keyStorePwd,
sslProtocol,
enableRPCCompression,
version);
@@ -135,6 +141,8 @@ public class NodesSupplier implements INodeSupplier,
Runnable {
boolean useSSL,
String trustStore,
String trustStorePwd,
+ String keyStore,
+ String keyStorePwd,
String sslProtocol,
boolean enableRPCCompression,
String version) {
@@ -144,6 +152,8 @@ public class NodesSupplier implements INodeSupplier,
Runnable {
this.useSSL = useSSL;
this.trustStore = trustStore;
this.trustStorePwd = trustStorePwd;
+ this.keyStore = keyStore;
+ this.keyStorePwd = keyStorePwd;
this.sslProtocol = sslProtocol;
this.enableRPCCompression = enableRPCCompression;
this.zoneId = zoneId == null ? ZoneId.systemDefault() : zoneId;
@@ -193,6 +203,8 @@ public class NodesSupplier implements INodeSupplier,
Runnable {
useSSL,
trustStore,
trustStorePwd,
+ keyStore,
+ keyStorePwd,
sslProtocol,
userName,
password,
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
index 648a63845b4..8d182122de5 100644
--- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
+++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
@@ -134,6 +134,8 @@ public class Session implements ISession {
protected boolean useSSL;
protected String trustStore;
protected String trustStorePwd;
+ protected String keyStore;
+ protected String keyStorePwd;
protected String sslProtocol;
/**
@@ -475,6 +477,8 @@ public class Session implements ISession {
this.useSSL = builder.useSSL;
this.trustStore = builder.trustStore;
this.trustStorePwd = builder.trustStorePwd;
+ this.keyStore = builder.keyStore;
+ this.keyStorePwd = builder.keyStorePwd;
this.sslProtocol = builder.sslProtocol;
this.enableAutoFetch = builder.enableAutoFetch;
this.maxRetryCount = builder.maxRetryCount;
@@ -545,6 +549,8 @@ public class Session implements ISession {
useSSL,
trustStore,
trustStorePwd,
+ keyStore,
+ keyStorePwd,
sslProtocol,
enableRPCCompaction,
version.toString());
@@ -4438,6 +4444,16 @@ public class Session implements ISession {
return this;
}
+ public Builder keyStore(String keyStore) {
+ this.keyStore = keyStore;
+ return this;
+ }
+
+ public Builder keyStorePwd(String keyStorePwd) {
+ this.keyStorePwd = keyStorePwd;
+ return this;
+ }
+
public Builder sslProtocol(String sslProtocol) {
this.sslProtocol = sslProtocol;
return this;
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
index 65c7f55c41f..49cb688510a 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
@@ -153,7 +153,13 @@ public class SessionConnection {
this.database = database;
try {
init(
- endPoint, session.useSSL, session.trustStore, session.trustStorePwd,
session.sslProtocol);
+ endPoint,
+ session.useSSL,
+ session.trustStore,
+ session.trustStorePwd,
+ session.keyStore,
+ session.keyStorePwd,
+ session.sslProtocol);
} catch (StatementExecutionException e) {
throw new IoTDBConnectionException(e.getMessage());
} catch (IoTDBConnectionException e) {
@@ -186,6 +192,8 @@ public class SessionConnection {
boolean useSSL,
String trustStore,
String trustStorePwd,
+ String keyStore,
+ String keyStorePwd,
String sslProtocol)
throws IoTDBConnectionException, StatementExecutionException {
DeepCopyRpcTransportFactory.setDefaultBufferCapacity(session.thriftDefaultBufferSize);
@@ -196,12 +204,14 @@ public class SessionConnection {
}
if (useSSL) {
transport =
- DeepCopyRpcTransportFactory.INSTANCE.getTransportWithSSLConfig(
+ DeepCopyRpcTransportFactory.INSTANCE.getTransport(
endPoint.getIp(),
endPoint.getPort(),
session.connectionTimeoutInMs,
trustStore,
trustStorePwd,
+ keyStore,
+ keyStorePwd,
sslProtocol);
} else {
transport =
@@ -278,6 +288,8 @@ public class SessionConnection {
session.useSSL,
session.trustStore,
session.trustStorePwd,
+ session.keyStore,
+ session.keyStorePwd,
session.sslProtocol);
} catch (IoTDBConnectionException e) {
if (!reconnect()) {
@@ -1103,6 +1115,8 @@ public class SessionConnection {
session.useSSL,
session.trustStore,
session.trustStorePwd,
+ session.keyStore,
+ session.keyStorePwd,
session.sslProtocol);
connectedSuccess = true;
} catch (IoTDBConnectionException e) {
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
index e724c0fd532..2dca0d351dc 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
@@ -239,6 +239,30 @@ public class TableSessionBuilder extends
AbstractSessionBuilder {
return this;
}
+ /**
+ * Sets the key store path for mutual SSL connections.
+ *
+ * @param keyStore the key store path.
+ * @return the current {@link TableSessionBuilder} instance.
+ * @defaultValue null
+ */
+ public TableSessionBuilder keyStore(String keyStore) {
+ this.keyStore = keyStore;
+ return this;
+ }
+
+ /**
+ * Sets the key store password for mutual SSL connections.
+ *
+ * @param keyStorePwd the key store password.
+ * @return the current {@link TableSessionBuilder} instance.
+ * @defaultValue null
+ */
+ public TableSessionBuilder keyStorePwd(String keyStorePwd) {
+ this.keyStorePwd = keyStorePwd;
+ return this;
+ }
+
/**
* Sets the SSL protocol for secure connections.
*
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
index 3ab3abd581d..2a7f970bd50 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
@@ -78,6 +78,8 @@ public class ThriftConnection {
boolean useSSL,
String trustStore,
String trustStorePwd,
+ String keyStore,
+ String keyStorePwd,
String sslProtocol,
String username,
String password,
@@ -90,12 +92,14 @@ public class ThriftConnection {
try {
if (useSSL) {
transport =
- DeepCopyRpcTransportFactory.INSTANCE.getTransportWithSSLConfig(
+ DeepCopyRpcTransportFactory.INSTANCE.getTransport(
endPoint.getIp(),
endPoint.getPort(),
connectionTimeoutInMs,
trustStore,
trustStorePwd,
+ keyStore,
+ keyStorePwd,
sslProtocol);
} else {
transport =
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
index 6a4107f5c0d..bed65e4870e 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
@@ -117,6 +117,10 @@ public class SessionPool implements ISessionPool {
private String trustStorePwd;
+ private String keyStore;
+
+ private String keyStorePwd;
+
private String sslProtocol = SessionConfig.DEFAULT_SSL_PROTOCOL;
private ZoneId zoneId;
@@ -540,6 +544,8 @@ public class SessionPool implements ISessionPool {
this.useSSL = builder.useSSL;
this.trustStore = builder.trustStore;
this.trustStorePwd = builder.trustStorePwd;
+ this.keyStore = builder.keyStore;
+ this.keyStorePwd = builder.keyStorePwd;
this.sslProtocol = builder.sslProtocol;
this.maxRetryCount = builder.maxRetryCount;
this.retryIntervalInMs = builder.retryIntervalInMs;
@@ -598,6 +604,8 @@ public class SessionPool implements ISessionPool {
.useSSL(useSSL)
.trustStore(trustStore)
.trustStorePwd(trustStorePwd)
+ .keyStore(keyStore)
+ .keyStorePwd(keyStorePwd)
.sslProtocol(sslProtocol)
.maxRetryCount(maxRetryCount)
.retryIntervalInMs(retryIntervalInMs)
@@ -624,6 +632,8 @@ public class SessionPool implements ISessionPool {
.useSSL(useSSL)
.trustStore(trustStore)
.trustStorePwd(trustStorePwd)
+ .keyStore(keyStore)
+ .keyStorePwd(keyStorePwd)
.sslProtocol(sslProtocol)
.maxRetryCount(maxRetryCount)
.retryIntervalInMs(retryIntervalInMs)
@@ -669,6 +679,8 @@ public class SessionPool implements ISessionPool {
useSSL,
trustStore,
trustStorePwd,
+ keyStore,
+ keyStorePwd,
sslProtocol,
enableThriftCompression,
version.toString());
@@ -3648,6 +3660,16 @@ public class SessionPool implements ISessionPool {
return this;
}
+ public Builder keyStore(String keyStore) {
+ this.keyStore = keyStore;
+ return this;
+ }
+
+ public Builder keyStorePwd(String keyStorePwd) {
+ this.keyStorePwd = keyStorePwd;
+ return this;
+ }
+
public Builder sslProtocol(String sslProtocol) {
this.sslProtocol = sslProtocol;
return this;
diff --git
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
index 2c7aba1a452..ab3e1f1c6a6 100644
---
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
+++
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
@@ -281,6 +281,30 @@ public class TableSessionPoolBuilder extends
AbstractSessionPoolBuilder {
return this;
}
+ /**
+ * Sets the key store path for mutual SSL connections.
+ *
+ * @param keyStore the key store path.
+ * @return the current {@link TableSessionPoolBuilder} instance.
+ * @defaultValue null
+ */
+ public TableSessionPoolBuilder keyStore(String keyStore) {
+ this.keyStore = keyStore;
+ return this;
+ }
+
+ /**
+ * Sets the key store password for mutual SSL connections.
+ *
+ * @param keyStorePwd the key store password.
+ * @return the current {@link TableSessionPoolBuilder} instance.
+ * @defaultValue null
+ */
+ public TableSessionPoolBuilder keyStorePwd(String keyStorePwd) {
+ this.keyStorePwd = keyStorePwd;
+ return this;
+ }
+
/**
* Sets the SSL protocol for secure connections.
*
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java
index 678056fb993..f6a079bd2d6 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/client/ConfigNodeClient.java
@@ -284,8 +284,7 @@ public class ConfigNodeClient implements
IConfigNodeRPCService.Iface, ThriftClie
commonConfig.getTrustStorePath(),
commonConfig.getTrustStorePwd(),
commonConfig.getKeyStorePath(),
- commonConfig.getKeyStorePwd(),
- commonConfig.getSslProtocol())
+ commonConfig.getKeyStorePwd())
: DeepCopyRpcTransportFactory.INSTANCE.getTransport(
// As there is a try-catch already, we do not need to use
TSocket.wrap
endpoint.getIp(), endpoint.getPort(), timeoutMs);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
index dcc8690de8a..08e0b90ee65 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/ExternalRPCService.java
@@ -65,34 +65,60 @@ public class ExternalRPCService extends ThriftService
implements ExternalRPCServ
@Override
public void initThriftServiceThread() throws IllegalAccessException {
try {
- thriftServiceThread =
- commonConfig.isEnableThriftClientSSL()
- ? new ThriftServiceThread(
- processor,
- getID().getName(),
- ThreadName.CLIENT_RPC_PROCESSOR.getName(),
- getBindIP(),
- getBindPort(),
- config.getRpcMaxConcurrentClientNum(),
- config.getThriftServerAwaitTimeForStopService(),
- new RPCServiceThriftHandler(impl),
- config.isRpcThriftCompressionEnable(),
- commonConfig.getKeyStorePath(),
- commonConfig.getKeyStorePwd(),
- ZeroCopyRpcTransportFactory.INSTANCE)
- : new ThriftServiceThread(
- processor,
- getID().getName(),
- ThreadName.CLIENT_RPC_PROCESSOR.getName(),
- getBindIP(),
- getBindPort(),
- config.getRpcMaxConcurrentClientNum(),
- config.getThriftServerAwaitTimeForStopService(),
- new RPCServiceThriftHandler(impl),
- config.isRpcThriftCompressionEnable(),
- ZeroCopyRpcTransportFactory.INSTANCE);
+ if (!commonConfig.isEnableThriftClientSSL()) {
+ thriftServiceThread =
+ new ThriftServiceThread(
+ processor,
+ getID().getName(),
+ ThreadName.CLIENT_RPC_PROCESSOR.getName(),
+ getBindIP(),
+ getBindPort(),
+ config.getRpcMaxConcurrentClientNum(),
+ config.getThriftServerAwaitTimeForStopService(),
+ new RPCServiceThriftHandler(impl),
+ config.isRpcThriftCompressionEnable(),
+ ZeroCopyRpcTransportFactory.INSTANCE);
+ } else if (commonConfig.isThriftSSLClientAuth()) {
+ if (!hasText(commonConfig.getTrustStorePath())) {
+ throw new IllegalAccessException(
+ "trust_store_path must be set when thrift_ssl_client_auth is
true");
+ }
+ thriftServiceThread =
+ new ThriftServiceThread(
+ processor,
+ getID().getName(),
+ ThreadName.CLIENT_RPC_PROCESSOR.getName(),
+ getBindIP(),
+ getBindPort(),
+ config.getRpcMaxConcurrentClientNum(),
+ config.getThriftServerAwaitTimeForStopService(),
+ new RPCServiceThriftHandler(impl),
+ config.isRpcThriftCompressionEnable(),
+ commonConfig.getKeyStorePath(),
+ commonConfig.getKeyStorePwd(),
+ commonConfig.getTrustStorePath(),
+ commonConfig.getTrustStorePwd(),
+ ZeroCopyRpcTransportFactory.INSTANCE);
+ } else {
+ thriftServiceThread =
+ new ThriftServiceThread(
+ processor,
+ getID().getName(),
+ ThreadName.CLIENT_RPC_PROCESSOR.getName(),
+ getBindIP(),
+ getBindPort(),
+ config.getRpcMaxConcurrentClientNum(),
+ config.getThriftServerAwaitTimeForStopService(),
+ new RPCServiceThriftHandler(impl),
+ config.isRpcThriftCompressionEnable(),
+ commonConfig.getKeyStorePath(),
+ commonConfig.getKeyStorePwd(),
+ ZeroCopyRpcTransportFactory.INSTANCE);
+ }
} catch (RPCServiceException e) {
- throw new IllegalAccessException(e.getMessage());
+ IllegalAccessException exception = new
IllegalAccessException(e.getMessage());
+ exception.initCause(e);
+ throw exception;
}
thriftServiceThread.setName(ThreadName.CLIENT_RPC_SERVICE.getName());
MetricService.getInstance().addMetricSet(new
RPCServiceMetrics(thriftServiceThread));
@@ -118,6 +144,10 @@ public class ExternalRPCService extends ThriftService
implements ExternalRPCServ
return getBindPort();
}
+ private boolean hasText(String value) {
+ return value != null && !value.trim().isEmpty();
+ }
+
private static class RPCServiceHolder {
private static final ExternalRPCService INSTANCE = new
ExternalRPCService();
diff --git
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
index f176c0e1761..23036c78414 100644
---
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
+++
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
@@ -444,6 +444,13 @@ dn_metric_internal_reporter_type=MEMORY
# Privilege: SECURITY
enable_thrift_ssl=false
+# Whether client authentication is required for Thrift SSL connections.
+# This only takes effect when enable_thrift_ssl=true.
+# effectiveMode: restart
+# Datatype: boolean
+# Privilege: SECURITY
+thrift_ssl_client_auth=false
+
# Whether enable SSL for Rest Service
# effectiveMode: restart
# Datatype: boolean
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
index 53127fa2cdc..257b2a8ad51 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
@@ -490,6 +490,9 @@ public class CommonConfig {
/** Enable the Thrift Client ssl. */
private boolean enableThriftClientSSL = false;
+ /** Whether the external Thrift SSL service requires client certificate
authentication. */
+ private boolean thriftSSLClientAuth = false;
+
/** Enable the cluster internal connection ssl. */
private boolean enableInternalSSL = false;
@@ -2998,6 +3001,14 @@ public class CommonConfig {
this.enableThriftClientSSL = enableThriftClientSSL;
}
+ public boolean isThriftSSLClientAuth() {
+ return thriftSSLClientAuth;
+ }
+
+ public void setThriftSSLClientAuth(boolean thriftSSLClientAuth) {
+ this.thriftSSLClientAuth = thriftSSLClientAuth;
+ }
+
public boolean isEnableInternalSSL() {
return enableInternalSSL;
}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
index eb24b0eeef8..9d7c6bdffc2 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
@@ -678,6 +678,10 @@ public class CommonDescriptor {
Boolean.parseBoolean(
properties.getProperty(
"enable_thrift_ssl",
Boolean.toString(config.isEnableThriftClientSSL()))));
+ config.setThriftSSLClientAuth(
+ Boolean.parseBoolean(
+ properties.getProperty(
+ "thrift_ssl_client_auth",
Boolean.toString(config.isThriftSSLClientAuth()))));
config.setKeyStorePath(properties.getProperty("key_store_path",
config.getKeyStorePath()));
config.setKeyStorePwd(properties.getProperty("key_store_pwd",
config.getKeyStorePwd()));
config.setTrustStorePath(