This is an automated email from the ASF dual-hosted git repository.
F21 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git
The following commit(s) were added to refs/heads/main by this push:
new c6c84f507 [CALCITE-7796] Avatica: hostname_verification=NONE has no
effect with Apache HttpClient 5.6+
c6c84f507 is described below
commit c6c84f507cebbf11c417401073dc2ffed62b7f0f
Author: Andreas Maechler <[email protected]>
AuthorDate: Thu Sep 3 13:41:17 2026 -0600
[CALCITE-7796] Avatica: hostname_verification=NONE has no effect with
Apache HttpClient 5.6+
With hostname_verification=NONE the remote driver is supposed to skip
TLS hostname verification. On Apache HttpClient 5.6 or newer it does
not: connections to a server whose certificate does not name the URL
host fail during the handshake with "No subject alternative DNS name
matching <host> found". The driver creates its DefaultClientTlsStrategy
with a HostnameVerifier but without a HostnameVerificationPolicy and
relies on the default. Up to HttpClient 5.5 that default was CLIENT, so
the NoopHostnameVerifier was the only check. Since 5.6 a strategy with
a verifier defaults to BOTH (and one without a verifier to BUILTIN);
either policy enables the JSSE endpoint identification during the
handshake, and the verifier has no influence on that check.
Set the policy explicitly: CLIENT with NoopHostnameVerifier for NONE,
and BOTH with the default verifier for STRICT, which matches the
current HttpClient default. The three-argument constructor exists since
HttpClient 5.4, so the fix also compiles against the current
dependency. The new SslHostnameVerificationTest passes on HttpClient 5.5
with and without the fix; it only fails on 5.6 or newer without it.
---
.../avatica/remote/CommonsHttpClientPoolCache.java | 19 +++--
.../org/apache/calcite/avatica/HttpBaseTest.java | 34 +++++---
.../avatica/SslHostnameVerificationTest.java | 97 ++++++++++++++++++++++
3 files changed, 129 insertions(+), 21 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/avatica/remote/CommonsHttpClientPoolCache.java
b/core/src/main/java/org/apache/calcite/avatica/remote/CommonsHttpClientPoolCache.java
index 5611e54a7..215db105f 100644
---
a/core/src/main/java/org/apache/calcite/avatica/remote/CommonsHttpClientPoolCache.java
+++
b/core/src/main/java/org/apache/calcite/avatica/remote/CommonsHttpClientPoolCache.java
@@ -21,6 +21,7 @@
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import
org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy;
import org.apache.hc.client5.http.ssl.HttpsSupport;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
@@ -34,7 +35,6 @@
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.ConcurrentHashMap;
-import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
/**
@@ -82,8 +82,7 @@ private static PoolingHttpClientConnectionManager
setupPool(ConnectionConfig con
private static TlsSocketStrategy createTlsSocketStrategy(ConnectionConfig
config) {
try {
- return new DefaultClientTlsStrategy(getSSLContext(config),
- getHostnameVerifier(config.hostnameVerification()));
+ return createTlsSocketStrategy(getSSLContext(config),
config.hostnameVerification());
} catch (Exception e) {
LOG.error("HTTPS TlsSocketStrategy configuration failed");
throw new RuntimeException(e);
@@ -120,15 +119,16 @@ private static void loadTrustStore(SSLContextBuilder
sslContextBuilder, Connecti
}
/**
- * Creates the {@code HostnameVerifier} given the provided {@code
verification}.
+ * Creates the {@code TlsSocketStrategy} given the provided {@code
verification}.
*
+ * @param sslContext The SSL context to use for the connections.
* @param verification The intended hostname verification action.
- * @return A verifier for the request verification.
+ * @return A strategy performing the requested verification.
* @throws IllegalArgumentException if the provided verification cannot be
* handled.
*/
@SuppressWarnings("deprecation")
- private static HostnameVerifier getHostnameVerifier(
+ private static TlsSocketStrategy createTlsSocketStrategy(SSLContext
sslContext,
org.apache.calcite.avatica.remote.
HostnameVerificationConfigurable.HostnameVerification verification) {
// Normally, the configuration logic would give us a default of STRICT if
it was
@@ -139,9 +139,12 @@ private static HostnameVerifier getHostnameVerifier(
}
switch (verification) {
case STRICT:
- return HttpsSupport.getDefaultHostnameVerifier();
+ return new DefaultClientTlsStrategy(sslContext,
HostnameVerificationPolicy.BOTH,
+ HttpsSupport.getDefaultHostnameVerifier());
case NONE:
- return NoopHostnameVerifier.INSTANCE;
+ // CLIENT leaves verification to the supplied verifier (which does
nothing)
+ return new DefaultClientTlsStrategy(sslContext,
HostnameVerificationPolicy.CLIENT,
+ NoopHostnameVerifier.INSTANCE);
default:
throw new IllegalArgumentException("Unhandled HostnameVerification: " +
verification.name());
}
diff --git a/server/src/test/java/org/apache/calcite/avatica/HttpBaseTest.java
b/server/src/test/java/org/apache/calcite/avatica/HttpBaseTest.java
index 1bee35632..58c024654 100644
--- a/server/src/test/java/org/apache/calcite/avatica/HttpBaseTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/HttpBaseTest.java
@@ -28,6 +28,8 @@
import org.bouncycastle.asn1.x500.style.RFC4519Style;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
@@ -86,15 +88,8 @@ public abstract class HttpBaseTest {
public static void setupClass() throws SQLException {
// Create a self-signed cert
- if (KEYSTORE.isFile()) {
- assertTrue("Failed to delete keystore: " + KEYSTORE, KEYSTORE.delete());
- }
- new CertTool().createSelfSignedCert(KEYSTORE, "avatica",
KEYSTORE_PASSWORD);
-
- if (EMPTY_PW_KEYSTORE.isFile()) {
- assertTrue("Failed to delete keystore: " + EMPTY_PW_KEYSTORE,
EMPTY_PW_KEYSTORE.delete());
- }
- new CertTool().createSelfSignedCert(EMPTY_PW_KEYSTORE, "avatica",
KEYSTORE_EMPTY_PASSWORD);
+ createSelfSignedKeyStore(KEYSTORE, "localhost", KEYSTORE_PASSWORD);
+ createSelfSignedKeyStore(EMPTY_PW_KEYSTORE, "localhost",
KEYSTORE_EMPTY_PASSWORD);
// Create a LocalService around HSQLDB
JdbcMeta jdbcMeta;
@@ -103,6 +98,17 @@ public static void setupClass() throws SQLException {
localService = new LocalService(jdbcMeta);
}
+ /**
+ * Creates a keystore holding a self-signed certificate for {@code hostname}.
+ */
+ protected static void createSelfSignedKeyStore(File keystore, String
hostname,
+ String keystorePassword) {
+ if (keystore.isFile()) {
+ assertTrue("Failed to delete keystore: " + keystore, keystore.delete());
+ }
+ new CertTool().createSelfSignedCert(keystore, "avatica", keystorePassword,
hostname);
+ }
+
@AfterClass public static void stopServers() throws KrbException {
for (HttpServer server : SERVERS_TO_STOP) {
server.stop();
@@ -131,7 +137,7 @@ private static class CertTool {
}
private void createSelfSignedCert(File targetKeystore, String keyName,
- String keystorePassword) {
+ String keystorePassword, String hostname) {
if (targetKeystore.exists()) {
throw new RuntimeException("Keystore already exists: " +
targetKeystore);
}
@@ -139,7 +145,7 @@ private void createSelfSignedCert(File targetKeystore,
String keyName,
try {
KeyPair kp = generateKeyPair();
- X509Certificate cert = generateCert(keyName, kp, true, kp.getPublic(),
+ X509Certificate cert = generateCert(hostname, kp, true, kp.getPublic(),
kp.getPrivate());
char[] password = keystorePassword.toCharArray();
@@ -161,7 +167,7 @@ private KeyPair generateKeyPair() throws
NoSuchAlgorithmException {
return gen.generateKeyPair();
}
- private X509Certificate generateCert(String keyName, KeyPair kp, boolean
isCertAuthority,
+ private X509Certificate generateCert(String hostname, KeyPair kp, boolean
isCertAuthority,
PublicKey signerPublicKey, PrivateKey
signerPrivateKey)
throws IOException, OperatorCreationException, CertificateException,
NoSuchAlgorithmException {
@@ -171,7 +177,7 @@ private X509Certificate generateCert(String keyName,
KeyPair kp, boolean isCertA
BigInteger serialNumber =
BigInteger.valueOf(startDate.getTimeInMillis());
X500Name issuer = new X500Name(
- IETFUtils.rDNsFromString("cn=localhost", RFC4519Style.INSTANCE));
+ IETFUtils.rDNsFromString("cn=" + hostname, RFC4519Style.INSTANCE));
JcaX509v3CertificateBuilder certGen = new
JcaX509v3CertificateBuilder(issuer,
serialNumber, startDate.getTime(), endDate.getTime(), issuer,
kp.getPublic());
JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
@@ -179,6 +185,8 @@ private X509Certificate generateCert(String keyName,
KeyPair kp, boolean isCertA
extensionUtils.createSubjectKeyIdentifier(kp.getPublic()));
certGen.addExtension(Extension.basicConstraints, false,
new BasicConstraints(isCertAuthority));
+ certGen.addExtension(Extension.subjectAlternativeName, false,
+ new GeneralNames(new GeneralName(GeneralName.dNSName, hostname)));
certGen.addExtension(Extension.authorityKeyIdentifier, false,
extensionUtils.createAuthorityKeyIdentifier(signerPublicKey));
if (isCertAuthority) {
diff --git
a/server/src/test/java/org/apache/calcite/avatica/SslHostnameVerificationTest.java
b/server/src/test/java/org/apache/calcite/avatica/SslHostnameVerificationTest.java
new file mode 100644
index 000000000..0ee1a9f6d
--- /dev/null
+++
b/server/src/test/java/org/apache/calcite/avatica/SslHostnameVerificationTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.calcite.avatica;
+
+import org.apache.calcite.avatica.remote.Driver.Serialization;
+import org.apache.calcite.avatica.server.HttpServer;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import javax.net.ssl.SSLException;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+
+/**
+ * Test case for the {@code hostname_verification} connection property of the
remote driver.
+ */
+public class SslHostnameVerificationTest extends HttpBaseTest {
+
+ private static final File MISMATCHED_KEYSTORE =
+ new File(TARGET_DIR, "avatica-test-mismatched-hostname.jks");
+
+ private static String url;
+
+ public SslHostnameVerificationTest() {
+ super(url);
+ }
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ // Skip TLS testing on IBM Java, see SslDriverTest
+ assumeFalse(
+ "Skip TLS testing on IBM Java due eclipse/jetty.project#2807",
+ System.getProperty("java.vendor").contains("IBM")
+ );
+
+ setupClass();
+ // The certificate is issued to a different hostname than the one we
connect to
+ createSelfSignedKeyStore(MISMATCHED_KEYSTORE, "not-localhost.test",
KEYSTORE_PASSWORD);
+
+ HttpServer httpServer = new HttpServer.Builder()
+ .withPort(0)
+ .withTLS(MISMATCHED_KEYSTORE, KEYSTORE_PASSWORD, MISMATCHED_KEYSTORE,
KEYSTORE_PASSWORD)
+ .withHandler(localService, Serialization.PROTOBUF)
+ .build();
+ httpServer.start();
+ SERVERS_TO_STOP.add(httpServer);
+
+ url = "jdbc:avatica:remote:url=https://localhost:" + httpServer.getPort()
+ + ";serialization=PROTOBUF;truststore=" +
MISMATCHED_KEYSTORE.getAbsolutePath()
+ + ";truststore_password=" + KEYSTORE_PASSWORD;
+ LOG.info("JDBC URL {}", url);
+ }
+
+ @Test
+ public void testHostnameVerificationNone() throws Exception {
+ String urlWithNone = jdbcUrl + ";hostname_verification=NONE";
+ try (Connection conn = DriverManager.getConnection(urlWithNone);
+ Statement stmt = conn.createStatement()) {
+ // Just make sure that the https connection is established
+ assertTrue(stmt.execute("SELECT * FROM EMP"));
+ }
+ }
+
+ @Test
+ public void testHostnameVerificationStrict() throws Exception {
+ String urlWithStrict = jdbcUrl + ";hostname_verification=STRICT";
+ try (Connection conn = DriverManager.getConnection(urlWithStrict);
+ Statement stmt = conn.createStatement()) {
+ stmt.execute("SELECT * FROM EMP");
+ fail("Exception should have been thrown for the url: " + urlWithStrict);
+ } catch (Exception e) {
+ assertTrue("Expected an SSLException as cause of: " + e,
+ e.getCause() instanceof SSLException);
+ }
+ }
+}