Skip to content

RUBY-2833 Add KMS TLS options #2382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/mongo/client_encryption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,20 @@ class ClientEncryption
# @see Mongo::Crypt::KMS::Credentials for list of options for every
# supported provider.
# @note There may be more than one KMS provider specified.
# @option options [ Hash ] :kms_tls_options TLS options to connect to KMS
# providers. Keys of the hash should be KSM provider names; values
# should be hashes of TLS connection options. The options are equivalent
# to TLS connection options of Mongo::Client.
# @see Mongo::Client#initialize for list of TLS options.
#
# @raise [ ArgumentError ] If required options are missing or incorrectly
# formatted.
def initialize(key_vault_client, options={})
@encrypter = Crypt::ExplicitEncrypter.new(
key_vault_client,
options[:key_vault_namespace],
Crypt::KMS::Credentials.new(options[:kms_providers])
Crypt::KMS::Credentials.new(options[:kms_providers]),
Crypt::KMS::Validations.validate_tls_options(options[:kms_tls_options])
)
end

Expand Down
6 changes: 6 additions & 0 deletions lib/mongo/crypt/auto_encrypter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class AutoEncrypter
# @see Mongo::Crypt::KMS::Credentials for list of options for every
# supported provider.
# @note There may be more than one KMS provider specified.
# @option options [ Hash ] :kms_tls_options TLS options to connect to KMS
# providers. Keys of the hash should be KSM provider names; values
# should be hashes of TLS connection options. The options are equivalent
# to TLS connection options of Mongo::Client.
# @see Mongo::Client#initialize for list of TLS options.
#
# @raise [ ArgumentError ] If required options are missing or incorrectly
# formatted.
Expand All @@ -74,6 +79,7 @@ def initialize(options)

@crypt_handle = Crypt::Handle.new(
Crypt::KMS::Credentials.new(@options[:kms_providers]),
Crypt::KMS::Validations.validate_tls_options(@options[:kms_tls_options]),
schema_map: @options[:schema_map]
)

Expand Down
45 changes: 45 additions & 0 deletions lib/mongo/crypt/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,51 @@ def self.ctx_next_kms_ctx(context)
end
end

# @!method self.mongocrypt_kms_ctx_get_kms_provider(crypt, kms_providers)
# @api private
#
# Get the KMS provider identifier associated with this KMS request.
#
# This is used to conditionally configure TLS connections based on the KMS
# request. It is useful for KMIP, which authenticates with a client
# certificate.
#
# @param [ FFI::Pointer ] kms Pointer mongocrypt_kms_ctx_t object.
# @param [ FFI::Pointer ] len (outparam) Receives the length of the
# returned string. It may be NULL. If it is not NULL, it is set to
# the length of the returned string without the NULL terminator.
#
# @returns [ FFI::Pointer ] One of the NULL terminated static strings: "aws", "azure", "gcp", or
# "kmip".
attach_function(
:mongocrypt_kms_ctx_get_kms_provider,
[:pointer, :pointer],
:pointer
)

# Get the KMS provider identifier associated with this KMS request.
#
# This is used to conditionally configure TLS connections based on the KMS
# request. It is useful for KMIP, which authenticates with a client
# certificate.
#
# @param [ FFI::Pointer ] kms Pointer mongocrypt_kms_ctx_t object.
#
# @returns [ Symbol | nil ] KMS provider identifier.
def self.kms_ctx_get_kms_provider(kms_context)
len_ptr = FFI::MemoryPointer.new(:uint32, 1)
provider = mongocrypt_kms_ctx_get_kms_provider(
kms_context.kms_ctx_p,
len_ptr
)
if len_ptr.nil?
nil
else
len = len_ptr.read(:uint32)
provider.read_string(len).to_sym
end
end

# @!method self.mongocrypt_kms_ctx_message(kms, msg)
# @api private
#
Expand Down
7 changes: 5 additions & 2 deletions lib/mongo/crypt/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ class Context
# that implements driver I/O methods required to run the
# state machine.
def initialize(mongocrypt_handle, io)
@mongocrypt_handle = mongocrypt_handle
# Ideally, this level of the API wouldn't be passing around pointer
# references between objects, so this method signature is subject to change.

