Skip to content

Commit 8e73232

Browse files
aratnoabsurdfarce
authored andcommitted
CASSANDRA-19180: Support reloading keystore in cassandra-java-driver
1 parent 8d5849c commit 8e73232

File tree

14 files changed

+627
-16
lines changed

14 files changed

+627
-16
lines changed

core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ public enum DefaultDriverOption implements DriverOption {
255255
* <p>Value-type: {@link String}
256256
*/
257257
SSL_KEYSTORE_PASSWORD("advanced.ssl-engine-factory.keystore-password"),
258+
/**
259+
* The duration between attempts to reload the keystore.
260+
*
261+
* <p>Value-type: {@link java.time.Duration}
262+
*/
263+
SSL_KEYSTORE_RELOAD_INTERVAL("advanced.ssl-engine-factory.keystore-reload-interval"),
258264
/**
259265
* The location of the truststore file.
260266
*

core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ public String toString() {
235235
/** The keystore password. */
236236
public static final TypedDriverOption<String> SSL_KEYSTORE_PASSWORD =
237237
new TypedDriverOption<>(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, GenericType.STRING);
238+
239+
/** The duration between attempts to reload the keystore. */
240+
public static final TypedDriverOption<Duration> SSL_KEYSTORE_RELOAD_INTERVAL =
241+
new TypedDriverOption<>(
242+
DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL, GenericType.DURATION);
243+
238244
/** The location of the truststore file. */
239245
public static final TypedDriverOption<String> SSL_TRUSTSTORE_PATH =
240246
new TypedDriverOption<>(DefaultDriverOption.SSL_TRUSTSTORE_PATH, GenericType.STRING);

core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@
2727
import java.net.InetSocketAddress;
2828
import java.net.SocketAddress;
2929
import java.nio.file.Files;
30+
import java.nio.file.Path;
3031
import java.nio.file.Paths;
3132
import java.security.KeyStore;
3233
import java.security.SecureRandom;
34+
import java.time.Duration;
3335
import java.util.List;
34-
import javax.net.ssl.KeyManagerFactory;
3536
import javax.net.ssl.SSLContext;
3637
import javax.net.ssl.SSLEngine;
3738
import javax.net.ssl.SSLParameters;
@@ -54,6 +55,7 @@
5455
* truststore-password = password123
5556
* keystore-path = /path/to/client.keystore
5657
* keystore-password = password123
58+
* keystore-reload-interval = 30 minutes
5759
* }
5860
* }
5961
* </pre>
@@ -66,6 +68,7 @@ public class DefaultSslEngineFactory implements SslEngineFactory {
6668
private final SSLContext sslContext;
6769
private final String[] cipherSuites;
6870
private final boolean requireHostnameValidation;
71+
private ReloadingKeyManagerFactory kmf;
6972

7073
/** Builds a new instance from the driver configuration. */
7174
public DefaultSslEngineFactory(DriverContext driverContext) {
@@ -132,20 +135,8 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio
132135
}
133136

134137
// initialize keystore if configured.
135-
KeyManagerFactory kmf = null;
136138
if (config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PATH)) {
137-
try (InputStream ksf =
138-
Files.newInputStream(
139-
Paths.get(config.getString(DefaultDriverOption.SSL_KEYSTORE_PATH)))) {
140-
KeyStore ks = KeyStore.getInstance("JKS");
141-
char[] password =
142-
config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PASSWORD)
143-
? config.getString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD).toCharArray()
144-
: null;
145-
ks.load(ksf, password);
146-
kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
147-
kmf.init(ks, password);
148-
}
139+
kmf = buildReloadingKeyManagerFactory(config);
149140
}
150141

151142
context.init(
@@ -159,8 +150,22 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio
159150
}
160151
}
161152

