Skip to content

Commit ea7704f

Browse files
RUBY-2833 Add KMS TLS options (#2382)
1 parent 5d34d4a commit ea7704f

13 files changed

+522
-376
lines changed

lib/mongo/client_encryption.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,20 @@ class ClientEncryption
3535
# @see Mongo::Crypt::KMS::Credentials for list of options for every
3636
# supported provider.
3737
# @note There may be more than one KMS provider specified.
38+
# @option options [ Hash ] :kms_tls_options TLS options to connect to KMS
39+
# providers. Keys of the hash should be KSM provider names; values
40+
# should be hashes of TLS connection options. The options are equivalent
41+
# to TLS connection options of Mongo::Client.
42+
# @see Mongo::Client#initialize for list of TLS options.
3843
#
3944
# @raise [ ArgumentError ] If required options are missing or incorrectly
4045
# formatted.
4146
def initialize(key_vault_client, options={})
4247
@encrypter = Crypt::ExplicitEncrypter.new(
4348
key_vault_client,
4449
options[:key_vault_namespace],
45-
Crypt::KMS::Credentials.new(options[:kms_providers])
50+
Crypt::KMS::Credentials.new(options[:kms_providers]),
51+
Crypt::KMS::Validations.validate_tls_options(options[:kms_tls_options])
4652
)
4753
end
4854

lib/mongo/crypt/auto_encrypter.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ class AutoEncrypter
6666
# @see Mongo::Crypt::KMS::Credentials for list of options for every
6767
# supported provider.
6868
# @note There may be more than one KMS provider specified.
69+
# @option options [ Hash ] :kms_tls_options TLS options to connect to KMS
70+
# providers. Keys of the hash should be KSM provider names; values
71+
# should be hashes of TLS connection options. The options are equivalent
72+
# to TLS connection options of Mongo::Client.
73+
# @see Mongo::Client#initialize for list of TLS options.
6974
#
7075
# @raise [ ArgumentError ] If required options are missing or incorrectly
7176
# formatted.
@@ -74,6 +79,7 @@ def initialize(options)
7479

7580
@crypt_handle = Crypt::Handle.new(
7681
Crypt::KMS::Credentials.new(@options[:kms_providers]),
82+
Crypt::KMS::Validations.validate_tls_options(@options[:kms_tls_options]),
7783
schema_map: @options[:schema_map]
7884
)
7985

lib/mongo/crypt/binding.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,51 @@ def self.ctx_next_kms_ctx(context)
758758
end
759759
end
760760

761+
# @!method self.mongocrypt_kms_ctx_get_kms_provider(crypt, kms_providers)
762+
# @api private
763+
#
764+
# Get the KMS provider identifier associated with this KMS request.
765+
#
766+
# This is used to conditionally configure TLS connections based on the KMS
767+
# request. It is useful for KMIP, which authenticates with a client
768+
# certificate.
769+
#
770+
# @param [ FFI::Pointer ] kms Pointer mongocrypt_kms_ctx_t object.
771+
# @param [ FFI::Pointer ] len (outparam) Receives the length of the
772+
# returned string. It may be NULL. If it is not NULL, it is set to
773+
# the length of the returned string without the NULL terminator.
774+
#
775+
# @returns [ FFI::Pointer ] One of the NULL terminated static strings: "aws", "azure", "gcp", or
776+
# "kmip".
777+
attach_function(
778+
:mongocrypt_kms_ctx_get_kms_provider,
779+
[:pointer, :pointer],
780+
:pointer
781+
)
782+
783+
# Get the KMS provider identifier associated with this KMS request.
784+
#
785+
# This is used to conditionally configure TLS connections based on the KMS
786+
# request. It is useful for KMIP, which authenticates with a client
787+
# certificate.
788+
#
789+
# @param [ FFI::Pointer ] kms Pointer mongocrypt_kms_ctx_t object.
790+
#
791+
# @returns [ Symbol | nil ] KMS provider identifier.
792+
def self.kms_ctx_get_kms_provider(kms_context)
793+
len_ptr = FFI::MemoryPointer.new(:uint32, 1)
794+
provider = mongocrypt_kms_ctx_get_kms_provider(
795+
kms_context.kms_ctx_p,
796+
len_ptr
797+
)
798+
if len_ptr.nil?
799+
nil
800+
else
801+
len = len_ptr.read(:uint32)
802+
provider.read_string(len).to_sym
803+
end
804+
end
805+
761806
# @!method self.mongocrypt_kms_ctx_message(kms, msg)
762807
# @api private
763808
#

lib/mongo/crypt/context.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ class Context
3535
# that implements driver I/O methods required to run the
3636
# state machine.
3737
def initialize(mongocrypt_handle, io)
38+
@mongocrypt_handle = mongocrypt_handle
3839
# Ideally, this level of the API wouldn't be passing around pointer
3940
# references between objects, so this method signature is subject to change.
4041

4142
# FFI::AutoPointer uses a custom release strategy to automatically free
4243
# the pointer once this object goes out of scope
4344
@ctx_p = FFI::AutoPointer.new(
44-
Binding.mongocrypt_ctx_new(mongocrypt_handle.ref),
45+
Binding.mongocrypt_ctx_new(@mongocrypt_handle.ref),
4546
Binding.method(:mongocrypt_ctx_destroy)
4647
)
4748

@@ -103,7 +104,9 @@ def run_state_machine
103104
mongocrypt_done
104105
when :need_kms
105106
while kms_context = Binding.ctx_next_kms_ctx(self) do
106-
@encryption_io.feed_kms(kms_context)
107+
provider = Binding.kms_ctx_get_kms_provider(kms_context)
108+
tls_options = @mongocrypt_handle.kms_tls_options(provider)
109+
@encryption_io.feed_kms(kms_context, tls_options)
107110
end
108111

109112
Binding.ctx_kms_done(self)

lib/mongo/crypt/encryption_io.rb

Lines changed: 18 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,10 @@ def mark_command(cmd)
131131
# corresponding to one remote KMS data key. Contains information about
132132
# the endpoint at which to establish a TLS connection and the message
133133
# to send on that connection.
134-
def feed_kms(kms_context)
135-
with_ssl_socket(kms_context.endpoint) do |ssl_socket|
136-
134+
# @param [ Hash ] tls_options. TLS options to connect to KMS provider.
135+
# The options are same as for Mongo::Client.
136+
def feed_kms(kms_context, tls_options)
137+
with_ssl_socket(kms_context.endpoint, tls_options) do |ssl_socket|
137138
Timeout.timeout(SOCKET_TIMEOUT, Error::SocketTimeoutError,
138139
'Socket write operation timed out'
139140
) do
@@ -242,6 +243,8 @@ def spawn_mongocryptd
242243
# Provide a TLS socket to be used for KMS calls in a block API
243244
#
244245
# @param [ String ] endpoint The URI at which to connect the TLS socket.
246+
# @param [ Hash ] tls_options. TLS options to connect to KMS provider.
247+
# The options are same as for Mongo::Client.
245248
# @yieldparam [ OpenSSL::SSL::SSLSocket ] ssl_socket Yields a TLS socket
246249
# connected to the specified endpoint.
247250
#
@@ -250,60 +253,21 @@ def spawn_mongocryptd
250253
#
251254
# @note The socket is always closed when the provided block has finished
252255
# executing
253-
def with_ssl_socket(endpoint)
254-
host, port = endpoint.split(':')
255-
port ||= 443 # Default port for AWS KMS API
256-
257-
# Create TCPSocket and set nodelay option
258-
tcp_socket = TCPSocket.open(host, port)
259-
begin
260-
tcp_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
261-
262-
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket)
263-
begin
264-
# tcp_socket will be closed when ssl_socket is closed
265-
ssl_socket.sync_close = true
266-
# perform SNI
267-
# It does not work with Azure, therefore commented out.
268-
# ssl_socket.hostname = "#{host}:#{port}"
269-
270-
Timeout.timeout(
271-
SOCKET_TIMEOUT,
272-
Error::SocketTimeoutError,
273-
"KMS socket connection timed out after #{SOCKET_TIMEOUT} seconds",
274-
) do
275-
ssl_socket.connect
276-
end
277-
278-
yield(ssl_socket)
279-
ensure
280-
begin
281-
Timeout.timeout(
282-
SOCKET_TIMEOUT,
283-
Error::SocketTimeoutError,
284-
'KMS TLS socket close timed out'
285-
) do
286-
ssl_socket.sysclose
287-
end
288-
rescue
289-
end
290-
end
291-
ensure
292-
# Still close tcp socket manually in case TLS socket creation
293-
# fails.
294-
begin
295-
Timeout.timeout(
296-
SOCKET_TIMEOUT,
297-
Error::SocketTimeoutError,
298-
'KMS TCP socket close timed out'
299-
) do
300-
tcp_socket.close
301-
end
302-
rescue
303-
end
256+
def with_ssl_socket(endpoint, tls_options)
257+
address = begin
258+
host, port = endpoint.split(':')
259+
port ||= 443 # All supported KMS APIs use this port by default.
260+
Address.new([host, port].join(':'))
304261
end
262+
mongo_socket = address.socket(
263+
SOCKET_TIMEOUT,
264+
tls_options.merge(ssl: true)
265+
)
266+
yield(mongo_socket.socket)
305267
rescue => e
306268
raise Error::KmsError, "Error decrypting data key: #{e.class}: #{e.message}"
269+
ensure
270+
mongo_socket&.close
307271
end
308272
end
309273
end

