Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyFactory> keyFactories)
throws SSLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,4 +96,25 @@ void setSslContext_AlreadyExists_Throws() throws Exception {
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("sslContext or trustManager has been previously configured");
}

@Test
Comment thread
Debashismitra01 marked this conversation as resolved.
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -79,6 +87,44 @@ void generatePrivateKey_Invalid() {
.hasMessage("Unable to generate key from supported algorithms: [EC]");
}

@Test
void defaultTrustManager() {
assertThatCode(TlsUtil::defaultTrustManager).doesNotThrowAnyException();
}

@Test
Comment thread
psx95 marked this conversation as resolved.
void defaultTrustManager_returnsX509TrustManager() throws Exception {
X509TrustManager trustManager = mock(X509TrustManager.class);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the purpose of this test to verify that the defaultTrustManager() gives you an instance of X509TrustManager ?

if so, then I don't think mocking for this test is necessary. Would the following assertion cover what you're trying to test here?

@Test
  void defaultTrustManager_returnsX509TrustManager() throws Exception {
    assertThat(TlsUtil.defaultTrustManager()).isInstanceOf(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");
}
Comment on lines +122 to +126

@psx95 psx95 Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: This test could benefit from mocking behavior and the method you added

  @Test
  @SuppressWarnings("CannotMockMethod")
  void defaultTrustManager_NoX509TrustManagerFound() {
    TrustManagerFactory  trustManagerFactory = mock(TrustManagerFactory.class);
    when(trustManagerFactory.getTrustManagers()).thenReturn(new TrustManager[0]);
    when(trustManagerFactory.getAlgorithm()).thenReturn("PKIX");

    assertThatCode(() -> TlsUtil.defaultTrustManager(trustManagerFactory))
        .isInstanceOf(SSLException.class)
        .hasMessage("No X509TrustManager found");
  }

This would allow you to remove private static TrustManagerFactory trustManagerFactory method and avoid mocking any static methods.

But I notice that the repository has not used this pattern before. So I'll let other reviewers weigh-in here.


/**
* Append <a href="https://datatracker.ietf.org/doc/html/rfc7468#section-5.2">explanatory text</a>
* prefix and verify {@link TlsUtil#keyManager(byte[], byte[])} succeeds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
}
Loading