# FFI::AutoPointer uses a custom release strategy to automatically free
# the pointer once this object goes out of scope
@ctx_p = FFI::AutoPointer.new(
Binding.mongocrypt_ctx_new(mongocrypt_handle.ref),
Binding.mongocrypt_ctx_new(@mongocrypt_handle.ref),
Binding.method(:mongocrypt_ctx_destroy)
)

Expand Down Expand Up @@ -103,7 +104,9 @@ def run_state_machine
mongocrypt_done
when :need_kms
while kms_context = Binding.ctx_next_kms_ctx(self) do
@encryption_io.feed_kms(kms_context)
provider = Binding.kms_ctx_get_kms_provider(kms_context)
tls_options = @mongocrypt_handle.kms_tls_options(provider)
@encryption_io.feed_kms(kms_context, tls_options)
end

Binding.ctx_kms_done(self)
Expand Down
72 changes: 18 additions & 54 deletions lib/mongo/crypt/encryption_io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@ def mark_command(cmd)
# corresponding to one remote KMS data key. Contains information about
# the endpoint at which to establish a TLS connection and the message
# to send on that connection.
def feed_kms(kms_context)
with_ssl_socket(kms_context.endpoint) do |ssl_socket|

# @param [ Hash ] tls_options. TLS options to connect to KMS provider.
# The options are same as for Mongo::Client.
def feed_kms(kms_context, tls_options)
with_ssl_socket(kms_context.endpoint, tls_options) do |ssl_socket|
Timeout.timeout(SOCKET_TIMEOUT, Error::SocketTimeoutError,
'Socket write operation timed out'
) do
Expand Down Expand Up @@ -242,6 +243,8 @@ def spawn_mongocryptd
# Provide a TLS socket to be used for KMS calls in a block API
#
# @param [ String ] endpoint The URI at which to connect the TLS socket.
# @param [ Hash ] tls_options. TLS options to connect to KMS provider.
# The options are same as for Mongo::Client.
# @yieldparam [ OpenSSL::SSL::SSLSocket ] ssl_socket Yields a TLS socket
# connected to the specified endpoint.
#
Expand All @@ -250,60 +253,21 @@ def spawn_mongocryptd
#
# @note The socket is always closed when the provided block has finished
# executing
def with_ssl_socket(endpoint)
host, port = endpoint.split(':')
port ||= 443 # Default port for AWS KMS API

# Create TCPSocket and set nodelay option
tcp_socket = TCPSocket.open(host, port)
begin
tcp_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)

ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket)
begin
# tcp_socket will be closed when ssl_socket is closed
ssl_socket.sync_close = true
# perform SNI
# It does not work with Azure, therefore commented out.
# ssl_socket.hostname = "#{host}:#{port}"

Timeout.timeout(
SOCKET_TIMEOUT,
Error::SocketTimeoutError,
"KMS socket connection timed out after #{SOCKET_TIMEOUT} seconds",
) do
ssl_socket.connect
end

