Skip to content

Commit

Permalink
Add TlsConfigHelper for additional TLS configurability (#5246)
Browse files Browse the repository at this point in the history
* add TlsConfigHelper for additional TLS configurability and wire up internal builders.

* add javadoc and spotless

* add keys for "validity" testing.

* fix tests

* fix tests

* address code review comments

* fix typo

* allow keymanager to be nullable.

* fix test

* so....much...null......away

* backfill tests

* checkstyle

* test coverage

* address code review comments
  • Loading branch information
breedx-splk committed Mar 7, 2023
1 parent e871805 commit 895075f
Show file tree
Hide file tree
Showing 19 changed files with 524 additions and 156 deletions.
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.
*/
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) {
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) {
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

0 comments on commit 895075f

Please sign in to comment.