Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TlsConfigHelper for additional TLS configurability #5246

Merged
merged 14 commits into from
Mar 7, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.internal;

import com.google.common.annotations.VisibleForTesting;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;

/**
* Utility class to help with management of TLS related components. This class is ultimately
* responsible for enabling TLS via callbacks passed to the configure[...]() methods. This class is
* only intended for internal OpenTelemetry exporter usage and should not be used by end-users.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
public class TlsConfigHelper {

private static final Logger logger = Logger.getLogger(TlsConfigHelper.class.getName());

private final TlsUtility tlsUtil;

@Nullable private X509KeyManager keyManager;
@Nullable private X509TrustManager trustManager;
@Nullable private SSLSocketFactory sslSocketFactory;

public TlsConfigHelper() {
this(new TlsUtility() {});
}

@VisibleForTesting
TlsConfigHelper(TlsUtility tlsUtil) {
this.tlsUtil = tlsUtil;
}

/** Sets the X509TrustManager. */
public TlsConfigHelper setTrustManager(X509TrustManager trustManager) {
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
this.trustManager = trustManager;
return this;
}

/**
* Creates a new X509TrustManager from the given cert content.
*
* @param trustedCertsPem Certificate in PEM format.
* @return this
*/
public TlsConfigHelper createTrustManager(byte[] trustedCertsPem) {
try {
this.trustManager = tlsUtil.trustManager(trustedCertsPem);
} catch (SSLException e) {
throw new IllegalStateException(
"Error creating X509TrustManager with provided certs. Are they valid X.509 in PEM format?",
e);
}
return this;
}

/**
* Creates a new X509KeyManager from the given private key and certificate, both in PEM format.
*
* @param privateKeyPem Private key content in PEM format.
* @param certificatePem Certificate content in PEM format.
* @return this
*/
public TlsConfigHelper createKeyManager(byte[] privateKeyPem, byte[] certificatePem) {
try {
if (keyManager != null) {
logger.warning(
"Previous X509 Key manager is being replaced. This is probably an error and should only be set once.");
}
keyManager = tlsUtil.keyManager(privateKeyPem, certificatePem);
return this;
} catch (SSLException e) {
throw new IllegalStateException(
"Error creating X509KeyManager with provided certs. Are they valid X.509 in PEM format?",
e);
}
}

/**
* Assigns the X509KeyManager.
*
* @return this
*/
public TlsConfigHelper setKeyManager(X509KeyManager keyManager) {
if (this.keyManager != null) {
logger.warning(
"Previous X509 Key manager is being replaced. This is probably an error and should only be set once.");
}
this.keyManager = keyManager;
return this;
}

/**
* Sets the SSLSocketFactory, which is passed into the callback within
* configureWithSocketFactory().
*/
public TlsConfigHelper setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
this.sslSocketFactory = sslSocketFactory;
return this;
}

/**
* Functional wrapper type used in configure methods. Exists primarily to declare checked
* SSLException.
*/
public interface SslSocketFactoryConfigurer {
void configure(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager)
throws SSLException;
}

/**
* Functional wrapper type used in configure methods. Exists primarily to declare checked
* SSLException.
*/
public interface KeyManagerConfigurer {
void configure(X509TrustManager trustManager, @Nullable X509KeyManager keyManager)
throws SSLException;
}

/**
* Configures TLS by invoking the given callback with the X509TrustManager and X509KeyManager. If
* the trust manager or key manager have not yet been configured, this method does nothing.
*/
public void configureWithKeyManager(KeyManagerConfigurer configurer) {
if (trustManager == null) {
return;
}
try {
configurer.configure(trustManager, keyManager);
} catch (SSLException e) {
wrapException(e);
}
}

/**
* Configures TLS by invoking the provided consumer with a new SSLSocketFactory and the
* preconfigured X509TrustManager. If the trust manager has not been configured, this method does
* nothing.
*/
public void configureWithSocketFactory(SslSocketFactoryConfigurer configurer) {
if (trustManager == null) {
warnIfOtherComponentsConfigured();
return;
}

try {
SSLSocketFactory sslSocketFactory = this.sslSocketFactory;
if (sslSocketFactory == null) {
sslSocketFactory = tlsUtil.sslSocketFactory(keyManager, trustManager);
}
configurer.configure(sslSocketFactory, trustManager);
} catch (SSLException e) {
wrapException(e);
}
}

