diff --git a/jaeger-core/src/main/java/io/jaegertracing/Configuration.java b/jaeger-core/src/main/java/io/jaegertracing/Configuration.java index b75bc77e4..45166fa75 100644 --- a/jaeger-core/src/main/java/io/jaegertracing/Configuration.java +++ b/jaeger-core/src/main/java/io/jaegertracing/Configuration.java @@ -152,6 +152,12 @@ public class Configuration { */ public static final String JAEGER_TRACEID_128BIT = JAEGER_PREFIX + "TRACEID_128BIT"; + /** + * TLS certificates (in comma-separated BASE64 SHA256 Hash) for certificates pinning, + * used in case of HTTPS communication to the endpoint. + */ + public static final String JAEGER_TLS_CERTIFICATE_PINNING = JAEGER_PREFIX + "TLS_CERTIFICATE_PINNING"; + /** * The supported trace context propagation formats. */ @@ -614,6 +620,11 @@ public static class SenderConfiguration { */ private String authPassword; + /** + * The comma-separated certificate list for the endpoint which is self-signed, for example + */ + private String[] serverCertificateHashes; + public SenderConfiguration() { } @@ -647,6 +658,13 @@ public SenderConfiguration withAuthPassword(String password) { return this; } + public SenderConfiguration withCertificatePinning(String certs) { + if (certs != null) { + this.serverCertificateHashes = certs.split(","); + } + return this; + } + /** * Returns a sender if one was given when creating the configuration, or attempts to create a sender based on the * configuration's state. @@ -671,6 +689,7 @@ public static SenderConfiguration fromEnv() { String authToken = getProperty(JAEGER_AUTH_TOKEN); String authUsername = getProperty(JAEGER_USER); String authPassword = getProperty(JAEGER_PASSWORD); + String certHashes = getProperty(JAEGER_TLS_CERTIFICATE_PINNING); return new SenderConfiguration() .withAgentHost(agentHost) @@ -678,6 +697,7 @@ public static SenderConfiguration fromEnv() { .withEndpoint(collectorEndpoint) .withAuthToken(authToken) .withAuthUsername(authUsername) + .withCertificatePinning(certHashes) .withAuthPassword(authPassword); } } diff --git a/jaeger-core/src/main/java/io/jaegertracing/internal/reporters/RemoteReporter.java b/jaeger-core/src/main/java/io/jaegertracing/internal/reporters/RemoteReporter.java index 57c18ed22..0f2b225eb 100644 --- a/jaeger-core/src/main/java/io/jaegertracing/internal/reporters/RemoteReporter.java +++ b/jaeger-core/src/main/java/io/jaegertracing/internal/reporters/RemoteReporter.java @@ -175,6 +175,7 @@ public void run() { try { command.execute(); } catch (SenderException e) { + log.warn("RemoteReporter failed to send:", e); metrics.reporterFailure.inc(e.getDroppedSpanCount()); } } catch (Exception e) { diff --git a/jaeger-core/src/test/java/io/jaegertracing/ConfigurationTest.java b/jaeger-core/src/test/java/io/jaegertracing/ConfigurationTest.java index 0d61541c1..71705fcd1 100644 --- a/jaeger-core/src/test/java/io/jaegertracing/ConfigurationTest.java +++ b/jaeger-core/src/test/java/io/jaegertracing/ConfigurationTest.java @@ -70,6 +70,7 @@ public void clearProperties() throws NoSuchFieldException, IllegalAccessExceptio System.clearProperty(Configuration.JAEGER_TAGS); System.clearProperty(Configuration.JAEGER_ENDPOINT); System.clearProperty(Configuration.JAEGER_AUTH_TOKEN); + System.clearProperty(Configuration.JAEGER_TLS_CERTIFICATE_PINNING); System.clearProperty(Configuration.JAEGER_USER); System.clearProperty(Configuration.JAEGER_PASSWORD); System.clearProperty(Configuration.JAEGER_PROPAGATION); diff --git a/jaeger-core/src/test/java/io/jaegertracing/internal/reporters/RemoteReporterTest.java b/jaeger-core/src/test/java/io/jaegertracing/internal/reporters/RemoteReporterTest.java index 5e3b616ce..dde94045b 100644 --- a/jaeger-core/src/test/java/io/jaegertracing/internal/reporters/RemoteReporterTest.java +++ b/jaeger-core/src/test/java/io/jaegertracing/internal/reporters/RemoteReporterTest.java @@ -31,6 +31,7 @@ import io.jaegertracing.internal.samplers.ConstSampler; import io.jaegertracing.internal.senders.InMemorySender; import io.jaegertracing.spi.Reporter; +import io.jaegertracing.spi.Sender; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -328,4 +329,56 @@ public int append(JaegerSpan span) { private JaegerSpan newSpan() { return tracer.buildSpan("x").start(); } + + private static class FailingSender implements Sender { + private static boolean invoked = false; + + @Override + public int append(JaegerSpan span) throws SenderException { + invoked = true; + throw new SenderException("FailingSender is to fail", 1); + } + + @Override + public int flush() throws SenderException { + throw new SenderException("FailingSender is to fail", 1); + } + + @Override + public int close() throws SenderException { + throw new SenderException("FailingSender is to fail", 1); + } + + public boolean invoked() { + return invoked; + } + } + + @Test + public void testReporterWithSenderException() { + FailingSender failingSender = new FailingSender(); + Reporter failingReporter = new RemoteReporter.Builder() + .withSender(failingSender) + .withFlushInterval(flushInterval) + .withMaxQueueSize(maxQueueSize) + .withMetrics(metrics) + .build(); + JaegerTracer failingTracer = new JaegerTracer.Builder("test-failing-reporter") + .withReporter(failingReporter) + .withSampler(new ConstSampler(true)) + .withMetrics(metrics) + .build(); + JaegerSpan span = failingTracer.buildSpan("raza").start(); + failingReporter.report(span); + // do sleep until automatic flush happens on 'reporter' + // added 20ms on top of 'flushInterval' to avoid corner cases + await() + .with() + .pollInterval(1, TimeUnit.MILLISECONDS) + .atMost(flushInterval + 20, TimeUnit.MILLISECONDS) + .until(() -> failingSender.invoked()); + + assertEquals(true, failingSender.invoked()); + assertEquals(1, metricsFactory.getCounter("jaeger_tracer_reporter_spans", "result=err")); + } } diff --git a/jaeger-crossdock/docker-compose.yml b/jaeger-crossdock/docker-compose.yml index 3c73d9519..c66cf29fc 100644 --- a/jaeger-crossdock/docker-compose.yml +++ b/jaeger-crossdock/docker-compose.yml @@ -8,13 +8,14 @@ services: - go - java-udp - java-http + - java-https - python - nodejs environment: - - WAIT_FOR=test_driver,go,java-udp,java-http,python,nodejs - - WAIT_FOR_TIMEOUT=60s + - WAIT_FOR=test_driver,go,java-udp,java-http,java-https,python,nodejs,jaeger-collector-https-proxy + - WAIT_FOR_TIMEOUT=90s - - CALL_TIMEOUT=60s + - CALL_TIMEOUT=90s - AXIS_CLIENT=go - AXIS_S1NAME=go,java-udp,python,nodejs @@ -27,7 +28,7 @@ services: - BEHAVIOR_TRACE=client,s1name,sampled,s2name,s2transport,s3name,s3transport - AXIS_TESTDRIVER=test_driver - - AXIS_SERVICES=java-udp,java-http + - AXIS_SERVICES=java-udp,java-http,java-https - BEHAVIOR_ENDTOEND=testdriver,services @@ -64,11 +65,31 @@ services: environment: - SENDER=http + java-https: + build: . + ports: + - "8080-8082" + environment: + - SENDER=https + - COLLECTOR_HOST_PIN=sha256/n6Ovey/sJws9vKJpESmWyQf9Oocak9J51mmPKGm4S0E= + depends_on: + - jaeger-collector + + jaeger-collector-https-proxy: + build: https-proxy + ports: + - "8080" + - "14443" + restart: on-failure + depends_on: + - jaeger-collector + test_driver: image: jaegertracing/test-driver depends_on: - jaeger-query - jaeger-collector - jaeger-agent + - jaeger-collector-https-proxy ports: - "8080" diff --git a/jaeger-crossdock/https-proxy/Dockerfile b/jaeger-crossdock/https-proxy/Dockerfile new file mode 100644 index 000000000..29d3976f6 --- /dev/null +++ b/jaeger-crossdock/https-proxy/Dockerfile @@ -0,0 +1,12 @@ +FROM nginx:alpine + +# Overwrite default configuration +ADD build/proxy.conf /etc/nginx/conf.d/default.conf +# Install generated certificates; run `gen.sh` first +ADD build/proxy.crt /etc/nginx/proxy.crt +ADD build/proxy.key /etc/nginx/proxy.key + +EXPOSE 8080 14443 + +# Poll healthcheck port until it returns 2xx or 3xx. +CMD ["sh", "-c", "while ! { wget --spider -S http://jaeger-collector:14269; }; do echo waiting for healthcheck; sleep 1; done; nginx -g 'daemon off;'"] diff --git a/jaeger-crossdock/https-proxy/authority.crt b/jaeger-crossdock/https-proxy/authority.crt new file mode 100644 index 000000000..18991aabe --- /dev/null +++ b/jaeger-crossdock/https-proxy/authority.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBbTCCARSgAwIBAgIJAJXCC2VdhrxqMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMM +B1RFU1QgQ0EwHhcNMTkwMzA3MTQzODIwWhcNMjkwMzA3MTQzODIwWjASMRAwDgYD +VQQDDAdURVNUIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAhYnw9zYU0G3 +VZ48nNlT5jAs096pX0zeHM/yxiJe+DS5Yj0EJXM/0A1Of2zqxbyJpaEIFcqTmTTy +TXCE7I6B6aNTMFEwHQYDVR0OBBYEFLXSxii6ZFvw427zs7ct/B+eHv2QMB8GA1Ud +IwQYMBaAFLXSxii6ZFvw427zs7ct/B+eHv2QMA8GA1UdEwEB/wQFMAMBAf8wCgYI +KoZIzj0EAwIDRwAwRAIgBrX7CX8zNoRLAZ48jGcqI8RuNlpkj0S+UShIQjwez3AC +IGuqhnGb9JZSiZmIQaYdSE6T/sQaX7iDnwEgKGMI8OB7 +-----END CERTIFICATE----- diff --git a/jaeger-crossdock/https-proxy/authority.key b/jaeger-crossdock/https-proxy/authority.key new file mode 100644 index 000000000..a30db3f38 --- /dev/null +++ b/jaeger-crossdock/https-proxy/authority.key @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAA24MB8sxrLG1an0nG1DCH6J32iqrtborxFOjdqWNCmoAoGCCqGSM49 +AwEHoUQDQgAEAhYnw9zYU0G3VZ48nNlT5jAs096pX0zeHM/yxiJe+DS5Yj0EJXM/ +0A1Of2zqxbyJpaEIFcqTmTTyTXCE7I6B6Q== +-----END EC PRIVATE KEY----- diff --git a/jaeger-crossdock/https-proxy/gen.sh b/jaeger-crossdock/https-proxy/gen.sh new file mode 100755 index 000000000..404cdbf22 --- /dev/null +++ b/jaeger-crossdock/https-proxy/gen.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -e + +if [[ $# -ne 2 ]]; then + echo usage: $0 ' ' + exit 1 +fi + +SERVER=$1 +FORWARD=$2 + +# Authority.crt & authority.key is pre-generated with hash code of sha256/n6Ovey/sJws9vKJpESmWyQf9Oocak9J51mmPKGm4S0E= +# About this file: +# CN: TEST CA +# Algorithm: ECDSA-SHA256 +# Validity: +# Not Before: Mar 7 14:38:20 2019 GMT +# Not After: Mar 7 14:38:20 2029 GMT +# +# To generate key: (not needed for cert renewal) +# openssl ecparam -genkey -name prime256v1 -out authority.key +# +# To regenerate: +# openssl req -new -sha256 -key authority.key -out authority.csr -subj "/CN=TEST CA/" +# openssl x509 -trustout -signkey authority.key -days 3652 -req -in authority.csr -out authority.crt +# +# To check pin: +# openssl ec -in authority.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64 +AUTHORITY=authority +TARGET=build/proxy + +# generate secrets +cd $(dirname $0) +mkdir -p ./build +openssl ecparam -genkey -name prime256v1 -out ${TARGET}.key +openssl req -new -sha256 -key ${TARGET}.key -out ${TARGET}.csr -subj \ + "/CN=${SERVER}/" +openssl x509 -req -sha256 -days 1 -CA ${AUTHORITY}.crt -CAkey ${AUTHORITY}.key -CAcreateserial -in ${TARGET}.csr -out ${TARGET}.crt +chmod 644 ${TARGET}.key + +cat ${AUTHORITY}.crt >> ${TARGET}.crt + +# generate nginx settings +SERVER=${SERVER} FORWARD=${FORWARD} envsubst < "proxy.template.conf" > ${TARGET}.conf diff --git a/jaeger-crossdock/https-proxy/proxy.template.conf b/jaeger-crossdock/https-proxy/proxy.template.conf new file mode 100644 index 000000000..c9f5f37c2 --- /dev/null +++ b/jaeger-crossdock/https-proxy/proxy.template.conf @@ -0,0 +1,24 @@ +server { + listen 8080; + server_name ${SERVER}; + location / { + root /usr/share/nginx/html/; + index index.html; + } +} + +server { + listen 14443 ssl http2; + server_name ${SERVER}; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_session_cache shared:SSL:10m; + ssl_certificate /etc/nginx/proxy.crt; + ssl_certificate_key /etc/nginx/proxy.key; + ssl_prefer_server_ciphers on; + ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH !EDH+aRSA !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"; + + location / { + proxy_pass http://${FORWARD}; + } +} diff --git a/jaeger-crossdock/rules.mk b/jaeger-crossdock/rules.mk index a6c083717..90bb6f3ea 100644 --- a/jaeger-crossdock/rules.mk +++ b/jaeger-crossdock/rules.mk @@ -5,14 +5,14 @@ JAEGER_COMPOSE_URL=https://raw.githubusercontent.com/jaegertracing/jaeger/master XDOCK_JAEGER_YAML=$(PROJECT)/jaeger-docker-compose.yml .PHONY: crossdock -crossdock: gradle-compile crossdock-download-jaeger - docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) kill java-udp java-http - docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) rm -f java-udp java-http - docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) build java-udp java-http +crossdock: gradle-compile crossdock-proxy-secret-gen crossdock-download-jaeger + docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) kill java-udp java-http java-https + docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) rm -f java-udp java-http java-https + docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) build java-udp java-http java-https docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) run crossdock .PHONY: crossdock-fresh -crossdock-fresh: gradle-compile crossdock-download-jaeger +crossdock-fresh: gradle-compile crossdock-proxy-secret-gen crossdock-download-jaeger docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) down --rmi all docker-compose -f $(XDOCK_YAML) -f $(XDOCK_JAEGER_YAML) run crossdock @@ -30,3 +30,7 @@ crossdock-clean: .PHONY: crossdock-download-jaeger crossdock-download-jaeger: curl -o $(XDOCK_JAEGER_YAML) $(JAEGER_COMPOSE_URL) + +.PHONY: crossdock-proxy-secret-gen +crossdock-proxy-secret-gen: + $(PROJECT)/https-proxy/gen.sh jaeger-collector-https-proxy jaeger-collector:14268 diff --git a/jaeger-crossdock/src/main/java/io/jaegertracing/crossdock/JerseyServer.java b/jaeger-crossdock/src/main/java/io/jaegertracing/crossdock/JerseyServer.java index d412e285d..96a33a85b 100644 --- a/jaeger-crossdock/src/main/java/io/jaegertracing/crossdock/JerseyServer.java +++ b/jaeger-crossdock/src/main/java/io/jaegertracing/crossdock/JerseyServer.java @@ -53,6 +53,7 @@ public class JerseyServer { private static final String SAMPLING_HOST_PORT = "SAMPLING_HOST_PORT"; private static final String AGENT_HOST = "AGENT_HOST"; private static final String COLLECTOR_HOST_PORT = "COLLECTOR_HOST_PORT"; + private static final String COLLECTOR_HOST_PIN = "COLLECTOR_HOST_PIN"; // TODO should not be static, should be final public static Client client; @@ -125,6 +126,8 @@ public static void main(String[] args) throws Exception { new EndToEndBehaviorResource(new EndToEndBehavior(getEvn(SAMPLING_HOST_PORT, "jaeger-agent:5778"), "crossdock-" + serviceName, senderFromEnv(getEvn(COLLECTOR_HOST_PORT, "jaeger-collector:14268"), + getEvn(COLLECTOR_HOST_PORT, "jaeger-collector-https-proxy:14443"), + getEvn(COLLECTOR_HOST_PIN, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="), getEvn(AGENT_HOST, "jaeger-agent")))), new HealthResource())); @@ -141,17 +144,23 @@ private static String getEvn(String envName, String defaultValue) { return env; } - private static Sender senderFromEnv(String collectorHostPort, String agentHost) { + private static Sender senderFromEnv(String collectorHostPort, + String collectorHttpsHostPort, String collectorHttpsPin, String agentHost) { String senderEnvVar = System.getenv(Constants.ENV_PROP_SENDER_TYPE); if ("http".equalsIgnoreCase(senderEnvVar)) { return new HttpSender.Builder(String.format("http://%s/api/traces", collectorHostPort)) .build(); + } else if ("https".equalsIgnoreCase(senderEnvVar)) { + return new HttpSender.Builder(String.format("https://%s/api/traces", collectorHttpsHostPort)) + .withCertificatePinning(new String[] {collectorHttpsPin}) + .acceptSelfSigned() + .build(); } else if ("udp".equalsIgnoreCase(senderEnvVar) || senderEnvVar == null || senderEnvVar.isEmpty()) { return new UdpSender(agentHost, 0, 0); } throw new IllegalStateException("Env variable " + Constants.ENV_PROP_SENDER_TYPE - + ", is not valid, choose 'udp' or 'http'"); + + ", is not valid, choose 'udp', 'http' or 'https'"); } private static String serviceNameFromEnv() { diff --git a/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/HttpSender.java b/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/HttpSender.java index ba627d4d8..2112b7b00 100644 --- a/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/HttpSender.java +++ b/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/HttpSender.java @@ -19,8 +19,20 @@ import io.jaegertracing.thriftjava.Process; import io.jaegertracing.thriftjava.Span; import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.ArrayList; import java.util.List; +import java.security.NoSuchAlgorithmException; +import java.security.KeyManagementException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; +import javax.net.ssl.TrustManager; import lombok.ToString; +import okhttp3.CertificatePinner; import okhttp3.Credentials; import okhttp3.HttpUrl; import okhttp3.Interceptor; @@ -46,6 +58,7 @@ protected HttpSender(Builder builder) { if (collectorUrl == null) { throw new IllegalArgumentException("Could not parse url."); } + this.httpClient = builder.clientBuilder.build(); this.requestBuilder = new Request.Builder().url(collectorUrl); } @@ -83,14 +96,18 @@ public void send(Process process, List spans) throws SenderException { String exceptionMessage = String.format("Could not send %d spans, response %d: %s", spans.size(), response.code(), responseBody); + throw new SenderException(exceptionMessage, null, spans.size()); } public static class Builder { private final String endpoint; + private CertificatePinner.Builder certificatePinnerBuilder = new CertificatePinner.Builder(); private int maxPacketSize = ONE_MB_IN_BYTES; private Interceptor authInterceptor; private OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + private List pins = new ArrayList(); + private boolean selfSigned = false; /** * @param endpoint jaeger-collector HTTP endpoint e.g. http://localhost:14268/api/traces @@ -119,13 +136,68 @@ public Builder withAuth(String authToken) { return this; } + public Builder withCertificatePinning(String[] sha256certs) { + pins.addAll(Arrays.asList(sha256certs)); + return this; + } + + /** + * Enable accepting self-signed certificates. This will only take effect if pinning is provided. + */ + public Builder acceptSelfSigned() { + this.selfSigned = true; + return this; + } + public HttpSender build() { if (authInterceptor != null) { clientBuilder.addInterceptor(authInterceptor); } + try { + // Just obtain hostname for SSL feature. Failure is OK if plain http is used. + final URI uri = new URI(endpoint); + String hostname = hostname = uri.getHost(); + + if ("https".equals(uri.getScheme())) { + if (!selfSigned && !pins.isEmpty()) { + // Pinning Certificate issued by public CA + for (String cert: pins) { + certificatePinnerBuilder.add(hostname, String.format("%s", cert)); + } + clientBuilder.certificatePinner(certificatePinnerBuilder.build()); + } else if (selfSigned && !pins.isEmpty()) { + /* Issued by private CA, OkHttp's pinner is unable to verify the pins. + * Instead, check the sha256 hash value by custom verifier. */ + acceptSelfSigned(clientBuilder, hostname, pins); + } + } + } catch (java.net.URISyntaxException e) { + // Early but similar validation & exception as what happens when HttpSender is called. + throw new IllegalArgumentException("Could not parse endpoint.", e); + } return new HttpSender(this); } + private void acceptSelfSigned(OkHttpClient.Builder clientBuilder, final String hostname, final List pinlist) { + try { + final TrustManager[] selfSignedServerTrustManager = new TrustManager[] { new SelfSignedTrustManager(hostname, pinlist) }; + final SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, selfSignedServerTrustManager, null); + clientBuilder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) selfSignedServerTrustManager[0]); + + } catch (NoSuchAlgorithmException e) { + // TLS is hardcoded above. No occasion to come here. + throw new RuntimeException(e); + } catch (KeyManagementException e) { + /* KeyManagementException will not occurs because sslContext uses default KeyManager (first argument). + * + * > Either of the first two parameters may be null in which case the installed security providers + * > will be searched for the highest priority implementation of the appropriate factory. + */ + throw new RuntimeException(e); + } + } + private Interceptor getAuthInterceptor(final String headerValue) { return new Interceptor() { @Override @@ -139,5 +211,60 @@ public Response intercept(Chain chain) throws IOException { } }; } + + private static class SelfSignedTrustManager implements X509TrustManager { + private final String subjectCN; + private final List pins; + + protected SelfSignedTrustManager(String hostname, List pins) { + this.subjectCN = "CN=" + hostname; + this.pins = pins; + } + + private boolean check(X509Certificate[] chain) { + // Intersection of pins and every cert in this chain + for (X509Certificate cert: chain) { + String hash = CertificatePinner.pin(cert); + for (String pin: pins) { + if (hash.equals(pin)) { + return true; + } + } + } + return false; + } + + private String describePins(X509Certificate[] chain) { + String message = "No pins matched with remote certificate chain. The pins are:"; + for (X509Certificate cert: chain) { + message = message + "\n\t" + CertificatePinner.pin(cert); + } + return message + "\n"; + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + for (X509Certificate cert: chain) { + String[] subject = cert.getSubjectDN().getName().split("/"); + for (String name: subject) { + if (subjectCN.equals(name)) { + // Found endpoint's hostname. This chain is going to be tested. + if (check(chain)) { + return; + } else { + // For TOFU (trust on first use) usecase, print the chain + throw new RuntimeException(describePins(chain)); + } + } + } + } + throw new CertificateException(); + } + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + throw new CertificateException(); // Nothing will be accepted + } + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } } } diff --git a/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/ThriftSenderFactory.java b/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/ThriftSenderFactory.java index 419105748..083edf6f5 100644 --- a/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/ThriftSenderFactory.java +++ b/jaeger-thrift/src/main/java/io/jaegertracing/thrift/internal/senders/ThriftSenderFactory.java @@ -26,6 +26,10 @@ public Sender getSender(Configuration.SenderConfiguration conf) { httpSenderBuilder.withAuth(conf.getAuthToken()); } + if (null != conf.getServerCertificateHashes() && 0 != conf.getServerCertificateHashes().length) { + httpSenderBuilder.withCertificatePinning(conf.getServerCertificateHashes()); + } + log.debug("Using the HTTP Sender to send spans directly to the endpoint."); return httpSenderBuilder.build(); } diff --git a/jaeger-thrift/src/test/java/io/jaegertracing/thrift/internal/senders/HttpSenderTest.java b/jaeger-thrift/src/test/java/io/jaegertracing/thrift/internal/senders/HttpSenderTest.java index c02db1257..bcd2b6430 100644 --- a/jaeger-thrift/src/test/java/io/jaegertracing/thrift/internal/senders/HttpSenderTest.java +++ b/jaeger-thrift/src/test/java/io/jaegertracing/thrift/internal/senders/HttpSenderTest.java @@ -52,6 +52,7 @@ public void reset() { System.clearProperty(Configuration.JAEGER_AUTH_TOKEN); System.clearProperty(Configuration.JAEGER_USER); System.clearProperty(Configuration.JAEGER_PASSWORD); + System.clearProperty(Configuration.JAEGER_TLS_CERTIFICATE_PINNING); } @Override @@ -83,6 +84,10 @@ public void misconfiguredUrl() throws Exception { new HttpSender.Builder("misconfiguredUrl").build(); } + @Test(expected = IllegalArgumentException.class) + public void misconfiguredQuery() throws Exception { + new HttpSender.Builder("http://some-server/api/traces^there=is&another=query").build(); + } @Test(expected = Exception.class) public void serverDoesntExist() throws Exception { HttpSender sender = new HttpSender.Builder("http://some-server/api/traces") @@ -124,6 +129,17 @@ public void sendWithTokenAuth() throws Exception { sender.send(new Process("robotrock"), generateSpans()); } + @Test + public void sendPlainHttpWithCertificatePinning() throws Exception { + System.setProperty(Configuration.JAEGER_ENDPOINT, target("/api/traces").getUri().toString()); + // Just confirm this is settable. Crossdock is used for TLS-level test. + System.setProperty(Configuration.JAEGER_TLS_CERTIFICATE_PINNING, + "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="); + + HttpSender sender = (HttpSender) Configuration.SenderConfiguration.fromEnv().getSender(); + sender.send(new Process("robotrock"), generateSpans()); + } + @Test public void sanityTestForTokenAuthTest() throws Exception { System.setProperty(Configuration.JAEGER_ENDPOINT, target("/api/bearer").getUri().toString());