Bug 62507

Summary: Insufficient control over keystore loading to support DKS-keystores
Product: Tomcat 8 Reporter: Kenny Stimson <kenny_stimson>
Component: MetaAssignee: Tomcat Developers Mailing List <dev>
Status: RESOLVED FIXED    
Severity: enhancement CC: awilkinson, kenny_stimson
Priority: P2    
Version: 8.5.x-trunk   
Target Milestone: ----   
Hardware: PC   
OS: All   

Description Kenny Stimson 2018-06-28 16:50:42 UTC
This ticket has been created out of a discussion originally raised here: 
https://github.com/spring-projects/spring-boot/issues/13590

Overview:
    I am trying to configure ssl using a dks-keystore through 'application.properties'. I raised a ticket with the team handling spring-boot and they have stated that there is currently no mechanism for them to be able to stop tomcat from calling 'java.security.KeyStore.load(InputStream, char[])' in favor of 'java.security.KeyStore.load(URI, DomainLoadStoreParameter)'

Here is the stacktrace I provided, showing the path that is taken for the configuration of the SSL context:
java.lang.UnsupportedOperationException: This keystore must be loaded using a DomainLoadStoreParameter
	at sun.security.provider.DomainKeyStore.engineLoad(DomainKeyStore.java:713) ~[na:1.8.0_111]
	at sun.security.provider.DomainKeyStore$DKS.engineLoad(DomainKeyStore.java:68) ~[na:1.8.0_111]
	at java.security.KeyStore.load(KeyStore.java:1445) ~[na:1.8.0_111]
	at org.apache.tomcat.util.net.SSLUtilBase.getStore(SSLUtilBase.java:136) ~[tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.tomcat.util.net.SSLHostConfigCertificate.getCertificateKeystore(SSLHostConfigCertificate.java:187) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.tomcat.util.net.jsse.JSSEUtil.getKeyManagers(JSSEUtil.java:185) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:112) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:85) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:225) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:978) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:628) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.catalina.connector.Connector.startInternal(Connector.java:993) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.apache.catalina.core.StandardService.addConnector(StandardService.java:225) [tomcat-embed-core-8.5.14.jar!/:8.5.14]
	at org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.addPreviouslyRemovedConnectors(TomcatEmbeddedServletContainer.java:247) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.start(TomcatEmbeddedServletContainer.java:190) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.startEmbeddedServletContainer(EmbeddedWebApplicationContext.java:297) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.finishRefresh(EmbeddedWebApplicationContext.java:145) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:545) [spring-context-4.3.8.RELEASE.jar!/:4.3.8.RELEASE]
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.3.RELEASE.jar!/:1.5.3.RELEASE]
	at [Redacted]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_111]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_111]
	at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48) [giant2-ccp-example.jar:1.19.20-SNAPSHOT]
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:87) [giant2-ccp-example.jar:1.19.20-SNAPSHOT]
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:50) [giant2-ccp-example.jar:1.19.20-SNAPSHOT]
	at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51) [giant2-ccp-example.jar:1.19.20-SNAPSHOT]
Comment 1 Mark Thomas 2018-06-29 11:53:58 UTC
This looks like it should be fairly easy to implement along the same lines as Spring Boot - i.e. 
store.load(new DomainLoadStoreParameter(url.toURI(), Collections.emptyMap()));

There is a small additional complication that this is a Java 8 feature and Tomcat 8.x has to run on Java 7. We'll need to go via Tomcat's JreCompat.

It would be good if you are able to test any implementation before it gets included in a release. Are you able to build Tomcat trunk (9.0.x) from source and test it?
Comment 2 Andy Wilkinson 2018-06-29 12:01:34 UTC
Cool. Thanks, Mark. I am willing and able to build from trunk (or a branch) or use a snapshot when something's available,
Comment 3 Mark Thomas 2018-06-29 12:06:43 UTC
Great. Thanks Andy. I've added a first pass at DKS support to trunk (9.0.x). You should be able to specify "DKS" for ConfigFileLoader.getURI(path) and the URI for certificateKeystoreFile.

