Skip to content

Commit 6fd2c95

Browse files
authored
Handle unexpected end of stream errors from KMS. (#1849)
JAVA-6015
1 parent 7200ed9 commit 6fd2c95

6 files changed

Lines changed: 138 additions & 31 deletions

File tree

.evergreen/run-kms-tls-tests.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,27 @@ echo "Running KMS TLS tests"
1717
cp ${JAVA_HOME}/lib/security/cacerts mongo-truststore
1818
${JAVA_HOME}/bin/keytool -importcert -trustcacerts -file ${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem -keystore mongo-truststore -storepass changeit -storetype JKS -noprompt
1919

20+
# Create keystore from server.pem to emulate KMS server in tests.
21+
openssl pkcs12 -export \
22+
-in ${DRIVERS_TOOLS}/.evergreen/x509gen/server.pem \
23+
-out server.p12 \
24+
-password pass:test
25+
2026
export GRADLE_EXTRA_VARS="-Pssl.enabled=true -Pssl.trustStoreType=jks -Pssl.trustStore=`pwd`/mongo-truststore -Pssl.trustStorePassword=changeit"
2127
export KMS_TLS_ERROR_TYPE=${KMS_TLS_ERROR_TYPE}
2228

2329
./gradlew -version
2430

2531
./gradlew --stacktrace --info ${GRADLE_EXTRA_VARS} -Dorg.mongodb.test.uri=${MONGODB_URI} \
2632
-Dorg.mongodb.test.kms.tls.error.type=${KMS_TLS_ERROR_TYPE} \
33+
-Dorg.mongodb.test.kms.keystore.location="$(pwd)" \
2734
driver-sync:cleanTest driver-sync:test --tests ClientSideEncryptionKmsTlsTest
2835
first=$?
2936
echo $first
3037

3138
./gradlew --stacktrace --info ${GRADLE_EXTRA_VARS} -Dorg.mongodb.test.uri=${MONGODB_URI} \
3239
-Dorg.mongodb.test.kms.tls.error.type=${KMS_TLS_ERROR_TYPE} \
40+
-Dorg.mongodb.test.kms.keystore.location="$(pwd)" \
3341
driver-reactive-streams:cleanTest driver-reactive-streams:test --tests ClientSideEncryptionKmsTlsTest
3442
second=$?
3543
echo $second

driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.mongodb.reactivestreams.client.internal.crypt;
1818

19+
import com.mongodb.MongoException;
1920
import com.mongodb.MongoOperationTimeoutException;
2021
import com.mongodb.MongoSocketException;
2122
import com.mongodb.MongoSocketReadTimeoutException;
@@ -131,6 +132,11 @@ private void streamRead(final Stream stream, final MongoKeyDecryptor keyDecrypto
131132

132133
@Override
133134
public void completed(final Integer integer, final Void aVoid) {
135+
if (integer == -1) {
136+
sink.error(new MongoException(
137+
"Unexpected end of stream from KMS provider " + keyDecryptor.getKmsProvider()));
138+
return;
139+
}
134140
buffer.flip();
135141
try {
136142
keyDecryptor.feed(buffer.asNIO());

driver-sync/src/main/com/mongodb/client/internal/Crypt.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,14 +369,17 @@ private void decryptKey(final MongoKeyDecryptor keyDecryptor, @Nullable final Ti
369369
while (bytesNeeded > 0) {
370370
byte[] bytes = new byte[bytesNeeded];
371371
int bytesRead = inputStream.read(bytes, 0, bytes.length);
372+
if (bytesRead == -1) {
373+
throw new MongoException("Unexpected end of stream from KMS provider " + keyDecryptor.getKmsProvider());
374+
}
372375
keyDecryptor.feed(ByteBuffer.wrap(bytes, 0, bytesRead));
373376
bytesNeeded = keyDecryptor.bytesNeeded();
374377
}
375378
}
376379
}
377380

378381
private MongoException wrapInMongoException(final Throwable t) {
379-
if (t instanceof MongoException) {
382+
if (t instanceof MongoClientException) {
380383
return (MongoException) t;
381384
} else {
382385
return new MongoClientException("Exception in encryption library: " + t.getMessage(), t);

driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideEncryptionKmsTlsTest.java

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,40 @@
2020
import com.mongodb.MongoClientException;
2121
import com.mongodb.client.model.vault.DataKeyOptions;
2222
import com.mongodb.client.vault.ClientEncryption;
23+
import com.mongodb.fixture.EncryptionFixture;
2324
import com.mongodb.lang.NonNull;
2425
import com.mongodb.lang.Nullable;
2526
import org.bson.BsonDocument;
2627
import org.junit.jupiter.api.Test;
2728

2829
import javax.net.ssl.SSLContext;
30+
import javax.net.ssl.SSLServerSocket;
31+
import javax.net.ssl.SSLServerSocketFactory;
2932
import javax.net.ssl.TrustManager;
3033
import javax.net.ssl.X509TrustManager;
34+
import java.io.IOException;
35+
import java.net.Socket;
3136
import java.security.KeyManagementException;
3237
import java.security.NoSuchAlgorithmException;
3338
import java.security.cert.CertificateException;
3439
import java.security.cert.CertificateExpiredException;
3540
import java.security.cert.X509Certificate;
3641
import java.util.HashMap;
3742
import java.util.Map;
43+
import java.util.concurrent.CompletableFuture;
44+
import java.util.concurrent.TimeUnit;
3845

3946
import static com.mongodb.ClusterFixture.getEnv;
4047
import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled;
4148
import static com.mongodb.client.Fixture.getMongoClientSettings;
4249
import static java.util.Objects.requireNonNull;
50+
import static org.junit.jupiter.api.Assertions.assertEquals;
4351
import static org.junit.jupiter.api.Assertions.assertNotNull;
4452
import static org.junit.jupiter.api.Assertions.assertThrows;
4553
import static org.junit.jupiter.api.Assertions.assertTrue;
4654
import static org.junit.jupiter.api.Assertions.fail;
4755
import static org.junit.jupiter.api.Assumptions.assumeTrue;
56+
4857
public abstract class AbstractClientSideEncryptionKmsTlsTest {
4958

5059
private static final String SYSTEM_PROPERTY_KEY = "org.mongodb.test.kms.tls.error.type";
@@ -128,7 +137,7 @@ public void testInvalidKmsCertificate() {
128137
* See <a href="https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#11-kms-tls-options-tests">
129138
* 11. KMS TLS Options Tests</a>.
130139
*/
131-
@Test()
140+
@Test
132141
public void testThatCustomSslContextIsUsed() {
133142
assumeTrue(hasEncryptionTestsEnabled());
134143

@@ -165,34 +174,71 @@ public void testThatCustomSslContextIsUsed() {
165174
}
166175
}
167176

177+
/**
178+
* Not a prose spec test. However, it is additional test case for better coverage.
179+
*/
180+
@Test
181+
public void testUnexpectedEndOfStreamFromKmsProvider() throws Exception {
182+
String kmsKeystoreLocation = System.getProperty("org.mongodb.test.kms.keystore.location");
183+
assumeTrue(kmsKeystoreLocation != null && !kmsKeystoreLocation.isEmpty(),
184+
"System property org.mongodb.test.kms.keystore.location is not set");
185+
186+
int kmsPort = 5555;
187+
ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder()
188+
.keyVaultMongoClientSettings(getMongoClientSettings())
189+
.keyVaultNamespace("keyvault.datakeys")
190+
.kmsProviders(new HashMap<String, Map<String, Object>>() {{
191+
put("kmip", new HashMap<String, Object>() {{
192+
put("endpoint", "localhost:" + kmsPort);
193+
}});
194+
}})
195+
.build();
196+
197+
Thread serverThread = null;
198+
try (ClientEncryption clientEncryption = getClientEncryption(clientEncryptionSettings)) {
199+
serverThread = startKmsServerSimulatingEof(EncryptionFixture.buildSslContextFromKeyStore(
200+
kmsKeystoreLocation,
201+
"server.p12"), kmsPort);
202+
203+
MongoClientException mongoException = assertThrows(MongoClientException.class,
204+
() -> clientEncryption.createDataKey("kmip", new DataKeyOptions()));
205+
assertEquals("Exception in encryption library: Unexpected end of stream from KMS provider kmip",
206+
mongoException.getMessage());
207+
} finally {
208+
if (serverThread != null) {
209+
serverThread.interrupt();
210+
}
211+
}
212+
}
213+
168214
private HashMap<String, Map<String, Object>> getKmsProviders() {
169215
return new HashMap<String, Map<String, Object>>() {{
170-
put("aws", new HashMap<String, Object>() {{
216+
put("aws", new HashMap<String, Object>() {{
171217
put("accessKeyId", getEnv("AWS_ACCESS_KEY_ID"));
172218
put("secretAccessKey", getEnv("AWS_SECRET_ACCESS_KEY"));
173219
}});
174-
put("aws:named", new HashMap<String, Object>() {{
220+
put("aws:named", new HashMap<String, Object>() {{
175221
put("accessKeyId", getEnv("AWS_ACCESS_KEY_ID"));
176222
put("secretAccessKey", getEnv("AWS_SECRET_ACCESS_KEY"));
177223
}});
178-
put("azure", new HashMap<String, Object>() {{
224+
put("azure", new HashMap<String, Object>() {{
179225
put("tenantId", getEnv("AZURE_TENANT_ID"));
180226
put("clientId", getEnv("AZURE_CLIENT_ID"));
181227
put("clientSecret", getEnv("AZURE_CLIENT_SECRET"));
182228
put("identityPlatformEndpoint", "login.microsoftonline.com:443");
183229
}});
184-
put("azure:named", new HashMap<String, Object>() {{
230+
put("azure:named", new HashMap<String, Object>() {{
185231
put("tenantId", getEnv("AZURE_TENANT_ID"));
186232
put("clientId", getEnv("AZURE_CLIENT_ID"));
187233
put("clientSecret", getEnv("AZURE_CLIENT_SECRET"));
188234
put("identityPlatformEndpoint", "login.microsoftonline.com:443");
189235
}});
190-
put("gcp", new HashMap<String, Object>() {{
236+
put("gcp", new HashMap<String, Object>() {{
191237
put("email", getEnv("GCP_EMAIL"));
192238
put("privateKey", getEnv("GCP_PRIVATE_KEY"));
193239
put("endpoint", "oauth2.googleapis.com:443");
194240
}});
195-
put("gcp:named", new HashMap<String, Object>() {{
241+
put("gcp:named", new HashMap<String, Object>() {{
196242
put("email", getEnv("GCP_EMAIL"));
197243
put("privateKey", getEnv("GCP_PRIVATE_KEY"));
198244
put("endpoint", "oauth2.googleapis.com:443");
@@ -257,5 +303,29 @@ public void checkServerTrusted(final X509Certificate[] certs, final String authT
257303
throw new RuntimeException(e);
258304
}
259305
}
306+
307+
private Thread startKmsServerSimulatingEof(final SSLContext sslContext, final int kmsPort)
308+
throws Exception {
309+
CompletableFuture<Void> confirmListening = new CompletableFuture<>();
310+
Thread serverThread = new Thread(() -> {
311+
try {
312+
SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory();
313+
try (SSLServerSocket sslServerSocket = (SSLServerSocket) serverSocketFactory.createServerSocket(kmsPort)) {
314+
sslServerSocket.setNeedClientAuth(false);
315+
confirmListening.complete(null);
316+
try (Socket accept = sslServerSocket.accept()) {
317+
accept.setSoTimeout(10000);
318+
accept.getInputStream().read();
319+
}
320+
}
321+
} catch (IOException e) {
322+
throw new RuntimeException(e);
323+
}
324+
}, "KMIP-EOF-Fake-Server");
325+
serverThread.setDaemon(true);
326+
serverThread.start();
327+
confirmListening.get(TimeUnit.SECONDS.toMillis(10), TimeUnit.MILLISECONDS);
328+
return serverThread;
329+
}
260330
}
261331

driver-sync/src/test/functional/com/mongodb/client/auth/AbstractX509AuthenticationTest.java

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.mongodb.client.Fixture;
2323
import com.mongodb.client.MongoClient;
2424
import com.mongodb.connection.NettyTransportSettings;
25+
import com.mongodb.fixture.EncryptionFixture;
2526
import io.netty.handler.ssl.SslContextBuilder;
2627
import io.netty.handler.ssl.SslProvider;
2728
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
@@ -34,14 +35,6 @@
3435

3536
import javax.net.ssl.KeyManagerFactory;
3637
import javax.net.ssl.SSLContext;
37-
import java.io.File;
38-
import java.io.FileInputStream;
39-
import java.io.IOException;
40-
import java.security.KeyStore;
41-
import java.security.KeyStoreException;
42-
import java.security.NoSuchAlgorithmException;
43-
import java.security.UnrecoverableKeyException;
44-
import java.security.cert.CertificateException;
4538
import java.util.stream.Stream;
4639

4740
import static com.mongodb.AuthenticationMechanism.MONGODB_X509;
@@ -52,7 +45,6 @@
5245
@ExtendWith(AbstractX509AuthenticationTest.X509AuthenticationPropertyCondition.class)
5346
public abstract class AbstractX509AuthenticationTest {
5447

55-
private static final String KEYSTORE_PASSWORD = "test";
5648
protected abstract MongoClient createMongoClient(MongoClientSettings mongoClientSettings);
5749

5850
private static Stream<Arguments> shouldAuthenticateWithClientCertificate() throws Exception {
@@ -128,22 +120,11 @@ private static Stream<Arguments> getArgumentForKeystore(final String keystoreFil
128120
}
129121

130122
private static SSLContext buildSslContextFromKeyStore(final String keystoreFileName) throws Exception {
131-
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keystoreFileName);
132-
SSLContext sslContext = SSLContext.getInstance("TLS");
133-
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
134-
return sslContext;
123+
return EncryptionFixture.buildSslContextFromKeyStore(getKeystoreLocation(), keystoreFileName);
135124
}
136125

137-
private static KeyManagerFactory getKeyManagerFactory(final String keystoreFileName)
138-
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
139-
KeyStore ks = KeyStore.getInstance("PKCS12");
140-
try (FileInputStream fis = new FileInputStream(getKeystoreLocation() + File.separator + keystoreFileName)) {
141-
ks.load(fis, KEYSTORE_PASSWORD.toCharArray());
142-
}
143-
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
144-
KeyManagerFactory.getDefaultAlgorithm());
145-
keyManagerFactory.init(ks, KEYSTORE_PASSWORD.toCharArray());
146-
return keyManagerFactory;
126+
private static KeyManagerFactory getKeyManagerFactory(final String keystoreFileName) throws Exception {
127+
return EncryptionFixture.getKeyManagerFactory(getKeystoreLocation(), keystoreFileName);
147128
}
148129

149130
private static String getKeystoreLocation() {

driver-sync/src/test/functional/com/mongodb/fixture/EncryptionFixture.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818

1919
package com.mongodb.fixture;
2020

21+
import javax.net.ssl.KeyManagerFactory;
22+
import javax.net.ssl.SSLContext;
23+
import java.io.File;
24+
import java.io.FileInputStream;
25+
import java.security.KeyStore;
2126
import java.util.HashMap;
2227
import java.util.Map;
2328

@@ -28,6 +33,8 @@
2833
*/
2934
public final class EncryptionFixture {
3035

36+
private static final String KEYSTORE_PASSWORD = "test";
37+
3138
private EncryptionFixture() {
3239
//NOP
3340
}
@@ -73,6 +80,38 @@ public static Map<String, Map<String, Object>> getKmsProviders(final KmsProvider
7380
}};
7481
}
7582

83+
/**
84+
* Creates a {@link KeyManagerFactory} from a PKCS12 keystore file for use in TLS connections.
85+
* The keystore is loaded using the password {@value #KEYSTORE_PASSWORD}.
86+
*
87+
* @return a {@link KeyManagerFactory initialized with the keystore's key material
88+
*/
89+
public static KeyManagerFactory getKeyManagerFactory(final String keystoreLocation, final String keystoreFileName) throws Exception {
90+
KeyStore ks = KeyStore.getInstance("PKCS12");
91+
try (FileInputStream fis = new FileInputStream(keystoreLocation + File.separator + keystoreFileName)) {
92+
ks.load(fis, KEYSTORE_PASSWORD.toCharArray());
93+
}
94+
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
95+
keyManagerFactory.init(ks, KEYSTORE_PASSWORD.toCharArray());
96+
return keyManagerFactory;
97+
}
98+
99+
/**
100+
* Creates an {@link SSLContext} from a PKCS12 keystore file for TLS connections.
101+
*
102+
* Allows configuring MongoClient with a custom {@link SSLContext} to test scenarios like TLS connections using specific certificates
103+
* (e.g., expired or invalid) and setting up KMS servers.
104+
*
105+
* @return an initialized {@link SSLContext} configured with the keystore's key material
106+
* @see #getKeyManagerFactory
107+
*/
108+
public static SSLContext buildSslContextFromKeyStore(final String keystoreLocation, final String keystoreFileName) throws Exception {
109+
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keystoreLocation, keystoreFileName);
110+
SSLContext sslContext = SSLContext.getInstance("TLS");
111+
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
112+
return sslContext;
113+
}
114+
76115
public enum KmsProviderType {
77116
LOCAL,
78117
AWS,

0 commit comments

Comments
 (0)