153+
private ReloadingKeyManagerFactory buildReloadingKeyManagerFactory(
154+
DriverExecutionProfile config) {
155+
Path keystorePath = Paths.get(config.getString(DefaultDriverOption.SSL_KEYSTORE_PATH));
156+
String password =
157+
config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PASSWORD)
158+
? config.getString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD)
159+
: null;
160+
Duration reloadInterval =
161+
config.isDefined(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL)
162+
? config.getDuration(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL)
163+
: Duration.ZERO;
164+
return ReloadingKeyManagerFactory.create(keystorePath, password, reloadInterval);
165+
}
166+
162167
@Override
163168
public void close() throws Exception {
164-
// nothing to do
169+
kmf.close();
165170
}
166171
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package com.datastax.oss.driver.internal.core.ssl;
19+
20+
import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting;
21+
import java.io.ByteArrayInputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.net.Socket;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
27+
import java.security.KeyStore;
28+
import java.security.KeyStoreException;
29+
import java.security.MessageDigest;
30+
import java.security.NoSuchAlgorithmException;
31+
import java.security.Principal;
32+
import java.security.PrivateKey;
33+
import java.security.Provider;
34+
import java.security.UnrecoverableKeyException;
35+
import java.security.cert.CertificateException;
36+
import java.security.cert.X509Certificate;
37+
import java.time.Duration;
38+
import java.util.Arrays;
39+
import java.util.concurrent.Executors;
40+
import java.util.concurrent.ScheduledExecutorService;
41+
import java.util.concurrent.TimeUnit;
42+
import java.util.concurrent.atomic.AtomicReference;
43+
import javax.net.ssl.KeyManager;
44+
import javax.net.ssl.KeyManagerFactory;
45+
import javax.net.ssl.KeyManagerFactorySpi;
46+
import javax.net.ssl.ManagerFactoryParameters;
47+
import javax.net.ssl.SSLEngine;
48+
import javax.net.ssl.X509ExtendedKeyManager;
49+
import org.slf4j.Logger;
50+
import org.slf4j.LoggerFactory;
51+
52+
public class ReloadingKeyManagerFactory extends KeyManagerFactory implements AutoCloseable {
53+
private static final Logger logger = LoggerFactory.getLogger(ReloadingKeyManagerFactory.class);
54+
private static final String KEYSTORE_TYPE = "JKS";
55+
private Path keystorePath;
56+
private String keystorePassword;
57+
private ScheduledExecutorService executor;
58+
private final Spi spi;
59+
60+
// We're using a single thread executor so this shouldn't need to be volatile, since all updates
61+
// to lastDigest should come from the same thread
62+
private volatile byte[] lastDigest;
63+
64+
/**
65+
* Create a new {@link ReloadingKeyManagerFactory} with the given keystore file and password,
66+
* reloading from the file's content at the given interval. This function will do an initial
67+
* reload before returning, to confirm that the file exists and is readable.
68+
*
69+
* @param keystorePath the keystore file to reload
70+
* @param keystorePassword the keystore password
71+
* @param reloadInterval the duration between reload attempts. Set to {@link
72+
* java.time.Duration#ZERO} to disable scheduled reloading.
73+
* @return
74+
*/
75+
public static ReloadingKeyManagerFactory create(
76+
Path keystorePath, String keystorePassword, Duration reloadInterval) {
77+
KeyManagerFactory kmf;
78+
try {
79+
kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
80+
} catch (NoSuchAlgorithmException e) {
81+
throw new RuntimeException(e);
82+
}
83+
84+
KeyStore ks;
85+
try (InputStream ksf = Files.newInputStream(keystorePath)) {
86+
ks = KeyStore.getInstance(KEYSTORE_TYPE);
87+
ks.load(ksf, keystorePassword.toCharArray());
88+
} catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) {
89+
throw new RuntimeException(e);
90+
}
91+
try {
92+
kmf.init(ks, keystorePassword.toCharArray());
93+
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
94+
throw new RuntimeException(e);
95+
}
96+
97+
ReloadingKeyManagerFactory reloadingKeyManagerFactory = new ReloadingKeyManagerFactory(kmf);
98+
reloadingKeyManagerFactory.start(keystorePath, keystorePassword, reloadInterval);
99+
return reloadingKeyManagerFactory;
100+
}
101+
102+
@VisibleForTesting
103+
protected ReloadingKeyManagerFactory(KeyManagerFactory initial) {
104+
this(
105+
new Spi((X509ExtendedKeyManager) initial.getKeyManagers()[0]),
106+
initial.getProvider(),
107+
initial.getAlgorithm());
108+
}
109+
110+
private ReloadingKeyManagerFactory(Spi spi, Provider provider, String algorithm) {
111+
super(spi, provider, algorithm);
112+
this.spi = spi;
113+
}
114+
115+
private void start(Path keystorePath, String keystorePassword, Duration reloadInterval) {
116+
this.keystorePath = keystorePath;
117+
this.keystorePassword = keystorePassword;
118+
this.executor =
119+
Executors.newScheduledThreadPool(
120+
1,
121+
runnable -> {
122+
Thread t = Executors.defaultThreadFactory().newThread(runnable);
123+
t.setDaemon(true);
124+
return t;
125+
});
126+
127+
// Ensure that reload is called once synchronously, to make sure the file exists etc.
128+
reload();
129+
130+
if (!reloadInterval.isZero())
131+
this.executor.scheduleWithFixedDelay(
132+
this::reload,
133+
reloadInterval.toMillis(),
134+
reloadInterval.toMillis(),
135+
TimeUnit.MILLISECONDS);
136+
}
137+
138+
@VisibleForTesting
139+
void reload() {
140+
try {
141+
reload0();
142+
} catch (Exception e) {
143+
logger.warn("Failed to reload", e);
144+
}
145+
}
146+
147+
private synchronized void reload0()
148+
throws NoSuchAlgorithmException, IOException, KeyStoreException, CertificateException,
149+
UnrecoverableKeyException {
150+
logger.debug("Checking KeyStore file {} for updates", keystorePath);
151+
152+
final byte[] keyStoreBytes = Files.readAllBytes(keystorePath);
153+
final byte[] newDigest = digest(keyStoreBytes);
154+
if (lastDigest != null && Arrays.equals(lastDigest, digest(keyStoreBytes))) {
155+
logger.debug("KeyStore file content has not changed; skipping update");
156+
return;
157+
}
158+
159+
final KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
160+
try (InputStream inputStream = new ByteArrayInputStream(keyStoreBytes)) {
161+
keyStore.load(inputStream, keystorePassword.toCharArray());
162+
}
163+
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
164+
kmf.init(keyStore, keystorePassword.toCharArray());
165+
logger.info("Detected updates to KeyStore file {}", keystorePath);
166+
167+
this.spi.keyManager.set((X509ExtendedKeyManager) kmf.getKeyManagers()[0]);
168+
this.lastDigest = newDigest;
169+
}
170+
171+
@Override
172+
public void close() throws Exception {
173+
if (executor != null) {
174+
executor.shutdown();
175+
}
176+
}
177+
178+
private static byte[] digest(byte[] payload) throws NoSuchAlgorithmException {
179+
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
180+
return digest.digest(payload);
181+
}
182+
183+
private static class Spi extends KeyManagerFactorySpi {
184+
DelegatingKeyManager keyManager;
185+
186+
Spi(X509ExtendedKeyManager initial) {
187+
this.keyManager = new DelegatingKeyManager(initial);
188+
}
189+
190+
@Override
191+
protected void engineInit(KeyStore ks, char[] password) {
192+
throw new UnsupportedOperationException();
193+
}
194+
195+
@Override
196+
protected void engineInit(ManagerFactoryParameters spec) {
197+
throw new UnsupportedOperationException();
198+
}
199+
200+
@Override
201+
protected KeyManager[] engineGetKeyManagers() {
202+
return new KeyManager[] {keyManager};
203+
}
204+
}
205+
206+
private static class DelegatingKeyManager extends X509ExtendedKeyManager {
207+
AtomicReference<X509ExtendedKeyManager> delegate;
208+
209+
DelegatingKeyManager(X509ExtendedKeyManager initial) {
210+
delegate = new AtomicReference<>(initial);
211+
}
212+
213+
void set(X509ExtendedKeyManager keyManager) {
214+
delegate.set(keyManager);
215+
}
216+
217+
@Override
218+
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
219+
return delegate.get().chooseEngineClientAlias(keyType, issuers, engine);
220+
}
221+
222+
@Override
223+
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
224+
return delegate.get().chooseEngineServerAlias(keyType, issuers, engine);
225+
}
226+
227+
@Override
228+
public String[] getClientAliases(String keyType, Principal[] issuers) {
229+
return delegate.get().getClientAliases(keyType, issuers);
230+
}
231+
232+
@Override
233+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
234+
return delegate.get().chooseClientAlias(keyType, issuers, socket);
235+
}
236+
237+
@Override
238+
public String[] getServerAliases(String keyType, Principal[] issuers) {
239+
return delegate.get().getServerAliases(keyType, issuers);
240+
}
241+
242+
@Override
243+
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
244+
return delegate.get().chooseServerAlias(keyType, issuers, socket);
245+
}
246+
247+
@Override
248+
public X509Certificate[] getCertificateChain(String alias) {
249+
return delegate.get().getCertificateChain(alias);
250+
}
251+
252+
@Override
253+
public PrivateKey getPrivateKey(String alias) {
254+
return delegate.get().getPrivateKey(alias);
255+
}
256+
}
257+
}

core/src/main/resources/reference.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,13 @@ datastax-java-driver {
790790
// truststore-password = password123
791791
// keystore-path = /path/to/client.keystore
792792
// keystore-password = password123
793+
794+
# The duration between attempts to reload the keystore from the contents of the file specified
795+
# by `keystore-path`. This is mainly relevant in environments where certificates have short
796+
# lifetimes and applications are restarted infrequently, since an expired client certificate
797+
# will prevent new connections from being established until the application is restarted. If
798+
# not set, defaults to not reload the keystore.
799+
// keystore-reload-interval = 30 minutes
793800
}
794801

795802
# The generator that assigns a microsecond timestamp to each request.

0 commit comments

Comments
 (0)