The issue
I would like to use HTTPS with spring boot (1.5.9) and I would like to get the KeyStore from memory (eventually from HashiCorp Vault) and not from disk. In order to do this, I implemented a SslStoreProvider and set it up through an EmbeddedServletContainerCustomizer; please check this example.
The example above fails because of SslStoreProviderUrlStreamHandlerFactory re-encrypts the KeyStore with an empty password so I get an error: java.io.IOException: keystore password was incorrect
:
ByteArrayOutputStream stream = new ByteArrayOutputStream();
this.keyStore.store(stream, new char[0]); //the second parameter is the password
return new ByteArrayInputStream(stream.toByteArray());
I tried to trick this with an empty password but when Tomcat tries to load the InputStream
of the KeyStore, in SSLUtilBase, it checks if the password is empty and it will use null
in this case:
char[] storePass = null;
if (pass != null && !"".equals(pass)) {
storePass = pass.toCharArray();
}
ks.load(istream, storePass);
Based on these code snippets, I can't really see how could this work, the password can never be empty when Tomcat tries to load the KeyStore.
Additional details
Tomcat tries to get the KeyStore this way: url.openConnection().getInputStream()
in ConfigFileLoader. In order to make this work, TomcatEmbeddedServletContainerFactory will set the url to springbootssl:keyStore
and register SslStoreProviderUrlStreamHandlerFactory for this url.
I'm using Spring-Boot 1.5.9-RELEASE, OpenJDK 1.8.0_152 and Gradle 4.4
I created an example which reproduces the issue. I fixed it with a hack (registering a custom url handler before the original one) just to see if this works, please see the commits.
Possible fix
I think this could be fixed in SslStoreProviderUrlStreamHandlerFactory
the way I did in the example because when TomcatEmbeddedServletContainerFactory
sets this up, it has the Ssl object which contains the password. If this approach could work, I'm happy to create a PR.
Comment From: jonatan-ivanov
@philwebb Could you please check this issue?
Comment From: wilkinsona
Thanks for the sample.
When you are using an SslStoreProvider
to provide the store there is no need to configure the server.ssl.key-store-password
property. If I check out commit b0e2713 in the sample and remove the property, the problem described in this issue no longer occurs.
It's worth noting that were you to switch to Jetty or Undertow, the property would have no effect and would not be used. I think what we need to do here is to make Tomcat behave in the same way. The current behaviour is an unfortunate side-effect of having to use a UrlStreamHandlerFactory
to get Tomcat to consume an in-memory store. Re-using the password from the Ssl
object would be one way to do that, but it doesn't feel quite right to me.
Comment From: philwebb
Possibly related to #11395
Comment From: mbhave
@wilkinsona Since the SslStoreProviderUrlStreamHandlerFactory
stores the KeyStore with an empty password, I wonder if we could ignore the password from the Ssl
object if there is a SslStoreProvider
. Maybe something like,
if (sslStoreProvider != null) {
protocol.setKeystorePass(null);
}
Comment From: wilkinsona
@mbhave My memory of this is a little hazy now, but that sounds like it would work.
Comment From: jonatan-ivanov
@mbhave @wilkinsona I was able to repro the issue using spring-boot 2.3.5.RELEASE
.
It seems if I return a KeyStore
in the SslStoreProvider
that has a non-empty password, Tomcat won't be able to load it.
I went down to SSLUtilBase
where Tomcat loads the keys: Key k = ks.getKey(keyAlias, keyPassArray);
.
Here the KeyStore
is what I provide (that has a non-empty password) and the password passed in keyPassArray
is empty which ends-up in an exception.
What do you think about reopening this issue?
Comment From: wilkinsona
We can't re-open the issue as a fix has already been shipped (in 2.0.2). What's the use case for returning a password-protected KeyStore
from SslStoreProvider
? It's already been read into memory so password protecting it doesn't provide any security benefit as far as I can tell. If you have a use case that we've overlooked, please open a new issue and we can consider making an enhancement.
Comment From: jonatan-ivanov
@wilkinsona The use case is using a keystore that I don't create, base64 encode it, put it into a secret storage (vault, aws secrets mgr, etc.), get it at bootstrap base64 decode it, load it into a keystore and make it available through SslStoreProvider
. The use case is not making anything more secure (if somebody has access the memory, it's already game over :)) but not doing any post processing with an already password protected keystore other than making it available for the servlet container.
Let's say something like this:
public class InMemorySslStoreProvider implements SslStoreProvider {
private final KeyStore keyStore;
private final KeyStore trustStore;
public InMemorySslStoreProvider(Ssl ssl, String encodedKeystore, String encodedTruststore) throws GeneralSecurityException, IOException {
this.keyStore = load(encodedKeystore, ssl.getKeyStoreType(), ssl.getKeyStorePassword());
this.trustStore = load(encodedTruststore, ssl.getTrustStoreType(), ssl.getTrustStorePassword());
}
@Override
public KeyStore getKeyStore() {
return keyStore;
}
@Override
public KeyStore getTrustStore() {
return trustStore;
}
private KeyStore load(String encoded, String type, String password) throws GeneralSecurityException, IOException {
return load(Base64.getDecoder().decode(encoded), type, password);
}
private KeyStore load(byte[] bytes, String type, String password) throws GeneralSecurityException, IOException {
try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
KeyStore keyStore = KeyStore.getInstance(type);
keyStore.load(inputStream, password.toCharArray());
return keyStore;
}
}
}
Comment From: wilkinsona
Thanks. That confirms I don't understand the problem. To get the KeyStore into Tomcat we store it with an empty password and then provide Tomcat with a custom URL that it can use to load it again. The KeyStore's original password should make no difference. Please open a new issue with some steps to reproduce and we can take a look at what, if anything, needs to be improved.
Comment From: jonatan-ivanov
@wilkinsona
To get the KeyStore into Tomcat we store it with an empty password and then provide Tomcat with a custom URL that it can use to load it again.
By this you mean the SslStoreProviderUrlStreamHandlerFactory
, right?
If so, then indeed, this stores the keystore with an empty password but the private key is also password protected and this class does not touch it so when SSLUtilBase
tries to get the key from a keystore: Key k = ks.getKey(keyAlias, keyPassArray);
it fails. Please notice that ks
is an instance of Keystore
, you need a password to load
it (the empty one) but here Tomcat is trying to get the private key which is protected by the original password. :)
I'm going to create a new issue and an example project with a suggested fix later this week.