1

Followed the answer - Encrypt tomcat keystore password

Extend the Http11Nio2Protocol class like this:

public class ReSetHttpProtocol extends Http11Nio2Protocol {
    @Override
    public void setKeystorePass(String certificateKeystorePassword) {
        Decoder decoder = Base64.getDecoder();
        String password = new String(decoder.decode(certificateKeystorePassword));
        super.setKeystorePass(password);
    }
}

I build a maven project and put the jar in tomcat/lib.

/conf/server.xml:

<Connector port="8443" protocol="com.fine.security.ReSetHttpProtocol"
               maxThreads="150" scheme="https" SSLEnabled="true" relaxedQueryChars="^{}[]|&quot;" >
        <SSLHostConfig   sslEnabledProtocols="TLSv1.2" ciphers="TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" >
            <Certificate certificateKeystoreFile="(absolute path of the .pfx file)"
                         certificateKeystoreType="JKS" certificateKeystorePassword="(encrypted password)" />
        </SSLHostConfig>
    </Connector>

When I use the origin password, it works normally. Then I changed certificateKeystorePassword, it failed, and the error info in catalina.out is:

** [main] org.apache.catalina.core.StandardService.initInternal Failed to initialize connector [Connector[com.fine.security.ReSetHttpProtocol-8443]]
        org.apache.catalina.LifecycleException: Protocol handler initialization failed
                at org.apache.catalina.connector.Connector.initInternal(Connector.java:1032)
                at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
                at org.apache.catalina.core.StandardService.initInternal(StandardService.java:552)
                at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
                at org.apache.catalina.core.StandardServer.initInternal(StandardServer.java:848)
                at org.apache.catalina.startup.Catalina.load(Catalina.java:662)
                at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
                at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
                at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
                at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:472)
        Caused by: java.lang.IllegalArgumentException: keystore password was incorrect
                at org.apache.catalina.startup.Catalina.load(Catalina.java:639)
                at org.apache.catalina.startup.Catalina.load(Catalina.java:662)
                at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
                at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
                at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
                at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:472)
        Caused by: java.lang.IllegalArgumentException: keystore password was incorrect
                at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:100)
                at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:72)
                at org.apache.tomcat.util.net.Nio2Endpoint.bind(Nio2Endpoint.java:158)
                at org.apache.tomcat.util.net.AbstractEndpoint.init(AbstractEndpoint.java:1118)
                at org.apache.tomcat.util.net.AbstractJsseEndpoint.init(AbstractJsseEndpoint.java:223)
                at org.apache.coyote.AbstractProtocol.init(AbstractProtocol.java:587)
                at org.apache.coyote.http11.AbstractHttp11Protocol.init(AbstractHttp11Protocol.java:74)
                at org.apache.catalina.connector.Connector.initInternal(Connector.java:1030)
                ... 13 more
        Caused by: java.io.IOException: keystore password was incorrect
                at sun.security.pkcs12.PKCS12KeyStore.engineLoad(PKCS12KeyStore.java:2059)
                at sun.security.provider.KeyStoreDelegator.engineLoad(KeyStoreDelegator.java:238)
                at sun.security.provider.JavaKeyStore$DualFormatJKS.engineLoad(JavaKeyStore.java:70)
                at java.security.KeyStore.load(KeyStore.java:1445)
                at org.apache.tomcat.util.security.KeyStoreUtil.load(KeyStoreUtil.java:69)
                at org.apache.tomcat.util.net.SSLUtilBase.getStore(SSLUtilBase.java:216)
                at org.apache.tomcat.util.net.SSLHostConfigCertificate.getCertificateKeystore(SSLHostConfigCertificate.java:206)
                at org.apache.tomcat.util.net.SSLUtilBase.getKeyManagers(SSLUtilBase.java:282)
                at org.apache.tomcat.util.net.SSLUtilBase.createSSLContext(SSLUtilBase.java:246)
                at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:98)
                ... 20 more
        Caused by: java.security.UnrecoverableKeyException: failed to decrypt safe contents entry: javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.

It seems like "keystore password was incorrect".

I searched 'setKeystorePass' in tomcat source code, but I can't find where it's called. So why can I extend the method to change the protocol configuration?

Thanks in advance!

Volcanoe
  • 13
  • 3

2 Answers2

1

The reason you can't find any call to setKeystorePass is that it is performed by reflection, mostly using IntrospectionUtils#setProperty (cf. Commons Digester for details).

