You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by st...@apache.org on 2026/04/20 15:21:46 UTC
(iceberg) branch main updated: Core: Expose HostnameVerificationPolicy in TLSConfigurer (#15500)
This is an automated email from the ASF dual-hosted git repository.
stevenzwu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new a8b7ba6d5f Core: Expose HostnameVerificationPolicy in TLSConfigurer (#15500)
a8b7ba6d5f is described below
commit a8b7ba6d5ffb47f3e6d4ba937e191c7648c0fe0f
Author: Alexandre Dutra <ad...@apache.org>
AuthorDate: Mon Apr 20 17:21:38 2026 +0200
Core: Expose HostnameVerificationPolicy in TLSConfigurer (#15500)
* Expose HostnameVerificationPolicy in TLSConfigurer
Apache HttpClient 5.4 introduced a new component: `HostnameVerificationPolicy`, which determines whether hostname verification is done by the JSSE provider (at socket level, during TLS handshake), the HttpClient (after TLS handshake), or both.
This change exposes `HostnameVerificationPolicy` in `TLSConfigurer`. This component is particularly useful when attempting to bypass hostname verification, e.g. by using the `NoopHostnameVerifier`. The default policy is set to `BOTH`, which produces the same result as before.
* set default to CLIENT
* declare all BC artifacts
* add test
* add comment
* don't expose HostnameVerificationPolicy
* Address review feedback: split try blocks
---
build.gradle | 7 ++
.../java/org/apache/iceberg/rest/HTTPClient.java | 10 +-
.../apache/iceberg/rest/auth/TLSConfigurer.java | 12 ++-
.../org/apache/iceberg/rest/TestHTTPClient.java | 109 +++++++++++++++++++++
gradle/libs.versions.toml | 4 +
5 files changed, 139 insertions(+), 3 deletions(-)
diff --git a/build.gradle b/build.gradle
index 0715c3f6cb..261dfabf04 100644
--- a/build.gradle
+++ b/build.gradle
@@ -410,6 +410,13 @@ project(':iceberg-core') {
exclude group: 'junit'
}
testImplementation libs.awaitility
+
+ // Lock BouncyCastle versions to avoid version mismatches
+ // when these dependencies are added transitively.
+ // Required for TLS tests with MockServer.
+ testImplementation libs.bouncycastle.bcpkix
+ testImplementation libs.bouncycastle.bcutil
+ testImplementation libs.bouncycastle.bcprov
}
}
diff --git a/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java b/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
index 86eceba21c..c359404ec6 100644
--- a/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
+++ b/core/src/main/java/org/apache/iceberg/rest/HTTPClient.java
@@ -29,6 +29,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+import javax.net.ssl.HostnameVerifier;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
@@ -43,6 +44,7 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuil
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
@@ -410,13 +412,19 @@ public class HTTPClient extends BaseHTTPClient {
TLSConfigurer tlsConfigurer = loadTlsConfigurer(properties);
if (tlsConfigurer != null) {
+ HostnameVerifier customVerifier = tlsConfigurer.hostnameVerifier();
+ HostnameVerificationPolicy verificationPolicy =
+ customVerifier != null
+ ? HostnameVerificationPolicy.CLIENT
+ : HostnameVerificationPolicy.BUILTIN;
connectionManagerBuilder.setTlsSocketStrategy(
new DefaultClientTlsStrategy(
tlsConfigurer.sslContext(),
tlsConfigurer.supportedProtocols(),
tlsConfigurer.supportedCipherSuites(),
SSLBufferMode.STATIC,
- tlsConfigurer.hostnameVerifier()));
+ verificationPolicy,
+ customVerifier));
}
return connectionManagerBuilder.build();
diff --git a/core/src/main/java/org/apache/iceberg/rest/auth/TLSConfigurer.java b/core/src/main/java/org/apache/iceberg/rest/auth/TLSConfigurer.java
index da22b5cb4c..4b3ed3a74c 100644
--- a/core/src/main/java/org/apache/iceberg/rest/auth/TLSConfigurer.java
+++ b/core/src/main/java/org/apache/iceberg/rest/auth/TLSConfigurer.java
@@ -19,9 +19,9 @@
package org.apache.iceberg.rest.auth;
import java.util.Map;
+import javax.annotation.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
-import org.apache.hc.client5.http.ssl.HttpsSupport;
import org.apache.hc.core5.ssl.SSLContexts;
public interface TLSConfigurer {
@@ -32,8 +32,16 @@ public interface TLSConfigurer {
return SSLContexts.createDefault();
}
+ /**
+ * Returns a custom {@link HostnameVerifier} to use for hostname verification, or {@code null} to
+ * use the default JSSE built-in hostname verifier.
+ *
+ * <p>If a non-null verifier is returned, only the custom verifier is executed and the JSSE
+ * built-in hostname verifier won't be executed.
+ */
+ @Nullable
default HostnameVerifier hostnameVerifier() {
- return HttpsSupport.getDefaultHostnameVerifier();
+ return null;
}
default String[] supportedProtocols() {
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java
index 8cf97bca32..701ae699f1 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java
@@ -35,22 +35,30 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.SocketTimeoutException;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.cert.CertificateException;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.iceberg.IcebergBuild;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Sets;
import org.apache.iceberg.rest.auth.AuthSession;
import org.apache.iceberg.rest.auth.TLSConfigurer;
import org.apache.iceberg.rest.responses.ErrorResponse;
@@ -58,14 +66,17 @@ import org.apache.iceberg.rest.responses.ErrorResponseParser;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockserver.configuration.Configuration;
import org.mockserver.integration.ClientAndServer;
+import org.mockserver.logging.MockServerLogger;
import org.mockserver.matchers.Times;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
+import org.mockserver.socket.tls.KeyStoreFactory;
import org.mockserver.verify.VerificationTimes;
/**
@@ -87,6 +98,7 @@ public class TestHTTPClient {
private static RESTClient restClient;
public static class DefaultTLSConfigurer implements TLSConfigurer {
+
public static int count = 0;
public DefaultTLSConfigurer() {
@@ -95,6 +107,7 @@ public class TestHTTPClient {
}
public static class TLSConfigurerMissingNoArgCtor implements TLSConfigurer {
+
TLSConfigurerMissingNoArgCtor(String str) {}
}
@@ -395,6 +408,101 @@ public class TestHTTPClient {
.hasMessageContaining("does not implement TLSConfigurer");
}
+ /** A TLSConfigurer that relies on the default (built-in) JSSE verifier. */
+ public static class BuiltInHostnameVerifierTLSConfigurer implements TLSConfigurer {
+
+ @Override
+ public SSLContext sslContext() {
+ return mockServerSSLContext();
+ }
+ }
+
+ /** A TLSConfigurer that overrides hostnameVerifier() to return a custom verifier. */
+ public static class CustomHostnameVerifierTLSConfigurer implements TLSConfigurer {
+
+ @Override
+ public SSLContext sslContext() {
+ return mockServerSSLContext();
+ }
+
+ @Override
+ public HostnameVerifier hostnameVerifier() {
+ return NoopHostnameVerifier.INSTANCE;
+ }
+ }
+
+ private static SSLContext mockServerSSLContext() {
+ try {
+ KeyStore keyStore =
+ new KeyStoreFactory(Configuration.configuration(), new MockServerLogger())
+ .loadOrCreateKeyStore();
+ TrustManagerFactory tmf =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(keyStore);
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(null, tmf.getTrustManagers(), null);
+ return sslContext;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create SSLContext", e);
+ }
+ }
+
+ @Test
+ public void testTLSConfigurerHostnameVerifier(@TempDir Path temp) throws IOException {
+
+ // Start a dedicated MockServer with a certificate that does NOT include
+ // 127.0.0.1 or localhost in its SANs.
+ Configuration tlsConfig = Configuration.configuration();
+ tlsConfig.proactivelyInitialiseTLS(true);
+ tlsConfig.preventCertificateDynamicUpdate(true);
+ tlsConfig.sslCertificateDomainName("example.com");
+ tlsConfig.sslSubjectAlternativeNameIps(Sets.newHashSet("1.2.3.4"));
+ tlsConfig.sslSubjectAlternativeNameDomains(Sets.newHashSet("example.com"));
+ tlsConfig.directoryToSaveDynamicSSLCertificate(temp.toFile().getAbsolutePath());
+
+ int tlsPort = PORT + 1;
+ try (ClientAndServer server = startClientAndServer(tlsConfig, tlsPort)) {
+
+ String path = "tls/hostname-verifier/path";
+ HttpRequest mockRequest =
+ request()
+ .withPath("/" + path)
+ .withMethod(HttpMethod.HEAD.name().toUpperCase(Locale.ROOT));
+ HttpResponse mockResponse = response().withStatusCode(200).withBody("TLS response");
+ server.when(mockRequest).respond(mockResponse);
+
+ // With no custom hostnameVerifier (null), the BUILTIN policy is used automatically,
+ // so the JSSE built-in verifier rejects the connection because the SANs don't match
+ try (HTTPClient builtInVerifierClient =
+ HTTPClient.builder(
+ Map.of(
+ HTTPClient.REST_TLS_CONFIGURER,
+ BuiltInHostnameVerifierTLSConfigurer.class.getName()))
+ .uri(String.format("https://127.0.0.1:%d", tlsPort))
+ .withAuthSession(AuthSession.EMPTY)
+ .build()) {
+ assertThatThrownBy(() -> builtInVerifierClient.head(path, Map.of(), (unused) -> {}))
+ .rootCause()
+ .isInstanceOf(CertificateException.class)
+ .hasMessage("No subject alternative names matching IP address 127.0.0.1 found");
+ }
+
+ // With a custom hostnameVerifier (NoopHostnameVerifier), the CLIENT policy is used
+ // automatically, so hostname verification is bypassed and the request succeeds
+ try (HTTPClient customVerifierClient =
+ HTTPClient.builder(
+ Map.of(
+ HTTPClient.REST_TLS_CONFIGURER,
+ CustomHostnameVerifierTLSConfigurer.class.getName()))
+ .uri(String.format("https://127.0.0.1:%d", tlsPort))
+ .withAuthSession(AuthSession.EMPTY)
+ .build()) {
+ assertThatCode(() -> customVerifierClient.head(path, Map.of(), (unused) -> {}))
+ .doesNotThrowAnyException();
+ }
+ }
+ }
+
@Test
public void testSocketTimeout() throws IOException {
long socketTimeoutMs = 2000L;
@@ -613,6 +721,7 @@ public class TestHTTPClient {
}
public static class Item implements RESTRequest, RESTResponse {
+
private Long id;
private String data;
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 18bc02e3c0..87ceb9012b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -36,6 +36,7 @@ awaitility = "4.3.0"
awssdk-bom = "2.42.33"
azuresdk-bom = "1.3.6"
awssdk-s3accessgrants = "2.4.1"
+bouncycastle = "1.82"
bson-ver = "4.11.5"
caffeine = "2.9.3"
calcite = "1.41.0"
@@ -110,6 +111,9 @@ awssdk-bom = { module = "software.amazon.awssdk:bom", version.ref = "awssdk-bom"
awssdk-s3accessgrants = { module = "software.amazon.s3.accessgrants:aws-s3-accessgrants-java-plugin", version.ref = "awssdk-s3accessgrants" }
azuresdk-bom = { module = "com.azure:azure-sdk-bom", version.ref = "azuresdk-bom" }
bson = { module = "org.mongodb:bson", version.ref = "bson-ver"}
+bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" }
+bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
+bouncycastle-bcutil = { module = "org.bouncycastle:bcutil-jdk18on", version.ref = "bouncycastle" }
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" }
calcite-core = { module = "org.apache.calcite:calcite-core", version.ref = "calcite" }
calcite-druid = { module = "org.apache.calcite:calcite-druid", version.ref = "calcite" }