diff --git a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/RestService.java b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/RestService.java index 6ecba9aece8c6..dc98fac35b212 100644 --- a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/RestService.java +++ b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/RestService.java @@ -41,6 +41,7 @@ import jakarta.servlet.DispatcherType; import java.util.EnumSet; +import java.util.regex.Pattern; public class RestService implements IExternalService { @@ -151,8 +152,11 @@ public void stop() { } } - private void configureSSL(SslContextFactory.Server sslContextFactory, String sslProtocol) { + void configureSSL(SslContextFactory.Server sslContextFactory, String sslProtocol) { String protocol = RpcSslUtils.normalizeProtocol(sslProtocol); sslContextFactory.setProtocol(protocol); + if (RpcSslUtils.requiresProtocolRestriction(protocol)) { + sslContextFactory.setIncludeProtocols("(?i)" + Pattern.quote(protocol)); + } } } diff --git a/external-service-impl/rest/src/test/java/org/apache/iotdb/rest/RestServiceTest.java b/external-service-impl/rest/src/test/java/org/apache/iotdb/rest/RestServiceTest.java new file mode 100644 index 0000000000000..0aebe96d0a2f5 --- /dev/null +++ b/external-service-impl/rest/src/test/java/org/apache/iotdb/rest/RestServiceTest.java @@ -0,0 +1,105 @@ +/* + * 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.rest; + +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Test; + +import java.security.Security; + +import static org.junit.Assert.assertArrayEquals; + +public class RestServiceTest { + + @Test + public void testVersionSpecificSslProtocols() throws Exception { + String[][] cases = { + {"TLSv1.2", "TLSv1.2"}, + {"tlsv1.2", "TLSv1.2"}, + {"TlsV1.2", "TLSv1.2"}, + {" TLSV1.2 ", "TLSv1.2"}, + {"TLSv1.3", "TLSv1.3"}, + {"tlsv1.3", "TLSv1.3"}, + {"TLSV1.3", "TLSv1.3"}, + {" tLsV1.3 ", "TLSv1.3"} + }; + for (String[] testCase : cases) { + String protocol = testCase[0]; + SslContextFactory.Server factory = new SslContextFactory.Server(); + new RestService().configureSSL(factory, protocol); + try { + factory.start(); + assertArrayEquals( + protocol, new String[] {testCase[1]}, factory.newSSLEngine().getEnabledProtocols()); + } finally { + factory.stop(); + } + } + } + + @Test + public void testSslProtocolNameIsMatchedLiterally() { + SslContextFactory.Server factory = new SslContextFactory.Server(); + new RestService().configureSSL(factory, "tlsv1.2"); + String[] supportedProtocols = {"TLSv1.2", "TLSv1X2", "TLSv1.3"}; + factory.selectProtocols(supportedProtocols, supportedProtocols); + assertArrayEquals(new String[] {"TLSv1.2"}, factory.getSelectedProtocols()); + } + + @Test + public void testGenericSslProtocolsKeepDefaults() throws Exception { + SslContextFactory.Server defaults = new SslContextFactory.Server(); + try { + defaults.start(); + for (String protocol : new String[] {"TLS", "tls", "SSL", "ssl", null, " "}) { + SslContextFactory.Server factory = new SslContextFactory.Server(); + new RestService().configureSSL(factory, protocol); + try { + factory.start(); + assertArrayEquals( + protocol, + defaults.newSSLEngine().getEnabledProtocols(), + factory.newSSLEngine().getEnabledProtocols()); + } finally { + factory.stop(); + } + } + } finally { + defaults.stop(); + } + } + + @Test + public void testTlcpSslProtocolKeepsProviderDefaults() throws Exception { + SslContextFactory.Server factory = new SslContextFactory.Server(); + new RestService().configureSSL(factory, "TLCPv1.1"); + assertArrayEquals(new String[0], factory.getIncludeProtocols()); + + // Verify the actual engine as well when running on a JDK that provides TLCP. + if (Security.getProviders("SSLContext.TLCPv1.1") != null) { + try { + factory.start(); + assertArrayEquals(new String[] {"TLCPv1.1"}, factory.newSSLEngine().getEnabledProtocols()); + } finally { + factory.stop(); + } + } + } +} 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 8bb83926e78b8..097c8d6c3de65 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 @@ -22,17 +22,20 @@ import org.apache.iotdb.rpc.i18n.RpcMessages; import org.apache.thrift.transport.TSSLTransportFactory; +import org.apache.thrift.transport.TServerSocket; import org.apache.thrift.transport.TTransportException; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.InetAddress; import java.nio.file.AccessDeniedException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -42,11 +45,13 @@ import java.security.cert.X509Certificate; import java.util.Enumeration; import java.util.Locale; +import java.util.Set; import java.util.stream.Stream; public final class RpcSslUtils { private static final String DEFAULT_PROTOCOL = "TLS"; + private static final Set GENERIC_PROTOCOLS = Set.of("TLS", "SSL", "TLCP"); private static final String PKCS12_STORE_TYPE = "PKCS12"; private static final String JKS_STORE_TYPE = "JKS"; @@ -61,6 +66,19 @@ public static void configure(String sslProtocol) { protocol = normalizeProtocol(sslProtocol); } + /** + * Whether the SSLContext algorithm needs an explicit protocol restriction. + * + *

Generic algorithms retain the provider defaults. TLCPv1.1 contexts already enable only + * TLCPv1.1; some Kona versions omit it from supported protocols, so a Jetty inclusion filter + * would disable it. + */ + public static boolean requiresProtocolRestriction(String sslProtocol) { + String normalizedProtocol = normalizeProtocol(sslProtocol); + return !GENERIC_PROTOCOLS.contains(normalizedProtocol.toUpperCase(Locale.ROOT)) + && !"TLCPv1.1".equalsIgnoreCase(normalizedProtocol); + } + public static TSSLTransportFactory.TSSLTransportParameters createTSSLTransportParameters() { return createTSSLTransportParameters(protocol); } @@ -70,6 +88,33 @@ public static TSSLTransportFactory.TSSLTransportParameters createTSSLTransportPa return new TSSLTransportFactory.TSSLTransportParameters(resolveProtocol(sslProtocol), null); } + public static TServerSocket createServerSocket( + int port, + int clientTimeout, + InetAddress address, + TSSLTransportFactory.TSSLTransportParameters params) + throws TTransportException { + String sslProtocol = protocol; + TServerSocket serverSocket = + TSSLTransportFactory.getServerSocket(port, clientTimeout, address, params); + try { + if (requiresProtocolRestriction(sslProtocol)) { + // A version-specific SSLContext does not restrict the server's enabled protocol versions. + SSLServerSocket sslServerSocket = (SSLServerSocket) serverSocket.getServerSocket(); + String enabledProtocol = + Stream.of(sslServerSocket.getSupportedProtocols()) + .filter(sslProtocol::equalsIgnoreCase) + .findFirst() + .orElse(sslProtocol); + sslServerSocket.setEnabledProtocols(new String[] {enabledProtocol}); + } + return serverSocket; + } catch (IllegalArgumentException e) { + serverSocket.close(); + throw new TTransportException(e); + } + } + public static void setKeyStore( TSSLTransportFactory.TSSLTransportParameters params, String keyStorePath, String keyStorePwd) throws TTransportException { 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 a061bc1506c4d..4702dd6082789 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 @@ -21,9 +21,19 @@ import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.thrift.transport.TSSLTransportFactory; +import org.apache.thrift.transport.TServerSocket; import org.junit.Assert; import org.junit.Test; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.net.InetAddress; +import java.security.KeyStore; import java.sql.Timestamp; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -122,6 +132,63 @@ public void testSslProtocolResolution() { } } + @Test + public void testSslServerSocketEnabledProtocols() throws Exception { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + ByteArrayOutputStream keyStoreBytes = new ByteArrayOutputStream(); + keyStore.store(keyStoreBytes, new char[0]); + + String originalProtocol = RpcSslUtils.getProtocol(); + String[][] cases = { + {"TLSv1.2", "TLSv1.2"}, + {"tlsv1.2", "TLSv1.2"}, + {"TlsV1.2", "TLSv1.2"}, + {" TLSV1.2 ", "TLSv1.2"}, + {"TLSv1.3", "TLSv1.3"}, + {"tlsv1.3", "TLSv1.3"}, + {"TLSV1.3", "TLSv1.3"}, + {" tLsV1.3 ", "TLSv1.3"}, + {"TLS", null}, + {"SSL", null}, + {"tls", null}, + {"ssl", null} + }; + try { + for (String[] testCase : cases) { + String sslProtocol = testCase[0]; + RpcSslUtils.configure(sslProtocol); + TSSLTransportFactory.TSSLTransportParameters params = + RpcSslUtils.createTSSLTransportParameters(); + params.setKeyStore( + new ByteArrayInputStream(keyStoreBytes.toByteArray()), + "", + KeyManagerFactory.getDefaultAlgorithm(), + "JKS"); + try (TServerSocket transport = + RpcSslUtils.createServerSocket(0, 0, InetAddress.getLoopbackAddress(), params)) { + SSLServerSocket serverSocket = (SSLServerSocket) transport.getServerSocket(); + if (testCase[1] == null) { + SSLContext context = SSLContext.getInstance(sslProtocol); + context.init(null, null, null); + try (SSLServerSocket defaultSocket = + (SSLServerSocket) context.getServerSocketFactory().createServerSocket()) { + Assert.assertArrayEquals( + sslProtocol, + defaultSocket.getEnabledProtocols(), + serverSocket.getEnabledProtocols()); + } + } else { + Assert.assertArrayEquals( + sslProtocol, new String[] {testCase[1]}, serverSocket.getEnabledProtocols()); + } + } + } + } finally { + RpcSslUtils.configure(originalProtocol); + } + } + @Test public void convertToTimestampKeepsPreEpochFractions() { // 1.5 seconds before the epoch in each precision diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/AbstractThriftServiceThread.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/AbstractThriftServiceThread.java index eb82bcd654881..ea4a6445a16ef 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/AbstractThriftServiceThread.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/AbstractThriftServiceThread.java @@ -192,7 +192,7 @@ protected AbstractThriftServiceThread( } InetSocketAddress socketAddress = new InetSocketAddress(bindAddress, port); serverTransport = - TSSLTransportFactory.getServerSocket( + RpcSslUtils.createServerSocket( socketAddress.getPort(), clientTimeout, socketAddress.getAddress(), params); TThreadPoolServer.Args poolArgs = initSyncedPoolArgs(processor, threadsName, maxWorkerThreads, timeoutSecond);