Skip to content
4 changes: 2 additions & 2 deletions config/spotbugs/exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@
<!-- MongoDB status: "False Positive", SpotBugs rank: 18 -->
<!-- Deliberately ignoring this, as the check for a null SSLParameters is actually necessary.
See https://jira.mongodb.org/browse/JAVA-2876 for details. -->
<Class name="com.mongodb.client.internal.KeyManagementService"/>
<Method name="enableHostNameVerification" params="javax.net.ssl.SSLSocket"/>
<Class name="com.mongodb.client.internal.KmsSocketConnector"/>
<Method name="enableHostNameVerification" params="javax.net.ssl.SSLSocket,java.lang.String"/>
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/>
</Match>

Expand Down
32 changes: 32 additions & 0 deletions driver-core/src/main/com/mongodb/AutoEncryptionSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public final class AutoEncryptionSettings {
private final String keyVaultNamespace;
private final Map<String, Map<String, Object>> kmsProviders;
private final Map<String, SSLContext> kmsProviderSslContextMap;
@Nullable
private final KmsConnectCallback kmsConnectCallback;
private final Map<String, Supplier<Map<String, Object>>> kmsProviderPropertySuppliers;
private final Map<String, BsonDocument> schemaMap;
private final Map<String, Object> extraOptions;
Expand All @@ -88,6 +90,8 @@ public static final class Builder {
private String keyVaultNamespace;
private Map<String, Map<String, Object>> kmsProviders;
private Map<String, SSLContext> kmsProviderSslContextMap = new HashMap<>();
@Nullable
private KmsConnectCallback kmsConnectCallback;
private Map<String, Supplier<Map<String, Object>>> kmsProviderPropertySuppliers = new HashMap<>();
private Map<String, BsonDocument> schemaMap = Collections.emptyMap();
private Map<String, Object> extraOptions = Collections.emptyMap();
Expand Down Expand Up @@ -162,6 +166,22 @@ public Builder kmsProviderSslContextMap(final Map<String, SSLContext> kmsProvide
return this;
}

/**
* Sets the callback that establishes connections to Key Management Service (KMS) hosts, enabling KMS requests
* to be routed through an intermediary such as an HTTP proxy.
*
* <p>Defaults to {@code null}, in which case the driver connects to KMS hosts directly.</p>
*
* @param kmsConnectCallback the KMS connect callback, or null to connect to KMS hosts directly
* @return this
* @see #getKmsConnectCallback()
* @since 5.11
*/
public Builder kmsConnectCallback(@Nullable final KmsConnectCallback kmsConnectCallback) {
this.kmsConnectCallback = kmsConnectCallback;
return this;
}

/**
* Sets the map from namespace to local schema document
*
Expand Down Expand Up @@ -406,6 +426,17 @@ public Map<String, SSLContext> getKmsProviderSslContextMap() {
return unmodifiableMap(kmsProviderSslContextMap);
}

/**
* Gets the callback that establishes connections to Key Management Service (KMS) hosts.
*
* @return the KMS connect callback, or null if the driver connects to KMS hosts directly
* @since 5.11
*/
@Nullable
public KmsConnectCallback getKmsConnectCallback() {
return kmsConnectCallback;
}

/**
* Gets the map of namespace to local JSON schema.
* <p>
Expand Down Expand Up @@ -529,6 +560,7 @@ private AutoEncryptionSettings(final Builder builder) {
this.keyVaultNamespace = notNull("keyVaultNamespace", builder.keyVaultNamespace);
this.kmsProviders = notNull("kmsProviders", builder.kmsProviders);
this.kmsProviderSslContextMap = notNull("kmsProviderSslContextMap", builder.kmsProviderSslContextMap);
this.kmsConnectCallback = builder.kmsConnectCallback;
this.kmsProviderPropertySuppliers = notNull("kmsProviderPropertySuppliers", builder.kmsProviderPropertySuppliers);
this.schemaMap = notNull("schemaMap", builder.schemaMap);
this.extraOptions = notNull("extraOptions", builder.extraOptions);
Expand Down
32 changes: 32 additions & 0 deletions driver-core/src/main/com/mongodb/ClientEncryptionSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public final class ClientEncryptionSettings {
private final Map<String, Supplier<Map<String, Object>>> kmsProviderPropertySuppliers;
private final Map<String, SSLContext> kmsProviderSslContextMap;
@Nullable
private final KmsConnectCallback kmsConnectCallback;
@Nullable
private final Long timeoutMS;
@Nullable
private final Long keyExpirationMS;
Expand All @@ -66,6 +68,8 @@ public static final class Builder {
private Map<String, Supplier<Map<String, Object>>> kmsProviderPropertySuppliers = new HashMap<>();
private Map<String, SSLContext> kmsProviderSslContextMap = new HashMap<>();
@Nullable
private KmsConnectCallback kmsConnectCallback;
@Nullable
private Long timeoutMS;
@Nullable
private Long keyExpirationMS;
Expand Down Expand Up @@ -136,6 +140,22 @@ public Builder kmsProviderSslContextMap(final Map<String, SSLContext> kmsProvide
return this;
}

/**
* Sets the callback that establishes connections to Key Management Service (KMS) hosts, enabling KMS requests
* to be routed through an intermediary such as an HTTP proxy.
*
* <p>Defaults to {@code null}, in which case the driver connects to KMS hosts directly.</p>
*
* @param kmsConnectCallback the KMS connect callback, or null to connect to KMS hosts directly
* @return this
* @see #getKmsConnectCallback()
* @since 5.11
*/
public Builder kmsConnectCallback(@Nullable final KmsConnectCallback kmsConnectCallback) {
this.kmsConnectCallback = kmsConnectCallback;
return this;
}

/**
* The cache expiration time for data encryption keys.
* <p>Defaults to {@code null} which defers to libmongocrypt's default which is currently 60000 ms. Set to 0 to disable key expiration.</p>
Expand Down Expand Up @@ -335,6 +355,17 @@ public Map<String, SSLContext> getKmsProviderSslContextMap() {
return unmodifiableMap(kmsProviderSslContextMap);
}

/**
* Gets the callback that establishes connections to Key Management Service (KMS) hosts.
*
* @return the KMS connect callback, or null if the driver connects to KMS hosts directly
* @since 5.11
*/
@Nullable
public KmsConnectCallback getKmsConnectCallback() {
return kmsConnectCallback;
}

/**
* Returns the cache expiration time for data encryption keys.
*
Expand Down Expand Up @@ -399,6 +430,7 @@ private ClientEncryptionSettings(final Builder builder) {
this.kmsProviders = notNull("kmsProviders", builder.kmsProviders);
this.kmsProviderPropertySuppliers = notNull("kmsProviderPropertySuppliers", builder.kmsProviderPropertySuppliers);
this.kmsProviderSslContextMap = notNull("kmsProviderSslContextMap", builder.kmsProviderSslContextMap);
this.kmsConnectCallback = builder.kmsConnectCallback;
this.timeoutMS = builder.timeoutMS;
this.keyExpirationMS = builder.keyExpirationMS;
}
Expand Down
92 changes: 92 additions & 0 deletions driver-core/src/main/com/mongodb/KmsConnectCallback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb;

import com.mongodb.annotations.ThreadSafe;

import java.io.IOException;
import java.net.Socket;

/**
* A callback that establishes the connection used for a Key Management Service (KMS) request made by in-use encryption
* (client-side field level encryption or queryable encryption).
*
* <p>When a callback is configured, the driver invokes it instead of connecting to the KMS host itself, and uses the
* socket it returns for the KMS request. This enables routing KMS requests through an intermediary, most commonly an
* HTTP proxy via the {@code HTTP CONNECT} method.</p>
*
* <p>The driver always negotiates TLS with the KMS host itself over the returned socket, using the
* {@link javax.net.ssl.SSLContext} configured for the KMS provider. Server Name Indication and certificate hostname
* verification target the KMS host named by the context, not the address the callback actually connected to.
* Implementations therefore MUST NOT negotiate TLS with the KMS host themselves; they must return a socket over which
* a TLS handshake with the KMS host can be performed. An implementation may use TLS for its own connection to an
Comment thread
jyemin marked this conversation as resolved.
* intermediary, in which case it returns an {@link javax.net.ssl.SSLSocket} and the driver layers the KMS host's TLS
* session on top of it.</p>
*
* <p>An {@link IOException} thrown by an implementation is treated as a transient network error.</p>
*
* <p>This is applicable only when using the synchronous variant of {@code MongoClient}. The reactive streams driver,
* and the drivers built on it, reject a configured callback rather than connecting to KMS hosts directly.</p>
*
* <p>Authenticating to an intermediary is the responsibility of the implementation. For a proxy requiring HTTP Basic
* authentication, for example, the implementation adds a {@code Proxy-Authorization} header to the {@code CONNECT}
* request.</p>
*
* <p>Example of an implementation that tunnels through an HTTP proxy:</p>
* <pre>{@code
* KmsConnectCallback callback = context -> {
* Socket socket = new Socket();
* try {
* socket.connect(new InetSocketAddress("proxy.example.com", 8080));
*
* String target = context.getHost() + ":" + context.getPort();
* socket.getOutputStream().write(
* ("CONNECT " + target + " HTTP/1.1\r\nHost: " + target + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII));
*
* // Read the status line and confirm a 2xx status, throwing an IOException otherwise. Match the status code
* // rather than the whole status line, as proxies differ in the HTTP version they reply with. Read the
* // response one byte at a time, up to the end of the header block, so that no byte of the TLS handshake that
* // the driver performs over this socket is consumed.
* readAndCheckProxyResponse(socket.getInputStream());
* } catch (IOException | RuntimeException e) {
* // The driver cannot close a socket that was never returned to it.
* socket.close();
* throw e;
* }
*
* return socket;
* };
* }</pre>
*
* @see ClientEncryptionSettings.Builder#kmsConnectCallback(KmsConnectCallback)
* @see AutoEncryptionSettings.Builder#kmsConnectCallback(KmsConnectCallback)
* @since 5.11
*/
@ThreadSafe
@FunctionalInterface
public interface KmsConnectCallback {
Comment thread
jyemin marked this conversation as resolved.
Comment thread
jyemin marked this conversation as resolved.
/**
* Returns a socket connected such that a TLS handshake with the KMS host can be performed over it.
*
* <p>Ownership of the returned socket passes to the driver, which closes it once the KMS request completes.</p>
*
* @param context the details of the connection to establish
* @return a connected socket, which must not have an established TLS session with the KMS host
* @throws IOException if the connection cannot be established. This is treated as a transient network error.
*/
Socket connect(KmsConnectContext context) throws IOException;
}
77 changes: 77 additions & 0 deletions driver-core/src/main/com/mongodb/KmsConnectContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb;

import com.mongodb.annotations.Immutable;

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;

/**
* The details of a Key Management Service (KMS) connection that a {@link KmsConnectCallback} is asked to establish.
*
* @see KmsConnectCallback
* @since 5.11
*/
@Immutable
public final class KmsConnectContext {
private final String host;
private final int port;

/**
* Construct a new instance.
*
* @param host the host name of the KMS host, which may not be null
* @param port the port of the KMS host, which must be positive
*/
public KmsConnectContext(final String host, final int port) {
this.host = notNull("host", host);
isTrueArgument("port > 0", port > 0);
this.port = port;
}

/**
* Gets the host name of the KMS host that the connection must ultimately reach.
*
* <p>This is not the host name of any intermediary such as a proxy. It is the host that the driver negotiates TLS
* with once the callback returns.</p>
*
* @return the host name of the KMS host, never null
*/
public String getHost() {
return host;
}

/**
* Gets the port of the KMS host that the connection must ultimately reach.
*
* <p>This is not the port of any intermediary such as a proxy.</p>
*
* @return the port of the KMS host, always positive
*/
public int getPort() {
return port;
}

@Override
public String toString() {
return "KmsConnectContext{"
+ "host='" + host + '\''
+ ", port=" + port
+ '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb;

import org.junit.jupiter.api.Test;

import java.net.Socket;
import java.util.HashMap;

import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;

final class KmsConnectCallbackSettingsTest {

private static final KmsConnectCallback CALLBACK = context -> new Socket();

@Test
void autoEncryptionSettingsShouldDefaultToNoCallback() {
AutoEncryptionSettings settings = AutoEncryptionSettings.builder()
.keyVaultNamespace("keyvault.datakeys")
.kmsProviders(new HashMap<>())
.build();

assertNull(settings.getKmsConnectCallback());
}

@Test
void autoEncryptionSettingsShouldRoundTripCallback() {
AutoEncryptionSettings settings = AutoEncryptionSettings.builder()
.keyVaultNamespace("keyvault.datakeys")
.kmsProviders(new HashMap<>())
.kmsConnectCallback(CALLBACK)
.build();

assertSame(CALLBACK, settings.getKmsConnectCallback());
}

@Test
void clientEncryptionSettingsShouldDefaultToNoCallback() {
ClientEncryptionSettings settings = clientEncryptionSettingsBuilder().build();

assertNull(settings.getKmsConnectCallback());
}

@Test
void clientEncryptionSettingsShouldRoundTripCallback() {
ClientEncryptionSettings settings = clientEncryptionSettingsBuilder()
.kmsConnectCallback(CALLBACK)
.build();

assertSame(CALLBACK, settings.getKmsConnectCallback());
}

private static ClientEncryptionSettings.Builder clientEncryptionSettingsBuilder() {
return ClientEncryptionSettings.builder()
.keyVaultMongoClientSettings(MongoClientSettings.builder().build())
.keyVaultNamespace("keyvault.datakeys")
.kmsProviders(new HashMap<>());
}
}
Loading