diff --git a/src/main/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactory.java b/src/main/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactory.java index b783ecf..e1b4c9e 100644 --- a/src/main/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactory.java +++ b/src/main/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactory.java @@ -63,9 +63,9 @@ private CloudFoundryContainerKeyManagerFactory(String algorithm, Path certificat this.privateKey = privateKey; this.systemKeyManagerFactory = getKeyManagerFactory(); - this.logger.fine(String.format("Algorithm: %s", algorithm)); - this.logger.fine(String.format("Certificates: %s", certificates)); - this.logger.fine(String.format("Private Key: %s", privateKey)); + this.logger.fine(() -> String.format("Algorithm: %s", algorithm)); + this.logger.fine(() -> String.format("Certificates: %s", certificates)); + this.logger.fine(() -> String.format("Private Key: %s", privateKey)); } @Override @@ -105,7 +105,7 @@ private static Path getProperty(String name) { private FileWatchingX509ExtendedKeyManager getContainerKeyManager() { synchronized (MONITOR) { if (CACHED_CONTAINER_KEY_MANAGER == null && this.certificates != null && Files.exists(this.certificates) && this.privateKey != null && Files.exists(this.privateKey)) { - this.logger.info(String.format("Adding Key Manager for %s and %s", this.privateKey, this.certificates)); + this.logger.info(() -> String.format("Adding Key Manager for %s and %s", this.privateKey, this.certificates)); CACHED_CONTAINER_KEY_MANAGER = new FileWatchingX509ExtendedKeyManager(this.certificates, this.privateKey, getKeyManagerFactory()); } @@ -126,7 +126,7 @@ private X509ExtendedKeyManager getSystemKeyManager() { if (this.cachedSystemKeyManager == null) { for (KeyManager candidate : this.systemKeyManagerFactory.getKeyManagers()) { if (candidate instanceof X509ExtendedKeyManager) { - this.logger.info("Adding System Key Manager"); + this.logger.fine("Adding System Key Manager"); this.cachedSystemKeyManager = (X509ExtendedKeyManager) candidate; break; } diff --git a/src/main/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactory.java b/src/main/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactory.java index 86287d7..ad1025d 100644 --- a/src/main/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactory.java +++ b/src/main/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactory.java @@ -59,8 +59,8 @@ private CloudFoundryContainerTrustManagerFactory(String algorithm, Path certific this.certificates = certificates; this.systemTrustManagerFactory = getTrustManagerFactory(); - this.logger.fine(String.format("Algorithm: %s", algorithm)); - this.logger.fine(String.format("Certificates: %s", certificates)); + this.logger.fine(() -> String.format("Algorithm: %s", algorithm)); + this.logger.fine(() -> String.format("Certificates: %s", certificates)); } @Override @@ -100,7 +100,7 @@ private static Path getCertificatesLocation() { private FileWatchingX509ExtendedTrustManager getContainerTrustManager() { synchronized (MONITOR) { if (CACHED_CONTAINER_TRUST_MANAGER == null && this.certificates != null && Files.exists(this.certificates)) { - this.logger.info(String.format("Adding TrustManager for %s", this.certificates)); + this.logger.info(() -> String.format("Adding TrustManager for %s", this.certificates)); CACHED_CONTAINER_TRUST_MANAGER = new FileWatchingX509ExtendedTrustManager(this.certificates, getTrustManagerFactory()); } @@ -113,7 +113,7 @@ private X509ExtendedTrustManager getSystemTrustManager() { if (this.cachedSystemTrustManager == null) { for (TrustManager candidate : this.systemTrustManagerFactory.getTrustManagers()) { if (candidate instanceof X509ExtendedTrustManager) { - this.logger.info("Adding System Trust Manager"); + this.logger.fine("Adding System Trust Manager"); this.cachedSystemTrustManager = (X509ExtendedTrustManager) candidate; break; } diff --git a/src/main/java/org/cloudfoundry/security/FileWatcher.java b/src/main/java/org/cloudfoundry/security/FileWatcher.java index 5412b32..cb30e07 100644 --- a/src/main/java/org/cloudfoundry/security/FileWatcher.java +++ b/src/main/java/org/cloudfoundry/security/FileWatcher.java @@ -62,7 +62,7 @@ public Thread newThread(Runnable r) { @Override public void run() { - this.logger.info(String.format("Start watching %s", this.source)); + this.logger.info(() -> String.format("Start watching %s", this.source)); WatchService watchService; WatchKey expected; @@ -81,7 +81,7 @@ public void run() { WatchKey actual = watchService.take(); if (!actual.equals(expected)) { - this.logger.warning(String.format("Unknown watch key: %s", actual)); + this.logger.warning(() -> String.format("Unknown watch key: %s", actual)); continue; } @@ -89,14 +89,14 @@ public void run() { Path changed = (Path) watchEvent.context(); if (!this.source.getFileName().equals(changed)) { - this.logger.fine(String.format("Discarding unimportant file change: %s", changed)); + this.logger.fine(() -> String.format("Discarding unimportant file change: %s", changed)); continue; } this.callback.run(); } if (!actual.reset()) { - this.logger.warning(String.format("Watch key is no longer valid: %s", actual)); + this.logger.warning(() -> String.format("Watch key is no longer valid: %s", actual)); break; } } catch (InterruptedException e) { @@ -106,7 +106,7 @@ public void run() { } } - this.logger.info(String.format("Stop watching %s", this.source)); + this.logger.info(() -> String.format("Stop watching %s", this.source)); } @Override diff --git a/src/main/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManager.java b/src/main/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManager.java index 9fcc7e8..a3e16ef 100644 --- a/src/main/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManager.java +++ b/src/main/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManager.java @@ -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(() -> String.format("Initialized TrustManager for %s", this.certificates)); } } @@ -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(() -> String.format("Initialized TrustManager for %s", FileWatchingX509ExtendedTrustManager.this.certificates)); } else { - FileWatchingX509ExtendedTrustManager.this.logger.info(String.format("Updated TrustManager for %s", FileWatchingX509ExtendedTrustManager.this.certificates)); + FileWatchingX509ExtendedTrustManager.this.logger.info(() -> String.format("Updated TrustManager for %s", FileWatchingX509ExtendedTrustManager.this.certificates)); } } diff --git a/src/test/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactoryTest.java b/src/test/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactoryTest.java index 4a065cc..9644c15 100644 --- a/src/test/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactoryTest.java +++ b/src/test/java/org/cloudfoundry/security/CloudFoundryContainerKeyManagerFactoryTest.java @@ -27,11 +27,25 @@ import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.LogRecord; import static org.assertj.core.api.Assertions.assertThat; public final class CloudFoundryContainerKeyManagerFactoryTest extends AbstractLoggingTest { + @Test + public void logsSystemKeyManagerAtFine() throws Exception { + List records = LogRecorder.record(CloudFoundryContainerKeyManagerFactory.SunX509.class, () -> { + CloudFoundryContainerKeyManagerFactory.SunX509 factory = new CloudFoundryContainerKeyManagerFactory.SunX509(null, null); + factory.engineInit(getKeyStore(), new char[0]); + factory.engineGetKeyManagers(); + }); + + assertThat(records).filteredOn(r -> "Adding System Key Manager".equals(r.getMessage())).extracting(LogRecord::getLevel).containsExactly(Level.FINE); + } + @Test public void customKeyManager() throws NoSuchProviderException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, UnrecoverableKeyException { CloudFoundryContainerKeyManagerFactory.SunX509 factory = new CloudFoundryContainerKeyManagerFactory.SunX509( diff --git a/src/test/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactoryTest.java b/src/test/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactoryTest.java index d6b1752..71566c0 100644 --- a/src/test/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactoryTest.java +++ b/src/test/java/org/cloudfoundry/security/CloudFoundryContainerTrustManagerFactoryTest.java @@ -26,6 +26,9 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.cert.CertificateException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.LogRecord; import static org.assertj.core.api.Assertions.assertThat; @@ -51,6 +54,17 @@ public void defaultTrustManagerForNullCertificatesLocation() throws CertificateE assertThat(((DelegatingX509ExtendedTrustManager) trustManager).size()).isEqualTo(1); } + @Test + public void logsSystemTrustManagerAtFine() throws Exception { + List 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); + } + private KeyStore getKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); diff --git a/src/test/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManagerTest.java b/src/test/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManagerTest.java index 44e2652..0147a9a 100644 --- a/src/test/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManagerTest.java +++ b/src/test/java/org/cloudfoundry/security/FileWatchingX509ExtendedTrustManagerTest.java @@ -51,9 +51,12 @@ public void watchesWatchedFile() throws IOException, InterruptedException, NoSuc assertThat(trustManager.getAcceptedIssuers()).hasSize(48); Thread.sleep(5_000); - Files.copy(Paths.get("src/test/resources/server-certificates-173.pem"), watchedCertificates, StandardCopyOption.REPLACE_EXISTING); + // Replace atomically, as a rename, so no update sees a half-written file. + Path replacement = Files.createTempFile(watchedCertificates.getParent(), "certificates-", ".tmp"); + Files.copy(Paths.get("src/test/resources/server-certificates-173.pem"), replacement, StandardCopyOption.REPLACE_EXISTING); + Files.move(replacement, watchedCertificates, StandardCopyOption.ATOMIC_MOVE); - long timeout = System.currentTimeMillis() + 300_000; + long timeout = System.currentTimeMillis() + 30_000; for (; ; ) { if (System.currentTimeMillis() > timeout) { fail("Failed to update within timeout"); diff --git a/src/test/java/org/cloudfoundry/security/LogRecorder.java b/src/test/java/org/cloudfoundry/security/LogRecorder.java new file mode 100644 index 0000000..af13fae --- /dev/null +++ b/src/test/java/org/cloudfoundry/security/LogRecorder.java @@ -0,0 +1,71 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +final class LogRecorder { + + private LogRecorder() { + } + + // Records everything logged to the logger named after the given class while the action runs, at all levels. + static List record(Class loggerClass, Action action) throws Exception { + Logger logger = Logger.getLogger(loggerClass.getName()); + List records = new ArrayList<>(); + Handler handler = new Handler() { + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + + }; + + Level level = logger.getLevel(); + logger.addHandler(handler); + logger.setLevel(Level.ALL); + try { + action.run(); + } finally { + logger.removeHandler(handler); + logger.setLevel(level); + } + + return records; + } + + interface Action { + + void run() throws Exception; + + } + +}