如何覆盖 Http11Nio2Protocol class 以加密 server.xml 中的密钥库密码

How to override Http11Nio2Protocol class to encrypt keystore password in server.xml

按照答案 - Encrypt tomcat keystore password

像这样扩展 Http11Nio2Protocol class:

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);
    }
}

我建一个maven工程,把jar放在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>

当我使用原始密码时,它可以正常工作。 然后我改了certificateKeystorePassword,还是失败了,catalina.out里面的错误信息是:

** [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.

似乎“密钥库密码不正确”。

我在tomcat源代码中搜索了'setKeystorePass',但找不到它的调用位置。那么为什么我可以扩展更改协议配置的方法呢?

提前致谢!

您找不到对 setKeystorePass 的任何调用的原因是它是通过反射执行的,主要使用 IntrospectionUtils#setProperty (cf. Commons Digester 了解详细信息)。

配置XML用于创建Java个对象:

  • <Connector protocol="com.fine.security.ReSetHttpProtocol" 创建连接器和协议处理程序,
  • 标签 <SSLHostConfig> 创建 SSLHostConfig 的实例,结束标签 </SSLHostConfig> 在连接器上调用 Connector#addSslHostConfig
  • 这同样适用于 <Certificate>,它创建 SSLHostConfigCertificate 的实例,配置它并在最后调用 SSLHostConfig#addCertificate.
  • <Connector><SSLHostConfig><Certificate> 标签的所有属性在相应对象上调用适当的 setters 或 setProperty。例如。 port="8443" 调用了 setter setPort(),但是 SSLEnabled="true" 调用了 setProperty("SSLEnabled", "true") 因为没有 setter,

因此,如果您希望 bootstrap 代码调用您的 setKeystorePassword 实现,您需要以这种方式定义连接器:

<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)" />

此语法自 Tomcat 8.5 起已过时,并已在 Tomcat 10 中删除,但它是完成所需内容的最简单方法。

如果您希望使用新语法,则需要对标准进行更多修改 Http11NioProtocol:

  1. 您需要为 SSLHostConfigCertificate 创建一个仅覆盖 setCertificateKeystorePassword
  2. 的代理
  3. 您需要为 SSLHostConfigCertificate 覆盖 addCertificate 创建一个代理,它将使用第 1 点
  4. 的代理调用原始 addCertificate
  5. 您需要通过使用前一点的代理调用原始 addSSLHostConfig 来覆盖协议中的 addSSLHostConfig

PS:您没有加密您的密码,您正在编码 它。要解码编码密码,您只需要知道转换(在您的情况下为 Base64),要解密加密密码,您需要知道加密算法和密钥。然后你可能需要加密加密密钥等等......我发现这两个程序都没用。

好吧,尽管理解了 Piotr 的回答背后的想法,但我还是完全糊涂了。 他的所有评论大部分都是正确的(也许完全正确!),但是由于“Digester”正在驱动 SSLHostConfig 和 SSLHostConfigCertificate 的实例化和添加,这里的代理模式不能很好地工作。 入口点是在连接器上配置的 class,但是您无法包装(或者我找不到如何包装)来自自定义连接器 class 的 SSLHostConfig。如果您扩展 SSLHostConfig 并覆盖连接器中的 class 以调用该“代理”,您最终会创建一个新的代理实例,然后您必须将属性(使用反射或手动)设置为“克隆”或保留消化器解析的最重要的属性。请记住,无论覆盖方法如何,摘要都会创建每个 XML 标记的实例。

在真正考虑到我没有很好地理解之后,当我意识到我已经考虑了几个小时应该工作但没有的东西时,我疯了(因为我不小心重复了server.xml.

最后我想出了:

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);
        }

我已经在 Tomcat 9 和 JDK11 中测试过,它确实有效。

欢迎提出意见、建议和更正。