diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java b/java/org/apache/tomcat/util/net/SSLHostConfig.java index 003c9b8b276b..a447c7fec03e 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfig.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java @@ -659,7 +659,7 @@ public KeyStore getTruststore() throws IOException { if (truststoreFile != null){ try { result = SSLUtilBase.getStore(getTruststoreType(), getTruststoreProvider(), - getTruststoreFile(), getTruststorePassword()); + getTruststoreFile(), getTruststorePassword(), null); } catch (IOException ioe) { Throwable cause = ioe.getCause(); if (cause instanceof UnrecoverableKeyException) { @@ -668,7 +668,7 @@ public KeyStore getTruststore() throws IOException { cause); // Re-try result = SSLUtilBase.getStore(getTruststoreType(), getTruststoreProvider(), - getTruststoreFile(), null); + getTruststoreFile(), null, null); } else { // Something else went wrong - re-throw throw ioe; diff --git a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java index ff635bf5880b..0c0ebb7274c7 100644 --- a/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java +++ b/java/org/apache/tomcat/util/net/SSLHostConfigCertificate.java @@ -59,10 +59,12 @@ public class SSLHostConfigCertificate implements Serializable { private final SSLHostConfig sslHostConfig; private final Type type; private String certificateKeyPassword = null; + private String certificateKeyPasswordFile = null; // JSSE private String certificateKeyAlias; private String certificateKeystorePassword = DEFAULT_KEYSTORE_PASSWORD; + private String certificateKeystorePasswordFile = null; private String certificateKeystoreFile = DEFAULT_KEYSTORE_FILE; private String certificateKeystoreProvider = DEFAULT_KEYSTORE_PROVIDER; private String certificateKeystoreType = DEFAULT_KEYSTORE_TYPE; @@ -131,6 +133,16 @@ public void setCertificateKeyPassword(String certificateKeyPassword) { } + public String getCertificateKeyPasswordFile() { + return certificateKeyPasswordFile; + } + + + public void setCertificateKeyPasswordFile(String certificateKeyPasswordFile) { + this.certificateKeyPasswordFile = certificateKeyPasswordFile; + } + + // JSSE public void setCertificateKeyAlias(String certificateKeyAlias) { @@ -171,6 +183,19 @@ public String getCertificateKeystorePassword() { } + public void setCertificateKeystorePasswordFile(String certificateKeystorePasswordFile) { + sslHostConfig.setProperty( + "Certificate.certificateKeystorePasswordFile", SSLHostConfig.Type.JSSE); + setStoreType("Certificate.certificateKeystorePasswordFile", StoreType.KEYSTORE); + this.certificateKeystorePasswordFile = certificateKeystorePasswordFile; + } + + + public String getCertificateKeystorePasswordFile() { + return certificateKeystorePasswordFile; + } + + public void setCertificateKeystoreProvider(String certificateKeystoreProvider) { sslHostConfig.setProperty( "Certificate.certificateKeystoreProvider", SSLHostConfig.Type.JSSE); @@ -208,7 +233,7 @@ public KeyStore getCertificateKeystore() throws IOException { if (result == null && storeType == StoreType.KEYSTORE) { result = SSLUtilBase.getStore(getCertificateKeystoreType(), getCertificateKeystoreProvider(), getCertificateKeystoreFile(), - getCertificateKeystorePassword()); + getCertificateKeystorePassword(), getCertificateKeystorePasswordFile()); } return result; diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java index 91c2929baa54..70735293ebe6 100644 --- a/java/org/apache/tomcat/util/net/SSLUtilBase.java +++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java @@ -16,9 +16,12 @@ */ package org.apache.tomcat.util.net; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.security.DomainLoadStoreParameter; import java.security.Key; import java.security.KeyStore; @@ -181,10 +184,10 @@ static List getEnabled(String name, Log log, boolean warnOnSkip, Collecti /* - * Gets the key- or truststore with the specified type, path, and password. + * Gets the key- or truststore with the specified type, path, password and password file. */ static KeyStore getStore(String type, String provider, String path, - String pass) throws IOException { + String pass, String passFile) throws IOException { KeyStore ks = null; InputStream istream = null; @@ -217,9 +220,21 @@ static KeyStore getStore(String type, String provider, String path, // - for JKS or PKCS12 only use null if pass is null // (because JKS will auto-switch to PKCS12) char[] storePass = null; - if (pass != null && (!"".equals(pass) || + String passToUse = null; + if (passFile != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader( + ConfigFileLoader.getSource().getResource(passFile).getInputStream(), + StandardCharsets.UTF_8))) { + passToUse = reader.readLine(); + } + } else { + passToUse = pass; + } + + if (passToUse != null && (!"".equals(passToUse) || "JKS".equalsIgnoreCase(type) || "PKCS12".equalsIgnoreCase(type))) { - storePass = pass.toCharArray(); + storePass = passToUse.toCharArray(); } ks.load(istream, storePass); } @@ -278,9 +293,13 @@ public void configureSessionContext(SSLSessionContext sslSessionContext) { public KeyManager[] getKeyManagers() throws Exception { String keyAlias = certificate.getCertificateKeyAlias(); String algorithm = sslHostConfig.getKeyManagerAlgorithm(); + String keyPassFile = certificate.getCertificateKeyPasswordFile(); String keyPass = certificate.getCertificateKeyPassword(); // This has to be here as it can't be moved to SSLHostConfig since the // defaults vary between JSSE and OpenSSL. + if (keyPassFile == null) { + keyPassFile = certificate.getCertificateKeystorePasswordFile(); + } if (keyPass == null) { keyPass = certificate.getCertificateKeystorePassword(); } @@ -299,8 +318,22 @@ public KeyManager[] getKeyManagers() throws Exception { * required key works around that. * Other keys stores (hardware, MS, etc.) will be used as is. */ + char[] keyPassArray = null; + String keyPassToUse = null; + if (keyPassFile != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader( + ConfigFileLoader.getSource().getResource(keyPassFile).getInputStream(), + StandardCharsets.UTF_8))) { + keyPassToUse = reader.readLine(); + } + } else { + keyPassToUse = keyPass; + } - char[] keyPassArray = keyPass.toCharArray(); + if (keyPassToUse != null) { + keyPassArray = keyPassToUse.toCharArray(); + } KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); if (kmf.getProvider().getInfo().contains("FIPS")) { @@ -319,7 +352,7 @@ public KeyManager[] getKeyManagers() throws Exception { PEMFile privateKeyFile = new PEMFile( certificate.getCertificateKeyFile() != null ? certificate.getCertificateKeyFile() : certificate.getCertificateFile(), - keyPass); + keyPass, keyPassFile, null); PEMFile certificateFile = new PEMFile(certificate.getCertificateFile()); Collection chain = new ArrayList<>(certificateFile.getCertificates()); @@ -335,7 +368,7 @@ public KeyManager[] getKeyManagers() throws Exception { // Switch to in-memory key store ksUsed = KeyStore.getInstance("JKS"); ksUsed.load(null, null); - ksUsed.setKeyEntry(keyAlias, privateKeyFile.getPrivateKey(), keyPass.toCharArray(), + ksUsed.setKeyEntry(keyAlias, privateKeyFile.getPrivateKey(), keyPassArray, chain.toArray(new Certificate[0])); } else { if (keyAlias != null && !ks.isKeyEntry(keyAlias)) { diff --git a/java/org/apache/tomcat/util/net/jsse/PEMFile.java b/java/org/apache/tomcat/util/net/jsse/PEMFile.java index 397282239882..7bed5bddbd3f 100644 --- a/java/org/apache/tomcat/util/net/jsse/PEMFile.java +++ b/java/org/apache/tomcat/util/net/jsse/PEMFile.java @@ -101,15 +101,30 @@ public PEMFile(String filename, String password, String keyAlgorithm) this(filename, ConfigFileLoader.getSource().getResource(filename).getInputStream(), password, keyAlgorithm); } + public PEMFile(String filename, String password, String passwordFilename, String keyAlgorithm) + throws IOException, GeneralSecurityException { + this(filename, ConfigFileLoader.getSource().getResource(filename).getInputStream(), password, + passwordFilename, passwordFilename != null ? ConfigFileLoader.getSource().getResource(passwordFilename).getInputStream() : null, + keyAlgorithm); + } + + public PEMFile(String filename, InputStream fileStream, String password, String keyAlgorithm) + throws IOException, GeneralSecurityException { + this(filename, fileStream, password, null, null, keyAlgorithm); + } + /** * @param filename the filename to mention in error messages, not used for anything else. * @param fileStream the stream containing the pem(s). * @param password password to load the pem objects. + * @param passwordFilename the password filename to mention in error messages, not used for anything else. + * @param passwordFileStream stream containing the password to load the pem objects. * @param keyAlgorithm the algorithm to help to know how to load the objects (guessed if null). * @throws IOException if input can't be read. * @throws GeneralSecurityException if input can't be parsed/loaded. */ - public PEMFile(String filename, InputStream fileStream, String password, String keyAlgorithm) + public PEMFile(String filename, InputStream fileStream, String password, String passwordFilename, + InputStream passwordFileStream, String keyAlgorithm) throws IOException, GeneralSecurityException { List parts = new ArrayList<>(); try (BufferedReader reader = @@ -140,6 +155,16 @@ public PEMFile(String filename, InputStream fileStream, String password, String } } + String passwordToUse = null; + if (passwordFileStream != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(passwordFileStream, StandardCharsets.UTF_8))) { + passwordToUse = reader.readLine(); + } + } else { + passwordToUse = password; + } + for (Part part : parts) { switch (part.type) { case Part.PRIVATE_KEY: @@ -149,7 +174,7 @@ public PEMFile(String filename, InputStream fileStream, String password, String privateKey = part.toPrivateKey(null, "EC", Format.RFC5915, filename); break; case Part.ENCRYPTED_PRIVATE_KEY: - privateKey = part.toPrivateKey(password, keyAlgorithm, Format.PKCS8, filename); + privateKey = part.toPrivateKey(passwordToUse, keyAlgorithm, Format.PKCS8, filename); break; case Part.RSA_PRIVATE_KEY: if (part.algorithm == null) { @@ -157,7 +182,7 @@ public PEMFile(String filename, InputStream fileStream, String password, String // (probably default) key password provided. privateKey = part.toPrivateKey(null, keyAlgorithm, Format.PKCS1, filename); } else { - privateKey = part.toPrivateKey(password, keyAlgorithm, Format.PKCS1, filename); + privateKey = part.toPrivateKey(passwordToUse, keyAlgorithm, Format.PKCS1, filename); } break; case Part.CERTIFICATE: diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java index 6ceaf89bb55d..a687e14571ff 100644 --- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java +++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java @@ -16,6 +16,9 @@ */ package org.apache.tomcat.util.net.openssl; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.InputStreamReader; import java.lang.ref.Cleaner; import java.lang.ref.Cleaner.Cleanable; import java.nio.charset.StandardCharsets; @@ -466,10 +469,22 @@ public void addCertificate(SSLHostConfigCertificate certificate) throws Exceptio // Load Server key and certificate if (certificate.getCertificateFile() != null) { // Set certificate + String passwordToUse = null; + if (certificate.getCertificateKeyPasswordFile() != null) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader( + new FileInputStream( + SSLHostConfig.adjustRelativePath(certificate.getCertificateKeyPasswordFile())), + StandardCharsets.UTF_8))) { + passwordToUse = reader.readLine(); + } + } else { + passwordToUse = certificate.getCertificateKeyPassword(); + } SSLContext.setCertificate(state.ctx, SSLHostConfig.adjustRelativePath(certificate.getCertificateFile()), SSLHostConfig.adjustRelativePath(certificate.getCertificateKeyFile()), - certificate.getCertificateKeyPassword(), getCertificateIndex(certificate)); + passwordToUse, getCertificateIndex(certificate)); // Set certificate chain file SSLContext.setCertificateChainFile(state.ctx, SSLHostConfig.adjustRelativePath(certificate.getCertificateChainFile()), false); diff --git a/test/org/apache/tomcat/util/net/TestSsl.java b/test/org/apache/tomcat/util/net/TestSsl.java index f3f889ed0e39..debabab9b7e4 100644 --- a/test/org/apache/tomcat/util/net/TestSsl.java +++ b/test/org/apache/tomcat/util/net/TestSsl.java @@ -224,7 +224,7 @@ public void testKeyPass() throws Exception { ctxt.addApplicationListener(WsContextListener.class.getName()); TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, - TesterSupport.JKS_PASS, TesterSupport.JKS_KEY_PASS); + TesterSupport.JKS_PASS, null, TesterSupport.JKS_KEY_PASS, null); TesterSupport.configureSSLImplementation(tomcat, sslImplementationName); @@ -243,6 +243,26 @@ public void testKeyPass() throws Exception { TesterSupport.getLastClientAuthRequestedIssuerCount() == 0); } + @Test + public void testKeyPassFile() throws Exception { + TesterSupport.configureClientSsl(); + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getBuildDirectory(), "webapps/examples"); + Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); + ctxt.addApplicationListener(WsContextListener.class.getName()); + + TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, + null, TesterSupport.JKS_PASS_FILE, null, TesterSupport.JKS_KEY_PASS_FILE); + + tomcat.start(); + ByteChunk res = getUrl("https://localhost:" + getPort() + + "/examples/servlets/servlet/HelloWorldExample"); + Assert.assertTrue(res.toString().indexOf("") > 0); + Assert.assertTrue("Checking no client issuer has been requested", + TesterSupport.getLastClientAuthRequestedIssuerCount() == 0); + } @Test public void testRenegotiateWorks() throws Exception { diff --git a/test/org/apache/tomcat/util/net/TesterSupport.java b/test/org/apache/tomcat/util/net/TesterSupport.java index 199be62beb83..03eb1d9bb995 100644 --- a/test/org/apache/tomcat/util/net/TesterSupport.java +++ b/test/org/apache/tomcat/util/net/TesterSupport.java @@ -78,7 +78,9 @@ public final class TesterSupport { public static final String LOCALHOST_RSA_JKS = SSL_DIR + "localhost-rsa.jks"; public static final String LOCALHOST_KEYPASS_JKS = SSL_DIR + "localhost-rsa-copy1.jks"; public static final String JKS_PASS = "changeit"; + public static final String JKS_PASS_FILE = SSL_DIR + "keystore-password"; public static final String JKS_KEY_PASS = "tomcatpass"; + public static final String JKS_KEY_PASS_FILE = SSL_DIR + "key-password"; public static final String CA_CERT_PEM = SSL_DIR + CA_ALIAS + "-cert.pem"; public static final String LOCALHOST_EC_CERT_PEM = SSL_DIR + "localhost-ec-cert.pem"; public static final String LOCALHOST_EC_KEY_PEM = SSL_DIR + "localhost-ec-key.pem"; @@ -133,11 +135,11 @@ public static boolean isTlsv13Available() { } public static void initSsl(Tomcat tomcat) { - initSsl(tomcat, LOCALHOST_RSA_JKS, null, null); + initSsl(tomcat, LOCALHOST_RSA_JKS, null, null, null, null); } protected static void initSsl(Tomcat tomcat, String keystore, - String keystorePass, String keyPass) { + String keystorePass, String keystorePassFile, String keyPass, String keyPassFile) { Connector connector = tomcat.getConnector(); connector.setSecure(true); @@ -159,9 +161,15 @@ protected static void initSsl(Tomcat tomcat, String keystore, sslHostConfig.setSslProtocol("tls"); certificate.setCertificateKeystoreFile(new File(keystore).getAbsolutePath()); sslHostConfig.setTruststoreFile(new File(CA_JKS).getAbsolutePath()); + if (keystorePassFile != null) { + certificate.setCertificateKeystorePasswordFile(new File(keystorePassFile).getAbsolutePath()); + } if (keystorePass != null) { certificate.setCertificateKeystorePassword(keystorePass); } + if (keyPassFile != null) { + certificate.setCertificateKeyPasswordFile(new File(keyPassFile).getAbsolutePath()); + } if (keyPass != null) { certificate.setCertificateKeyPassword(keyPass); } diff --git a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java index 272295c3abeb..dc10b2b9dc86 100644 --- a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java +++ b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java @@ -19,13 +19,21 @@ import java.io.File; import java.io.IOException; import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +@RunWith(Parameterized.class) public class TestPEMFile { private static final String KEY_PASSWORD = "changeit"; + private static final String KEY_PASSWORD_FILE = "key-password"; private static final String KEY_PKCS1 = "key-pkcs1.pem"; private static final String KEY_ENCRYPTED_PKCS1_DES_CBC = "key-encrypted-pkcs1-des-cbc.pem"; @@ -33,16 +41,34 @@ public class TestPEMFile { private static final String KEY_ENCRYPTED_PKCS1_AES256 = "key-encrypted-pkcs1-aes256.pem"; private static final String KEY_ENCRYPTED_PKCS8 = "key-encrypted-pkcs8.pem"; + @Parameterized.Parameters + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + parameterSets.add(new Object[] { KEY_PASSWORD, null }); + parameterSets.add(new Object[] { null, KEY_PASSWORD_FILE }); + parameterSets.add(new Object[] { KEY_PASSWORD, KEY_PASSWORD_FILE }); + + return parameterSets; + } + + + @Parameter(0) + public String password; + + @Parameter(1) + public String passwordFile; + @Test public void testKeyPkcs1() throws Exception { - testKey(KEY_PKCS1, null); + testKey(KEY_PKCS1, null, null); } @Test public void testKeyPkcs1WithUnnecessaryPassword() throws Exception { - testKey(KEY_PKCS1, "ignore-me"); + testKey(KEY_PKCS1, "ignore-me", null); } @@ -71,18 +97,21 @@ public void testKeyEncryptedPkcs8() throws Exception { private void testKeyEncrypted(String file) throws Exception { - testKey(file, KEY_PASSWORD); + testKey(file, password, passwordFile); } - private void testKey(String file, String password) throws Exception { - PEMFile pemFile = new PEMFile(getPath(file), password); + private void testKey(String file, String password, String passwordFile) throws Exception { + PEMFile pemFile = new PEMFile(getPath(file), password, getPath(passwordFile), null); PrivateKey pk = pemFile.getPrivateKey(); Assert.assertNotNull(pk); } private String getPath(String file) throws IOException { + if (file == null) { + return null; + } String packageName = this.getClass().getPackageName(); String path = packageName.replace(".", File.separator); File f = new File("test" + File.separator + path + File.separator + file); diff --git a/test/org/apache/tomcat/util/net/jsse/key-password b/test/org/apache/tomcat/util/net/jsse/key-password new file mode 100644 index 000000000000..5bbaf8758194 --- /dev/null +++ b/test/org/apache/tomcat/util/net/jsse/key-password @@ -0,0 +1 @@ +changeit \ No newline at end of file diff --git a/test/org/apache/tomcat/util/net/key-password b/test/org/apache/tomcat/util/net/key-password new file mode 100644 index 000000000000..05557f916806 --- /dev/null +++ b/test/org/apache/tomcat/util/net/key-password @@ -0,0 +1 @@ +tomcatpass \ No newline at end of file diff --git a/test/org/apache/tomcat/util/net/keystore-password b/test/org/apache/tomcat/util/net/keystore-password new file mode 100644 index 000000000000..5bbaf8758194 --- /dev/null +++ b/test/org/apache/tomcat/util/net/keystore-password @@ -0,0 +1 @@ +changeit \ No newline at end of file diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index eb1a0aad0973..85805e6cad44 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -107,6 +107,10 @@
+ + 66670: Add SSLHostConfig#certificateKeyPasswordFile and + SSLHostConfig#certificateKeystorePasswordFile. (michaelo) + 67667: TLSCertificateReloadListener prints unreadable rendering of X509Certificate#getNotAfter(). (michaelo) diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml index 96a37d3baad9..2faa6f002f98 100644 --- a/webapps/docs/config/http.xml +++ b/webapps/docs/config/http.xml @@ -1470,7 +1470,18 @@ certificate from the specified file.

If not specified, the default behaviour for JSSE is to use the certificateKeystorePassword. For OpenSSL the default - behaviour is not to use a password.

+ behaviour is not to use a password, but OpenSSL will prompt for one, + if required.

+ + + +

The password file used to access the private key associated with the server + certificate from the specified file. This attribute takes precedence over + certificateKeyPassword.

+

If not specified, the default behaviour for JSSE is to use the + certificateKeystorePasswordFile. For OpenSSL the default + behaviour is not to use a password (file), but OpenSSL will prompt for one, + if required.

@@ -1496,6 +1507,13 @@ changeit will be used.

+ +

JSSE only.

+

The password file to use to access the keystore containing the server's + private key and certificate. This attribute takes precedence over + certificateKeystorePassword.

+
+

JSSE only.

The name of the keystore provider to be used for the server