diff --git a/CHANGELOG.md b/CHANGELOG.md index c399e74acfa..df07f050d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### SDK + +#### Exporters + +* Fix OkHttp client mTLS when using the platform default trust store ([#8565](https://github.com/open-telemetry/opentelemetry-java/pull/8565)) + ## Version 1.63.0 (2026-06-05) ### API diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsConfigHelper.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsConfigHelper.java index 5c2f89e9372..f66abdf250e 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsConfigHelper.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsConfigHelper.java @@ -111,6 +111,13 @@ public X509TrustManager getTrustManager() { return trustManager; } + private X509TrustManager getEffectiveTrustManager() throws SSLException { + if (trustManager != null) { + return trustManager; + } + return TlsUtil.defaultTrustManager(); + } + /** Get the {@link SSLContext}. */ @Nullable public SSLContext getSslContext() { @@ -122,10 +129,10 @@ public SSLContext getSslContext() { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init( keyManager == null ? null : new KeyManager[] {keyManager}, - trustManager == null ? null : new TrustManager[] {trustManager}, + new TrustManager[] {getEffectiveTrustManager()}, null); return sslContext; - } catch (NoSuchAlgorithmException | KeyManagementException e) { + } catch (NoSuchAlgorithmException | KeyManagementException | SSLException e) { throw new IllegalArgumentException(e); } } diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsUtil.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsUtil.java index 500bc9978c1..587e7fe9629 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsUtil.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/TlsUtil.java @@ -96,6 +96,32 @@ public static X509KeyManager keyManager(byte[] privateKeyPem, byte[] certificate } } + /** Returns the platform default {@link X509TrustManager}. */ + public static X509TrustManager defaultTrustManager() throws SSLException { + try { + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + + // Initialize with the platform default trust store. + tmf.init((KeyStore) null); + + return defaultTrustManager(tmf); + } catch (KeyStoreException | NoSuchAlgorithmException e) { + throw new SSLException("Could not build default TrustManager.", e); + } + } + + // Visible for testing + static X509TrustManager defaultTrustManager(TrustManagerFactory tmf) throws SSLException { + for (TrustManager trustManager : tmf.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + + throw new SSLException("No X509TrustManager found"); + } + // Visible for testing static PrivateKey generatePrivateKey(PKCS8EncodedKeySpec keySpec, List keyFactories) throws SSLException { diff --git a/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsConfigHelperTest.java b/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsConfigHelperTest.java index 83736d5eb06..2f716097f6c 100644 --- a/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsConfigHelperTest.java +++ b/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsConfigHelperTest.java @@ -10,8 +10,10 @@ import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; import io.opentelemetry.internal.testing.slf4j.SuppressLogger; +import java.security.Security; import java.security.cert.CertificateEncodingException; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; import javax.net.ssl.X509TrustManager; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -94,4 +96,25 @@ void setSslContext_AlreadyExists_Throws() throws Exception { .isInstanceOf(IllegalStateException.class) .hasMessageContaining("sslContext or trustManager has been previously configured"); } + + @Test + void getSslContext_wrapsDefaultTrustManagerFailure() { + String originalAlgorithm = Security.getProperty("ssl.TrustManagerFactory.algorithm"); + + try { + Security.setProperty("ssl.TrustManagerFactory.algorithm", "invalid"); + + assertThatThrownBy(() -> helper.getSslContext()) + .isInstanceOf(IllegalArgumentException.class) + .hasCauseInstanceOf(SSLException.class); + + } finally { + Security.setProperty("ssl.TrustManagerFactory.algorithm", originalAlgorithm); + } + } + + @Test + void getSslContext_usesDefaultTrustManagerWhenUnset() { + assertThat(helper.getSslContext()).isNotNull(); + } } diff --git a/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsUtilTest.java b/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsUtilTest.java index 1d25cc6cf9d..c4ae0f0b3f3 100644 --- a/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsUtilTest.java +++ b/exporters/common/src/test/java/io/opentelemetry/exporter/internal/TlsUtilTest.java @@ -5,7 +5,9 @@ package io.opentelemetry.exporter.internal; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; import com.linecorp.armeria.internal.common.util.SelfSignedCertificate; import java.io.File; @@ -15,13 +17,19 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.security.KeyFactory; +import java.security.KeyStore; import java.security.cert.CertificateException; import java.security.spec.PKCS8EncodedKeySpec; import java.time.Instant; import java.util.Collections; import java.util.Date; import java.util.stream.Stream; +import javax.net.ssl.ManagerFactoryParameters; import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.TrustManagerFactorySpi; +import javax.net.ssl.X509TrustManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -79,6 +87,44 @@ void generatePrivateKey_Invalid() { .hasMessage("Unable to generate key from supported algorithms: [EC]"); } + @Test + void defaultTrustManager() { + assertThatCode(TlsUtil::defaultTrustManager).doesNotThrowAnyException(); + } + + @Test + void defaultTrustManager_returnsX509TrustManager() throws Exception { + X509TrustManager trustManager = mock(X509TrustManager.class); + + assertThat(TlsUtil.defaultTrustManager(trustManagerFactory(new TrustManager[] {trustManager}))) + .isSameAs(trustManager); + } + + private static TrustManagerFactory trustManagerFactory(TrustManager[] trustManagers) { + return new TrustManagerFactory( + new TrustManagerFactorySpi() { + @Override + protected void engineInit(KeyStore keyStore) {} + + @Override + protected void engineInit(ManagerFactoryParameters spec) {} + + @Override + protected TrustManager[] engineGetTrustManagers() { + return trustManagers; + } + }, + null, + "test") {}; + } + + @Test + void defaultTrustManager_NoX509TrustManagerFound() { + assertThatCode(() -> TlsUtil.defaultTrustManager(trustManagerFactory(new TrustManager[0]))) + .isInstanceOf(SSLException.class) + .hasMessage("No X509TrustManager found"); + } + /** * Append explanatory text * prefix and verify {@link TlsUtil#keyManager(byte[], byte[])} succeeds. diff --git a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java index 19026e7691f..5a60eb58a73 100644 --- a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java +++ b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java @@ -7,6 +7,7 @@ import io.opentelemetry.api.impl.InstrumentationUtil; import io.opentelemetry.exporter.internal.RetryUtil; +import io.opentelemetry.exporter.internal.TlsUtil; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.export.Compressor; import io.opentelemetry.sdk.common.export.HttpResponse; @@ -28,6 +29,7 @@ import java.util.logging.Logger; import javax.annotation.Nullable; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; import javax.net.ssl.X509TrustManager; import okhttp3.Call; import okhttp3.Callback; @@ -108,8 +110,17 @@ public OkHttpHttpSender( boolean isPlainHttp = endpoint.getScheme().equals("http"); if (isPlainHttp) { builder.connectionSpecs(Collections.singletonList(ConnectionSpec.CLEARTEXT)); - } else if (sslContext != null && trustManager != null) { - builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager); + } else if (sslContext != null) { + X509TrustManager effectiveTrustManager = trustManager; + + if (effectiveTrustManager == null) { + try { + effectiveTrustManager = TlsUtil.defaultTrustManager(); + } catch (SSLException e) { + throw new IllegalStateException("Unable to initialize default trust manager", e); + } + } + builder.sslSocketFactory(sslContext.getSocketFactory(), effectiveTrustManager); } this.client = builder.build(); diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java index a74695df9af..deff0a42715 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java @@ -6,17 +6,22 @@ package io.opentelemetry.exporter.sender.okhttp.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import io.opentelemetry.sdk.common.export.HttpResponse; import io.opentelemetry.sdk.common.export.MessageWriter; import java.io.OutputStream; import java.net.URI; +import java.security.Security; import java.time.Duration; import java.util.Collections; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; import org.junit.jupiter.api.Test; class OkHttpHttpSenderTest { @@ -60,4 +65,60 @@ public int getContentLength() { return 0; } } + + @Test + void constructor_usesDefaultTrustManagerWhenTrustManagerIsNull() throws Exception { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + + assertDoesNotThrow( + () -> + new OkHttpHttpSender( + URI.create("https://localhost"), + "text/plain", + null, + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Collections::emptyMap, + null, + null, + sslContext, + null, + null, + Long.MAX_VALUE)); + } + + @Test + void constructor_wrapsDefaultTrustManagerFailure() throws Exception { + String originalAlgorithm = Security.getProperty("ssl.TrustManagerFactory.algorithm"); + + try { + Security.setProperty("ssl.TrustManagerFactory.algorithm", "invalid"); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + + assertThatThrownBy( + () -> + new OkHttpHttpSender( + URI.create("https://localhost"), + "text/plain", + null, + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Collections::emptyMap, + null, + null, + sslContext, + null, + null, + Long.MAX_VALUE)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to initialize default trust manager") + .hasCauseInstanceOf(SSLException.class); + + } finally { + Security.setProperty("ssl.TrustManagerFactory.algorithm", originalAlgorithm); + } + } }