This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 6527a10452 Review "Refactor certificate loading"
6527a10452 is described below

commit 6527a1045257c2df14b345500b9d6331af3adc8e
Author: Mark Thomas <[email protected]>
AuthorDate: Tue Nov 3 14:23:01 2026 +0000

    Review "Refactor certificate loading"
    
    Restore logging certificate errors at ERROR.
    Add debug logging for load failures to CertificateLoader
---
 .../util/net/openssl/panama/CertificateLoader.java |  37 ++--
 .../net/openssl/panama/LocalStrings.properties     |   1 +
 .../tomcat/util/net/TestSSLHostConfigCompat.java   | 194 ++++++++++++++++++++-
 3 files changed, 211 insertions(+), 21 deletions(-)

diff --git 
a/java/org/apache/tomcat/util/net/openssl/panama/CertificateLoader.java 
b/java/org/apache/tomcat/util/net/openssl/panama/CertificateLoader.java
index 454c54c007..2decfcbdd8 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/CertificateLoader.java
+++ b/java/org/apache/tomcat/util/net/openssl/panama/CertificateLoader.java
@@ -173,7 +173,7 @@ public final class CertificateLoader {
                 if (keyPassFile == null) {
                     keyPassFile = 
certificate.getCertificateKeystorePasswordFile();
                 }
-                
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName, 
keyPassFile), ioe);
+                
log.error(sm.getString("certificateLoader.certificateLoadFailed", hostName, 
keyPassFile), ioe);
                 return null;
             }
 
@@ -190,7 +190,7 @@ public final class CertificateLoader {
             }
             return loadKeyStore(certificate, hostName, keyPassToUse, arena);
         } catch (Throwable t) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
                     t.getClass().getName() + (t.getMessage() != null ? ": " + 
t.getMessage() : "")), t);
             return null;
         }
@@ -320,12 +320,12 @@ public final class CertificateLoader {
 
         byte[] certBytes = readConfigFileBytes(certFile);
         if (certBytes == null) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, certFile));
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, certFile));
             return null;
         }
         byte[] keyBytes = readConfigFileBytes(keyFile);
         if (keyBytes == null) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, keyFile));
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, keyFile));
             return null;
         }
 
@@ -347,7 +347,7 @@ public final class CertificateLoader {
             for (MemorySegment extra : chain) {
                 openssl_h.X509_free(extra);
             }
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
                     certFile + " (" + OpenSSLLibrary.getLastError() + ")"));
             return null;
         }
@@ -365,7 +365,7 @@ public final class CertificateLoader {
                 openssl_h.X509_free(extra);
             }
             openssl_h.X509_free(leaf);
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
                     keyFile + " (" + OpenSSLLibrary.getLastError() + ")"));
             return null;
         }
@@ -396,7 +396,7 @@ public final class CertificateLoader {
             String certFile, String keyPassToUse, Arena arena) {
         byte[] fileBytes = readConfigFileBytes(certFile);
         if (fileBytes == null) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, certFile));
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, certFile));
             return null;
         }
 
@@ -421,11 +421,11 @@ public final class CertificateLoader {
             return entry;
         } catch (IOException e) {
             // Covers a wrong password: the JDK fails the MAC check
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
                     certFile + " (" + 
sm.getString("certificateLoader.badPassword") + ")"));
             return null;
         } catch (Throwable t) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
                     certFile + " (" + t.getClass().getName() +
                     (t.getMessage() != null ? ": " + t.getMessage() : "") + 
")"), t);
             return null;
@@ -452,12 +452,12 @@ public final class CertificateLoader {
         try {
             keyStore = certificate.getCertificateKeystore();
         } catch (Throwable e) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
                     certificate.getCertificateKeystoreFile()), e);
             return null;
         }
         if (keyStore == null) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName,
                     certificate.getCertificateKeystoreFile()));
             return null;
         }
