From 25a2205810705b65cb16ea4a1cd83843a7120529 Mon Sep 17 00:00:00 2001 From: Jannik Brand Date: Thu, 10 Sep 2026 09:31:15 +0200 Subject: [PATCH 1/4] Make server-ca optional in Cloud Logging credentials Signed-off-by: Jannik Brand --- .../agent/ext/exporter/CloudLoggingCredentials.java | 5 ++--- .../ext/exporter/CloudLoggingLogsExporterProvider.java | 7 ++++++- .../ext/exporter/CloudLoggingMetricsExporterProvider.java | 7 ++++++- .../ext/exporter/CloudLoggingSpanExporterProvider.java | 7 ++++++- .../agent/ext/exporter/CloudLoggingCredentialsTest.java | 5 +++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java index cbb67f45..0c0dcaef 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java @@ -56,9 +56,8 @@ public boolean validate() { } if (isNullOrEmpty(serverCert)) { - LOG.warning( - "Credential \"" + CRED_OTLP_SERVER_CERT + "\" not found. Skipping cloud-logging exporter configuration"); - return false; + LOG.fine("Credential \"" + CRED_OTLP_SERVER_CERT + + "\" not present in service binding. Falling back to the JVM default trust store for the ingest endpoint."); } return true; } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java index f5eecc1f..626637b0 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java @@ -65,7 +65,12 @@ private LogRecordExporter createExporter(ConfigProperties config, CloudFoundrySe OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) - .setTrustedCertificates(credentials.getServerCert()).setRetryPolicy(RetryPolicy.getDefault()); + .setRetryPolicy(RetryPolicy.getDefault()); + + byte[] serverCert = credentials.getServerCert(); + if (serverCert != null && serverCert.length > 0) { + builder.setTrustedCertificates(serverCert); + } Duration timeOut = getTimeOut(config); if (timeOut != null) { diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java index 414c0328..5b336476 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java @@ -114,10 +114,15 @@ private MetricExporter createExporter(ConfigProperties config, CloudFoundryServi OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) - .setTrustedCertificates(credentials.getServerCert()).setRetryPolicy(RetryPolicy.getDefault()) + .setRetryPolicy(RetryPolicy.getDefault()) .setAggregationTemporalitySelector(getAggregationTemporalitySelector(config)) .setDefaultAggregationSelector(getDefaultAggregationSelector(config)); + byte[] serverCert = credentials.getServerCert(); + if (serverCert != null && serverCert.length > 0) { + builder.setTrustedCertificates(serverCert); + } + Duration timeOut = getTimeOut(config); if (timeOut != null) { builder.setTimeout(timeOut); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java index c6b22897..73b25fb2 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java @@ -65,7 +65,12 @@ private SpanExporter createExporter(ConfigProperties config, CloudFoundryService OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) - .setTrustedCertificates(credentials.getServerCert()).setRetryPolicy(RetryPolicy.getDefault()); + .setRetryPolicy(RetryPolicy.getDefault()); + + byte[] serverCert = credentials.getServerCert(); + if (serverCert != null && serverCert.length > 0) { + builder.setTrustedCertificates(serverCert); + } Duration timeOut = getTimeOut(config); if (timeOut != null) { diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentialsTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentialsTest.java index 934e8b36..05979b25 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentialsTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentialsTest.java @@ -58,12 +58,13 @@ void missingClientCert() { } @Test - void missingServerCert() { + void missingServerCertIsOptional() { CloudFoundryCredentials.Builder builder = builder().add("ingest-otlp-endpoint", "test-endpoint").add("ingest-otlp-cert", VALID_CLIENT_CERT) .add("ingest-otlp-key", VALID_CLIENT_KEY); CloudLoggingCredentials credentials = PARSER.parse(builder.build()); - assertThat(credentials.validate()).isFalse(); + assertThat(credentials.validate()).isTrue(); + assertThat(credentials.getServerCert()).isNull(); } @Test From afd87de876d45d10eaf5e11f72425516d8977b33 Mon Sep 17 00:00:00 2001 From: Jannik Brand Date: Thu, 10 Sep 2026 13:03:08 +0200 Subject: [PATCH 2/4] Append server-ca to JVM default trust store instead of replacing it Signed-off-by: Jannik Brand --- .../exporter/AppendedTrustCertificates.java | 81 +++++++++++++++++++ .../CloudLoggingLogsExporterProvider.java | 2 +- .../CloudLoggingMetricsExporterProvider.java | 2 +- .../CloudLoggingSpanExporterProvider.java | 2 +- .../AppendedTrustCertificatesTest.java | 70 ++++++++++++++++ 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java new file mode 100644 index 00000000..ee9c928c --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java @@ -0,0 +1,81 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.logging.Logger; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * Merges a service-binding-supplied server CA with the JVM's default trust store + * so that both are honored by the OTLP exporter. + * + *

SAP Cloud Logging's ingest endpoint chains to a public root (currently Let's + * Encrypt ISRG Root X1), which the JVM already trusts by default. Passing the raw + * {@code server-ca} value from the binding to + * {@link io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder#setTrustedCertificates(byte[])} + * would replace the JVM trust store with that single anchor, which is brittle + * against future root rotations. This helper concatenates the JVM's system trust + * anchors with the supplied server CA, so the exporter trusts both.

+ */ +final class AppendedTrustCertificates { + + private static final Logger LOG = Logger.getLogger(AppendedTrustCertificates.class.getName()); + private static final String PEM_LINE_SEP = "\n"; + private static final int PEM_LINE_LENGTH = 64; + + private AppendedTrustCertificates() { + } + + /** + * Returns a PEM-encoded byte array containing every trust anchor from the JVM's + * default trust manager, followed by the supplied {@code serverCert} bytes + * verbatim. If the JVM default trust anchors cannot be enumerated, the + * {@code serverCert} bytes are returned unchanged. + * + * @param serverCert the PEM-encoded server CA from the service binding; must not be {@code null} + * @return a PEM-encoded byte array suitable for + * {@code OtlpGrpc*ExporterBuilder.setTrustedCertificates(byte[])} + */ + static byte[] mergedWithSystemDefaults(byte[] serverCert) { + StringBuilder pem = new StringBuilder(); + try { + for (X509Certificate cert : getSystemTrustAnchors()) { + appendPem(pem, cert); + } + } catch (GeneralSecurityException | IOException e) { + LOG.warning("Failed to enumerate JVM default trust anchors, falling back to server-ca only: " + + e.getMessage()); + return serverCert; + } + byte[] systemPem = pem.toString().getBytes(java.nio.charset.StandardCharsets.US_ASCII); + byte[] merged = new byte[systemPem.length + serverCert.length]; + System.arraycopy(systemPem, 0, merged, 0, systemPem.length); + System.arraycopy(serverCert, 0, merged, systemPem.length, serverCert.length); + return merged; + } + + private static X509Certificate[] getSystemTrustAnchors() throws GeneralSecurityException, IOException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + return ((X509TrustManager) tm).getAcceptedIssuers(); + } + } + return new X509Certificate[0]; + } + + private static void appendPem(StringBuilder pem, X509Certificate cert) throws GeneralSecurityException { + String encoded = Base64.getMimeEncoder(PEM_LINE_LENGTH, PEM_LINE_SEP.getBytes(java.nio.charset.StandardCharsets.US_ASCII)) + .encodeToString(cert.getEncoded()); + pem.append("-----BEGIN CERTIFICATE-----").append(PEM_LINE_SEP); + pem.append(encoded).append(PEM_LINE_SEP); + pem.append("-----END CERTIFICATE-----").append(PEM_LINE_SEP); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java index 626637b0..b5a2a178 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java @@ -69,7 +69,7 @@ private LogRecordExporter createExporter(ConfigProperties config, CloudFoundrySe byte[] serverCert = credentials.getServerCert(); if (serverCert != null && serverCert.length > 0) { - builder.setTrustedCertificates(serverCert); + builder.setTrustedCertificates(AppendedTrustCertificates.mergedWithSystemDefaults(serverCert)); } Duration timeOut = getTimeOut(config); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java index 5b336476..fee162af 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java @@ -120,7 +120,7 @@ private MetricExporter createExporter(ConfigProperties config, CloudFoundryServi byte[] serverCert = credentials.getServerCert(); if (serverCert != null && serverCert.length > 0) { - builder.setTrustedCertificates(serverCert); + builder.setTrustedCertificates(AppendedTrustCertificates.mergedWithSystemDefaults(serverCert)); } Duration timeOut = getTimeOut(config); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java index 73b25fb2..6196370b 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java @@ -69,7 +69,7 @@ private SpanExporter createExporter(ConfigProperties config, CloudFoundryService byte[] serverCert = credentials.getServerCert(); if (serverCert != null && serverCert.length > 0) { - builder.setTrustedCertificates(serverCert); + builder.setTrustedCertificates(AppendedTrustCertificates.mergedWithSystemDefaults(serverCert)); } Duration timeOut = getTimeOut(config); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java new file mode 100644 index 00000000..3b6cb6c8 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java @@ -0,0 +1,70 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import static org.assertj.core.api.Assertions.assertThat; + +class AppendedTrustCertificatesTest { + + @Test + void mergedPemContainsBothSystemAnchorsAndServerCert() throws Exception { + X509Certificate[] systemAnchors = getSystemAnchors(); + assertThat(systemAnchors).as("JVM default trust store must expose at least one anchor").isNotEmpty(); + + // Use the first system anchor as our stand-in "server-ca" so we don't have to fabricate one. + // The merged output should still contain (systemAnchors.length + 1) BEGIN CERTIFICATE markers + // because the cert appears once via the system-anchor loop and once as the appended trailer. + byte[] serverCert = toPem(systemAnchors[0]).getBytes(StandardCharsets.US_ASCII); + byte[] merged = AppendedTrustCertificates.mergedWithSystemDefaults(serverCert); + + String mergedText = new String(merged, StandardCharsets.US_ASCII); + int markers = countOccurrences(mergedText, "-----BEGIN CERTIFICATE-----"); + assertThat(markers).isEqualTo(systemAnchors.length + 1); + + // The appended cert bytes must be preserved verbatim at the end of the output. + assertThat(mergedText).endsWith(new String(serverCert, StandardCharsets.US_ASCII)); + } + + @Test + void resultStartsWithPemHeader() { + byte[] merged = AppendedTrustCertificates.mergedWithSystemDefaults(new byte[0]); + String s = new String(merged, StandardCharsets.US_ASCII); + assertThat(s).startsWith("-----BEGIN CERTIFICATE-----"); + } + + private static X509Certificate[] getSystemAnchors() throws Exception { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + return ((X509TrustManager) tm).getAcceptedIssuers(); + } + } + return new X509Certificate[0]; + } + + private static String toPem(X509Certificate cert) throws Exception { + String encoded = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)) + .encodeToString(cert.getEncoded()); + return "-----BEGIN CERTIFICATE-----\n" + encoded + "\n-----END CERTIFICATE-----\n"; + } + + private static int countOccurrences(String haystack, String needle) { + int count = 0; + int idx = 0; + while ((idx = haystack.indexOf(needle, idx)) != -1) { + count++; + idx += needle.length(); + } + return count; + } +} From d264cbb739a472464732c47c431bc09e8d20cddd Mon Sep 17 00:00:00 2001 From: Jannik Brand Date: Thu, 10 Sep 2026 15:12:51 +0200 Subject: [PATCH 3/4] Refactor server-ca handling into TLS source abstraction --- .../exporter/AppendedTrustCertificates.java | 81 ------------------- .../ext/exporter/CloudLoggingCredentials.java | 2 +- .../CloudLoggingLogsExporterProvider.java | 11 +-- .../CloudLoggingMetricsExporterProvider.java | 11 +-- .../CloudLoggingSpanExporterProvider.java | 11 +-- .../tls/BindingServerCertificateSource.java | 43 ++++++++++ .../DownloadedServerCertificateSource.java | 49 +++++++++++ .../agent/ext/tls/PemEncoder.java | 32 ++++++++ .../ext/tls/ServerCertificateDownloader.java | 11 +-- .../ext/tls/SystemTrustAnchorSource.java | 44 ++++++++++ .../ext/tls/TrustedCertificatesJoiner.java | 44 ++++++++++ .../agent/ext/tls/X509CertificateSource.java | 24 ++++++ .../AppendedTrustCertificatesTest.java | 70 ---------------- .../BindingServerCertificateSourceTest.java | 48 +++++++++++ ...DownloadedServerCertificateSourceTest.java | 53 ++++++++++++ .../agent/ext/tls/PemEncoderTest.java | 59 ++++++++++++++ .../ext/tls/SystemTrustAnchorSourceTest.java | 28 +++++++ .../tls/TrustedCertificatesJoinerTest.java | 55 +++++++++++++ 18 files changed, 499 insertions(+), 177 deletions(-) delete mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java delete mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java deleted file mode 100644 index ee9c928c..00000000 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificates.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.cert.X509Certificate; -import java.util.Base64; -import java.util.logging.Logger; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -/** - * Merges a service-binding-supplied server CA with the JVM's default trust store - * so that both are honored by the OTLP exporter. - * - *

SAP Cloud Logging's ingest endpoint chains to a public root (currently Let's - * Encrypt ISRG Root X1), which the JVM already trusts by default. Passing the raw - * {@code server-ca} value from the binding to - * {@link io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder#setTrustedCertificates(byte[])} - * would replace the JVM trust store with that single anchor, which is brittle - * against future root rotations. This helper concatenates the JVM's system trust - * anchors with the supplied server CA, so the exporter trusts both.

- */ -final class AppendedTrustCertificates { - - private static final Logger LOG = Logger.getLogger(AppendedTrustCertificates.class.getName()); - private static final String PEM_LINE_SEP = "\n"; - private static final int PEM_LINE_LENGTH = 64; - - private AppendedTrustCertificates() { - } - - /** - * Returns a PEM-encoded byte array containing every trust anchor from the JVM's - * default trust manager, followed by the supplied {@code serverCert} bytes - * verbatim. If the JVM default trust anchors cannot be enumerated, the - * {@code serverCert} bytes are returned unchanged. - * - * @param serverCert the PEM-encoded server CA from the service binding; must not be {@code null} - * @return a PEM-encoded byte array suitable for - * {@code OtlpGrpc*ExporterBuilder.setTrustedCertificates(byte[])} - */ - static byte[] mergedWithSystemDefaults(byte[] serverCert) { - StringBuilder pem = new StringBuilder(); - try { - for (X509Certificate cert : getSystemTrustAnchors()) { - appendPem(pem, cert); - } - } catch (GeneralSecurityException | IOException e) { - LOG.warning("Failed to enumerate JVM default trust anchors, falling back to server-ca only: " - + e.getMessage()); - return serverCert; - } - byte[] systemPem = pem.toString().getBytes(java.nio.charset.StandardCharsets.US_ASCII); - byte[] merged = new byte[systemPem.length + serverCert.length]; - System.arraycopy(systemPem, 0, merged, 0, systemPem.length); - System.arraycopy(serverCert, 0, merged, systemPem.length, serverCert.length); - return merged; - } - - private static X509Certificate[] getSystemTrustAnchors() throws GeneralSecurityException, IOException { - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init((KeyStore) null); - for (TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509TrustManager) { - return ((X509TrustManager) tm).getAcceptedIssuers(); - } - } - return new X509Certificate[0]; - } - - private static void appendPem(StringBuilder pem, X509Certificate cert) throws GeneralSecurityException { - String encoded = Base64.getMimeEncoder(PEM_LINE_LENGTH, PEM_LINE_SEP.getBytes(java.nio.charset.StandardCharsets.US_ASCII)) - .encodeToString(cert.getEncoded()); - pem.append("-----BEGIN CERTIFICATE-----").append(PEM_LINE_SEP); - pem.append(encoded).append(PEM_LINE_SEP); - pem.append("-----END CERTIFICATE-----").append(PEM_LINE_SEP); - } -} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java index 0c0dcaef..54ef0d05 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java @@ -56,7 +56,7 @@ public boolean validate() { } if (isNullOrEmpty(serverCert)) { - LOG.fine("Credential \"" + CRED_OTLP_SERVER_CERT + LOG.info("Credential \"" + CRED_OTLP_SERVER_CERT + "\" not present in service binding. Falling back to the JVM default trust store for the ingest endpoint."); } return true; diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java index b5a2a178..313c4de3 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java @@ -3,6 +3,9 @@ import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudFoundryServiceInstance; import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider; import com.sap.hcf.cf.logging.opentelemetry.agent.ext.config.ExtensionConfigurations.EXPORTER; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.BindingServerCertificateSource; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.SystemTrustAnchorSource; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.TrustedCertificatesJoiner; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -65,13 +68,11 @@ private LogRecordExporter createExporter(ConfigProperties config, CloudFoundrySe OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) + .setTrustedCertificates(TrustedCertificatesJoiner.toPemBytes( + new SystemTrustAnchorSource(), + new BindingServerCertificateSource(credentials.getServerCert()))) .setRetryPolicy(RetryPolicy.getDefault()); - byte[] serverCert = credentials.getServerCert(); - if (serverCert != null && serverCert.length > 0) { - builder.setTrustedCertificates(AppendedTrustCertificates.mergedWithSystemDefaults(serverCert)); - } - Duration timeOut = getTimeOut(config); if (timeOut != null) { builder.setTimeout(timeOut); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java index fee162af..d7bdfcbc 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java @@ -3,6 +3,9 @@ import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudFoundryServiceInstance; import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider; import com.sap.hcf.cf.logging.opentelemetry.agent.ext.config.ExtensionConfigurations.EXPORTER; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.BindingServerCertificateSource; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.SystemTrustAnchorSource; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.TrustedCertificatesJoiner; import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -114,15 +117,13 @@ private MetricExporter createExporter(ConfigProperties config, CloudFoundryServi OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) + .setTrustedCertificates(TrustedCertificatesJoiner.toPemBytes( + new SystemTrustAnchorSource(), + new BindingServerCertificateSource(credentials.getServerCert()))) .setRetryPolicy(RetryPolicy.getDefault()) .setAggregationTemporalitySelector(getAggregationTemporalitySelector(config)) .setDefaultAggregationSelector(getDefaultAggregationSelector(config)); - byte[] serverCert = credentials.getServerCert(); - if (serverCert != null && serverCert.length > 0) { - builder.setTrustedCertificates(AppendedTrustCertificates.mergedWithSystemDefaults(serverCert)); - } - Duration timeOut = getTimeOut(config); if (timeOut != null) { builder.setTimeout(timeOut); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java index 6196370b..96b747c9 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java @@ -3,6 +3,9 @@ import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudFoundryServiceInstance; import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider; import com.sap.hcf.cf.logging.opentelemetry.agent.ext.config.ExtensionConfigurations.EXPORTER; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.BindingServerCertificateSource; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.SystemTrustAnchorSource; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls.TrustedCertificatesJoiner; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -65,13 +68,11 @@ private SpanExporter createExporter(ConfigProperties config, CloudFoundryService OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) + .setTrustedCertificates(TrustedCertificatesJoiner.toPemBytes( + new SystemTrustAnchorSource(), + new BindingServerCertificateSource(credentials.getServerCert()))) .setRetryPolicy(RetryPolicy.getDefault()); - byte[] serverCert = credentials.getServerCert(); - if (serverCert != null && serverCert.length > 0) { - builder.setTrustedCertificates(AppendedTrustCertificates.mergedWithSystemDefaults(serverCert)); - } - Duration timeOut = getTimeOut(config); if (timeOut != null) { builder.setTimeout(timeOut); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java new file mode 100644 index 00000000..ff7d11ba --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java @@ -0,0 +1,43 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +/** + * Supplies the X.509 certificate carried by a Cloud Foundry service binding's + * {@code server-ca} field (or an equivalent field name for other bindings). + * + *

The input is expected to be the raw PEM bytes as they arrive in the binding. + * A {@code null} or empty input yields an empty stream, so the source can be used + * unconditionally with {@link TrustedCertificatesJoiner}.

+ */ +public class BindingServerCertificateSource implements X509CertificateSource { + + private static final Logger LOG = Logger.getLogger(BindingServerCertificateSource.class.getName()); + + private final byte[] pemBytes; + + public BindingServerCertificateSource(byte[] pemBytes) { + this.pemBytes = pemBytes; + } + + @Override + public Stream get() { + if (pemBytes == null || pemBytes.length == 0) { + return Stream.empty(); + } + try { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(pemBytes)); + return Stream.of(certificate); + } catch (CertificateException e) { + LOG.log(Level.WARNING, e, () -> "Failed to parse server-ca from service binding; it will be omitted from the trust anchors."); + return Stream.empty(); + } + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java new file mode 100644 index 00000000..3f6b5327 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java @@ -0,0 +1,49 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +/** + * Supplies the X.509 leaf certificate returned by {@link ServerCertificateDownloader} + * for a fixed OTLP endpoint URL. + * + *

This is the fallback path used when a service binding does not carry a + * {@code server-ca} field. It connects to the endpoint with TLS (validation disabled) + * and reads back the leaf certificate the server presents.

+ */ +public class DownloadedServerCertificateSource implements X509CertificateSource { + + private static final Logger LOG = Logger.getLogger(DownloadedServerCertificateSource.class.getName()); + + private final ServerCertificateDownloader downloader; + private final String endpointUrl; + + public DownloadedServerCertificateSource(ServerCertificateDownloader downloader, String endpointUrl) { + this.downloader = downloader; + this.endpointUrl = endpointUrl; + } + + @Override + public Stream get() { + String pem = downloader.download(endpointUrl); + if (pem == null || pem.isEmpty()) { + return Stream.empty(); + } + try { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + X509Certificate certificate = (X509Certificate) factory + .generateCertificate(new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8))); + return Stream.of(certificate); + } catch (CertificateException e) { + LOG.log(Level.WARNING, e, () -> "Failed to parse server certificate downloaded from " + endpointUrl + + "; it will be omitted from the trust anchors."); + return Stream.empty(); + } + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java new file mode 100644 index 00000000..b0d94e21 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java @@ -0,0 +1,32 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Base64; + +/** + * Encodes X.509 certificates as PEM strings. + * + *

Line separator is {@code "\n"} and Base64 body is wrapped at 64 characters, + * matching the format expected by OpenTelemetry's {@code setTrustedCertificates(byte[])}.

+ */ +final class PemEncoder { + + private static final String LINE_SEPARATOR = "\n"; + private static final Base64.Encoder BASE64_ENCODER = + Base64.getMimeEncoder(64, LINE_SEPARATOR.getBytes(StandardCharsets.US_ASCII)); + + private PemEncoder() { + } + + /** + * Returns the PEM encoding of the given certificate, including the + * {@code BEGIN CERTIFICATE} / {@code END CERTIFICATE} armor and a trailing newline. + */ + static String encode(X509Certificate certificate) throws CertificateEncodingException { + return "-----BEGIN CERTIFICATE-----" + LINE_SEPARATOR + + BASE64_ENCODER.encodeToString(certificate.getEncoded()) + LINE_SEPARATOR + + "-----END CERTIFICATE-----" + LINE_SEPARATOR; + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/ServerCertificateDownloader.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/ServerCertificateDownloader.java index be8880a5..acf99e8b 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/ServerCertificateDownloader.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/ServerCertificateDownloader.java @@ -3,22 +3,18 @@ import javax.net.ssl.*; import java.io.IOException; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; -import java.util.Base64; import java.util.logging.Level; import java.util.logging.Logger; public class ServerCertificateDownloader { private static final Logger LOG = Logger.getLogger(ServerCertificateDownloader.class.getName()); - private static final byte[] LINE_SEPARATOR = "\n".getBytes(StandardCharsets.UTF_8); - private static final Base64.Encoder BASE64_ENCODER = Base64.getMimeEncoder(64, LINE_SEPARATOR); private final SSLSocketFactory sslSocketFactory; @@ -78,12 +74,7 @@ public String download(String endpointUrl) { return null; } - X509Certificate x509Cert = (X509Certificate) serverCertificates[0]; - byte[] encoded = x509Cert.getEncoded(); - return "-----BEGIN CERTIFICATE-----\n" // - + BASE64_ENCODER.encodeToString(encoded) // - + "\n-----END CERTIFICATE-----\n"; - + return PemEncoder.encode((X509Certificate) serverCertificates[0]); } } catch (CertificateEncodingException | IOException e) { LOG.log(Level.WARNING, e, () -> "Failed to download server certificate from " + endpointUrl); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java new file mode 100644 index 00000000..d73287ab --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java @@ -0,0 +1,44 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +/** + * Supplies the X.509 trust anchors known to the JVM's default trust store + * (typically {@code $JAVA_HOME/lib/security/cacerts}). + * + *

This is the same set the platform uses to validate ordinary HTTPS connections, + * so any endpoint whose server certificate chains to a public root is trusted without + * additional configuration.

+ */ +public class SystemTrustAnchorSource implements X509CertificateSource { + + private static final Logger LOG = Logger.getLogger(SystemTrustAnchorSource.class.getName()); + + @Override + public Stream get() { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + return Arrays.stream(((X509TrustManager) tm).getAcceptedIssuers()); + } + } + LOG.warning("No X509TrustManager returned by the default TrustManagerFactory; system trust anchors will be omitted."); + return Stream.empty(); + } catch (NoSuchAlgorithmException | KeyStoreException e) { + LOG.log(Level.WARNING, e, () -> "Failed to enumerate JVM default trust anchors; system trust anchors will be omitted."); + return Stream.empty(); + } + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java new file mode 100644 index 00000000..b99d35a2 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java @@ -0,0 +1,44 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Concatenates the PEM encodings of the certificates from one or more + * {@link X509CertificateSource} instances into a single byte array suitable for + * {@code OtlpGrpc*ExporterBuilder.setTrustedCertificates(byte[])}. + * + *

Sources are consumed in the order given; certificates that fail to encode + * are logged and skipped so a single bad certificate does not break the exporter.

+ */ +public final class TrustedCertificatesJoiner { + + private static final Logger LOG = Logger.getLogger(TrustedCertificatesJoiner.class.getName()); + + private TrustedCertificatesJoiner() { + } + + /** + * Encodes every certificate produced by the given sources as PEM and returns the + * concatenated bytes (UTF-8). Empty sources contribute nothing; the returned array + * is empty if no source yields a certificate. + */ + public static byte[] toPemBytes(X509CertificateSource... sources) { + StringBuilder pem = new StringBuilder(); + for (X509CertificateSource source : sources) { + source.get().forEach(cert -> appendPem(pem, cert)); + } + return pem.toString().getBytes(StandardCharsets.UTF_8); + } + + private static void appendPem(StringBuilder pem, X509Certificate cert) { + try { + pem.append(PemEncoder.encode(cert)); + } catch (CertificateEncodingException e) { + LOG.log(Level.WARNING, e, () -> "Failed to PEM-encode a trust anchor; it will be omitted."); + } + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java new file mode 100644 index 00000000..7a39882d --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java @@ -0,0 +1,24 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import java.security.cert.X509Certificate; +import java.util.stream.Stream; + +/** + * Supplies zero or more X.509 certificates that should be added to the OTLP exporter's + * trust anchors. Implementations may source the certificates from a service binding, + * a network download, the JVM's default trust store, or any other origin. + * + *

The returned stream must be finite and may be empty if the source cannot deliver + * a certificate (e.g. a binding field is missing). Implementations should not throw; + * they should log and return an empty stream so callers can safely aggregate multiple + * sources with {@link TrustedCertificatesJoiner}.

+ */ +@FunctionalInterface +public interface X509CertificateSource { + + /** + * Returns the certificates supplied by this source. Callers must consume the + * stream fully; sources are not required to be repeatable. + */ + Stream get(); +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java deleted file mode 100644 index 3b6cb6c8..00000000 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/AppendedTrustCertificatesTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; - -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; -import java.security.KeyStore; -import java.security.cert.X509Certificate; -import java.util.Base64; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import static org.assertj.core.api.Assertions.assertThat; - -class AppendedTrustCertificatesTest { - - @Test - void mergedPemContainsBothSystemAnchorsAndServerCert() throws Exception { - X509Certificate[] systemAnchors = getSystemAnchors(); - assertThat(systemAnchors).as("JVM default trust store must expose at least one anchor").isNotEmpty(); - - // Use the first system anchor as our stand-in "server-ca" so we don't have to fabricate one. - // The merged output should still contain (systemAnchors.length + 1) BEGIN CERTIFICATE markers - // because the cert appears once via the system-anchor loop and once as the appended trailer. - byte[] serverCert = toPem(systemAnchors[0]).getBytes(StandardCharsets.US_ASCII); - byte[] merged = AppendedTrustCertificates.mergedWithSystemDefaults(serverCert); - - String mergedText = new String(merged, StandardCharsets.US_ASCII); - int markers = countOccurrences(mergedText, "-----BEGIN CERTIFICATE-----"); - assertThat(markers).isEqualTo(systemAnchors.length + 1); - - // The appended cert bytes must be preserved verbatim at the end of the output. - assertThat(mergedText).endsWith(new String(serverCert, StandardCharsets.US_ASCII)); - } - - @Test - void resultStartsWithPemHeader() { - byte[] merged = AppendedTrustCertificates.mergedWithSystemDefaults(new byte[0]); - String s = new String(merged, StandardCharsets.US_ASCII); - assertThat(s).startsWith("-----BEGIN CERTIFICATE-----"); - } - - private static X509Certificate[] getSystemAnchors() throws Exception { - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init((KeyStore) null); - for (TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509TrustManager) { - return ((X509TrustManager) tm).getAcceptedIssuers(); - } - } - return new X509Certificate[0]; - } - - private static String toPem(X509Certificate cert) throws Exception { - String encoded = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)) - .encodeToString(cert.getEncoded()); - return "-----BEGIN CERTIFICATE-----\n" + encoded + "\n-----END CERTIFICATE-----\n"; - } - - private static int countOccurrences(String haystack, String needle) { - int count = 0; - int idx = 0; - while ((idx = haystack.indexOf(needle, idx)) != -1) { - count++; - idx += needle.length(); - } - return count; - } -} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java new file mode 100644 index 00000000..b3f6aa8e --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java @@ -0,0 +1,48 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +class BindingServerCertificateSourceTest { + + private static byte[] validPemBytes() throws Exception { + try (InputStream is = BindingServerCertificateSourceTest.class.getClassLoader() + .getResourceAsStream("certificate.pem")) { + assertThat(is).isNotNull(); + return is.readAllBytes(); + } + } + + @Test + void parsesValidPemIntoASingleCertificate() throws Exception { + BindingServerCertificateSource source = new BindingServerCertificateSource(validPemBytes()); + + assertThat(source.get()).hasSize(1); + } + + @Test + void yieldsEmptyStreamForNullInput() { + BindingServerCertificateSource source = new BindingServerCertificateSource(null); + + assertThat(source.get()).isEmpty(); + } + + @Test + void yieldsEmptyStreamForEmptyInput() { + BindingServerCertificateSource source = new BindingServerCertificateSource(new byte[0]); + + assertThat(source.get()).isEmpty(); + } + + @Test + void yieldsEmptyStreamForMalformedInput() { + BindingServerCertificateSource source = + new BindingServerCertificateSource("not a pem".getBytes(StandardCharsets.UTF_8)); + + assertThat(source.get()).isEmpty(); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java new file mode 100644 index 00000000..7cec1867 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java @@ -0,0 +1,53 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DownloadedServerCertificateSourceTest { + + private static final String ENDPOINT = "https://example.com:443"; + + @Mock + private ServerCertificateDownloader downloader; + + private static String validPem() throws Exception { + try (InputStream is = DownloadedServerCertificateSourceTest.class.getClassLoader() + .getResourceAsStream("certificate.pem")) { + assertThat(is).isNotNull(); + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + @Test + void parsesDownloadedPemIntoASingleCertificate() throws Exception { + when(downloader.download(ENDPOINT)).thenReturn(validPem()); + DownloadedServerCertificateSource source = new DownloadedServerCertificateSource(downloader, ENDPOINT); + + assertThat(source.get()).hasSize(1); + } + + @Test + void yieldsEmptyStreamWhenDownloaderReturnsNull() { + when(downloader.download(ENDPOINT)).thenReturn(null); + DownloadedServerCertificateSource source = new DownloadedServerCertificateSource(downloader, ENDPOINT); + + assertThat(source.get()).isEmpty(); + } + + @Test + void yieldsEmptyStreamWhenDownloaderReturnsMalformedPem() { + when(downloader.download(ENDPOINT)).thenReturn("not a pem"); + DownloadedServerCertificateSource source = new DownloadedServerCertificateSource(downloader, ENDPOINT); + + assertThat(source.get()).isEmpty(); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java new file mode 100644 index 00000000..38846842 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java @@ -0,0 +1,59 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import static org.assertj.core.api.Assertions.assertThat; + +class PemEncoderTest { + + static X509Certificate loadTestCertificate() throws Exception { + try (InputStream is = PemEncoderTest.class.getClassLoader().getResourceAsStream("certificate.pem")) { + assertThat(is).isNotNull(); + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); + } + } + + @Test + void encodesCertificateWithArmorAndTrailingNewline() throws Exception { + String pem = PemEncoder.encode(loadTestCertificate()); + + assertThat(pem).startsWith("-----BEGIN CERTIFICATE-----\n").endsWith("-----END CERTIFICATE-----\n"); + } + + @Test + void encodedOutputRoundtripsBackToTheSameCertificate() throws Exception { + X509Certificate original = loadTestCertificate(); + + String pem = PemEncoder.encode(original); + + X509Certificate parsed = (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8))); + assertThat(parsed).isEqualTo(original); + } + + @Test + void wrapsBase64BodyAt64Characters() throws Exception { + String pem = PemEncoder.encode(loadTestCertificate()); + String body = pem.replace("-----BEGIN CERTIFICATE-----\n", "").replace("\n-----END CERTIFICATE-----\n", ""); + + for (String line : body.split("\n")) { + // The last line may be shorter; every other line must be exactly 64 chars. + assertThat(line.length()).isLessThanOrEqualTo(64); + } + } + + @Test + void isCompatibleWithPreviousServerCertificateDownloaderFormat() throws Exception { + // Guard against accidental drift: the format must remain interchangeable with the + // one previously produced by ServerCertificateDownloader#download. + String pem = PemEncoder.encode(loadTestCertificate()); + + assertThat(pem).matches("(?s)^-----BEGIN CERTIFICATE-----\\n([A-Za-z0-9+/=]{1,64}\\n)+-----END CERTIFICATE-----\\n$"); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java new file mode 100644 index 00000000..933a8dd6 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java @@ -0,0 +1,28 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import org.junit.jupiter.api.Test; + +import java.security.cert.X509Certificate; + +import static org.assertj.core.api.Assertions.assertThat; + +class SystemTrustAnchorSourceTest { + + @Test + void yieldsTheJvmDefaultTrustAnchors() { + SystemTrustAnchorSource source = new SystemTrustAnchorSource(); + + assertThat(source.get()).isNotEmpty() + .allSatisfy(cert -> assertThat(cert).isInstanceOf(X509Certificate.class)); + } + + @Test + void repeatedInvocationsProduceIndependentStreams() { + SystemTrustAnchorSource source = new SystemTrustAnchorSource(); + + long first = source.get().count(); + long second = source.get().count(); + + assertThat(first).isPositive().isEqualTo(second); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java new file mode 100644 index 00000000..15ed6bbf --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java @@ -0,0 +1,55 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +class TrustedCertificatesJoinerTest { + + private static byte[] validPemBytes() throws Exception { + try (InputStream is = TrustedCertificatesJoinerTest.class.getClassLoader() + .getResourceAsStream("certificate.pem")) { + assertThat(is).isNotNull(); + return is.readAllBytes(); + } + } + + @Test + void returnsEmptyBytesWhenNoSourcesProvided() { + assertThat(TrustedCertificatesJoiner.toPemBytes()).isEmpty(); + } + + @Test + void returnsEmptyBytesWhenAllSourcesAreEmpty() { + byte[] result = TrustedCertificatesJoiner.toPemBytes(new BindingServerCertificateSource(null), + new BindingServerCertificateSource(new byte[0])); + + assertThat(result).isEmpty(); + } + + @Test + void concatenatesCertificatesFromMultipleSourcesInOrder() throws Exception { + byte[] result = TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(), + new BindingServerCertificateSource(validPemBytes())); + + String pem = new String(result, StandardCharsets.UTF_8); + long beginMarkers = pem.lines().filter(l -> l.equals("-----BEGIN CERTIFICATE-----")).count(); + long endMarkers = pem.lines().filter(l -> l.equals("-----END CERTIFICATE-----")).count(); + assertThat(beginMarkers).isEqualTo(endMarkers).isGreaterThan(1); + assertThat(pem).endsWith("-----END CERTIFICATE-----\n"); + } + + @Test + void bindingCertificateAppearsLastInTheJoinedOutput() throws Exception { + byte[] result = TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(), + new BindingServerCertificateSource(validPemBytes())); + + // Compare against the encoding of the binding cert alone: it must appear as the + // suffix of the joined output. + byte[] bindingOnly = TrustedCertificatesJoiner.toPemBytes(new BindingServerCertificateSource(validPemBytes())); + assertThat(new String(result, StandardCharsets.UTF_8)).endsWith(new String(bindingOnly, StandardCharsets.UTF_8)); + } +} From e7e4123b7767f30f7896cc9dad956d78b2956579 Mon Sep 17 00:00:00 2001 From: Jannik Brand Date: Fri, 11 Sep 2026 15:09:16 +0200 Subject: [PATCH 4/4] Address review: DI trusted certs provider, stream(), assumeThat - Inject Function into all three exporter providers so the joiner can be replaced in tests. - Rename X509CertificateSource#get() to stream(). - SystemTrustAnchorSource: merge accepted issuers from every X509TrustManager returned by the default TrustManagerFactory. - PemEncoder: use UTF-8 for the base64 line separator. - Tests: share the loaded certificate as a constant in PemEncoderTest; use Assumptions.assumeThat for the resource-not-null precondition in the tls tests; fold the join-order assertion into the concatenation test. --- .../CloudLoggingLogsExporterProvider.java | 14 ++++++++----- .../CloudLoggingMetricsExporterProvider.java | 14 ++++++++----- .../CloudLoggingSpanExporterProvider.java | 14 ++++++++----- .../tls/BindingServerCertificateSource.java | 2 +- .../DownloadedServerCertificateSource.java | 2 +- .../agent/ext/tls/PemEncoder.java | 2 +- .../ext/tls/SystemTrustAnchorSource.java | 18 ++++++++-------- .../ext/tls/TrustedCertificatesJoiner.java | 2 +- .../agent/ext/tls/X509CertificateSource.java | 2 +- .../CloudLoggingLogsExporterProviderTest.java | 11 +++++----- ...oudLoggingMetricsExporterProviderTest.java | 11 +++++----- .../CloudLoggingSpanExporterProviderTest.java | 11 +++++----- .../BindingServerCertificateSourceTest.java | 11 +++++----- ...DownloadedServerCertificateSourceTest.java | 9 ++++---- .../agent/ext/tls/PemEncoderTest.java | 21 +++++++++++-------- .../ext/tls/SystemTrustAnchorSourceTest.java | 8 +++---- .../tls/TrustedCertificatesJoinerTest.java | 15 ++++--------- 17 files changed, 90 insertions(+), 77 deletions(-) diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java index 313c4de3..34754a13 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java @@ -26,15 +26,21 @@ public class CloudLoggingLogsExporterProvider implements ConfigurableLogRecordEx private final Function> servicesProvider; private final CloudLoggingCredentials.Parser credentialParser; + private final Function trustedCertificatesProvider; public CloudLoggingLogsExporterProvider() { - this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser()); + this(config -> new CloudLoggingServicesProvider(config).get(), + CloudLoggingCredentials.parser(), + credentials -> TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(), + new BindingServerCertificateSource(credentials.getServerCert()))); } CloudLoggingLogsExporterProvider(Function> serviceProvider, - CloudLoggingCredentials.Parser credentialParser) { + CloudLoggingCredentials.Parser credentialParser, + Function trustedCertificatesProvider) { this.servicesProvider = serviceProvider; this.credentialParser = credentialParser; + this.trustedCertificatesProvider = trustedCertificatesProvider; } private static String getCompression(ConfigProperties config) { @@ -68,9 +74,7 @@ private LogRecordExporter createExporter(ConfigProperties config, CloudFoundrySe OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) - .setTrustedCertificates(TrustedCertificatesJoiner.toPemBytes( - new SystemTrustAnchorSource(), - new BindingServerCertificateSource(credentials.getServerCert()))) + .setTrustedCertificates(trustedCertificatesProvider.apply(credentials)) .setRetryPolicy(RetryPolicy.getDefault()); Duration timeOut = getTimeOut(config); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java index d7bdfcbc..c204358f 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java @@ -35,15 +35,21 @@ public class CloudLoggingMetricsExporterProvider implements ConfigurableMetricEx private final Function> servicesProvider; private final CloudLoggingCredentials.Parser credentialParser; + private final Function trustedCertificatesProvider; public CloudLoggingMetricsExporterProvider() { - this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser()); + this(config -> new CloudLoggingServicesProvider(config).get(), + CloudLoggingCredentials.parser(), + credentials -> TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(), + new BindingServerCertificateSource(credentials.getServerCert()))); } CloudLoggingMetricsExporterProvider(Function> serviceProvider, - CloudLoggingCredentials.Parser credentialParser) { + CloudLoggingCredentials.Parser credentialParser, + Function trustedCertificatesProvider) { this.servicesProvider = serviceProvider; this.credentialParser = credentialParser; + this.trustedCertificatesProvider = trustedCertificatesProvider; } private static String getCompression(ConfigProperties config) { @@ -117,9 +123,7 @@ private MetricExporter createExporter(ConfigProperties config, CloudFoundryServi OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) - .setTrustedCertificates(TrustedCertificatesJoiner.toPemBytes( - new SystemTrustAnchorSource(), - new BindingServerCertificateSource(credentials.getServerCert()))) + .setTrustedCertificates(trustedCertificatesProvider.apply(credentials)) .setRetryPolicy(RetryPolicy.getDefault()) .setAggregationTemporalitySelector(getAggregationTemporalitySelector(config)) .setDefaultAggregationSelector(getDefaultAggregationSelector(config)); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java index 96b747c9..e2962d81 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java @@ -26,15 +26,21 @@ public class CloudLoggingSpanExporterProvider implements ConfigurableSpanExporte private final Function> servicesProvider; private final CloudLoggingCredentials.Parser credentialParser; + private final Function trustedCertificatesProvider; public CloudLoggingSpanExporterProvider() { - this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser()); + this(config -> new CloudLoggingServicesProvider(config).get(), + CloudLoggingCredentials.parser(), + credentials -> TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(), + new BindingServerCertificateSource(credentials.getServerCert()))); } CloudLoggingSpanExporterProvider(Function> serviceProvider, - CloudLoggingCredentials.Parser credentialParser) { + CloudLoggingCredentials.Parser credentialParser, + Function trustedCertificatesProvider) { this.servicesProvider = serviceProvider; this.credentialParser = credentialParser; + this.trustedCertificatesProvider = trustedCertificatesProvider; } private static String getCompression(ConfigProperties config) { @@ -68,9 +74,7 @@ private SpanExporter createExporter(ConfigProperties config, CloudFoundryService OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder(); builder.setEndpoint(credentials.getEndpoint()).setCompression(getCompression(config)) .setClientTls(credentials.getClientKey(), credentials.getClientCert()) - .setTrustedCertificates(TrustedCertificatesJoiner.toPemBytes( - new SystemTrustAnchorSource(), - new BindingServerCertificateSource(credentials.getServerCert()))) + .setTrustedCertificates(trustedCertificatesProvider.apply(credentials)) .setRetryPolicy(RetryPolicy.getDefault()); Duration timeOut = getTimeOut(config); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java index ff7d11ba..e56efe44 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSource.java @@ -27,7 +27,7 @@ public BindingServerCertificateSource(byte[] pemBytes) { } @Override - public Stream get() { + public Stream stream() { if (pemBytes == null || pemBytes.length == 0) { return Stream.empty(); } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java index 3f6b5327..b9ba079d 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSource.java @@ -30,7 +30,7 @@ public DownloadedServerCertificateSource(ServerCertificateDownloader downloader, } @Override - public Stream get() { + public Stream stream() { String pem = downloader.download(endpointUrl); if (pem == null || pem.isEmpty()) { return Stream.empty(); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java index b0d94e21..74fa7d25 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoder.java @@ -15,7 +15,7 @@ final class PemEncoder { private static final String LINE_SEPARATOR = "\n"; private static final Base64.Encoder BASE64_ENCODER = - Base64.getMimeEncoder(64, LINE_SEPARATOR.getBytes(StandardCharsets.US_ASCII)); + Base64.getMimeEncoder(64, LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8)); private PemEncoder() { } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java index d73287ab..6fea9453 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSource.java @@ -1,6 +1,5 @@ package com.sap.hcf.cf.logging.opentelemetry.agent.ext.tls; -import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.security.KeyStore; @@ -19,23 +18,24 @@ *

This is the same set the platform uses to validate ordinary HTTPS connections, * so any endpoint whose server certificate chains to a public root is trusted without * additional configuration.

+ * + *

The stream aggregates the accepted issuers of every + * {@link X509TrustManager} returned by the default {@link TrustManagerFactory}, so a + * runtime with multiple configured trust managers contributes all of them.

*/ public class SystemTrustAnchorSource implements X509CertificateSource { private static final Logger LOG = Logger.getLogger(SystemTrustAnchorSource.class.getName()); @Override - public Stream get() { + public Stream stream() { try { TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init((KeyStore) null); - for (TrustManager tm : tmf.getTrustManagers()) { - if (tm instanceof X509TrustManager) { - return Arrays.stream(((X509TrustManager) tm).getAcceptedIssuers()); - } - } - LOG.warning("No X509TrustManager returned by the default TrustManagerFactory; system trust anchors will be omitted."); - return Stream.empty(); + return Arrays.stream(tmf.getTrustManagers()) + .filter(X509TrustManager.class::isInstance) + .map(X509TrustManager.class::cast) + .flatMap(tm -> Arrays.stream(tm.getAcceptedIssuers())); } catch (NoSuchAlgorithmException | KeyStoreException e) { LOG.log(Level.WARNING, e, () -> "Failed to enumerate JVM default trust anchors; system trust anchors will be omitted."); return Stream.empty(); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java index b99d35a2..cd563e3a 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoiner.java @@ -29,7 +29,7 @@ private TrustedCertificatesJoiner() { public static byte[] toPemBytes(X509CertificateSource... sources) { StringBuilder pem = new StringBuilder(); for (X509CertificateSource source : sources) { - source.get().forEach(cert -> appendPem(pem, cert)); + source.stream().forEach(cert -> appendPem(pem, cert)); } return pem.toString().getBytes(StandardCharsets.UTF_8); } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java index 7a39882d..2392c218 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/X509CertificateSource.java @@ -20,5 +20,5 @@ public interface X509CertificateSource { * Returns the certificates supplied by this source. Callers must consume the * stream fully; sources are not required to be repeatable. */ - Stream get(); + Stream stream(); } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProviderTest.java index 8c556039..08055f70 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProviderTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProviderTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; @@ -32,21 +31,24 @@ public class CloudLoggingLogsExporterProviderTest { @Mock private CloudLoggingCredentials.Parser credentialParser; + @Mock(strictness = LENIENT) + private Function trustedCertificatesProvider; + @Mock(strictness = LENIENT) private ConfigProperties config; - @InjectMocks private CloudLoggingLogsExporterProvider exporterProvider; @BeforeEach - void setUp() { + void setUp() throws IOException { when(config.getString(any(), any())).thenAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { return invocation.getArguments()[1]; } }); - + when(trustedCertificatesProvider.apply(any())).thenReturn(PEMUtil.read("certificate.pem")); + exporterProvider = new CloudLoggingLogsExporterProvider(servicesProvider, credentialParser, trustedCertificatesProvider); } @Test @@ -89,7 +91,6 @@ void registersExportersWithValidBindings() throws IOException { when(validCredentials.getEndpoint()).thenReturn("https://otlp-example.sap"); when(validCredentials.getClientCert()).thenReturn(PEMUtil.read("certificate.pem")); when(validCredentials.getClientKey()).thenReturn(PEMUtil.read("private.pem")); - when(validCredentials.getServerCert()).thenReturn(PEMUtil.read("certificate.pem")); when(credentialParser.parse(any())).thenReturn(invalidCredentials).thenReturn(validCredentials); LogRecordExporter exporter = exporterProvider.createExporter(config); assertThat(exporter).isNotNull(); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProviderTest.java index 38770416..d4960bb1 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProviderTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProviderTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; @@ -34,21 +33,24 @@ public class CloudLoggingMetricsExporterProviderTest { @Mock private CloudLoggingCredentials.Parser credentialParser; + @Mock(strictness = LENIENT) + private Function trustedCertificatesProvider; + @Mock(strictness = LENIENT) private ConfigProperties config; - @InjectMocks private CloudLoggingMetricsExporterProvider exporterProvider; @BeforeEach - void setUp() { + void setUp() throws IOException { when(config.getString(any(), any())).thenAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { return invocation.getArguments()[1]; } }); - + when(trustedCertificatesProvider.apply(any())).thenReturn(PEMUtil.read("certificate.pem")); + exporterProvider = new CloudLoggingMetricsExporterProvider(servicesProvider, credentialParser, trustedCertificatesProvider); } @Test @@ -91,7 +93,6 @@ void registersExportersWithValidBindings() throws IOException { when(validCredentials.getEndpoint()).thenReturn("https://otlp-example.sap"); when(validCredentials.getClientCert()).thenReturn(PEMUtil.read("certificate.pem")); when(validCredentials.getClientKey()).thenReturn(PEMUtil.read("private.pem")); - when(validCredentials.getServerCert()).thenReturn(PEMUtil.read("certificate.pem")); when(credentialParser.parse(any())).thenReturn(invalidCredentials).thenReturn(validCredentials); MetricExporter exporter = exporterProvider.createExporter(config); assertThat(exporter).isNotNull(); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProviderTest.java index 788790f6..1f70bafa 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProviderTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProviderTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; @@ -34,21 +33,24 @@ public class CloudLoggingSpanExporterProviderTest { @Mock private CloudLoggingCredentials.Parser credentialParser; + @Mock(strictness = LENIENT) + private Function trustedCertificatesProvider; + @Mock(strictness = LENIENT) private ConfigProperties config; - @InjectMocks private CloudLoggingSpanExporterProvider exporterProvider; @BeforeEach - void setUp() { + void setUp() throws IOException { when(config.getString(any(), any())).thenAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { return invocation.getArguments()[1]; } }); - + when(trustedCertificatesProvider.apply(any())).thenReturn(PEMUtil.read("certificate.pem")); + exporterProvider = new CloudLoggingSpanExporterProvider(servicesProvider, credentialParser, trustedCertificatesProvider); } @Test @@ -91,7 +93,6 @@ void registersExportersWithValidBindings() throws IOException { when(validCredentials.getEndpoint()).thenReturn("https://otlp-example.sap"); when(validCredentials.getClientCert()).thenReturn(PEMUtil.read("certificate.pem")); when(validCredentials.getClientKey()).thenReturn(PEMUtil.read("private.pem")); - when(validCredentials.getServerCert()).thenReturn(PEMUtil.read("certificate.pem")); when(credentialParser.parse(any())).thenReturn(invalidCredentials).thenReturn(validCredentials); SpanExporter exporter = exporterProvider.createExporter(config); assertThat(exporter).isNotNull(); diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java index b3f6aa8e..b0042756 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/BindingServerCertificateSourceTest.java @@ -6,13 +6,14 @@ import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; class BindingServerCertificateSourceTest { private static byte[] validPemBytes() throws Exception { try (InputStream is = BindingServerCertificateSourceTest.class.getClassLoader() .getResourceAsStream("certificate.pem")) { - assertThat(is).isNotNull(); + assumeThat(is).as("test resource certificate.pem must be present on the classpath").isNotNull(); return is.readAllBytes(); } } @@ -21,21 +22,21 @@ private static byte[] validPemBytes() throws Exception { void parsesValidPemIntoASingleCertificate() throws Exception { BindingServerCertificateSource source = new BindingServerCertificateSource(validPemBytes()); - assertThat(source.get()).hasSize(1); + assertThat(source.stream()).hasSize(1); } @Test void yieldsEmptyStreamForNullInput() { BindingServerCertificateSource source = new BindingServerCertificateSource(null); - assertThat(source.get()).isEmpty(); + assertThat(source.stream()).isEmpty(); } @Test void yieldsEmptyStreamForEmptyInput() { BindingServerCertificateSource source = new BindingServerCertificateSource(new byte[0]); - assertThat(source.get()).isEmpty(); + assertThat(source.stream()).isEmpty(); } @Test @@ -43,6 +44,6 @@ void yieldsEmptyStreamForMalformedInput() { BindingServerCertificateSource source = new BindingServerCertificateSource("not a pem".getBytes(StandardCharsets.UTF_8)); - assertThat(source.get()).isEmpty(); + assertThat(source.stream()).isEmpty(); } } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java index 7cec1867..2f69342a 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/DownloadedServerCertificateSourceTest.java @@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -22,7 +23,7 @@ class DownloadedServerCertificateSourceTest { private static String validPem() throws Exception { try (InputStream is = DownloadedServerCertificateSourceTest.class.getClassLoader() .getResourceAsStream("certificate.pem")) { - assertThat(is).isNotNull(); + assumeThat(is).as("test resource certificate.pem must be present on the classpath").isNotNull(); return new String(is.readAllBytes(), StandardCharsets.UTF_8); } } @@ -32,7 +33,7 @@ void parsesDownloadedPemIntoASingleCertificate() throws Exception { when(downloader.download(ENDPOINT)).thenReturn(validPem()); DownloadedServerCertificateSource source = new DownloadedServerCertificateSource(downloader, ENDPOINT); - assertThat(source.get()).hasSize(1); + assertThat(source.stream()).hasSize(1); } @Test @@ -40,7 +41,7 @@ void yieldsEmptyStreamWhenDownloaderReturnsNull() { when(downloader.download(ENDPOINT)).thenReturn(null); DownloadedServerCertificateSource source = new DownloadedServerCertificateSource(downloader, ENDPOINT); - assertThat(source.get()).isEmpty(); + assertThat(source.stream()).isEmpty(); } @Test @@ -48,6 +49,6 @@ void yieldsEmptyStreamWhenDownloaderReturnsMalformedPem() { when(downloader.download(ENDPOINT)).thenReturn("not a pem"); DownloadedServerCertificateSource source = new DownloadedServerCertificateSource(downloader, ENDPOINT); - assertThat(source.get()).isEmpty(); + assertThat(source.stream()).isEmpty(); } } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java index 38846842..aa89c691 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/PemEncoderTest.java @@ -9,37 +9,40 @@ import java.security.cert.X509Certificate; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; class PemEncoderTest { - static X509Certificate loadTestCertificate() throws Exception { + private static final X509Certificate CERTIFICATE = loadTestCertificate(); + + private static X509Certificate loadTestCertificate() { try (InputStream is = PemEncoderTest.class.getClassLoader().getResourceAsStream("certificate.pem")) { - assertThat(is).isNotNull(); + assumeThat(is).as("test resource certificate.pem must be present on the classpath").isNotNull(); return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); + } catch (Exception e) { + throw new IllegalStateException("Failed to load test certificate", e); } } @Test void encodesCertificateWithArmorAndTrailingNewline() throws Exception { - String pem = PemEncoder.encode(loadTestCertificate()); + String pem = PemEncoder.encode(CERTIFICATE); assertThat(pem).startsWith("-----BEGIN CERTIFICATE-----\n").endsWith("-----END CERTIFICATE-----\n"); } @Test void encodedOutputRoundtripsBackToTheSameCertificate() throws Exception { - X509Certificate original = loadTestCertificate(); - - String pem = PemEncoder.encode(original); + String pem = PemEncoder.encode(CERTIFICATE); X509Certificate parsed = (X509Certificate) CertificateFactory.getInstance("X.509") .generateCertificate(new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8))); - assertThat(parsed).isEqualTo(original); + assertThat(parsed).isEqualTo(CERTIFICATE); } @Test void wrapsBase64BodyAt64Characters() throws Exception { - String pem = PemEncoder.encode(loadTestCertificate()); + String pem = PemEncoder.encode(CERTIFICATE); String body = pem.replace("-----BEGIN CERTIFICATE-----\n", "").replace("\n-----END CERTIFICATE-----\n", ""); for (String line : body.split("\n")) { @@ -52,7 +55,7 @@ void wrapsBase64BodyAt64Characters() throws Exception { void isCompatibleWithPreviousServerCertificateDownloaderFormat() throws Exception { // Guard against accidental drift: the format must remain interchangeable with the // one previously produced by ServerCertificateDownloader#download. - String pem = PemEncoder.encode(loadTestCertificate()); + String pem = PemEncoder.encode(CERTIFICATE); assertThat(pem).matches("(?s)^-----BEGIN CERTIFICATE-----\\n([A-Za-z0-9+/=]{1,64}\\n)+-----END CERTIFICATE-----\\n$"); } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java index 933a8dd6..d57ed1c8 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/SystemTrustAnchorSourceTest.java @@ -12,16 +12,16 @@ class SystemTrustAnchorSourceTest { void yieldsTheJvmDefaultTrustAnchors() { SystemTrustAnchorSource source = new SystemTrustAnchorSource(); - assertThat(source.get()).isNotEmpty() - .allSatisfy(cert -> assertThat(cert).isInstanceOf(X509Certificate.class)); + assertThat(source.stream()).isNotEmpty() + .allSatisfy(cert -> assertThat(cert).isInstanceOf(X509Certificate.class)); } @Test void repeatedInvocationsProduceIndependentStreams() { SystemTrustAnchorSource source = new SystemTrustAnchorSource(); - long first = source.get().count(); - long second = source.get().count(); + long first = source.stream().count(); + long second = source.stream().count(); assertThat(first).isPositive().isEqualTo(second); } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java index 15ed6bbf..11764c0a 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/tls/TrustedCertificatesJoinerTest.java @@ -6,13 +6,14 @@ import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; class TrustedCertificatesJoinerTest { private static byte[] validPemBytes() throws Exception { try (InputStream is = TrustedCertificatesJoinerTest.class.getClassLoader() .getResourceAsStream("certificate.pem")) { - assertThat(is).isNotNull(); + assumeThat(is).as("test resource certificate.pem must be present on the classpath").isNotNull(); return is.readAllBytes(); } } @@ -39,17 +40,9 @@ void concatenatesCertificatesFromMultipleSourcesInOrder() throws Exception { long beginMarkers = pem.lines().filter(l -> l.equals("-----BEGIN CERTIFICATE-----")).count(); long endMarkers = pem.lines().filter(l -> l.equals("-----END CERTIFICATE-----")).count(); assertThat(beginMarkers).isEqualTo(endMarkers).isGreaterThan(1); - assertThat(pem).endsWith("-----END CERTIFICATE-----\n"); - } - - @Test - void bindingCertificateAppearsLastInTheJoinedOutput() throws Exception { - byte[] result = TrustedCertificatesJoiner.toPemBytes(new SystemTrustAnchorSource(), - new BindingServerCertificateSource(validPemBytes())); - // Compare against the encoding of the binding cert alone: it must appear as the - // suffix of the joined output. + // The binding source is passed last, so its PEM must appear at the end of the joined output. byte[] bindingOnly = TrustedCertificatesJoiner.toPemBytes(new BindingServerCertificateSource(validPemBytes())); - assertThat(new String(result, StandardCharsets.UTF_8)).endsWith(new String(bindingOnly, StandardCharsets.UTF_8)); + assertThat(pem).endsWith(new String(bindingOnly, StandardCharsets.UTF_8)); } }