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!