Allow client certificate (#4194)

* Allow client certificate

* Allow client certificate - changes signature and add parameters

* Allow client certificate - rename chain to certificate

* Allow client certificate - fix first set of comments

* Allow client certificate - comments

* Allow client certificate - compile fix

* Allow client certificate - tests

* Allow client certificate - rename privateKeyChainPem to certificatePem

* Allow client certificate - remove incorrect test

* Allow client certificate - removed unused function

* Allow client certificate - spotlessApply

* Allow client certificate - match expected and thrown error messages

* Allow client certificate - spotlessApply

* Allow client certificate - from spec project it was requested to remove logs entries

* Allow client certificate - match expected and thrown error messages

* Allow client certificate - improve test coverage

* Allow client certificate - document also LOG version of env variables
This commit is contained in:
jkamon 2022-03-21 09:19:17 +01:00 committed by GitHub
parent 345060e8a3
commit a355c347b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 525 additions and 39 deletions

View File

@ -96,6 +96,12 @@ public final class JaegerGrpcSpanExporterBuilder {
return this;
}
/** Sets the client key and chain to use for verifying servers when mTLS is enabled. */
public JaegerGrpcSpanExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
delegate.setClientTls(privateKeyPem, certificatePem);
return this;
}
/**
* Constructs a new instance of the exporter based on the builder's values.
*

View File

@ -301,8 +301,7 @@ class JaegerGrpcSpanExporterTest {
}
@Test
@SuppressWarnings("PreferJavaTimeOverload")
void validConfig() {
void validTrustedConfig() {
assertThatCode(
() ->
JaegerGrpcSpanExporter.builder()
@ -310,6 +309,17 @@ class JaegerGrpcSpanExporterTest {
.doesNotThrowAnyException();
}
@Test
void validClientKeyConfig() {
assertThatCode(
() ->
JaegerGrpcSpanExporter.builder()
.setClientTls(
"foobar".getBytes(StandardCharsets.UTF_8),
"foobar".getBytes(StandardCharsets.UTF_8)))
.doesNotThrowAnyException();
}
@Test
@SuppressWarnings("PreferJavaTimeOverload")
void invalidConfig() {

View File

@ -84,6 +84,14 @@ public final class OtlpHttpLogExporterBuilder {
return this;
}
/**
* Sets ths client key and the certificate chain to use for verifying client when TLS is enabled.
*/
public OtlpHttpLogExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
delegate.setClientTls(privateKeyPem, certificatePem);
return this;
}
/**
* Sets the {@link MeterProvider} to use to collect metrics related to export. If not set, metrics
* will not be collected.

View File

@ -143,6 +143,14 @@ class OtlpHttpLogExporterTest {
OtlpHttpLogExporter.builder()
.setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8)))
.doesNotThrowAnyException();
assertThatCode(
() ->
OtlpHttpLogExporter.builder()
.setClientTls(
"foobar".getBytes(StandardCharsets.UTF_8),
"foobar".getBytes(StandardCharsets.UTF_8)))
.doesNotThrowAnyException();
}
@Test

View File

@ -88,6 +88,14 @@ public final class OtlpHttpMetricExporterBuilder {
return this;
}
/**
* Sets ths client key and the certificate chain to use for verifying client when TLS is enabled.
*/
public OtlpHttpMetricExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
delegate.setClientTls(privateKeyPem, certificatePem);
return this;
}
/**
* Set the preferred aggregation temporality. If unset, defaults to {@link
* AggregationTemporality#CUMULATIVE}.

View File

@ -137,6 +137,14 @@ class OtlpHttpMetricExporterTest {
.setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8)))
.doesNotThrowAnyException();
assertThatCode(
() ->
OtlpHttpMetricExporter.builder()
.setClientTls(
"foobar".getBytes(StandardCharsets.UTF_8),
"foobar".getBytes(StandardCharsets.UTF_8)))
.doesNotThrowAnyException();
assertThatCode(
() ->
OtlpHttpMetricExporter.builder()

View File

@ -84,6 +84,14 @@ public final class OtlpHttpSpanExporterBuilder {
return this;
}
/**
* Sets ths client key and the certificate chain to use for verifying client when TLS is enabled.
*/
public OtlpHttpSpanExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
delegate.setClientTls(privateKeyPem, certificatePem);
return this;
}
/**
* Sets the {@link MeterProvider} to use to collect metrics related to export. If not set, metrics
* will not be collected.

View File

@ -136,6 +136,14 @@ class OtlpHttpSpanExporterTest {
OtlpHttpSpanExporter.builder()
.setTrustedCertificates("foobar".getBytes(StandardCharsets.UTF_8)))
.doesNotThrowAnyException();
assertThatCode(
() ->
OtlpHttpSpanExporter.builder()
.setClientTls(
"foobar".getBytes(StandardCharsets.UTF_8),
"foobar".getBytes(StandardCharsets.UTF_8)))
.doesNotThrowAnyException();
}
@Test

View File

@ -9,18 +9,30 @@ import static java.util.Objects.requireNonNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
/**
@ -31,13 +43,20 @@ import javax.net.ssl.X509TrustManager;
*/
public final class TlsUtil {
/** Returns a {@link SSLSocketFactory} configured to use the given trust manager. */
public static SSLSocketFactory sslSocketFactory(TrustManager trustManager) throws SSLException {
private TlsUtil() {}
/** Returns a {@link SSLSocketFactory} configured to use the given key and trust manager. */
public static SSLSocketFactory sslSocketFactory(
@Nullable KeyManager keyManager, TrustManager trustManager) throws SSLException {
SSLContext sslContext;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {trustManager}, null);
if (keyManager == null) {
sslContext.init(null, new TrustManager[] {trustManager}, null);
} else {
sslContext.init(new KeyManager[] {keyManager}, new TrustManager[] {trustManager}, null);
}
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new SSLException(
"Could not set trusted certificates for TLS connection, are they valid "
@ -47,6 +66,46 @@ public final class TlsUtil {
return sslContext.getSocketFactory();
}
/**
* Creates {@link KeyManager} initiaded by keystore containing single private key with matching
* certificate chain.
*/
public static X509KeyManager keyManager(byte[] privateKeyPem, byte[] certificatePem)
throws SSLException {
requireNonNull(privateKeyPem, "privateKeyPem");
requireNonNull(certificatePem, "certificatePem");
try {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null);
KeyFactory factory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyPem);
PrivateKey key = factory.generatePrivate(keySpec);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
List<Certificate> chain = new ArrayList<>();
ByteArrayInputStream is = new ByteArrayInputStream(certificatePem);
while (is.available() > 0) {
chain.add(cf.generateCertificate(is));
}
ks.setKeyEntry("trusted", key, "".toCharArray(), chain.toArray(new Certificate[] {}));
KeyManagerFactory kmf =
KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "".toCharArray());
return (X509KeyManager) kmf.getKeyManagers()[0];
} catch (CertificateException
| KeyStoreException
| IOException
| NoSuchAlgorithmException
| UnrecoverableKeyException
| InvalidKeySpecException e) {
throw new SSLException("Could not build KeyManagerFactory from clientKeysPem.", e);
}
}
/** Returns a {@link TrustManager} for the given trusted certificates. */
public static X509TrustManager trustManager(byte[] trustedCertificatesPem) throws SSLException {
requireNonNull(trustedCertificatesPem, "trustedCertificatesPem");
@ -71,6 +130,4 @@ public final class TlsUtil {
throw new SSLException("Could not build TrustManagerFactory from trustedCertificatesPem.", e);
}
}
private TlsUtil() {}
}

