Skip to content

Commit dc1b995

Browse files
committed
Merge commit 'ea2e475185b5863ef6eed347f57286d6a3bfd8a9' of https://github.com/apache/cassandra-java-driver into pull-upstream-4.18.1-v3
2 parents 5c26a7f + ea2e475 commit dc1b995

File tree

14 files changed

+630
-16
lines changed

14 files changed

+630
-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
@@ -997,6 +997,12 @@ public enum DefaultDriverOption implements DriverOption {
997997
* <p>Value-type: boolean
998998
*/
999999
METRICS_GENERATE_AGGREGABLE_HISTOGRAMS("advanced.metrics.histograms.generate-aggregable"),
1000+
/**
1001+
* The duration between attempts to reload the keystore.
1002+
*
1003+
* <p>Value-type: {@link java.time.Duration}
1004+
*/
1005+
SSL_KEYSTORE_RELOAD_INTERVAL("advanced.ssl-engine-factory.keystore-reload-interval"),
10001006
;
10011007

10021008
private final String path;

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
@@ -238,6 +238,12 @@ public String toString() {
238238
/** The keystore password. */
239239
public static final TypedDriverOption<String> SSL_KEYSTORE_PASSWORD =
240240
new TypedDriverOption<>(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, GenericType.STRING);
241+
242+
/** The duration between attempts to reload the keystore. */
243+
public static final TypedDriverOption<Duration> SSL_KEYSTORE_RELOAD_INTERVAL =
244+
new TypedDriverOption<>(
245+
DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL, GenericType.DURATION);
246+
241247
/** The location of the truststore file. */
242248
public static final TypedDriverOption<String> SSL_TRUSTSTORE_PATH =
243249
new TypedDriverOption<>(DefaultDriverOption.SSL_TRUSTSTORE_PATH, GenericType.STRING);

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
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;
36+
import java.util.Optional;
3537
import javax.net.ssl.SSLContext;
3638
import javax.net.ssl.SSLEngine;
3739
import javax.net.ssl.SSLParameters;
@@ -54,6 +56,7 @@
5456
* truststore-password = password123
5557
* keystore-path = /path/to/client.keystore
5658
* keystore-password = password123
59+
* keystore-reload-interval = 30 minutes
5760
* }
5861
* }
5962
* </pre>
@@ -66,6 +69,7 @@ public class DefaultSslEngineFactory implements SslEngineFactory {
6669
private final SSLContext sslContext;
6770
private final String[] cipherSuites;
6871
private final boolean requireHostnameValidation;
72+
private ReloadingKeyManagerFactory kmf;
6973

7074
/** Builds a new instance from the driver configuration. */
7175
public DefaultSslEngineFactory(DriverContext driverContext) {
@@ -132,20 +136,8 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio
132136
}
133137

134138
// initialize keystore if configured.
135-
KeyManagerFactory kmf = null;
136139
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-
}
140+
kmf = buildReloadingKeyManagerFactory(config);
149141
}
150142

151143
context.init(
@@ -159,8 +151,19 @@ protected SSLContext buildContext(DriverExecutionProfile config) throws Exceptio
159151
}
160152
}
161153