yield(ssl_socket)
ensure
begin
Timeout.timeout(
SOCKET_TIMEOUT,
Error::SocketTimeoutError,
'KMS TLS socket close timed out'
) do
ssl_socket.sysclose
end
rescue
end
end
ensure
# Still close tcp socket manually in case TLS socket creation
# fails.
begin
Timeout.timeout(
SOCKET_TIMEOUT,
Error::SocketTimeoutError,
'KMS TCP socket close timed out'
) do
tcp_socket.close
end
rescue
end
def with_ssl_socket(endpoint, tls_options)
address = begin
host, port = endpoint.split(':')
port ||= 443 # All supported KMS APIs use this port by default.
Address.new([host, port].join(':'))
end
mongo_socket = address.socket(
SOCKET_TIMEOUT,
tls_options.merge(ssl: true)
)
yield(mongo_socket.socket)
rescue => e
raise Error::KmsError, "Error decrypting data key: #{e.class}: #{e.message}"
ensure
mongo_socket&.close
end
end
end
Expand Down
10 changes: 7 additions & 3 deletions lib/mongo/crypt/explicit_encrypter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ class ExplicitEncrypter
# to connect to the key vault collection.
# @param [ String ] key_vault_namespace The namespace of the key vault
# collection in the format "db_name.collection_name".
# @param [ Crypt::KMS::Credentials ] :kms_providers A hash of key management service
# @param [ Crypt::KMS::Credentials ] kms_providers A hash of key management service
# configuration information.
def initialize(key_vault_client, key_vault_namespace, kms_providers)
@crypt_handle = Handle.new(kms_providers)
# @param [ Hash ] kms_tls_options TLS options to connect to KMS
# providers. Keys of the hash should be KSM provider names; values
# should be hashes of TLS connection options. The options are equivalent
# to TLS connection options of Mongo::Client.
def initialize(key_vault_client, key_vault_namespace, kms_providers, kms_tls_options)
@crypt_handle = Handle.new(kms_providers, kms_tls_options)
@encryption_io = EncryptionIO.new(
key_vault_client: key_vault_client,
key_vault_namespace: key_vault_namespace
Expand Down
18 changes: 17 additions & 1 deletion lib/mongo/crypt/handle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,22 @@ class Handle
#
# @option options [ Hash | nil ] :schema_map A hash representing the JSON schema
# of the collection that stores auto encrypted documents.
# @param [ Hash ] kms_tls_options TLS options to connect to KMS
# providers. Keys of the hash should be KSM provider names; values
# should be hashes of TLS connection options. The options are equivalent
# to TLS connection options of Mongo::Client.
# @option options [ Logger ] :logger A Logger object to which libmongocrypt logs
# will be sent
def initialize(kms_providers, options={})
def initialize(kms_providers, kms_tls_options, options={})
# FFI::AutoPointer uses a custom release strategy to automatically free
# the pointer once this object goes out of scope
@mongocrypt = FFI::AutoPointer.new(
Binding.mongocrypt_new,
Binding.method(:mongocrypt_destroy)
)

@kms_tls_options = kms_tls_options

@schema_map = options[:schema_map]
set_schema_map if @schema_map

Expand All @@ -64,6 +70,16 @@ def ref
@mongocrypt
end

# Return TLS options for KMS provider. If there are no TLS options set,
# empty hash is returned.
#
# @param [ String ] provider KSM provider name.
#
# @return [ Hash ] TLS options to connect to KMS provider.
def kms_tls_options(provider)
@kms_tls_options.fetch(provider, {})
end

private

# Set the schema map option on the underlying mongocrypt_t object
Expand Down
37 changes: 37 additions & 0 deletions lib/mongo/crypt/kms.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,43 @@ def validate_param(key, opts, format_hint, required: true)
nil
end
end

# Validate KMS TLS options.
#
# @param [ Hash | nil ] options TLS options to connect to KMS
# providers. Keys of the hash should be KSM provider names; values
# should be hashes of TLS connection options. The options are equivalent
# to TLS connection options of Mongo::Client.
#
# @return [ Hash ] Provided TLS options if valid.
#
# @raise [ ArgumentError ] If required options are missing or incorrectly
# formatted.
def validate_tls_options(options)
opts = options || {}
opts.each do |provider, provider_opts|
if provider_opts[:ssl] == false || opts[:tls] == false
raise ArgumentError.new(
"Incorrect TLS options for #{provider}: TLS is required"
)
end
%i(
ssl_verify_certificate
ssl_verify_hostname
ssl_verify_ocsp_endpoint
).each do |opt|
if provider_opts[opt] == false
raise ArgumentError.new(
"Incorrect TLS options for #{provider}: " +
'Insecure TLS options prohibited, ' +
"#{opt} cannot be set to false for KMS"
)
end
end
end
opts
end
module_function :validate_tls_options
end
end
end
Expand Down
7 changes: 6 additions & 1 deletion spec/mongo/crypt/data_key_context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
include_context 'define shared FLE helpers'

let(:credentials) { Mongo::Crypt::KMS::Credentials.new(kms_providers) }

let(:kms_tls_options) do
{}
end

let(:mongocrypt) do
Mongo::Crypt::Handle.new(credentials)
Mongo::Crypt::Handle.new(credentials, kms_tls_options)
end

let(:io) { double("Mongo::Crypt::EncryptionIO") }
Expand Down
3 changes: 2 additions & 1 deletion spec/mongo/crypt/handle_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

describe '#initialize' do
let(:credentials) { Mongo::Crypt::KMS::Credentials.new(kms_providers) }
let(:handle) { described_class.new(credentials, schema_map: schema_map) }
let(:kms_tls_options) { {} }
let(:handle) { described_class.new(credentials, kms_tls_options, schema_map: schema_map) }
let(:schema_map) { nil }

shared_examples 'a functioning Mongo::Crypt::Handle' do
Expand Down
Loading