The configuration XML is used to create Java objects:

  • <Connector protocol="com.fine.security.ReSetHttpProtocol" creates a connector and protocol handler,
  • the tag <SSLHostConfig> creates an instance of SSLHostConfig and the closing tag </SSLHostConfig> calls Connector#addSslHostConfig on the connector,
  • the same applies to <Certificate> which creates an instance of SSLHostConfigCertificate, configures it and at the end calls SSLHostConfig#addCertificate.
  • all attributes of the <Connector>, <SSLHostConfig> and <Certificate> tags call the appropriate setters or setProperty on the corresponding objects. E.g. port="8443" calls the setter setPort(), but SSLEnabled="true" calls setProperty("SSLEnabled", "true") because there is no setter,

So, if you want the bootstrap code to call your implementation of setKeystorePassword you need to define the connector in this way:

<Connector port="8443"
           protocol="com.fine.security.ReSetHttpProtocol"
           maxThreads="150"
           scheme="https"
           SSLEnabled="true"
           relaxedQueryChars="^{}[]|&quot;"
           sslEnabledProtocols="TLSv1.2"
           ciphers="..."
           keystoreFile="(absolute path of the .pfx file)"
           keystoreType="JKS"
           keystorePass="(encrypted password)" />

This syntax is obsolete since Tomcat 8.5 and was removed in Tomcat 10, but it is the easiest way to accomplish what you want.

If you wish to use the new syntax, you'll need to do more modifications to the standard Http11NioProtocol:

  1. You'll need to create a proxy for SSLHostConfigCertificate that overrides just setCertificateKeystorePassword,
  2. You'll need to create a proxy for SSLHostConfigCertificate the overrides addCertificate which will call the original addCertificate with the proxy from point 1,
  3. You'll need to override addSSLHostConfig in your protocol by calling the original addSSLHostConfig with the proxy from the previous point.

PS: you are not encrypting your password, you are encoding it. To decode an encoded password you just need to know the transformation (in your case Base64), to decrypt an encrypted password you need to know the encryption algorithm and a secret key. Then you'll probably need to encrypt the encryption key and so on... I find both procedures useless.

Piotr P. Karwasz
  • 12,857
  • 3
  • 20
  • 43
  • Thank you very much for solving my confusion! Since I use Tomcat 8.5.51, I need to learn how to create a proxy. Maybe you can give me some more detailed suggestions? – Volcanoe Feb 22 '21 at 03:26
  • 1
    It works in Tomcat 8.5 when I defined the connector in this way: `` (not keystorePassword but keystorePass). Thank you very much! – Volcanoe Feb 22 '21 at 06:34
  • Thanks, I corrected `keystorePassword` -> `keystorePass` in the answer. – Piotr P. Karwasz Feb 22 '21 at 07:41
1

Well, despite having understood the idea behind Piotr's answer, I ended up totally confused. He's mostly right (maybe completely right!) in all his comments, but since the "Digester" is driving the instantiation and addition of SSLHostConfig and SSLHostConfigCertificate, the proxy pattern here does not work very well. The entry point is the class configured at the connector, but then you cannot wrap (or I could not find how) the SSLHostConfig from your custom connector class. If you extend SSLHostConfig and override the class in your connector to call that "proxy", you end up creating a new instance of the proxy, and then you have to set attributes (using reflection or manually) to kind of "clone" or keep the most important attributes parsed by the digester. Bear in mind that the digester creates instances of each XML tag regardless overriding the methods.

After really considering that I had not understood well, I went mad when I realised that I had been thinking for hours on something that should work, but didn't (cos I accidentally duplicated the "Certificate" tag in the server.xml.

Finally I came up with:

import java.util.Optional;

import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;

public class CustomHttp11NioProtocol extends org.apache.coyote.http11.Http11NioProtocol {

    @Override
    public void addSslHostConfig(SSLHostConfig conf) {
        Optional<SSLHostConfigCertificate> cert = conf.getCertificates().stream().findFirst();
        if (cert.isPresent())
            cert.get().setCertificateKeystorePassword(
                    new TomcatPasswordDecryption().decrypt(cert.get().getCertificateKeystorePassword()));
        super.addSslHostConfig(conf);
    }

}

In my case I only have one certificate, so findFirst() is enough. But you could do the password replacement for the entire stream.
Actually this is even better:

    @Override
        public void addSslHostConfig(SSLHostConfig conf) {
            conf.getCertificates().stream().forEach(a -> a.setCertificateKeystorePassword(
                    new TomcatPasswordDecryption().decrypt(a.getCertificateKeystorePassword())));
    
            super.addSslHostConfig(conf);
        }

I have tested this in Tomcat 9 and JDK11 and it does work.

Comments, suggestions and corrections are more than welcome.