View File

@ -43,6 +43,8 @@ public final class DefaultGrpcExporterBuilder<T extends Marshaler>
private boolean compressionEnabled = false;
@Nullable private Metadata metadata;
@Nullable private byte[] trustedCertificatesPem;
@Nullable private byte[] privateKeyPem;
@Nullable private byte[] certificatePem;
@Nullable RetryPolicy retryPolicy;
private MeterProvider meterProvider = MeterProvider.noop();
@ -96,6 +98,13 @@ public final class DefaultGrpcExporterBuilder<T extends Marshaler>
return this;
}
@Override
public GrpcExporterBuilder<T> setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
this.privateKeyPem = privateKeyPem;
this.certificatePem = certificatePem;
return this;
}
@Override
public DefaultGrpcExporterBuilder<T> addHeader(String key, String value) {
if (metadata == null) {
@ -136,8 +145,8 @@ public final class DefaultGrpcExporterBuilder<T extends Marshaler>
if (trustedCertificatesPem != null) {
try {
ManagedChannelUtil.setTrustedCertificatesPem(
managedChannelBuilder, trustedCertificatesPem);
ManagedChannelUtil.setClientKeysAndTrustedCertificatesPem(
managedChannelBuilder, privateKeyPem, certificatePem, trustedCertificatesPem);
} catch (SSLException e) {
throw new IllegalStateException(
"Could not set trusted certificates for gRPC TLS connection, are they valid "

View File

@ -31,6 +31,8 @@ public interface GrpcExporterBuilder<T extends Marshaler> {
GrpcExporterBuilder<T> setTrustedCertificates(byte[] trustedCertificatesPem);
GrpcExporterBuilder<T> setClientTls(byte[] privateKeyPem, byte[] certificatePem);
GrpcExporterBuilder<T> addHeader(String key, String value);
GrpcExporterBuilder<T> setRetryPolicy(RetryPolicy retryPolicy);

View File

@ -23,7 +23,9 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
/**
@ -42,19 +44,27 @@ public final class ManagedChannelUtil {
*
* @throws SSLException if error occur processing the certificates
*/
public static void setTrustedCertificatesPem(
ManagedChannelBuilder<?> managedChannelBuilder, byte[] trustedCertificatesPem)
public static void setClientKeysAndTrustedCertificatesPem(
ManagedChannelBuilder<?> managedChannelBuilder,
@Nullable byte[] privateKeyPem,
@Nullable byte[] certificatePem,
byte[] trustedCertificatesPem)
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);
}
// 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().trustManager(tmf).build());
nettyBuilder.sslContext(
GrpcSslContexts.forClient().keyManager(kmf).trustManager(tmf).build());
} else if (managedChannelBuilder
.getClass()
.getName()
@ -62,14 +72,17 @@ public final class ManagedChannelUtil {
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).build());
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(tmf));
okHttpBuilder.sslSocketFactory(TlsUtil.sslSocketFactory(kmf, tmf));
} else {
throw new SSLException(
"TLS certificate configuration not supported for unrecognized ManagedChannelBuilder "

View File

@ -20,6 +20,7 @@ import java.util.Collections;
import java.util.concurrent.TimeUnit;
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;
@ -42,6 +43,8 @@ public final class OkHttpGrpcExporterBuilder<T extends Marshaler>
private boolean compressionEnabled = false;
private final Headers.Builder headers = new Headers.Builder();
@Nullable private byte[] trustedCertificatesPem;
@Nullable private byte[] privateKeyPem;
@Nullable private byte[] certificatePem;
@Nullable private RetryPolicy retryPolicy;
private MeterProvider meterProvider = MeterProvider.noop();
@ -89,6 +92,13 @@ public final class OkHttpGrpcExporterBuilder<T extends Marshaler>
return this;
}
@Override
public GrpcExporterBuilder<T> setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
this.privateKeyPem = privateKeyPem;
this.certificatePem = certificatePem;
return this;
}
@Override
public OkHttpGrpcExporterBuilder<T> addHeader(String key, String value) {
headers.add(key, value);
@ -117,7 +127,12 @@ public final class OkHttpGrpcExporterBuilder<T extends Marshaler>
if (trustedCertificatesPem != null) {
try {
X509TrustManager trustManager = TlsUtil.trustManager(trustedCertificatesPem);
clientBuilder.sslSocketFactory(TlsUtil.sslSocketFactory(trustManager), trustManager);
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);

View File

@ -16,6 +16,7 @@ import java.time.Duration;
import java.util.concurrent.TimeUnit;
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;
@ -38,6 +39,8 @@ public final class OkHttpExporterBuilder<T extends Marshaler> {
private boolean compressionEnabled = false;
@Nullable private Headers.Builder headersBuilder;
@Nullable private byte[] trustedCertificatesPem;
@Nullable private byte[] privateKeyPem;
@Nullable private byte[] certificatePem;
@Nullable private RetryPolicy retryPolicy;
private MeterProvider meterProvider = MeterProvider.noop();
@ -82,6 +85,12 @@ public final class OkHttpExporterBuilder<T extends Marshaler> {
return this;
}
public OkHttpExporterBuilder<T> setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
this.privateKeyPem = privateKeyPem;
this.certificatePem = certificatePem;
return this;
}
public OkHttpExporterBuilder<T> setMeterProvider(MeterProvider meterProvider) {
this.meterProvider = meterProvider;
return this;
@ -101,7 +110,12 @@ public final class OkHttpExporterBuilder<T extends Marshaler> {
if (trustedCertificatesPem != null) {
try {
X509TrustManager trustManager = TlsUtil.trustManager(trustedCertificatesPem);
clientBuilder.sslSocketFactory(TlsUtil.sslSocketFactory(trustManager), trustManager);
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 certificate for OTLP HTTP connection, are they valid X.509 in PEM format?",

View File

@ -112,6 +112,14 @@ public final class OtlpGrpcLogExporterBuilder {
return this;
}
/**
* Sets ths client key and the certificate chain to use for verifying client when TLS is enabled.
*/
public OtlpGrpcLogExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
delegate.setClientTls(privateKeyPem, certificatePem);
return this;
}
/**
* Add header to request. Optional. Applicable only if {@link
* OtlpGrpcLogExporterBuilder#setChannel(ManagedChannel)} is not used to set channel.

View File

@ -90,6 +90,13 @@ class OtlpGrpcLogExporterTest extends AbstractGrpcTelemetryExporterTest<LogData,
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -91,6 +91,13 @@ class OtlpGrpcNettyLogExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -80,6 +80,13 @@ class OtlpGrpcNettyShadedLogExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -80,6 +80,13 @@ class OtlpGrpcNettyOkHttpLogExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<LogData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -116,6 +116,14 @@ public final class OtlpGrpcMetricExporterBuilder {
return this;
}
/**
* Sets ths client key and the certificate chain to use for verifying client when TLS is enabled.
*/
public OtlpGrpcMetricExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
delegate.setClientTls(privateKeyPem, certificatePem);
return this;
}
/**
* Add header to request. Optional. Applicable only if {@link
* OtlpGrpcMetricExporterBuilder#setChannel(ManagedChannel)} is not used to set channel.

View File

@ -94,6 +94,13 @@ class OtlpGrpcMetricExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -94,6 +94,13 @@ class OtlpGrpcNettyMetricExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -83,6 +83,13 @@ class OtlpGrpcNettyShadedMetricExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -83,6 +83,13 @@ class OtlpGrpcOkHttpMetricExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<MetricData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -79,6 +79,11 @@ public abstract class AbstractGrpcTelemetryExporterTest<T, U extends Message> {
@RegisterExtension
@Order(2)
static final SelfSignedCertificateExtension clientCertificate =
new SelfSignedCertificateExtension();
@RegisterExtension
@Order(3)
static final ServerExtension server =
new ServerExtension() {
@Override
@ -105,6 +110,7 @@ public abstract class AbstractGrpcTelemetryExporterTest<T, U extends Message> {
sb.http(0);
sb.https(0);
sb.tls(certificate.certificateFile(), certificate.privateKeyFile());
sb.tlsCustomizer(ssl -> ssl.trustManager(clientCertificate.certificate()));
sb.decorator(LoggingService.newDecorator());
}
};
@ -249,6 +255,25 @@ public abstract class AbstractGrpcTelemetryExporterTest<T, U extends Message> {
.hasMessageContaining("Could not set trusted certificates");
}
@Test
void clientTls() throws Exception {
TelemetryExporter<T> exporter =
exporterBuilder()
.setEndpoint(server.httpsUri().toString())
.setTrustedCertificates(Files.readAllBytes(certificate.certificateFile().toPath()))
.setClientTls(
clientCertificate.privateKey().getEncoded(),
clientCertificate.certificate().getEncoded())
.build();
try {
CompletableResultCode result =
exporter.export(Collections.singletonList(generateFakeTelemetry()));
assertThat(result.join(10, TimeUnit.SECONDS).isSuccess()).isTrue();
} finally {
exporter.shutdown();
}
}
@Test
void deadlineSetPerExport() throws InterruptedException {
TelemetryExporter<T> exporter =

View File

@ -22,6 +22,8 @@ public interface TelemetryExporterBuilder<T> {
TelemetryExporterBuilder<T> setTrustedCertificates(byte[] certificates);
TelemetryExporterBuilder<T> setClientTls(byte[] privateKeyPem, byte[] certificatePem);
TelemetryExporterBuilder<T> setRetryPolicy(RetryPolicy retryPolicy);
TelemetryExporter<T> build();

View File

@ -112,6 +112,14 @@ public final class OtlpGrpcSpanExporterBuilder {
return this;
}
/**
* Sets ths client key and the certificate chain to use for verifying client when TLS is enabled.
*/
public OtlpGrpcSpanExporterBuilder setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
delegate.setClientTls(privateKeyPem, certificatePem);
return this;
}
/**
* Add header to request. Optional. Applicable only if {@link
* OtlpGrpcSpanExporterBuilder#setChannel(ManagedChannel)} is not called.

View File

@ -95,6 +95,13 @@ class OtlpGrpcSpanExporterTest extends AbstractGrpcTelemetryExporterTest<SpanDat
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -96,6 +96,13 @@ class OtlpGrpcNettySpanExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -85,6 +85,13 @@ class OtlpGrpcNettyShadedSpanExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -85,6 +85,13 @@ class OtlpGrpcOkHttpSpanExporterTest
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
builder.setClientTls(privateKeyPem, certificatePem);
return this;
}
@Override
public TelemetryExporterBuilder<SpanData> setRetryPolicy(RetryPolicy retryPolicy) {
builder.delegate.setRetryPolicy(retryPolicy);

View File

@ -62,6 +62,14 @@ The [OpenTelemetry Protocol (OTLP)](https://github.com/open-telemetry/openteleme
| otel.exporter.otlp.traces.certificate | OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP trace server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default the host platform's trusted root certificates are used. |
| otel.exporter.otlp.metrics.certificate | OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP metric server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default the host platform's trusted root certificates are used. |
| otel.exporter.otlp.logs.certificate | OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP log server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default the host platform's trusted root certificates are used. |
| otel.exporter.otlp.client.key | OTEL_EXPORTER_OTLP_CERTIFICATE | The path to the file containing private client key to use when verifying an OTLP trace, metric, or log client's TLS credentials. The file should contain one private key PKCS8 PEM format. By default no client key is used. |
| otel.exporter.otlp.traces.client.key | OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP trace client's TLS credentials. The file should contain one private key PKCS8 PEM format. By default no client key file is used. |
| otel.exporter.otlp.metrics.client.key | OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP metric client's TLS credentials. The file should contain one private key PKCS8 PEM format. By default no client key file is used. |
| otel.exporter.otlp.logs.client.key | OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY | The path to the file containing private client key to use when verifying an OTLP log client's TLS credentials. The file should contain one private key PKCS8 PEM format. By default no client key file is used. |
| otel.exporter.otlp.client.certificate | OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP trace, metric, or log client's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default no chain file is used. |
| otel.exporter.otlp.traces.client.certificate | OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP trace server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default no chain file is used. |
| otel.exporter.otlp.metrics.client.certificate | OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP metric server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default no chain file is used. |
| otel.exporter.otlp.logs.client.certificate | OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE | The path to the file containing trusted certificates to use when verifying an OTLP log server's TLS credentials. The file should contain one or more X.509 certificates in PEM format. By default no chain file is used. |
| otel.exporter.otlp.headers | OTEL_EXPORTER_OTLP_HEADERS | Key-value pairs separated by commas to pass as request headers on OTLP trace, metric, and log requests. |
| otel.exporter.otlp.traces.headers | OTEL_EXPORTER_OTLP_TRACES_HEADERS | Key-value pairs separated by commas to pass as request headers on OTLP trace requests. |
| otel.exporter.otlp.metrics.headers | OTEL_EXPORTER_OTLP_METRICS_HEADERS | Key-value pairs separated by commas to pass as request headers on OTLP metrics requests. |

View File

@ -106,6 +106,7 @@ class LogExporterConfiguration {
builder::setCompression,
builder::setTimeout,
builder::setTrustedCertificates,
builder::setClientTls,
retryPolicy -> RetryUtil.setRetryPolicyOnDelegate(builder, retryPolicy));
builder.setMeterProvider(meterProvider);
@ -131,6 +132,7 @@ class LogExporterConfiguration {
builder::setCompression,
builder::setTimeout,
builder::setTrustedCertificates,
builder::setClientTls,
retryPolicy -> RetryUtil.setRetryPolicyOnDelegate(builder, retryPolicy));
builder.setMeterProvider(meterProvider);

View File

@ -115,6 +115,7 @@ final class MetricExporterConfiguration {
builder::setCompression,
builder::setTimeout,
builder::setTrustedCertificates,
builder::setClientTls,
retryPolicy -> RetryUtil.setRetryPolicyOnDelegate(builder, retryPolicy));
OtlpConfigUtil.configureOtlpAggregationTemporality(config, builder::setPreferredTemporality);
@ -141,6 +142,7 @@ final class MetricExporterConfiguration {
builder::setCompression,
builder::setTimeout,
builder::setTrustedCertificates,
builder::setClientTls,
retryPolicy -> RetryUtil.setRetryPolicyOnDelegate(builder, retryPolicy));
OtlpConfigUtil.configureOtlpAggregationTemporality(config, builder::setPreferredTemporality);

View File

@ -46,6 +46,7 @@ final class OtlpConfigUtil {
Consumer<String> setCompression,
Consumer<Duration> setTimeout,
Consumer<byte[]> setTrustedCertificates,
BiConsumer<byte[], byte[]> setClientTls,
Consumer<RetryPolicy> setRetryPolicy) {
String protocol = getOtlpProtocol(dataType, config);
boolean isHttpProtobuf = protocol.equals(PROTOCOL_HTTP_PROTOBUF);
@ -93,24 +94,34 @@ final class OtlpConfigUtil {
setTimeout.accept(timeout);
}
String certificate = config.getString("otel.exporter.otlp." + dataType + ".certificate");
if (certificate == null) {
certificate = config.getString("otel.exporter.otlp.certificate");
String certificatePath =
config.getString(
determinePropertyByType(config, "otel.exporter.otlp", dataType, "certificate"));
String clientKeyPath =
config.getString(
determinePropertyByType(config, "otel.exporter.otlp", dataType, "client.key"));
String clientKeyChainPath =
config.getString(
determinePropertyByType(config, "otel.exporter.otlp", dataType, "client.certificate"));
if (clientKeyPath != null && clientKeyChainPath == null) {
throw new ConfigurationException("Client key provided but certification chain is missing");
} else if (clientKeyPath == null && clientKeyChainPath != null) {
throw new ConfigurationException("Client key chain provided but key is missing");
}
if (certificate != null) {
Path path = Paths.get(certificate);
if (!Files.exists(path)) {
throw new ConfigurationException("Invalid OTLP certificate path: " + path);
}
byte[] certificateBytes;
try {
certificateBytes = Files.readAllBytes(path);
} catch (IOException e) {
throw new ConfigurationException("Error reading OTLP certificate.", e);
}
byte[] certificateBytes = readFileBytes(certificatePath);
if (certificateBytes != null) {
setTrustedCertificates.accept(certificateBytes);
}
byte[] clientKeyBytes = readFileBytes(clientKeyPath);
byte[] clientKeyChainBytes = readFileBytes(clientKeyChainPath);
if (clientKeyBytes != null && clientKeyChainBytes != null) {
setClientTls.accept(clientKeyBytes, clientKeyChainBytes);
}
Boolean retryEnabled = config.getBoolean("otel.experimental.exporter.otlp.retry.enabled");
if (retryEnabled != null && retryEnabled) {
setRetryPolicy.accept(RetryPolicy.getDefault());
@ -171,6 +182,33 @@ final class OtlpConfigUtil {
return endpointUrl;
}
@Nullable
private static byte[] readFileBytes(@Nullable String filePath) {
if (filePath == null) {
return null;
}
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new ConfigurationException("Invalid OTLP certificate/key path: " + path);
}
try {
return Files.readAllBytes(path);
} catch (IOException e) {
throw new ConfigurationException("Error reading content of file (" + path + ")", e);
}
}
private static String determinePropertyByType(
ConfigProperties config, String prefix, String dataType, String suffix) {
String propertyToRead = prefix + "." + dataType + "." + suffix;
String value = config.getString(propertyToRead);
if (value == null) {
return prefix + "." + suffix;
} else {
return propertyToRead;
}
}
private static String signalPath(String dataType) {
switch (dataType) {
case DATA_TYPE_METRICS:

View File

@ -126,6 +126,7 @@ final class SpanExporterConfiguration {
builder::setCompression,
builder::setTimeout,
builder::setTrustedCertificates,
builder::setClientTls,
retryPolicy -> RetryUtil.setRetryPolicyOnDelegate(builder, retryPolicy));
builder.setMeterProvider(meterProvider);
@ -146,6 +147,7 @@ final class SpanExporterConfiguration {
builder::setCompression,
builder::setTimeout,
builder::setTrustedCertificates,
builder::setClientTls,
retryPolicy -> RetryUtil.setRetryPolicyOnDelegate(builder, retryPolicy));
builder.setMeterProvider(meterProvider);

View File

@ -319,6 +319,7 @@ class OtlpConfigUtilTest {
value -> {},
value -> {},
value -> {},
(value1, value2) -> {},
value -> {});
return endpoint.get();

View File

@ -242,16 +242,62 @@ class OtlpGrpcConfigTest {
SpanExporterConfiguration.configureExporter(
"otlp", properties, NamedSpiManager.createEmpty(), MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
.hasMessageContaining("Invalid OTLP certificate/key path:");
assertThatThrownBy(() -> MetricExporterConfiguration.configureOtlpMetrics(properties))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
.hasMessageContaining("Invalid OTLP certificate/key path:");
assertThatThrownBy(
() -> LogExporterConfiguration.configureOtlpLogs(properties, MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
.hasMessageContaining("Invalid OTLP certificate/key path:");
}
@Test
void configureTlsMissingClientCertificatePath() {
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.client.key", Paths.get("foo", "bar", "baz").toString());
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"otlp", properties, NamedSpiManager.createEmpty(), MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key provided but certification chain is missing");
assertThatThrownBy(() -> MetricExporterConfiguration.configureOtlpMetrics(properties))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key provided but certification chain is missing");
assertThatThrownBy(
() -> LogExporterConfiguration.configureOtlpLogs(properties, MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key provided but certification chain is missing");
}
@Test
void configureTlsMissingClientKeyPath() {
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.client.certificate", Paths.get("foo", "bar", "baz").toString());
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"otlp", properties, NamedSpiManager.createEmpty(), MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key chain provided but key is missing");
assertThatThrownBy(() -> MetricExporterConfiguration.configureOtlpMetrics(properties))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key chain provided but key is missing");
assertThatThrownBy(
() -> LogExporterConfiguration.configureOtlpLogs(properties, MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key chain provided but key is missing");
}
@Test

View File

@ -268,16 +268,64 @@ class OtlpHttpConfigTest {
SpanExporterConfiguration.configureExporter(
"otlp", properties, NamedSpiManager.createEmpty(), MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
.hasMessageContaining("Invalid OTLP certificate/key path:");
assertThatThrownBy(() -> MetricExporterConfiguration.configureOtlpMetrics(properties))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
.hasMessageContaining("Invalid OTLP certificate/key path:");
assertThatThrownBy(
() -> LogExporterConfiguration.configureOtlpLogs(properties, MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Invalid OTLP certificate path:");
.hasMessageContaining("Invalid OTLP certificate/key path:");
}
@Test
void configureTlsMissingClientCertificatePath() {
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.protocol", "http/protobuf");
props.put("otel.exporter.otlp.client.key", Paths.get("foo", "bar", "baz").toString());
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"otlp", properties, NamedSpiManager.createEmpty(), MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key provided but certification chain is missing");
assertThatThrownBy(() -> MetricExporterConfiguration.configureOtlpMetrics(properties))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key provided but certification chain is missing");
assertThatThrownBy(
() -> LogExporterConfiguration.configureOtlpLogs(properties, MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key provided but certification chain is missing");
}
@Test
void configureTlsMissingClientKeyPath() {
Map<String, String> props = new HashMap<>();
props.put("otel.exporter.otlp.protocol", "http/protobuf");
props.put("otel.exporter.otlp.client.certificate", Paths.get("foo", "bar", "baz").toString());
ConfigProperties properties = DefaultConfigProperties.createForTest(props);
assertThatThrownBy(
() ->
SpanExporterConfiguration.configureExporter(
"otlp", properties, NamedSpiManager.createEmpty(), MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key chain provided but key is missing");
assertThatThrownBy(() -> MetricExporterConfiguration.configureOtlpMetrics(properties))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key chain provided but key is missing");
assertThatThrownBy(
() -> LogExporterConfiguration.configureOtlpLogs(properties, MeterProvider.noop()))
.isInstanceOf(ConfigurationException.class)
.hasMessageContaining("Client key chain provided but key is missing");
}
@Test

View File

@ -41,6 +41,8 @@ final class DefaultGrpcServiceBuilder<ReqT extends Marshaler, ResT extends UnMar
private boolean compressionEnabled = false;
@Nullable private Metadata metadata;
@Nullable private byte[] trustedCertificatesPem;
@Nullable private byte[] privateKeyPem;
@Nullable private byte[] certificatePem;
@Nullable private RetryPolicy retryPolicy;
/** Creates a new {@link OkHttpGrpcExporterBuilder}. */
@ -105,6 +107,13 @@ final class DefaultGrpcServiceBuilder<ReqT extends Marshaler, ResT extends UnMar
return this;
}
@Override
public GrpcServiceBuilder<ReqT, ResT> setClientTls(byte[] privateKeyPem, byte[] certificatePem) {
this.privateKeyPem = privateKeyPem;
this.certificatePem = certificatePem;
return this;
}
@Override
public DefaultGrpcServiceBuilder<ReqT, ResT> addHeader(String key, String value) {
requireNonNull(key, "key");
@ -142,8 +151,8 @@ final class DefaultGrpcServiceBuilder<ReqT extends Marshaler, ResT extends UnMar
if (trustedCertificatesPem != null) {
try {
ManagedChannelUtil.setTrustedCertificatesPem(
managedChannelBuilder, trustedCertificatesPem);
ManagedChannelUtil.setClientKeysAndTrustedCertificatesPem(
managedChannelBuilder, privateKeyPem, certificatePem, trustedCertificatesPem);
} catch (SSLException e) {
throw new IllegalStateException(
"Could not set trusted certificates for gRPC TLS connection, are they valid "

View File

@ -25,6 +25,9 @@ interface GrpcServiceBuilder<ReqMarshalerT extends Marshaler, ResUnMarshalerT ex
GrpcServiceBuilder<ReqMarshalerT, ResUnMarshalerT> setTrustedCertificates(
byte[] trustedCertificatesPem);
GrpcServiceBuilder<ReqMarshalerT, ResUnMarshalerT> setClientTls(
byte[] privateKeyPem, byte[] certificatePem);
GrpcServiceBuilder<ReqMarshalerT, ResUnMarshalerT> addHeader(String key, String value);
GrpcServiceBuilder<ReqMarshalerT, ResUnMarshalerT> addRetryPolicy(RetryPolicy retryPolicy);

View File

@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.concurrent.TimeUnit;
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;
@ -40,6 +41,8 @@ final class OkHttpGrpcServiceBuilder<
private boolean compressionEnabled = false;
private final Headers.Builder headers = new Headers.Builder();
@Nullable private byte[] trustedCertificatesPem;
@Nullable private byte[] privateKeyPem;
@Nullable private byte[] certificatePem;
@Nullable private RetryPolicy retryPolicy;
OkHttpGrpcServiceBuilder(
@ -97,6 +100,16 @@ final class OkHttpGrpcServiceBuilder<
return this;
}
@Override
public GrpcServiceBuilder<ReqMarshalerT, ResUnMarshalerT> setClientTls(
byte[] privateKeyPem, byte[] certificatePem) {
requireNonNull(privateKeyPem, "privateKeyPem");
requireNonNull(certificatePem, "certificatePem");
this.privateKeyPem = privateKeyPem;
this.certificatePem = certificatePem;
return this;
}
@Override
public OkHttpGrpcServiceBuilder<ReqMarshalerT, ResUnMarshalerT> addHeader(
String key, String value) {
@ -124,7 +137,12 @@ final class OkHttpGrpcServiceBuilder<
if (trustedCertificatesPem != null) {
try {
X509TrustManager trustManager = TlsUtil.trustManager(trustedCertificatesPem);
clientBuilder.sslSocketFactory(TlsUtil.sslSocketFactory(trustManager), trustManager);
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);