Context: on my machine I have an environment variable JAVA_TOOL_OPTIONS set to -Djavax.net.ssl.keyStoreType=KeychainStore. This allows Java applications such as Maven to authenticate using a client certificate stored in the macOS KeyChain.

Problem: when I start a Spring Boot application that uses an embedded Tomcat with a custom SSL configuration, a random user certificate from the KeyChain is used as the server certificate instead of the one configured in server.ssl.*.

My application.yaml looks like this:

server.ssl.key-alias: myserver
server.ssl.key-store: "classpath:myserver.jks"
server.ssl.key-store-password: "password"
server.ssl.key-store-type: JKS

But SSL debug logs show a user certificate is used instead:

javax.net.ssl|DEBUG|61|https-jsse-nio-auto-1-exec-1|2023-10-18 10:00:04.500 CEST|CertificateMessage.java:1022|Produced server Certificate message (
"Certificate": {
...

      "subject"            : "CN=JANSEN Bastien, EMAILADDRESS=Bastien.JANSEN@...",

I was able to track down the problem to this code in org.apache.tomcat.util.net.SSLUtilBase:

Key k = ks.getKey(keyAlias, keyPassArray);
if (k != null && !"DKS".equalsIgnoreCase(this.certificate.getCertificateKeystoreType()) && "PKCS#8".equalsIgnoreCase(k.getFormat())) {
    alias = this.certificate.getCertificateKeystoreProvider();
    if (alias == null) {
        ksUsed = KeyStore.getInstance(this.certificate.getCertificateKeystoreType());
    } else {
        ksUsed = KeyStore.getInstance(this.certificate.getCertificateKeystoreType(), alias);
    }

    ksUsed.load((InputStream)null, (char[])null);
    ksUsed.setKeyEntry(keyAlias, k, keyPassArray, ks.getCertificateChain(keyAlias));
}

From what I understand, it basically reloads a keystore using information contained in this.certificate. The problem in my case is that this.certificate.getCertificateKeystoreType() is KeychainStore instead of JKS!

I believe a way to fix the problem is to modify org.springframework.boot.web.embedded.tomcat.SslConnectorCustomizer#configureSslStoreProvider to set the keystore type in addition to the keystore itself:

SslStoreBundle stores = this.sslBundle.getStores();
if (stores.getKeyStore() != null) {
    certificate.setCertificateKeystore(stores.getKeyStore());

    // This line should be added
    certificate.setCertificateKeystoreType(stores.getKeyStore().getType());
}
if (stores.getTrustStore() != null) {
    sslHostConfig.setTrustStore(stores.getTrustStore());
}

When I call certificate.setCertificateKeystoreType(stores.getKeyStore().getType()) in my debugger at that location, then the correct certificate is used by Tomcat.

Minimal application to reproduce the project: https://github.com/bjansen/spring-boot-ssl-bug And use export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.keyStoreType=KeychainStore" (or Windows-MY on Windows).

Comment From: wilkinsona

Thanks. This appears to be a regression in 3.1.x. Previously, it would have only affected an application using SslStoreProvider but the introduction of support for SSL bundles has broadened the problem. That said, I wonder if we've really just given greater exposure to something that may be a Tomcat bug.

@markt-asf given that we're calling setCertificateKeystore(stores.getKeyStore()) on org.apache.tomcat.util.net.SSLHostConfigCertificate should we have to call setCertificateKeystoreType(stores.getKeyStore().getType()) as well? It feels redundant as we've already passed in the type through the KeyStore instance. Could Tomcat infer the type automatically from the KeyStore instance instead?

Comment From: wilkinsona

@bjansen Here's a workaround that copies the KeyStore's type into the certificate:

@Bean
TomcatConnectorCustomizer sslCustomizer() {
    return (connector) -> {
        SSLHostConfig[] configs = ((AbstractHttp11JsseProtocol<?>) connector.getProtocolHandler())
            .findSslHostConfigs();
        for (SSLHostConfig config : configs) {
            for (SSLHostConfigCertificate certificate : config.getCertificates()) {
                try {
                    KeyStore keyStore = certificate.getCertificateKeystore();
                    certificate.setCertificateKeystoreType(keyStore.getType());
                }
                catch (IOException ex) {
                    throw new RuntimeException();
                }
            }
        }
    };
}

Comment From: markt-asf

@wilkinsona Seems reasonable. Just running the unit tests to see if anything breaks. If not, I'll commit and it will be in the next release round.

Comment From: wilkinsona

Thanks, @markt-asf. I see you've pushed https://github.com/apache/tomcat/commit/cd4903db91714822b38d5017169599d4e15544aa for 10.1.x.

@bjansen, once this has been released, we'll pick it up as part of our usual dependency upgrade process. In the meantime, please use the workaround above.

Comment From: bjansen

Great, thank you very much!