154+
private ReloadingKeyManagerFactory buildReloadingKeyManagerFactory(DriverExecutionProfile config)
155+
throws Exception {
156+
Path keystorePath = Paths.get(config.getString(DefaultDriverOption.SSL_KEYSTORE_PATH));
157+
String password = config.getString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, null);
158+
Optional<Duration> reloadInterval =
159+
Optional.ofNullable(
160+
config.getDuration(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL, null));
161+
162+
return ReloadingKeyManagerFactory.create(keystorePath, password, reloadInterval);
163+
}
164+
162165
@Override
163166
public void close() throws Exception {
164-
// nothing to do
167+
kmf.close();
165168
}
166169
}
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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.Optional;
40+
import java.util.concurrent.Executors;
41+
import java.util.concurrent.ScheduledExecutorService;
42+
import java.util.concurrent.TimeUnit;
43+
import java.util.concurrent.atomic.AtomicReference;
44+
import javax.net.ssl.KeyManager;
45+
import javax.net.ssl.KeyManagerFactory;
46+
import javax.net.ssl.KeyManagerFactorySpi;
47+
import javax.net.ssl.ManagerFactoryParameters;
48+
import javax.net.ssl.SSLEngine;
49+
import javax.net.ssl.X509ExtendedKeyManager;
50+
import org.slf4j.Logger;
51+
import org.slf4j.LoggerFactory;
52+
53+
public class ReloadingKeyManagerFactory extends KeyManagerFactory implements AutoCloseable {
54+
private static final Logger logger = LoggerFactory.getLogger(ReloadingKeyManagerFactory.class);
55+
private static final String KEYSTORE_TYPE = "JKS";
56+
private Path keystorePath;
57+
private String keystorePassword;
58+
private ScheduledExecutorService executor;
59+
private final Spi spi;
60+
61+
// We're using a single thread executor so this shouldn't need to be volatile, since all updates
62+
// to lastDigest should come from the same thread
63+
private volatile byte[] lastDigest;
64+
65+
/**
66+
* Create a new {@link ReloadingKeyManagerFactory} with the given keystore file and password,
67+
* reloading from the file's content at the given interval. This function will do an initial
68+
* reload before returning, to confirm that the file exists and is readable.
69+
*
70+
* @param keystorePath the keystore file to reload
71+
* @param keystorePassword the keystore password
72+
* @param reloadInterval the duration between reload attempts. Set to {@link Optional#empty()} to
73+
* disable scheduled reloading.
74+
* @return
75+
*/
76+
static ReloadingKeyManagerFactory create(
77+
Path keystorePath, String keystorePassword, Optional<Duration> reloadInterval)
78+
throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException,
79+
CertificateException, IOException {
80+
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
81+
82+
KeyStore ks;
83+
try (InputStream ksf = Files.newInputStream(keystorePath)) {
84+
ks = KeyStore.getInstance(KEYSTORE_TYPE);
85+
ks.load(ksf, keystorePassword.toCharArray());
86+
}
87+
kmf.init(ks, keystorePassword.toCharArray());
88+
89+
ReloadingKeyManagerFactory reloadingKeyManagerFactory = new ReloadingKeyManagerFactory(kmf);
90+
reloadingKeyManagerFactory.start(keystorePath, keystorePassword, reloadInterval);
91+
return reloadingKeyManagerFactory;
92+
}
93+
94+
@VisibleForTesting
95+
protected ReloadingKeyManagerFactory(KeyManagerFactory initial) {
96+
this(
97+
new Spi((X509ExtendedKeyManager) initial.getKeyManagers()[0]),
98+
initial.getProvider(),
99+
initial.getAlgorithm());
100+
}
101+
102+
private ReloadingKeyManagerFactory(Spi spi, Provider provider, String algorithm) {
103+
super(spi, provider, algorithm);
104+
this.spi = spi;
105+
}
106+
107+
private void start(
108+
Path keystorePath, String keystorePassword, Optional<Duration> reloadInterval) {
109+
this.keystorePath = keystorePath;
110+
this.keystorePassword = keystorePassword;
111+
112+
// Ensure that reload is called once synchronously, to make sure the file exists etc.
113+
reload();
114+
115+
if (!reloadInterval.isPresent() || reloadInterval.get().isZero()) {
116+
final String msg =
117+
"KeyStore reloading is disabled. If your Cassandra cluster requires client certificates, "
118+
+ "client application restarts are infrequent, and client certificates have short lifetimes, then your client "
119+
+ "may fail to re-establish connections to Cassandra hosts. To enable KeyStore reloading, see "
120+
+ "`advanced.ssl-engine-factory.keystore-reload-interval` in reference.conf.";
121+
logger.info(msg);
122+
} else {
123+
logger.info("KeyStore reloading is enabled with interval {}", reloadInterval.get());
124+
125+
this.executor =
126+
Executors.newScheduledThreadPool(
127+
1,
128+
runnable -> {
129+
Thread t = Executors.defaultThreadFactory().newThread(runnable);
130+
t.setName(String.format("%s-%%d", this.getClass().getSimpleName()));
131+
t.setDaemon(true);
132+
return t;
133+
});
134+
this.executor.scheduleWithFixedDelay(
135+
this::reload,
136+
reloadInterval.get().toMillis(),
137+
reloadInterval.get().toMillis(),
138+
TimeUnit.MILLISECONDS);
139+
}
140+
}
141+
142+
@VisibleForTesting
143+
void reload() {
144+
try {
145+
reload0();
146+
} catch (Exception e) {
147+
String msg =
148+
"Failed to reload KeyStore. If this continues to happen, your client may use stale identity"
149+
+ " certificates and fail to re-establish connections to Cassandra hosts.";
150+
logger.warn(msg, e);
151+
}
152+
}
153+
154+
private synchronized void reload0()
155+
throws NoSuchAlgorithmException, IOException, KeyStoreException, CertificateException,
156+
UnrecoverableKeyException {
157+
logger.debug("Checking KeyStore file {} for updates", keystorePath);
158+
159+
final byte[] keyStoreBytes = Files.readAllBytes(keystorePath);
160+
final byte[] newDigest = digest(keyStoreBytes);
161+
if (lastDigest != null && Arrays.equals(lastDigest, digest(keyStoreBytes))) {
162+
logger.debug("KeyStore file content has not changed; skipping update");
163+
return;
164+
}
165+
166+
final KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
167+
try (InputStream inputStream = new ByteArrayInputStream(keyStoreBytes)) {
168+
keyStore.load(inputStream, keystorePassword.toCharArray());
169+
}
170+
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
171+
kmf.init(keyStore, keystorePassword.toCharArray());
172+
logger.info("Detected updates to KeyStore file {}", keystorePath);
173+
174+
this.spi.keyManager.set((X509ExtendedKeyManager) kmf.getKeyManagers()[0]);
175+
this.lastDigest = newDigest;
176+
}
177+
178+
@Override
179+
public void close() throws Exception {
180+
if (executor != null) {
181+
executor.shutdown();
182+
}
183+
}
184+
185+
private static byte[] digest(byte[] payload) throws NoSuchAlgorithmException {
186+
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
187+
return digest.digest(payload);
188+
}
189+
190+
private static class Spi extends KeyManagerFactorySpi {
191+
DelegatingKeyManager keyManager;
192+
193+
Spi(X509ExtendedKeyManager initial) {
194+
this.keyManager = new DelegatingKeyManager(initial);
195+
}
196+
197+
@Override
198+
protected void engineInit(KeyStore ks, char[] password) {
199+
throw new UnsupportedOperationException();
200+
}
201+
202+
@Override
203+
protected void engineInit(ManagerFactoryParameters spec) {
204+
throw new UnsupportedOperationException();
205+
}
206+
207+
@Override
208+
protected KeyManager[] engineGetKeyManagers() {
209+
return new KeyManager[] {keyManager};
210+
}
211+
}
212+
213+
private static class DelegatingKeyManager extends X509ExtendedKeyManager {
214+
AtomicReference<X509ExtendedKeyManager> delegate;
215+
216+
DelegatingKeyManager(X509ExtendedKeyManager initial) {
217+
delegate = new AtomicReference<>(initial);
218+
}
219+
220+
void set(X509ExtendedKeyManager keyManager) {
221+
delegate.set(keyManager);
222+
}
223+
224+
@Override
225+
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
226+
return delegate.get().chooseEngineClientAlias(keyType, issuers, engine);
227+
}
228+
229+
@Override
230+
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
231+
return delegate.get().chooseEngineServerAlias(keyType, issuers, engine);
232+
}
233+
234+
@Override
235+
public String[] getClientAliases(String keyType, Principal[] issuers) {
236+
return delegate.get().getClientAliases(keyType, issuers);
237+
}
238+
239+
@Override
240+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
241+
return delegate.get().chooseClientAlias(keyType, issuers, socket);
242+
}
243+
244+
@Override
245+
public String[] getServerAliases(String keyType, Principal[] issuers) {
246+
return delegate.get().getServerAliases(keyType, issuers);
247+
}
248+
249+
@Override
250+
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
251+
return delegate.get().chooseServerAlias(keyType, issuers, socket);
252+
}
253+
254+
@Override
255+
public X509Certificate[] getCertificateChain(String alias) {
256+
return delegate.get().getCertificateChain(alias);
257+
}
258+
259+
@Override
260+
public PrivateKey getPrivateKey(String alias) {
261+
return delegate.get().getPrivateKey(alias);
262+
}
263+
}
264+
}

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)