private static void wrapException(SSLException e) {
throw new IllegalStateException(
"Could not configure TLS connection, are certs in valid X.509 in PEM format?", e);
}

private void warnIfOtherComponentsConfigured() {
if (sslSocketFactory != null) {
logger.warning("sslSocketFactory has been configured without an X509TrustManager.");
return;
}
if (keyManager != null) {
logger.warning("An X509KeyManager has been configured without an X509TrustManager.");
}
}

// Exists for testing
interface TlsUtility {
default SSLSocketFactory sslSocketFactory(
@Nullable X509KeyManager keyManager, X509TrustManager trustManager) throws SSLException {
return TlsUtil.sslSocketFactory(keyManager, trustManager);
}

default X509TrustManager trustManager(byte[] trustedCertificatesPem) throws SSLException {
return TlsUtil.trustManager(trustedCertificatesPem);
}

default X509KeyManager keyManager(byte[] privateKeyPem, byte[] certificatePem)
throws SSLException {
return TlsUtil.keyManager(privateKeyPem, certificatePem);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public static SSLSocketFactory sslSocketFactory(
}

/**
* Creates {@link KeyManager} initiaded by keystore containing single private key with matching
* Creates {@link KeyManager} initiated by keystore containing single private key with matching
* certificate chain.
*/
public static X509KeyManager keyManager(byte[] privateKeyPem, byte[] certificatePem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.exporter.internal.ExporterBuilderUtil;
import io.opentelemetry.exporter.internal.TlsUtil;
import io.opentelemetry.exporter.internal.TlsConfigHelper;
import io.opentelemetry.exporter.internal.marshal.Marshaler;
import io.opentelemetry.exporter.internal.okhttp.OkHttpUtil;
import io.opentelemetry.exporter.internal.retry.RetryInterceptor;
Expand All @@ -29,9 +29,6 @@
import java.util.function.BiFunction;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
Expand All @@ -55,9 +52,7 @@ public class GrpcExporterBuilder<T extends Marshaler> {
private URI endpoint;
private boolean compressionEnabled = false;
private final Map<String, String> headers = new HashMap<>();
@Nullable private byte[] trustedCertificatesPem;
@Nullable private byte[] privateKeyPem;
@Nullable private byte[] certificatePem;
private final TlsConfigHelper tlsConfigHelper = new TlsConfigHelper();
@Nullable private RetryPolicy retryPolicy;
private Supplier<MeterProvider> meterProviderSupplier = GlobalOpenTelemetry::getMeterProvider;

Expand Down Expand Up @@ -103,14 +98,13 @@ public GrpcExporterBuilder<T> setCompression(String compressionMethod) {
return this;
}

public GrpcExporterBuilder<T> setTrustedCertificates(byte[] trustedCertificatesPem) {
this.trustedCertificatesPem = trustedCertificatesPem;
public GrpcExporterBuilder<T> configureTrustManager(byte[] trustedCertificatesPem) {
tlsConfigHelper.createTrustManager(trustedCertificatesPem);
return this;
}

public GrpcExporterBuilder<T> setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
this.privateKeyPem = privateKeyPem;
this.certificatePem = certificatePem;
public GrpcExporterBuilder<T> configureKeyManager(byte[] privateKeyPem, byte[] certificatePem) {
tlsConfigHelper.createKeyManager(privateKeyPem, certificatePem);
return this;
}

Expand Down Expand Up @@ -139,20 +133,7 @@ public GrpcExporter<T> build() {

clientBuilder.callTimeout(Duration.ofNanos(timeoutNanos));

if (trustedCertificatesPem != null) {
try {
X509TrustManager trustManager = TlsUtil.trustManager(trustedCertificatesPem);
X509KeyManager keyManager = null;
if (privateKeyPem != null && certificatePem != null) {
keyManager = TlsUtil.keyManager(privateKeyPem, certificatePem);
}
clientBuilder.sslSocketFactory(
TlsUtil.sslSocketFactory(keyManager, trustManager), trustManager);
} catch (SSLException e) {
throw new IllegalStateException(
"Could not set trusted certificates, are they valid X.509 in PEM format?", e);
}
}
tlsConfigHelper.configureWithSocketFactory(clientBuilder::sslSocketFactory);

String endpoint = this.endpoint.resolve(grpcEndpointPath).toString();
if (endpoint.startsWith("http://")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.grpc.ManagedChannelBuilder;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.handler.ssl.SslContext;
import io.opentelemetry.exporter.internal.TlsUtil;
import io.opentelemetry.exporter.internal.retry.RetryPolicy;
import io.opentelemetry.exporter.internal.retry.RetryUtil;
Expand Down Expand Up @@ -46,47 +47,45 @@ public final class ManagedChannelUtil {
*/
public static void setClientKeysAndTrustedCertificatesPem(
ManagedChannelBuilder<?> managedChannelBuilder,
@Nullable byte[] privateKeyPem,
@Nullable byte[] certificatePem,
byte[] trustedCertificatesPem)
X509TrustManager tmf,
@Nullable X509KeyManager kmf)
throws SSLException {
requireNonNull(managedChannelBuilder, "managedChannelBuilder");
requireNonNull(trustedCertificatesPem, "trustedCertificatesPem");

X509TrustManager tmf = TlsUtil.trustManager(trustedCertificatesPem);
X509KeyManager kmf = null;
if (privateKeyPem != null && certificatePem != null) {
kmf = TlsUtil.keyManager(privateKeyPem, certificatePem);
}
requireNonNull(tmf, "X509TrustManager");

// gRPC does not abstract TLS configuration so we need to check the implementation and act
// accordingly.
if (managedChannelBuilder.getClass().getName().equals("io.grpc.netty.NettyChannelBuilder")) {
NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) managedChannelBuilder;
nettyBuilder.sslContext(
GrpcSslContexts.forClient().keyManager(kmf).trustManager(tmf).build());
} else if (managedChannelBuilder
.getClass()
.getName()
.equals("io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder")) {
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder nettyBuilder =
(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder) managedChannelBuilder;
nettyBuilder.sslContext(
io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts.forClient()
.trustManager(tmf)
.keyManager(kmf)
.build());
} else if (managedChannelBuilder
.getClass()
.getName()
.equals("io.grpc.okhttp.OkHttpChannelBuilder")) {
io.grpc.okhttp.OkHttpChannelBuilder okHttpBuilder =
(io.grpc.okhttp.OkHttpChannelBuilder) managedChannelBuilder;
okHttpBuilder.sslSocketFactory(TlsUtil.sslSocketFactory(kmf, tmf));
} else {
throw new SSLException(
"TLS certificate configuration not supported for unrecognized ManagedChannelBuilder "
+ managedChannelBuilder.getClass().getName());
String channelBuilderClassName = managedChannelBuilder.getClass().getName();
switch (channelBuilderClassName) {
case "io.grpc.netty.NettyChannelBuilder":
{
NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) managedChannelBuilder;
SslContext sslContext =
GrpcSslContexts.forClient().keyManager(kmf).trustManager(tmf).build();
nettyBuilder.sslContext(sslContext);
break;
}
case "io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder":
{
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder nettyBuilder =
(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder) managedChannelBuilder;
io.grpc.netty.shaded.io.netty.handler.ssl.SslContext sslContext =
io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts.forClient()
.trustManager(tmf)
.keyManager(kmf)
.build();
nettyBuilder.sslContext(sslContext);
break;
}
case "io.grpc.okhttp.OkHttpChannelBuilder":
io.grpc.okhttp.OkHttpChannelBuilder okHttpBuilder =
(io.grpc.okhttp.OkHttpChannelBuilder) managedChannelBuilder;
okHttpBuilder.sslSocketFactory(TlsUtil.sslSocketFactory(kmf, tmf));
break;
default:
throw new SSLException(
"TLS certificate configuration not supported for unrecognized ManagedChannelBuilder "
+ channelBuilderClassName);
}
}

Expand Down
Loading