@@ -489,7 +489,7 @@ public final class CertificateLoader {
         try {
             String alias = resolveKeyAlias(keyStore, configuredAlias);
             if (alias == null || !keyStore.isKeyEntry(alias)) {
-                
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+                
log.error(sm.getString("certificateLoader.certificateLoadFailed", hostName,
                         source + " (" + 
sm.getString("certificateLoader.noKeyEntry") + ")"));
                 return null;
             }
@@ -497,21 +497,21 @@ public final class CertificateLoader {
             char[] keyPass = (keyPassToUse != null) ? 
keyPassToUse.toCharArray() : new char[0];
             java.security.Key keyObj = keyStore.getKey(alias, keyPass);
             if (!(keyObj instanceof PrivateKey)) {
-                
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+                
log.error(sm.getString("certificateLoader.certificateLoadFailed", hostName,
                         source + " (" + 
sm.getString("certificateLoader.noKeyEntry") + ")"));
                 return null;
             }
             PrivateKey javaKey = (PrivateKey) keyObj;
             Certificate[] chain = keyStore.getCertificateChain(alias);
             if (chain == null || chain.length == 0) {
-                
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+                
log.error(sm.getString("certificateLoader.certificateLoadFailed", hostName,
                         source + " (" + 
sm.getString("certificateLoader.noKeyEntry") + ")"));
                 return null;
             }
             X509Certificate[] x509Chain = new X509Certificate[chain.length];
             for (int i = 0; i < chain.length; i++) {
                 if (!(chain[i] instanceof X509Certificate)) {
-                    
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+                    
log.error(sm.getString("certificateLoader.certificateLoadFailed", hostName,
                             source + " (" + 
sm.getString("certificateLoader.noKeyEntry") + ")"));
                     return null;
                 }
@@ -520,13 +520,13 @@ public final class CertificateLoader {
 
             CertificateEntry entry = toCertEntry(javaKey, x509Chain, arena);
             if (entry == null) {
-                
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+                
log.error(sm.getString("certificateLoader.certificateLoadFailed", hostName,
                         source + " (" + OpenSSLLibrary.getLastError() + ")"));
                 return null;
             }
             return entry;
         } catch (Throwable e) {
-            log.warn(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, source), e);
+            log.error(sm.getString("certificateLoader.certificateLoadFailed", 
hostName, source), e);
             return null;
         }
     }
@@ -671,6 +671,9 @@ public final class CertificateLoader {
             }
             return bos.toByteArray();
         } catch (Exception e) {
+            if (log.isDebugEnabled()) {
+                log.debug(sm.getString("certificateLoader.readFailed", path), 
e);
+            }
             return null;
         }
     }
diff --git 
a/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties 
b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties
index ea2ebdab5e..443bca259e 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties
@@ -21,6 +21,7 @@ certificateLoader.chainFileIgnored=Certificate for host 
[{0}]: the certificate c
 certificateLoader.chainReadError=Error reading the certificate chain file: {0}
 certificateLoader.noKeyEntry=No usable key entry in key store
 certificateLoader.pskFindSessionError=Error in the pre-shared key find-session 
callback; the offered identity is rejected
+certificateLoader.readFailed=Failed to read the file [{0}]
 
 engine.emptyCipherSuite=Empty cipher suite
 engine.engineClosed=Engine is closed
diff --git a/test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java 
b/test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java
index f01252d52b..3b3fc5e867 100644
--- a/test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java
+++ b/test/org/apache/tomcat/util/net/TestSSLHostConfigCompat.java
@@ -19,12 +19,18 @@ package org.apache.tomcat.util.net;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
 
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocket;
@@ -39,6 +45,7 @@ import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 
 import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.startup.TesterServlet;
 import org.apache.catalina.startup.Tomcat;
@@ -352,6 +359,169 @@ public class TestSSLHostConfigCompat extends 
TomcatBaseTest {
     }
 
 