Once this works (hopefully it will work first time) I'll add some docs and back-port it to 8.5.x.
Comment 4 Andy Wilkinson 2018-07-02 10:02:21 UTC
I've built trunk and the DKS keystore is now being handled specially but it doesn't appear to be working correctly:

org.apache.catalina.LifecycleException: Protocol handler start failed
	at org.apache.catalina.connector.Connector.startInternal(Connector.java:960) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.catalina.core.StandardService.addConnector(StandardService.java:225) [tomcat-embed-core.jar:9.0.11-dev]
	at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.addPreviouslyRemovedConnectors(TomcatWebServer.java:256) [classes/:na]
	at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:198) [classes/:na]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.startWebServer(ServletWebServerApplicationContext.java:300) [classes/:na]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:162) [classes/:na]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:553) [spring-context-5.1.0.BUILD-SNAPSHOT.jar:5.1.0.BUILD-SNAPSHOT]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) [classes/:na]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:769) [classes/:na]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:405) [classes/:na]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) [classes/:na]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1252) [classes/:na]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1240) [classes/:na]
	at sample.tomcat.ssl.SampleTomcatSslApplication.main(SampleTomcatSslApplication.java:26) [classes/:na]
Caused by: java.lang.IllegalArgumentException: Error setting key entry for 'app1 spring-boot-ssl-sample'
	at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:114) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:85) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:224) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:1107) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:550) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.catalina.connector.Connector.startInternal(Connector.java:957) ~[tomcat-embed-core.jar:9.0.11-dev]
	... 14 common frames omitted
Caused by: java.security.KeyStoreException: Error setting key entry for 'app1 spring-boot-ssl-sample'
	at sun.security.provider.DomainKeyStore.engineSetKeyEntry(DomainKeyStore.java:269) ~[na:1.8.0_151]
	at sun.security.provider.DomainKeyStore$DKS.engineSetKeyEntry(DomainKeyStore.java:68) ~[na:1.8.0_151]
	at java.security.KeyStore.setKeyEntry(KeyStore.java:1140) ~[na:1.8.0_151]
	at org.apache.tomcat.util.net.jsse.JSSEUtil.getKeyManagers(JSSEUtil.java:257) ~[tomcat-embed-core.jar:9.0.11-dev]
	at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:112) ~[tomcat-embed-core.jar:9.0.11-dev]
	... 19 common frames omitted

This error is occurring because DomainKeyStore$DKS.getKeyStoreForWriting is being called with 'app1 spring-boot-ssl-sample' and returning null. The passed in String is split on space and the first component, app1, is used as the key for a map lookup. The map only contains a single entry with the key
iostream1 so it returns null when asked for app1. The iostream1 entry is written as a result of the ksUsed.load(null,  null) call on line 256 of JSSEUtil.getKeyManagers(). Prior to the call to load, the map is empty. By contrast to ksUsed, at this point the map within ks contains a single entry named app1. In short, it appears that the switch to an in-memory store for a PKSC#8 key does not work.

I'm insufficiently experienced with DKS to know if the above is expected behaviour. If it is expected, the diagnostics could be approved as I could
only determine the above by stepping through in the debugger.
Comment 5 Mark Thomas 2018-07-05 12:20:26 UTC
I've disabled in-memory keystores for DKS in trunk. If you could test again and let me know how you get on...
Comment 6 Andy Wilkinson 2018-07-05 12:31:46 UTC
Thanks, Mark. It looks good to me now. I've got embedded Tomcat using a JKS KeyStore that's been loaded via an entry in a DKS KeyStore.
Comment 7 Mark Thomas 2018-07-05 22:01:26 UTC
Thanks for the testing. I've back-ported it to 8.5.x as well although I had to use reflection as DKS requires Java 8 (and 8.5.x is Java 7 or later).

Fixed in:
- trunk for 9.0.11 onwards
- 8.5.x for 8.5.33 onwards