Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

abstract class CloudFoundryContainerTrustManagerFactory extends TrustManagerFactorySpi {
Expand All @@ -44,6 +45,8 @@ abstract class CloudFoundryContainerTrustManagerFactory extends TrustManagerFact

private static FileWatchingX509ExtendedTrustManager CACHED_CONTAINER_TRUST_MANAGER;

private static boolean SYSTEM_SUMMARY_LOGGED;

private final Logger logger = Logger.getLogger(this.getClass().getName());

private final String algorithm;
Expand All @@ -54,6 +57,8 @@ abstract class CloudFoundryContainerTrustManagerFactory extends TrustManagerFact

private X509ExtendedTrustManager cachedSystemTrustManager;

private String systemTrustSource;

private CloudFoundryContainerTrustManagerFactory(String algorithm, Path certificates) {
this.algorithm = algorithm;
this.certificates = certificates;
Expand Down Expand Up @@ -83,13 +88,15 @@ protected final TrustManager[] engineGetTrustManagers() {
@Override
protected final void engineInit(ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException {
this.systemTrustManagerFactory.init(managerFactoryParameters);
invalidateSystemTrustManager();
invalidateSystemTrustManager("ManagerFactoryParameters passed to init");
}

@Override
protected final void engineInit(KeyStore keyStore) throws KeyStoreException {
this.systemTrustManagerFactory.init(keyStore);
invalidateSystemTrustManager();
// init(null) reads the JVM trust store configuration now, so describe it now rather than when the managers are retrieved.
invalidateSystemTrustManager(keyStore != null ? "KeyStore passed to init"
: TrustStoreDiagnostics.systemTrustStore());
}

private static Path getCertificatesLocation() {
Expand All @@ -113,8 +120,8 @@ private X509ExtendedTrustManager getSystemTrustManager() {
if (this.cachedSystemTrustManager == null) {
for (TrustManager candidate : this.systemTrustManagerFactory.getTrustManagers()) {
if (candidate instanceof X509ExtendedTrustManager) {
this.logger.fine("Adding System Trust Manager");
this.cachedSystemTrustManager = (X509ExtendedTrustManager) candidate;
logSystemSummary();
break;
}
}
Expand All @@ -124,6 +131,19 @@ private X509ExtendedTrustManager getSystemTrustManager() {
}
}

// Logged at INFO once per JVM; factories are created often (e.g. per connection), so later ones log at FINE (see #17).
private void logSystemSummary() {
Level level = SYSTEM_SUMMARY_LOGGED ? Level.FINE : Level.INFO;
SYSTEM_SUMMARY_LOGGED = true;
Comment thread
stokpop marked this conversation as resolved.
Resolved

this.logger.log(level, TrustStoreDiagnostics.safe(() -> String.format("Adding System TrustManager (%s) with %d trusted certificates",
this.algorithm, this.cachedSystemTrustManager.getAcceptedIssuers().length)));
if (this.systemTrustSource != null) {
this.logger.log(level, () -> String.format("System trust store: %s", this.systemTrustSource));
}
this.logger.log(level, TrustStoreDiagnostics.safe(() -> String.format("Blocked: %s", TrustStoreDiagnostics.blocked())));
}

private TrustManagerFactory getTrustManagerFactory() {
try {
return TrustManagerFactory.getInstance(this.algorithm, "SunJSSE");
Expand All @@ -132,9 +152,10 @@ private TrustManagerFactory getTrustManagerFactory() {
}
}

private void invalidateSystemTrustManager() {
private void invalidateSystemTrustManager(String source) {
synchronized (MONITOR) {
this.cachedSystemTrustManager = null;
this.systemTrustSource = source;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ final class FileWatchingX509ExtendedTrustManager extends X509ExtendedTrustManage
new FileWatcher(this.certificates, new FileWatcherCallback()).watch();

if (this.trustManager.compareAndSet(null, getTrustManager(getKeyStore()))) {
this.logger.info(() -> String.format("Initialized TrustManager for %s", this.certificates));
this.logger.info(TrustStoreDiagnostics.safe(() -> String.format("Initialized TrustManager for %s with %d trusted certificates", this.certificates, this.trustManager.get().getAcceptedIssuers().length)));
}
}

Expand Down Expand Up @@ -123,9 +123,9 @@ private class FileWatcherCallback implements Runnable {
@Override
public void run() {
if (FileWatchingX509ExtendedTrustManager.this.trustManager.getAndSet(getTrustManager(getKeyStore())) == null) {
FileWatchingX509ExtendedTrustManager.this.logger.info(() -> String.format("Initialized TrustManager for %s", FileWatchingX509ExtendedTrustManager.this.certificates));
FileWatchingX509ExtendedTrustManager.this.logger.info(TrustStoreDiagnostics.safe(() -> String.format("Initialized TrustManager for %s with %d trusted certificates", FileWatchingX509ExtendedTrustManager.this.certificates, FileWatchingX509ExtendedTrustManager.this.getAcceptedIssuers().length)));
Comment thread
stokpop marked this conversation as resolved.
Resolved
} else {
FileWatchingX509ExtendedTrustManager.this.logger.info(() -> String.format("Updated TrustManager for %s", FileWatchingX509ExtendedTrustManager.this.certificates));
FileWatchingX509ExtendedTrustManager.this.logger.info(TrustStoreDiagnostics.safe(() -> String.format("Updated TrustManager for %s with %d trusted certificates", FileWatchingX509ExtendedTrustManager.this.certificates, FileWatchingX509ExtendedTrustManager.this.getAcceptedIssuers().length)));
Comment thread
stokpop marked this conversation as resolved.
Resolved
}
}

Expand Down
104 changes: 104 additions & 0 deletions src/main/java/org/cloudfoundry/security/TrustStoreDiagnostics.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.cloudfoundry.security;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Security;
import java.util.List;
import java.util.function.Supplier;
import java.util.regex.Pattern;

/**
* Describes, for logging, which system trust store the JVM uses and which certificates the JDK blocks or distrusts.
*/
final class TrustStoreDiagnostics {

private static final Pattern FINGERPRINT = Pattern.compile("[0-9A-Fa-f]{40,}=?");

private TrustStoreDiagnostics() {
}

// Reports the trust store properties as set, without interpreting them: the JDK does not expose which store it loaded, and its
// rules (defaults, empty or special values, store types) are internal. The certificate count shows what was actually loaded.
// Returns null when neither property is set, which is the default and needs no logging.
static String systemTrustStore() {
return bestEffort(() -> {
if (System.getProperty("javax.net.ssl.trustStore") == null && System.getProperty("javax.net.ssl.trustStoreType") == null) {
return null;
}

return String.format("javax.net.ssl.trustStore=%s, javax.net.ssl.trustStoreType=%s",
valueOrNotSet("javax.net.ssl.trustStore"), valueOrNotSet("javax.net.ssl.trustStoreType"));
});
}

private static String valueOrNotSet(String property) {
String value = System.getProperty(property);
return value != null ? value : "not set";
}

static String blocked() {
return bestEffort(() -> blocked(Paths.get(System.getProperty("java.home", ""), "lib", "security")));
}

// Diagnostics must never prevent TLS setup, for example when a SecurityManager denies a property or file read or a path is malformed.
static String bestEffort(Supplier<String> diagnostic) {
try {
return diagnostic.get();
} catch (SecurityException e) {
return "unavailable (access denied)";
} catch (RuntimeException e) {
return "unavailable (" + e.getClass().getSimpleName() + ")";
}
}

static Supplier<String> safe(Supplier<String> message) {
return () -> bestEffort(message);
}

static String blocked(Path security) {
String distrust = Security.getProperty("jdk.security.caDistrustPolicies");
String policies = distrust == null || distrust.isEmpty() ? "none" : distrust;
Integer count = countBlockedCertificates(security);
return count == null
? String.format("unknown (no readable blocked.certs or blacklisted.certs), caDistrustPolicies=%s", policies)
: String.format("%d blocked certificates, caDistrustPolicies=%s", count, policies);
}

// Java 8 names the file blacklisted.certs; newer JDKs use blocked.certs. Returns null when neither is readable.
private static Integer countBlockedCertificates(Path security) {
for (String name : new String[]{"blocked.certs", "blacklisted.certs"}) {
try {
List<String> lines = Files.readAllLines(security.resolve(name));
return (int) lines.stream().filter(TrustStoreDiagnostics::isFingerprintLine).count();
} catch (IOException e) {
// try the next name
}
}

return null;
}

// The blocklist holds one hex fingerprint per line (optionally with an empty "=" value), mixed with comments and blank lines.
private static boolean isFingerprintLine(String line) {
return FINGERPRINT.matcher(line.trim()).matches();
Comment thread
stokpop marked this conversation as resolved.
Resolved
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@

import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.LogRecord;
Expand Down Expand Up @@ -54,15 +60,131 @@ public void defaultTrustManagerForNullCertificatesLocation() throws CertificateE
assertThat(((DelegatingX509ExtendedTrustManager) trustManager).size()).isEqualTo(1);
}

// Logging the summary at INFO for every factory init (e.g. per new connection) was the problem fixed in #17.
@Test
public void logsSystemTrustManagerAtFine() throws Exception {
public void logsSummaryAtInfoOnceThenAtFine() throws Exception {
String store = System.getProperty("javax.net.ssl.trustStore");
String type = System.getProperty("javax.net.ssl.trustStoreType");
System.clearProperty("javax.net.ssl.trustStore");
System.clearProperty("javax.net.ssl.trustStoreType");
List<LogRecord> records;
try {
records = recordSummaryOfTwoInitsInIsolatedClassLoader();
} finally {
restoreProperty("javax.net.ssl.trustStore", store);
restoreProperty("javax.net.ssl.trustStoreType", type);
}

// With the default configuration there is no trust store line: only the count and the blocklist.
assertThat(records).extracting(LogRecord::getMessage).noneMatch(m -> m.startsWith("System trust store:"));
assertThat(records).extracting(LogRecord::getLevel).containsExactly(Level.INFO, Level.INFO, Level.FINE, Level.FINE);
}

@Test
public void systemSummaryContainsCertificateCount() throws Exception {

assertThat(recordSystemSummary()).extracting(LogRecord::getMessage).anyMatch(m -> m.matches("Adding System TrustManager \\(PKIX\\) with \\d+ trusted certificates"));
}

@Test
public void logsSuppliedKeyStoreAsSystemTrustStoreSource() throws Exception {
List<LogRecord> records = LogRecorder.record(CloudFoundryContainerTrustManagerFactory.PKIXFactory.class, () -> {
CloudFoundryContainerTrustManagerFactory.PKIXFactory factory = new CloudFoundryContainerTrustManagerFactory.PKIXFactory(null);
factory.engineInit(getKeyStore());
factory.engineGetTrustManagers();
});

assertThat(records).filteredOn(r -> "Adding System Trust Manager".equals(r.getMessage())).extracting(LogRecord::getLevel).containsExactly(Level.FINE);
assertThat(records).extracting(LogRecord::getMessage).contains("System trust store: KeyStore passed to init");
}

@Test
public void systemTrustStoreSourceIsCapturedAtInit() throws Exception {
String original = System.getProperty("javax.net.ssl.trustStore");
String cacerts = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts").toString();

List<LogRecord> records = LogRecorder.record(CloudFoundryContainerTrustManagerFactory.PKIXFactory.class, () -> {
CloudFoundryContainerTrustManagerFactory.PKIXFactory factory = new CloudFoundryContainerTrustManagerFactory.PKIXFactory(null);
System.setProperty("javax.net.ssl.trustStore", cacerts);
try {
factory.engineInit((KeyStore) null);
} finally {
restoreProperty("javax.net.ssl.trustStore", original);
}
factory.engineGetTrustManagers();
});

assertThat(records).extracting(LogRecord::getMessage)
.contains("System trust store: javax.net.ssl.trustStore=" + cacerts + ", javax.net.ssl.trustStoreType=not set");
}

@Test
public void summaryFailureDoesNotBreakInit() throws Exception {
String javaHome = System.getProperty("java.home");
System.setProperty("java.home", "bad\0path");
try {
List<LogRecord> records = LogRecorder.record(CloudFoundryContainerTrustManagerFactory.PKIXFactory.class, () -> {
CloudFoundryContainerTrustManagerFactory.PKIXFactory factory = new CloudFoundryContainerTrustManagerFactory.PKIXFactory(null);
factory.engineInit((KeyStore) null);
assertThat(factory.engineGetTrustManagers()).isNotEmpty();
});

assertThat(records).extracting(LogRecord::getMessage).contains("Blocked: unavailable (InvalidPathException)");
} finally {
System.setProperty("java.home", javaHome);
}
}

// The once-per-JVM state is static, so a fresh class loader gives every run a clean state without test hooks in the main code.
private List<LogRecord> recordSummaryOfTwoInitsInIsolatedClassLoader() throws Exception {
URL classes = Paths.get("target/classes").toUri().toURL();
try (URLClassLoader isolated = new URLClassLoader(new URL[]{classes}, ClassLoader.getSystemClassLoader().getParent())) {
Class<?> factoryClass = isolated.loadClass(CloudFoundryContainerTrustManagerFactory.PKIXFactory.class.getName());
Constructor<?> constructor = factoryClass.getDeclaredConstructor(Path.class);
constructor.setAccessible(true);
Method init = factoryClass.getSuperclass().getDeclaredMethod("engineInit", KeyStore.class);
init.setAccessible(true);
Method getTrustManagers = factoryClass.getSuperclass().getDeclaredMethod("engineGetTrustManagers");
getTrustManagers.setAccessible(true);

return filterSummary(LogRecorder.record(CloudFoundryContainerTrustManagerFactory.PKIXFactory.class, () -> {
for (int i = 0; i < 2; i++) {
Object factory = constructor.newInstance((Path) null);
init.invoke(factory, new Object[]{null});
getTrustManagers.invoke(factory);
}
}));
}
}

private static void restoreProperty(String key, String value) {
if (value == null) {
System.clearProperty(key);
} else {
System.setProperty(key, value);
}
}

private List<LogRecord> recordSystemSummary() throws Exception {
List<LogRecord> records = LogRecorder.record(CloudFoundryContainerTrustManagerFactory.PKIXFactory.class, () -> {
CloudFoundryContainerTrustManagerFactory.PKIXFactory factory = new CloudFoundryContainerTrustManagerFactory.PKIXFactory(null);
factory.engineInit(getKeyStore());
factory.engineGetTrustManagers();
});

return filterSummary(records);
}

private static List<LogRecord> filterSummary(List<LogRecord> records) {
List<LogRecord> summary = new ArrayList<>();
for (LogRecord record : records) {
String message = record.getMessage();
if (message.startsWith("Adding System TrustManager") || message.startsWith("System trust store:")
|| message.startsWith("Blocked:")) {
summary.add(record);
}
}

return summary;
}

private KeyStore getKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
Expand Down
Loading
Loading