+    /*
+     * A failure to read the server certificate or key is logged at ERROR with 
the failure to read the file logged at
+     * DEBUG. A failure to read a chain file only needs to be logged at WARN. 
These are features of the OpenSSL FFM
+     * implementation.
+     */
+    @Test
+    public void testHostRSAPemMissingCertificateFile() throws Exception {
+        assumeOpenSSLFFMAndPem();
+
+        SSLHostConfigCertificate certificate = 
addHostRSAPem(getPath(TesterSupport.LOCALHOST_RSA_KEY_PEM));
+        String missing = new 
File("test/org/apache/tomcat/util/net/missing-cert.pem").getAbsolutePath();
+        certificate.setCertificateFile(missing);
+
+        try (LogCapture loaderLog = new LogCapture(CERTIFICATE_LOADER_LOGGER, 
Level.FINE)) {
+            Assert.assertThrows(LifecycleException.class, () -> 
getTomcatInstance().start());
+
+            Assert.assertTrue("No ERROR for the certificate file", 
loaderLog.contains(Level.SEVERE, missing));
+            Assert.assertTrue("No DEBUG for the failure to read the file",
+                    loaderLog.contains(Level.FINE, missing));
+        }
+    }
+
+
+    @Test
+    public void testHostRSAPemMissingKeyFile() throws Exception {
+        assumeOpenSSLFFMAndPem();
+
+        String missing = new 
File("test/org/apache/tomcat/util/net/missing-key.pem").getAbsolutePath();
+        addHostRSAPem(missing);
+
+        try (LogCapture loaderLog = new LogCapture(CERTIFICATE_LOADER_LOGGER, 
Level.FINE)) {
+            Assert.assertThrows(LifecycleException.class, () -> 
getTomcatInstance().start());
+
+            Assert.assertTrue("No ERROR for the key file", 
loaderLog.contains(Level.SEVERE, missing));
+            Assert.assertTrue("No DEBUG for the failure to read the file",
+                    loaderLog.contains(Level.FINE, missing));
+        }
+    }
+
+
+    @Test
+    public void testHostRSAPemMissingChainFile() throws Exception {
+        assumeOpenSSLFFMAndPem();
+
+        SSLHostConfigCertificate certificate = 
addHostRSAPem(getPath(TesterSupport.LOCALHOST_RSA_KEY_PEM));
+        
certificate.setCertificateFile(getPath(TesterSupport.LOCALHOST_RSA_CERT_PEM));
+        String missing = new 
File("test/org/apache/tomcat/util/net/missing-chain.pem").getAbsolutePath();
+        certificate.setCertificateChainFile(missing);
+
+        try (LogCapture loaderLog = new LogCapture(CERTIFICATE_LOADER_LOGGER, 
Level.FINE)) {
+            getTomcatInstance().start();
+
+            // The certificate is still used
+            Assert.assertEquals(1, getServerCertificateCount());
+            Assert.assertTrue("No WARN for the chain file", 
loaderLog.contains(Level.WARNING, missing));
+            Assert.assertFalse("The chain file was logged at ERROR", 
loaderLog.contains(Level.SEVERE, missing));
+        }
+    }
+
+
+    @Test
+    public void testHostRSAPemUnreadableCaCertificateFile() throws Exception {
+        assumeOpenSSLFFMAndPem();
+
+        SSLHostConfigCertificate certificate = 
addHostRSAPem(getPath(TesterSupport.LOCALHOST_RSA_KEY_PEM));
+        
certificate.setCertificateFile(getPath(TesterSupport.LOCALHOST_RSA_CERT_PEM));
+
+        // A file that exists but does not contain any CA certificates
+        File caFile = File.createTempFile("tomcat-test-", ".pem");
+        caFile.deleteOnExit();
+        Files.writeString(caFile.toPath(), "This is not a certificate\n");
+        getSSLHostConfig().setCaCertificateFile(caFile.getAbsolutePath());
+
+        try (LogCapture contextLog = new LogCapture(CONTEXT_LOGGER, 
Level.INFO)) {
+            getTomcatInstance().start();
+
+            Assert.assertTrue("No ERROR for the CA certificate file", 
contextLog.contains(Level.SEVERE, null));
+        }
+    }
+
+
+    private void assumeOpenSSLFFMAndPem() {
+        Assume.assumeTrue("Only needs to be run once", storeType == 
StoreType.PEM);
+        Assume.assumeTrue("Only applies to the OpenSSL FFM implementation",
+                
"org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation".equals(sslImplementationName));
+    }
+
+
+    private SSLHostConfigCertificate addHostRSAPem(String keyFile) {
+        SSLHostConfig sslHostConfig = getSSLHostConfig();
+        SSLHostConfigCertificate certificate = new 
SSLHostConfigCertificate(sslHostConfig, Type.RSA);
+        
certificate.setCertificateFile(getPath(TesterSupport.LOCALHOST_RSA_CERT_PEM));
+        certificate.setCertificateKeyFile(keyFile);
+        certificate.setCertificateKeyPassword(TesterSupport.JKS_PASS);
+        sslHostConfig.addCertificate(certificate);
+        return certificate;
+    }
+
+
+    private static final String CERTIFICATE_LOADER_LOGGER =
+            "org.apache.tomcat.util.net.openssl.panama.CertificateLoader";
+    private static final String CONTEXT_LOGGER = 
"org.apache.tomcat.util.net.openssl.panama.OpenSSLContext";
+
+
+    /*
+     * Captures the log records of a logger. The logger is referenced so it is 
not garbage collected while the capture
+     * is in use.
+     */
+    private static class LogCapture extends Handler implements AutoCloseable {
+
+        private final Logger logger;
+        private final Level originalLevel;
+        private final List<LogRecord> records = new CopyOnWriteArrayList<>();
+
+        LogCapture(String loggerName, Level level) {
+            logger = Logger.getLogger(loggerName);
+            originalLevel = logger.getLevel();
+            logger.setLevel(level);
+            setLevel(Level.ALL);
+            logger.addHandler(this);
+        }
+
+        @Override
+        public void publish(LogRecord record) {
+            records.add(record);
+        }
+
+        @Override
+        public void flush() {
+            // NO-OP
+        }
+
+        @Override
+        public void close() {
+            logger.removeHandler(this);
+            logger.setLevel(originalLevel);
+        }
+
+        /*
+         * Is there a record at the given level that mentions the text (any 
text if null)?
+         */
+        boolean contains(Level level, String text) {
+            for (LogRecord record : records) {
+                if (record.getLevel().equals(level)) {
+                    String message = record.getMessage();
+                    Object[] parameters = record.getParameters();
+                    if (text == null || message != null && 
message.contains(text)) {
+                        return true;
+                    }
+                    if (parameters != null) {
+                        for (Object parameter : parameters) {
+                            if (parameter != null && 
parameter.toString().contains(text)) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+
     private void configureHostRSAChainFileTest(File certificateFile, boolean 
pkcs12) throws Exception {
         Assume.assumeTrue("Only needs to be run once for each implementation", 
storeType == StoreType.PEM);
         if (pkcs12) {
@@ -373,6 +543,14 @@ public class TestSSLHostConfigCompat extends 
TomcatBaseTest {
     }
 
 
+    /*
+     * Returns the number of certificates the server sends.
+     */
+    private int getServerCertificateCount() throws Exception {
+        return getServerCertificates().length;
+    }
+
+
     /*
      * Checks the server sends the server certificate followed by the 
certificate in the given file and nothing else.
      */
@@ -382,6 +560,17 @@ public class TestSSLHostConfigCompat extends 
TomcatBaseTest {
             expected = (X509Certificate) 
CertificateFactory.getInstance("X.509").generateCertificate(is);
         }
 
+        Certificate[] peerCertificates = getServerCertificates();
+
+        Assert.assertEquals(2, peerCertificates.length);
+        Assert.assertEquals(expected, peerCertificates[1]);
+    }
+
+
+    /*
+     * Returns the certificates the server sends.
+     */
+    private Certificate[] getServerCertificates() throws Exception {
         // Trust everything, the test is about what the server sends
         SSLContext clientContext = SSLContext.getInstance("TLS");
         clientContext.init(null, new TrustManager[] { new X509TrustManager() {
@@ -404,10 +593,7 @@ public class TestSSLHostConfigCompat extends 
TomcatBaseTest {
         try (SSLSocket socket = (SSLSocket) 
clientContext.getSocketFactory().createSocket("localhost", getPort())) {
             socket.setSoTimeout(5000);
             socket.startHandshake();
-            Certificate[] peerCertificates = 
socket.getSession().getPeerCertificates();
-
-            Assert.assertEquals(2, peerCertificates.length);
-            Assert.assertEquals(expected, peerCertificates[1]);
+            return socket.getSession().getPeerCertificates();
         }
     }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to