lib/mongo/crypt/explicit_encrypter.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ class ExplicitEncrypter
2929
# to connect to the key vault collection.
3030
# @param [ String ] key_vault_namespace The namespace of the key vault
3131
# collection in the format "db_name.collection_name".
32-
# @param [ Crypt::KMS::Credentials ] :kms_providers A hash of key management service
32+
# @param [ Crypt::KMS::Credentials ] kms_providers A hash of key management service
3333
# configuration information.
34-
def initialize(key_vault_client, key_vault_namespace, kms_providers)
35-
@crypt_handle = Handle.new(kms_providers)
34+
# @param [ Hash ] kms_tls_options TLS options to connect to KMS
35+
# providers. Keys of the hash should be KSM provider names; values
36+
# should be hashes of TLS connection options. The options are equivalent
37+
# to TLS connection options of Mongo::Client.
38+
def initialize(key_vault_client, key_vault_namespace, kms_providers, kms_tls_options)
39+
@crypt_handle = Handle.new(kms_providers, kms_tls_options)
3640
@encryption_io = EncryptionIO.new(
3741
key_vault_client: key_vault_client,
3842
key_vault_namespace: key_vault_namespace

lib/mongo/crypt/handle.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,22 @@ class Handle
3535
#
3636
# @option options [ Hash | nil ] :schema_map A hash representing the JSON schema
3737
# of the collection that stores auto encrypted documents.
38+
# @param [ Hash ] kms_tls_options TLS options to connect to KMS
39+
# providers. Keys of the hash should be KSM provider names; values
40+
# should be hashes of TLS connection options. The options are equivalent
41+
# to TLS connection options of Mongo::Client.
3842
# @option options [ Logger ] :logger A Logger object to which libmongocrypt logs
3943
# will be sent
40-
def initialize(kms_providers, options={})
44+
def initialize(kms_providers, kms_tls_options, options={})
4145
# FFI::AutoPointer uses a custom release strategy to automatically free
4246
# the pointer once this object goes out of scope
4347
@mongocrypt = FFI::AutoPointer.new(
4448
Binding.mongocrypt_new,
4549
Binding.method(:mongocrypt_destroy)
4650
)
4751

52+
@kms_tls_options = kms_tls_options
53+
4854
@schema_map = options[:schema_map]
4955
set_schema_map if @schema_map
5056

@@ -64,6 +70,16 @@ def ref
6470
@mongocrypt
6571
end
6672

73+
# Return TLS options for KMS provider. If there are no TLS options set,
74+
# empty hash is returned.
75+
#
76+
# @param [ String ] provider KSM provider name.
77+
#
78+
# @return [ Hash ] TLS options to connect to KMS provider.
79+
def kms_tls_options(provider)
80+
@kms_tls_options.fetch(provider, {})
81+
end
82+
6783
private
6884

6985
# Set the schema map option on the underlying mongocrypt_t object

lib/mongo/crypt/kms.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,43 @@ def validate_param(key, opts, format_hint, required: true)
6666
nil
6767
end
6868
end
69+
70+
# Validate KMS TLS options.
71+
#
72+
# @param [ Hash | nil ] options TLS options to connect to KMS
73+
# providers. Keys of the hash should be KSM provider names; values
74+
# should be hashes of TLS connection options. The options are equivalent
75+
# to TLS connection options of Mongo::Client.
76+
#
77+
# @return [ Hash ] Provided TLS options if valid.
78+
#
79+
# @raise [ ArgumentError ] If required options are missing or incorrectly
80+
# formatted.
81+
def validate_tls_options(options)
82+
opts = options || {}
83+
opts.each do |provider, provider_opts|
84+
if provider_opts[:ssl] == false || opts[:tls] == false
85+
raise ArgumentError.new(
86+
"Incorrect TLS options for #{provider}: TLS is required"
87+
)
88+
end
89+
%i(
90+
ssl_verify_certificate
91+
ssl_verify_hostname
92+
ssl_verify_ocsp_endpoint
93+
).each do |opt|
94+
if provider_opts[opt] == false
95+
raise ArgumentError.new(
96+
"Incorrect TLS options for #{provider}: " +
97+
'Insecure TLS options prohibited, ' +
98+
"#{opt} cannot be set to false for KMS"
99+
)
100+
end
101+
end
102+
end
103+
opts
104+
end
105+
module_function :validate_tls_options
69106
end
70107
end
71108
end

spec/mongo/crypt/data_key_context_spec.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
include_context 'define shared FLE helpers'
1111

1212
let(:credentials) { Mongo::Crypt::KMS::Credentials.new(kms_providers) }
13+
14+
let(:kms_tls_options) do
15+
{}
16+
end
17+
1318
let(:mongocrypt) do
14-
Mongo::Crypt::Handle.new(credentials)
19+
Mongo::Crypt::Handle.new(credentials, kms_tls_options)
1520
end
1621

1722
let(:io) { double("Mongo::Crypt::EncryptionIO") }

spec/mongo/crypt/handle_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
describe '#initialize' do
1313
let(:credentials) { Mongo::Crypt::KMS::Credentials.new(kms_providers) }
14-
let(:handle) { described_class.new(credentials, schema_map: schema_map) }
14+
let(:kms_tls_options) { {} }
15+
let(:handle) { described_class.new(credentials, kms_tls_options, schema_map: schema_map) }
1516
let(:schema_map) { nil }
1617

1718
shared_examples 'a functioning Mongo::Crypt::Handle' do

0 commit comments

Comments
 (0)