diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e20ecb..54ce9ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.7.0 + - Reviewed and deprecated SSL settings to comply with Logstash's naming convention [#165](https://github.com/logstash-plugins/logstash-input-http/pull/165) + - Deprecated `ssl` in favor of `ssl_enabled` + - Deprecated `ssl_verify_mode` in favor of `ssl_client_authentication` + - Deprecated `keystore` in favor of `ssl_keystore_path` + - Deprecated `keystore_password` in favor of `ssl_keystore_password` + ## 3.6.1 - Update Netty dependency to 4.1.87 [#162](https://github.com/logstash-plugins/logstash-input-http/pull/162) diff --git a/VERSION b/VERSION index 9575d51b..7c69a55d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.1 +3.7.0 diff --git a/build.gradle b/build.gradle index 78cdba5f..3d276930 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ description = "HTTP Input Netty implementation" String log4jVersion = '2.17.0' String nettyVersion = '4.1.87.Final' +String junitVersion = '5.9.2' sourceCompatibility = 1.8 targetCompatibility = 1.8 @@ -21,8 +22,9 @@ repositories { } dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.hamcrest:hamcrest-library:1.3' + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" + testImplementation 'org.hamcrest:hamcrest-library:2.2' testImplementation "org.apache.logging.log4j:log4j-core:${log4jVersion}" implementation "io.netty:netty-buffer:${nettyVersion}" @@ -36,8 +38,10 @@ dependencies { } test { + useJUnitPlatform() systemProperties.put "io.netty.leakDetectionLevel", "paranoid" testLogging { + events "passed", "skipped", "failed" exceptionFormat = 'full' } } diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 0d21f4e8..2ad05927 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -101,15 +101,19 @@ This plugin supports the following configuration options plus the <> |<>|No | <> |<>|No | <> |<>, one of `[200, 201, 202, 204]`|No -| <> |<>|No +| <> |<>|__Deprecated__ | <> |a valid filesystem path|No | <> |<>|No | <> |<>|No +| <> |<>, one of `["none", "optional", "required"]`|No +| <> |<>|No | <> |<>|No | <> |a valid filesystem path|No | <> |<>|No +| <> |<>|No +| <> |<>|No | <> |<>|No -| <> |<>, one of `["none", "peer", "force_peer"]`|No +| <> |<>, one of `["none", "peer", "force_peer"]`|__Deprecated__ | <> |<>|No | <> |<>|__Deprecated__ | <> |<>|__Deprecated__ @@ -214,7 +218,7 @@ The host or ip to bind [id="plugins-{type}s-{plugin}-keystore"] ===== `keystore` -deprecated[3.1.0, Use <> and <> instead] +deprecated[3.7.0, Use <> instead] * Value type is <> * There is no default value for this setting. @@ -223,12 +227,12 @@ The JKS keystore to validate the client's certificates [id="plugins-{type}s-{plugin}-keystore_password"] ===== `keystore_password` -deprecated[3.1.0, Use <> and <> instead] +deprecated[3.7.0, Use <> instead] * Value type is <> * There is no default value for this setting. -Set the truststore password +Set the keystore password [id="plugins-{type}s-{plugin}-password"] ===== `password` @@ -308,11 +312,12 @@ specify target field for the client host of the http request [id="plugins-{type}s-{plugin}-ssl"] ===== `ssl` +deprecated[3.7.0, Replaced by <>] * Value type is <> * Default value is `false` -Events are by default sent in plain text. You can +Events are, by default, sent in plain text. You can enable encryption by setting `ssl` to true and configuring the `ssl_certificate` and `ssl_key` options. @@ -332,8 +337,8 @@ SSL certificate to use. Validate client certificates against these authorities. You can define multiple files or paths. All the certificates will -be read and added to the trust store. You need to configure the `ssl_verify_mode` -to `peer` or `force_peer` to enable the verification. +be read and added to the trust store. You need to configure the <> +to `optional` or `required` to enable the verification. [id="plugins-{type}s-{plugin}-ssl_cipher_suites"] @@ -347,6 +352,27 @@ This default list applies for OpenJDK 11.0.14 and higher. For older JDK versions, the default list includes only suites supported by that version. For example, the ChaCha20 family of ciphers is not supported in older versions. +[id="plugins-{type}s-{plugin}-ssl_client_authentication"] +===== `ssl_client_authentication` + +* Value can be any of: `none`, `optional`, `required` +* Default value is `"none"` + +Controls the server's behavior in regard to requesting a certificate from client connections: +`required` forces a client to present a certificate, while `optional` requests a client certificate +but the client is not required to present one. Defaults to `none`, which disables the client authentication. + +NOTE: This setting can be used only if <> is set. + +[id="plugins-{type}s-{plugin}-ssl_enabled"] +===== `ssl_enabled` + +* Value type is <> +* Default value is `false` + +Events are, by default, sent in plain text. You can enable encryption by setting `ssl_enabled` to true and configuring +the <> and <> options. + [id="plugins-{type}s-{plugin}-ssl_handshake_timeout"] ===== `ssl_handshake_timeout` @@ -373,6 +399,22 @@ for more information. SSL key passphrase to use. +[id="plugins-{type}s-{plugin}-ssl_keystore_path"] +===== `ssl_keystore_path` + + * Value type is <> + * There is no default value for this setting. + +The JKS keystore to validate the client's certificates + +[id="plugins-{type}s-{plugin}-ssl_keystore_password"] +===== `ssl_keystore_password` + + * Value type is <> + * There is no default value for this setting. + +Set the JKS keystore password + [id="plugins-{type}s-{plugin}-ssl_supported_protocols"] ===== `ssl_supported_protocols` @@ -392,6 +434,7 @@ the *$JDK_HOME/conf/security/java.security* configuration file. That is, `TLSv1. [id="plugins-{type}s-{plugin}-ssl_verify_mode"] ===== `ssl_verify_mode` +deprecated[3.7.0, Replaced by <>] * Value can be any of: `none`, `peer`, `force_peer` * Default value is `"none"` @@ -404,7 +447,7 @@ If the client provides a certificate, it will be validated. `force_peer` will make the server ask the client to provide a certificate. If the client doesn't provide a certificate, the connection will be closed. -This option needs to be used with `ssl_certificate_authorities` and a defined list of CAs. +This option needs to be used with <> and a defined list of CAs. [id="plugins-{type}s-{plugin}-threads"] ===== `threads` diff --git a/lib/logstash/inputs/http.rb b/lib/logstash/inputs/http.rb index 5045a6cd..df1aeeb8 100644 --- a/lib/logstash/inputs/http.rb +++ b/lib/logstash/inputs/http.rb @@ -4,6 +4,7 @@ require "stud/interval" require "logstash-input-http_jars" require "logstash/plugin_mixins/ecs_compatibility_support" +require "logstash/plugin_mixins/normalize_config_support" # Using this input you can receive single or multiline events over http(s). # Applications can send a HTTP POST request with a body to the endpoint started by this @@ -27,6 +28,9 @@ # class LogStash::Inputs::Http < LogStash::Inputs::Base include LogStash::PluginMixins::ECSCompatibilitySupport(:disabled, :v1, :v8 => :v1) + + include LogStash::PluginMixins::NormalizeConfigSupport + require "logstash/inputs/http/tls" java_import "io.netty.handler.codec.http.HttpUtil" @@ -54,7 +58,12 @@ class LogStash::Inputs::Http < LogStash::Inputs::Base # Events are by default sent in plain text. You can # enable encryption by setting `ssl` to true and configuring # the `ssl_certificate` and `ssl_key` options. - config :ssl, :validate => :boolean, :default => false + config :ssl, :validate => :boolean, :default => false, :deprecated => "Set 'ssl_enabled' instead." + + # Events are by default sent in plain text. You can + # enable encryption by setting `ssl` to true and configuring + # the `ssl_certificate` and `ssl_key` options. + config :ssl_enabled, :validate => :boolean, :default => false # SSL certificate to use. config :ssl_certificate, :validate => :path @@ -64,15 +73,29 @@ class LogStash::Inputs::Http < LogStash::Inputs::Base # for more information. config :ssl_key, :validate => :path + # The JKS keystore password + config :ssl_keystore_password, :validate => :password + + # The JKS keystore to validate the client's certificates + config :ssl_keystore_path, :validate => :path + # SSL key passphrase to use. config :ssl_key_passphrase, :validate => :password # Validate client certificates against these authorities. # You can define multiple files or paths. All the certificates will - # be read and added to the trust store. You need to configure the `ssl_verify_mode` - # to `peer` or `force_peer` to enable the verification. + # be read and added to the trust store. You need to configure the `ssl_client_authentication` + # to `optional` or `required` to enable the verification. config :ssl_certificate_authorities, :validate => :array, :default => [] + # Controls the server’s behavior in regard to requesting a certificate from client connections. + # `none`: No client authentication + # `optional`: Requests a client certificate but the client is not required to present one. + # `required`: Forces a client to present a certificate. + # + # This option needs to be used with `ssl_certificate_authorities` and a defined list of CAs. + config :ssl_client_authentication, :validate => %w[none optional required], :default => 'none' + # By default the server doesn't do any client verification. # # `peer` will make the server ask the client to provide a certificate. @@ -82,7 +105,7 @@ class LogStash::Inputs::Http < LogStash::Inputs::Base # If the client doesn't provide a certificate, the connection will be closed. # # This option needs to be used with `ssl_certificate_authorities` and a defined list of CAs. - config :ssl_verify_mode, :validate => ["none", "peer", "force_peer"], :default => "none" + config :ssl_verify_mode, :validate => ["none", "peer", "force_peer"], :default => "none", :deprecated => "Set 'ssl_client_authentication' instead." # Time in milliseconds for an incomplete ssl handshake to timeout config :ssl_handshake_timeout, :validate => :number, :default => 10000 @@ -118,10 +141,13 @@ class LogStash::Inputs::Http < LogStash::Inputs::Base # Deprecated options # The JKS keystore to validate the client's certificates - config :keystore, :validate => :path, :deprecated => "Set 'ssl_certificate' and 'ssl_key' instead." - config :keystore_password, :validate => :password, :deprecated => "Set 'ssl_key_passphrase' instead." + config :keystore, :validate => :path, :deprecated => "Set 'ssl_keystore_path' instead." + + # The JKS keystore password + config :keystore_password, :validate => :password, :deprecated => "Set 'ssl_keystore_password' instead." + + config :verify_mode, :validate => ['none', 'peer', 'force_peer'], :default => 'none', :deprecated => "Set 'ssl_client_authentication' instead." - config :verify_mode, :validate => ['none', 'peer', 'force_peer'], :default => 'none', :deprecated => "Set 'ssl_verify_mode' instead." config :cipher_suites, :validate => :array, :default => [], :deprecated => "Set 'ssl_cipher_suites' instead." # The minimum TLS version allowed for the encrypted connections. The value must be one of the following: @@ -134,9 +160,36 @@ class LogStash::Inputs::Http < LogStash::Inputs::Base attr_reader :codecs + NON_PREFIXED_SSL_CONFIGS = Set[ + 'keystore', + 'keystore_password', + 'verify_mode', + 'tls_min_version', + 'tls_max_version', + 'cipher_suites', + ].freeze + + SSL_CLIENT_AUTH_NONE = 'none'.freeze + SSL_CLIENT_AUTH_OPTIONAL = 'optional'.freeze + SSL_CLIENT_AUTH_REQUIRED = 'required'.freeze + + SSL_VERIFY_MODE_TO_CLIENT_AUTHENTICATION_MAP = { + 'none' => SSL_CLIENT_AUTH_NONE, + 'peer' => SSL_CLIENT_AUTH_OPTIONAL, + 'force_peer' => SSL_CLIENT_AUTH_REQUIRED + }.freeze + + private_constant :SSL_CLIENT_AUTH_NONE + private_constant :SSL_CLIENT_AUTH_OPTIONAL + private_constant :SSL_CLIENT_AUTH_REQUIRED + private_constant :NON_PREFIXED_SSL_CONFIGS + private_constant :SSL_VERIFY_MODE_TO_CLIENT_AUTHENTICATION_MAP + public def register + setup_ssl_params! + validate_ssl_settings! if @user && @password @@ -234,78 +287,123 @@ def self.get_domain_port(http_host) end def validate_ssl_settings! - if !@ssl - @logger.warn("SSL Certificate will not be used") if @ssl_certificate - @logger.warn("SSL Key will not be used") if @ssl_key - @logger.warn("SSL Java Key Store will not be used") if @keystore - return # code bellow assumes `ssl => true` + ssl_config_name = original_params.include?('ssl') ? 'ssl' : 'ssl_enabled' + + unless @ssl_enabled + ignored_ssl_settings = original_params.select { |k| k != 'ssl_enabled' && k.start_with?('ssl_') || NON_PREFIXED_SSL_CONFIGS.include?(k) } + @logger.warn("Configured SSL settings are not used when `#{ssl_config_name}` is set to `false`: #{ignored_ssl_settings.keys}") if ignored_ssl_settings.any? + return # code bellow assumes `ssl_enabled => true` end - if !(ssl_key_configured? || ssl_jks_configured?) - raise LogStash::ConfigurationError, "Certificate or JKS must be configured" + if @ssl_certificate && !@ssl_key + raise LogStash::ConfigurationError, "Using an `ssl_certificate` requires an `ssl_key`" + elsif @ssl_key && !@ssl_certificate + raise LogStash::ConfigurationError, 'An `ssl_certificate` is required when using an `ssl_key`' end - if original_params.key?("verify_mode") && original_params.key?("ssl_verify_mode") - raise LogStash::ConfigurationError, "Both `ssl_verify_mode` and (deprecated) `verify_mode` were set. Use only `ssl_verify_mode`." - elsif original_params.key?("verify_mode") - @ssl_verify_mode_final = @verify_mode - else - @ssl_verify_mode_final = @ssl_verify_mode + unless ssl_key_configured? || ssl_jks_configured? + raise LogStash::ConfigurationError, "Either an `ssl_certificate` or `ssl_keystore_path` is required when SSL is enabled `#{ssl_config_name} => true`" end - if original_params.key?('cipher_suites') && original_params.key?('ssl_cipher_suites') - raise LogStash::ConfigurationError, "Both `ssl_cipher_suites` and (deprecated) `cipher_suites` were set. Use only `ssl_cipher_suites`." - elsif original_params.key?('cipher_suites') - @ssl_cipher_suites_final = @cipher_suites - else - @ssl_cipher_suites_final = @ssl_cipher_suites + if require_certificate_authorities? && !certificate_authorities_configured? + config_name, optional, required = provided_client_authentication_config([SSL_CLIENT_AUTH_OPTIONAL, SSL_CLIENT_AUTH_REQUIRED]) + raise LogStash::ConfigurationError, "Using `#{config_name}` set to `#{optional}` or `#{required}`, requires the configuration of `ssl_certificate_authorities`" end - if original_params.key?('tls_min_version') && original_params.key?('ssl_supported_protocols') - raise LogStash::ConfigurationError, "Both `ssl_supported_protocols` and (deprecated) `tls_min_ciphers` were set. Use only `ssl_supported_protocols`." - elsif original_params.key?('tls_max_version') && original_params.key?('ssl_supported_protocols') - raise LogStash::ConfigurationError, "Both `ssl_supported_protocols` and (deprecated) `tls_max_ciphers` were set. Use only `ssl_supported_protocols`." - else - if original_params.key?('tls_min_version') || original_params.key?('tls_max_version') - @ssl_supported_protocols_final = TLS.get_supported(tls_min_version..tls_max_version).map(&:name) - else - @ssl_supported_protocols_final = @ssl_supported_protocols + if !require_certificate_authorities? && certificate_authorities_configured? + config_name, optional, required = provided_client_authentication_config([SSL_CLIENT_AUTH_OPTIONAL, SSL_CLIENT_AUTH_REQUIRED]) + raise LogStash::ConfigurationError, "The configuration of `ssl_certificate_authorities` requires setting `#{config_name}` to `#{optional}` or '#{required}'" + end + end + + def setup_ssl_params! + @ssl_enabled = normalize_config(:ssl_enabled) do |normalizer| + normalizer.with_deprecated_alias(:ssl) + end + + @ssl_cipher_suites = normalize_config(:ssl_cipher_suites) do |normalizer| + normalizer.with_deprecated_alias(:cipher_suites) + end + + @ssl_supported_protocols = normalize_config(:ssl_supported_protocols) do |normalizer| + normalizer.with_deprecated_mapping(:tls_min_version, :tls_max_version) do |tls_min_version, tls_max_version| + TLS.get_supported(tls_min_version..tls_max_version).map(&:name) + end + end + + @ssl_client_authentication = normalize_config(:ssl_client_authentication) do |normalizer| + normalizer.with_deprecated_mapping(:verify_mode, :ssl_verify_mode) do |verify_mode, ssl_verify_mode| + normalize_ssl_client_authentication_value!(verify_mode, ssl_verify_mode) end end - if require_certificate_authorities? && !client_authentication? - raise LogStash::ConfigurationError, "Using `ssl_verify_mode` (or `verify_mode`) set to PEER or FORCE_PEER, requires the configuration of `ssl_certificate_authorities`" - elsif !require_certificate_authorities? && client_authentication? - raise LogStash::ConfigurationError, "The configuration of `ssl_certificate_authorities` requires setting `ssl_verify_mode` (or `verify_mode`) to PEER or FORCE_PEER" + @ssl_keystore_path = normalize_config(:ssl_keystore_path) do |normalizer| + normalizer.with_deprecated_alias(:keystore) + end + + @ssl_keystore_password = normalize_config(:ssl_keystore_password) do |normalizer| + normalizer.with_deprecated_alias(:keystore_password) end + + params['ssl_enabled'] = @ssl_enabled unless @ssl_enabled.nil? + params['ssl_cipher_suites'] = @ssl_cipher_suites unless @ssl_cipher_suites.nil? + params['ssl_supported_protocols'] = @ssl_supported_protocols unless @ssl_supported_protocols.nil? + params['ssl_client_authentication'] = @ssl_client_authentication unless @ssl_client_authentication.nil? + params['ssl_keystore_path'] = @ssl_keystore_path unless @ssl_keystore_path.nil? + params['ssl_keystore_password'] = @ssl_keystore_password unless @ssl_keystore_password.nil? + end + + def normalize_ssl_client_authentication_value!(verify_mode, ssl_verify_mode) + verify_mode_explicitly_set = original_params.key?("verify_mode") + + if verify_mode_explicitly_set && original_params.key?("ssl_verify_mode") + raise LogStash::ConfigurationError, "Both (deprecated) `ssl_verify_mode` and `verify_mode` were set. Use only `ssl_verify_mode`" + end + + deprecated_value = (verify_mode_explicitly_set ? verify_mode : ssl_verify_mode).downcase + SSL_VERIFY_MODE_TO_CLIENT_AUTHENTICATION_MAP[deprecated_value] end def create_http_server(message_handler) org.logstash.plugins.inputs.http.NettyHttpServer.new( - @host, @port, message_handler, build_ssl_params(), @threads, @max_pending_requests, @max_content_length, @response_code) + @host, @port, message_handler, build_ssl_params, @threads, @max_pending_requests, @max_content_length, @response_code) end def build_ssl_params - return nil unless @ssl + return nil unless @ssl_enabled - if @keystore && @keystore_password - ssl_builder = org.logstash.plugins.inputs.http.util.JksSslBuilder.new(@keystore, @keystore_password.value) + if @ssl_keystore_path && @ssl_keystore_password + ssl_builder = org.logstash.plugins.inputs.http.util.JksSslBuilder.new(@ssl_keystore_path, @ssl_keystore_password.value) else - begin - ssl_builder = org.logstash.plugins.inputs.http.util.SslSimpleBuilder - .new(@ssl_certificate, @ssl_key, @ssl_key_passphrase.nil? ? nil : @ssl_key_passphrase.value) - .setCipherSuites(normalized_cipher_suites) - rescue java.lang.IllegalArgumentException => e - @logger.error("SSL configuration invalid", error_details(e)) - raise LogStash::ConfigurationError, e - end + ssl_builder = new_ssl_simple_builder + end + + new_ssl_handshake_provider(ssl_builder) + end + + def new_ssl_simple_builder + passphrase = @ssl_key_passphrase.nil? ? nil : @ssl_key_passphrase.value + begin + ssl_context_builder = SslSimpleBuilder.new(@ssl_certificate, @ssl_key, passphrase) + .setProtocols(@ssl_supported_protocols) + .setCipherSuites(normalized_cipher_suites) - if client_authentication? - ssl_builder.setCertificateAuthorities(@ssl_certificate_authorities) + if client_authentication_enabled? + ssl_context_builder.setClientAuthentication(ssl_simple_builder_verify_mode, @ssl_certificate_authorities) end + + ssl_context_builder + rescue java.lang.IllegalArgumentException => e + @logger.error("SSL configuration invalid", error_details(e)) + raise LogStash::ConfigurationError, e end + end - new_ssl_handshake_provider(ssl_builder) + def ssl_simple_builder_verify_mode + return SslSimpleBuilder::SslClientVerifyMode::OPTIONAL if client_authentication_optional? + return SslSimpleBuilder::SslClientVerifyMode::REQUIRED if client_authentication_required? + return SslSimpleBuilder::SslClientVerifyMode::NONE if client_authentication_none? + raise LogStash::ConfigurationError, "Invalid `ssl_client_authentication` value #{@ssl_client_authentication}" end def ssl_key_configured? @@ -313,30 +411,52 @@ def ssl_key_configured? end def ssl_jks_configured? - !!(@keystore && @keystore_password) + !!(@ssl_keystore_path && @ssl_keystore_password) end - def client_authentication? - @ssl_certificate_authorities && @ssl_certificate_authorities.size > 0 + def client_authentication_enabled? + client_authentication_optional? || client_authentication_required? end def require_certificate_authorities? - @ssl_verify_mode_final == "force_peer" || @ssl_verify_mode_final == "peer" + client_authentication_required? || client_authentication_optional? + end + + def certificate_authorities_configured? + @ssl_certificate_authorities && @ssl_certificate_authorities.size > 0 + end + + def client_authentication_required? + @ssl_client_authentication && @ssl_client_authentication.downcase == SSL_CLIENT_AUTH_REQUIRED + end + + def client_authentication_none? + @ssl_client_authentication && @ssl_client_authentication.downcase == SSL_CLIENT_AUTH_NONE + end + + def client_authentication_optional? + @ssl_client_authentication && @ssl_client_authentication.downcase == SSL_CLIENT_AUTH_OPTIONAL + end + + def provided_client_authentication_config(values = [@ssl_client_authentication]) + if original_params.include?('ssl_verify_mode') + ['ssl_verify_mode', *values.map { |v| SSL_VERIFY_MODE_TO_CLIENT_AUTHENTICATION_MAP.key(v) }] + elsif original_params.include?('verify_mode') + ['verify_mode', *values.map { |v| SSL_VERIFY_MODE_TO_CLIENT_AUTHENTICATION_MAP.key(v) }] + else + ['ssl_client_authentication', *values] + end end private def normalized_cipher_suites - @ssl_cipher_suites_final.map(&:upcase) + @ssl_cipher_suites.map(&:upcase) end def new_ssl_handshake_provider(ssl_builder) begin - ssl_handler_provider = org.logstash.plugins.inputs.http.util.SslHandlerProvider.new(ssl_builder.build()) - ssl_handler_provider.setVerifyMode(@ssl_verify_mode_final.upcase) - ssl_handler_provider.setProtocols(@ssl_supported_protocols_final) - ssl_handler_provider.setHandshakeTimeoutMilliseconds(@ssl_handshake_timeout) - ssl_handler_provider + org.logstash.plugins.inputs.http.util.SslHandlerProvider.new(ssl_builder.build(), @ssl_handshake_timeout) rescue java.lang.IllegalArgumentException => e @logger.error("SSL configuration invalid", error_details(e)) raise LogStash::ConfigurationError, e diff --git a/logstash-input-http.gemspec b/logstash-input-http.gemspec index 3636bd87..6c40d33c 100644 --- a/logstash-input-http.gemspec +++ b/logstash-input-http.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'logstash-codec-plain' s.add_runtime_dependency 'jar-dependencies', '~> 0.3', '>= 0.3.4' s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~>1.2' + s.add_runtime_dependency 'logstash-mixin-normalize_config_support', '~>1.0' s.add_development_dependency 'logstash-devutils' s.add_development_dependency 'logstash-codec-json' diff --git a/spec/inputs/helpers.rb b/spec/inputs/helpers.rb new file mode 100644 index 00000000..5f65761c --- /dev/null +++ b/spec/inputs/helpers.rb @@ -0,0 +1,6 @@ +# encoding: utf-8 +CERTS_DIR = File.expand_path('../fixtures/certs/generated', File.dirname(__FILE__)) + +def certificate_path(filename) + File.join(CERTS_DIR, filename) +end \ No newline at end of file diff --git a/spec/inputs/http_spec.rb b/spec/inputs/http_spec.rb index b5c51686..b594d0ca 100644 --- a/spec/inputs/http_spec.rb +++ b/spec/inputs/http_spec.rb @@ -7,6 +7,7 @@ require "zlib" require "stringio" require 'logstash/plugin_mixins/ecs_compatibility_support/spec_helper' +require 'inputs/helpers' java_import "io.netty.handler.ssl.util.SelfSignedCertificate" @@ -165,22 +166,20 @@ let(:url) { super().sub('http://', 'https://') } - certs_dir = File.expand_path('../fixtures/certs/generated', File.dirname(__FILE__)) - let(:config) do - super().merge 'ssl' => true, - 'ssl_certificate_authorities' => [ File.join(certs_dir, 'root.crt') ], - 'ssl_certificate' => File.join(certs_dir, 'server_from_root.crt'), - 'ssl_key' => File.join(certs_dir, 'server_from_root.key.pkcs8'), - 'ssl_verify_mode' => 'peer' + super().merge 'ssl_enabled' => true, + 'ssl_certificate_authorities' => [certificate_path('root.crt')], + 'ssl_certificate' => certificate_path( 'server_from_root.crt'), + 'ssl_key' => certificate_path( 'server_from_root.key.pkcs8'), + 'ssl_client_authentication' => 'optional' end let(:client_options) do super().merge ssl: { verify: false, - ca_file: File.join(certs_dir, 'root.crt'), - client_cert: File.join(certs_dir, 'client_from_root.crt'), - client_key: File.join(certs_dir, 'client_from_root.key.pkcs8'), + ca_file: certificate_path( 'root.crt'), + client_cert: certificate_path( 'client_from_root.crt'), + client_key: certificate_path( 'client_from_root.key.pkcs8'), } end @@ -538,15 +537,27 @@ def setup_server_client(url = self.url) end end - context "with :ssl => false" do - subject { LogStash::Inputs::Http.new("port" => port, "ssl" => false) } + context "with :ssl_enabled => false" do + let(:config) { {"port" => port, "ssl_enabled" => false} } + it "should not raise exception" do expect { subject.register }.to_not raise_exception end + + context "and `ssl_` settings provided" do + let(:ssc) { SelfSignedCertificate.new } + let(:config) { { "port" => 0, "ssl_enabled" => false, "ssl_certificate" => ssc.certificate.path, "ssl_client_authentication" => "none", "cipher_suites" => ["TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"] } } + + it "should warn about not using the configs" do + expect(subject.logger).to receive(:warn).with(/^Configured SSL settings are not used when `ssl_enabled` is set to `false`: \[("ssl_certificate"(,\s)?|"ssl_client_authentication"(,\s)?|"cipher_suites"(,\s)?)*\]$/) + subject.register + end + end end - context "with :ssl => true" do + + context "with :ssl_enabled => true" do context "without :ssl_certificate" do - subject { LogStash::Inputs::Http.new("port" => port, "ssl" => true) } + subject { LogStash::Inputs::Http.new("port" => port, "ssl_enabled" => true) } it "should raise exception" do expect { subject.register }.to raise_exception(LogStash::ConfigurationError) end @@ -563,7 +574,7 @@ def setup_server_client(url = self.url) let(:ssl_key) { ssc.private_key } let(:config) do - { "port" => port, "ssl" => true, "ssl_certificate" => ssl_certificate.path, "ssl_key" => ssl_key.path } + { "port" => port, "ssl_enabled" => true, "ssl_certificate" => ssl_certificate.path, "ssl_key" => ssl_key.path } end after(:each) { ssc.delete } @@ -575,46 +586,37 @@ def setup_server_client(url = self.url) end context "with ssl_verify_mode = none" do - subject { LogStash::Inputs::Http.new(config.merge("ssl_verify_mode" => "none")) } + subject { LogStash::Inputs::Http.new(config.merge("ssl_client_authentication" => "none")) } it "should not raise exception" do expect { subject.register }.to_not raise_exception end end - ["peer", "force_peer"].each do |verify_mode| - context "with ssl_verify_mode = #{verify_mode}" do - subject { LogStash::Inputs::Http.new("port" => port, "ssl" => true, - "ssl_certificate" => ssl_certificate.path, - "ssl_certificate_authorities" => ssl_certificate.path, - "ssl_key" => ssl_key.path, - "ssl_verify_mode" => verify_mode - ) } - it "should not raise exception" do - expect { subject.register }.to_not raise_exception + ["ssl_verify_mode", "verify_mode"].each do |config_name| + ["peer", "force_peer"].each do |verify_mode| + context "with deprecated #{config_name} = #{verify_mode}" do + subject { LogStash::Inputs::Http.new("port" => port, + "ssl_enabled" => true, + "ssl_certificate" => ssl_certificate.path, + "ssl_certificate_authorities" => ssl_certificate.path, + "ssl_key" => ssl_key.path, + config_name => verify_mode + ) } + it "should not raise exception" do + expect { subject.register }.to_not raise_exception + end end end end - context "with verify_mode = none" do - subject { LogStash::Inputs::Http.new(config.merge("verify_mode" => "none")) } + ["ssl_verify_mode", "verify_mode"].each do |config_name| + context "with deprecated #{config_name} = none" do + subject { LogStash::Inputs::Http.new(config.merge(config_name => "none")) } - it "should not raise exception" do - expect { subject.register }.to_not raise_exception - end - end - ["peer", "force_peer"].each do |verify_mode| - context "with verify_mode = #{verify_mode}" do - subject { LogStash::Inputs::Http.new("port" => port, "ssl" => true, - "ssl_certificate" => ssl_certificate.path, - "ssl_certificate_authorities" => ssl_certificate.path, - "ssl_key" => ssl_key.path, - "verify_mode" => verify_mode - ) } it "should not raise exception" do expect { subject.register }.to_not raise_exception end end end - context "with invalid ssl certificate" do before do cert = File.readlines path = config["ssl_certificate"] @@ -646,7 +648,7 @@ def setup_server_client(url = self.url) context "with invalid ssl certificate_authorities" do let(:config) do - super().merge("ssl_verify_mode" => "peer", "ssl_certificate_authorities" => [ ssc.certificate.path, ssc.private_key.path ]) + super().merge("ssl_client_authentication" => "optional", "ssl_certificate_authorities" => [ ssc.certificate.path, ssc.private_key.path ]) end it "should raise a cert error" do @@ -662,13 +664,33 @@ def setup_server_client(url = self.url) end end - context "with both verify_mode options set" do + context "with both verify_mode and ssl_verify_mode options set" do let(:config) do - super().merge('ssl_verify_mode' => 'peer', 'verify_mode' => 'none') + super().merge('verify_mode' => 'none', 'ssl_verify_mode' => 'none') end it "should raise a configuration error" do - expect { subject.register }.to raise_error LogStash::ConfigurationError, /Use only .?ssl_verify_mode.?/i + expect { subject.register }.to raise_error LogStash::ConfigurationError, /Use only .?ssl_verify_mode`.?/i + end + end + + context "with both ssl_client_authentication and ssl_verify_mode options set" do + let(:config) do + super().merge('ssl_client_authentication' => 'optional', 'ssl_verify_mode' => 'none') + end + + it "should raise a configuration error" do + expect { subject.register }.to raise_error LogStash::ConfigurationError, /Use only .?ssl_client_authentication.?/i + end + end + + context "with both ssl_client_authentication and verify_mode options set" do + let(:config) do + super().merge('ssl_client_authentication' => 'optional', 'verify_mode' => 'none') + end + + it "should raise a configuration error" do + expect { subject.register }.to raise_error LogStash::ConfigurationError, /Use only .?ssl_client_authentication.?/i end end @@ -703,6 +725,101 @@ def setup_server_client(url = self.url) end end + context "with both ssl and ssl_enabled set" do + let(:config) do + super().merge('ssl' => true, 'ssl_enabled' => true ) + end + + it "should raise a configuration error" do + expect { subject.register }.to raise_error LogStash::ConfigurationError, /Use only .?ssl_enabled.?/i + end + end + + context "with ssl_client_authentication" do + context "normalized from ssl_verify_mode 'none'" do + let(:config) { super().merge("ssl_verify_mode" => "none") } + + it "should transform the value to 'none'" do + subject.register + expect(subject.params).to match hash_including("ssl_client_authentication" => "none") + expect(subject.instance_variable_get(:@ssl_client_authentication)).to eql("none") + end + + context "and ssl_certificate_authorities is set" do + let(:config) { super().merge("ssl_certificate_authorities" => [certificate_path( 'root.crt')]) } + it "raise a configuration error" do + expect { subject.register }.to raise_error(LogStash::ConfigurationError, "The configuration of `ssl_certificate_authorities` requires setting `ssl_verify_mode` to `peer` or 'force_peer'") + end + end + end + + [%w[peer optional], %w[force_peer required]].each do |ssl_verify_mode, ssl_client_authentication| + context "normalized from ssl_verify_mode '#{ssl_verify_mode}'" do + let(:config) { super().merge("ssl_verify_mode" => ssl_verify_mode, "ssl_certificate_authorities" => [certificate_path( 'root.crt')]) } + + it "should transform the value to '#{ssl_client_authentication}'" do + subject.register + expect(subject.params).to match hash_including("ssl_client_authentication" => ssl_client_authentication) + expect(subject.instance_variable_get(:@ssl_client_authentication)).to eql(ssl_client_authentication) + end + + context "with no ssl_certificate_authorities set " do + let(:config) { super().reject { |key| "ssl_certificate_authorities".eql?(key) } } + it "raise a configuration error" do + expect {subject.register}.to raise_error(LogStash::ConfigurationError, "Using `ssl_verify_mode` set to `peer` or `force_peer`, requires the configuration of `ssl_certificate_authorities`") + end + end + end + end + + context "configured to 'none'" do + let(:config) { super().merge("ssl_client_authentication" => "none") } + + it "doesn't raise an error when certificate_authorities is not set" do + expect {subject.register}.to_not raise_error + end + + context "with certificate_authorities set" do + let(:config) { super().merge("ssl_certificate_authorities" => [certificate_path( 'root.crt')]) } + + it "raise a configuration error" do + expect {subject.register}.to raise_error(LogStash::ConfigurationError, "The configuration of `ssl_certificate_authorities` requires setting `ssl_client_authentication` to `optional` or 'required'") + end + end + end + + context "configured to 'required'" do + let(:config) { super().merge("ssl_client_authentication" => "required") } + + it "raise a ConfigurationError when certificate_authorities is not set" do + expect {subject.register}.to raise_error(LogStash::ConfigurationError, "Using `ssl_client_authentication` set to `optional` or `required`, requires the configuration of `ssl_certificate_authorities`") + end + + context "with ssl_certificate_authorities set" do + let(:config) { super().merge("ssl_certificate_authorities" => [certificate_path( 'root.crt')]) } + + it "doesn't raise a configuration error" do + expect {subject.register}.not_to raise_error + end + end + end + + context "configured to 'optional'" do + let(:config) { super().merge("ssl_client_authentication" => "optional") } + + it "raise a ConfigurationError when certificate_authorities is not set" do + expect {subject.register}.to raise_error(LogStash::ConfigurationError, "Using `ssl_client_authentication` set to `optional` or `required`, requires the configuration of `ssl_certificate_authorities`") + end + + context "with certificate_authorities set" do + let(:config) { super().merge("ssl_certificate_authorities" => [certificate_path( 'root.crt')]) } + + it "doesn't raise a configuration error" do + expect {subject.register}.not_to raise_error + end + end + end + end end end end diff --git a/src/main/java/org/logstash/plugins/inputs/http/util/SslHandlerProvider.java b/src/main/java/org/logstash/plugins/inputs/http/util/SslHandlerProvider.java index 489ba4d9..269a9990 100644 --- a/src/main/java/org/logstash/plugins/inputs/http/util/SslHandlerProvider.java +++ b/src/main/java/org/logstash/plugins/inputs/http/util/SslHandlerProvider.java @@ -3,68 +3,20 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.net.ssl.SSLEngine; -import java.util.Arrays; public class SslHandlerProvider { - private final static Logger logger = LogManager.getLogger(SslSimpleBuilder.class); private final SslContext sslContext; - private SslClientVerifyMode verifyMode = SslClientVerifyMode.NONE; - private long handshakeTimeoutMilliseconds = 10000; - - enum SslClientVerifyMode { - VERIFY_PEER, - FORCE_PEER, - NONE - } - - private String[] protocols = new String[] { "TLSv1.2", "TLSv1.3" }; + private final int sslHandshakeTimeoutMillis; - public SslHandlerProvider(SslContext sslContext) { - this.sslContext = sslContext; + public SslHandlerProvider(SslContext context, int sslHandshakeTimeoutMillis){ + this.sslContext = context; + this.sslHandshakeTimeoutMillis = sslHandshakeTimeoutMillis; } public SslHandler getSslHandler(ByteBufAllocator bufferAllocator) { - - SslHandler sslHandler = sslContext.newHandler(bufferAllocator); - - SSLEngine engine = sslHandler.engine(); - engine.setEnabledProtocols(protocols); - engine.setUseClientMode(false); - - if (verifyMode == SslClientVerifyMode.FORCE_PEER) { - // Explicitly require a client certificate - engine.setNeedClientAuth(true); - } else if (verifyMode == SslClientVerifyMode.VERIFY_PEER) { - // If the client supply a client certificate we will verify it. - engine.setWantClientAuth(true); - } - - sslHandler.setHandshakeTimeoutMillis(handshakeTimeoutMilliseconds); - - return sslHandler; - } - - public void setVerifyMode(String verifyMode) { - if (verifyMode.equals("FORCE_PEER")) { - this.verifyMode = SslClientVerifyMode.FORCE_PEER; - } else if (verifyMode.equals("PEER")) { - this.verifyMode = SslClientVerifyMode.VERIFY_PEER; - } - } - - public void setProtocols(String[] protocols) { - if (logger.isDebugEnabled()) - logger.debug("TLS: " + Arrays.toString(protocols)); - - this.protocols = protocols; - } - - public void setHandshakeTimeoutMilliseconds(long handshakeTimeoutMilliseconds) { - this.handshakeTimeoutMilliseconds = handshakeTimeoutMilliseconds; + SslHandler handler = sslContext.newHandler(bufferAllocator); + handler.setHandshakeTimeoutMillis(sslHandshakeTimeoutMillis); + return handler; } } diff --git a/src/main/java/org/logstash/plugins/inputs/http/util/SslSimpleBuilder.java b/src/main/java/org/logstash/plugins/inputs/http/util/SslSimpleBuilder.java index 09027cbf..25f17fa5 100644 --- a/src/main/java/org/logstash/plugins/inputs/http/util/SslSimpleBuilder.java +++ b/src/main/java/org/logstash/plugins/inputs/http/util/SslSimpleBuilder.java @@ -1,5 +1,6 @@ package org.logstash.plugins.inputs.http.util; +import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import org.apache.logging.log4j.LogManager; @@ -24,6 +25,22 @@ public class SslSimpleBuilder implements SslBuilder { + public enum SslClientVerifyMode { + NONE(ClientAuth.NONE), + OPTIONAL(ClientAuth.OPTIONAL), + REQUIRED(ClientAuth.REQUIRE); + + private final ClientAuth clientAuth; + + SslClientVerifyMode(ClientAuth clientAuth) { + this.clientAuth = clientAuth; + } + + public ClientAuth toClientAuth() { + return clientAuth; + } + } + private final static Logger logger = LogManager.getLogger(SslSimpleBuilder.class); public static final Set SUPPORTED_CIPHERS = new HashSet<>(Arrays.asList( @@ -67,16 +84,33 @@ public class SslSimpleBuilder implements SslBuilder { "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" }; + private String[] protocols = new String[] { "TLSv1.2", "TLSv1.3" }; private String[] ciphers = getDefaultCiphers(); - private File sslKeyFile; - private File sslCertificateFile; + private final File sslKeyFile; + private final File sslCertificateFile; private String[] certificateAuthorities; - private String passPhrase; + private final String passphrase; + private SslClientVerifyMode verifyMode = SslClientVerifyMode.NONE; public SslSimpleBuilder(String sslCertificateFilePath, String sslKeyFilePath, String pass) { sslCertificateFile = new File(sslCertificateFilePath); + if (!sslCertificateFile.canRead()) { + throw new IllegalArgumentException( + String.format("Certificate file cannot be read. Please confirm the user running Logstash has permissions to read: %s", sslCertificateFilePath)); + } + sslKeyFile = new File(sslKeyFilePath); - passPhrase = pass; + if (!sslKeyFile.canRead()) { + throw new IllegalArgumentException( + String.format("Private key file cannot be read. Please confirm the user running Logstash has permissions to read: %s", sslKeyFilePath)); + } + + passphrase = pass; + } + + public SslSimpleBuilder setProtocols(String[] protocols) { + this.protocols = protocols; + return this; } public SslSimpleBuilder setCipherSuites(String[] ciphersSuite) throws IllegalArgumentException { @@ -95,26 +129,42 @@ public SslSimpleBuilder setCipherSuites(String[] ciphersSuite) throws IllegalArg return this; } - public SslSimpleBuilder setCertificateAuthorities(String[] cert) { - certificateAuthorities = cert; + public SslSimpleBuilder setClientAuthentication(SslClientVerifyMode verifyMode, String[] certificateAuthorities) { + if (isClientAuthenticationEnabled(verifyMode) && (certificateAuthorities == null || certificateAuthorities.length < 1)) { + throw new IllegalArgumentException("Certificate authorities are required to enable client authentication"); + } + + this.verifyMode = verifyMode; + this.certificateAuthorities = certificateAuthorities; return this; } - public SslContext build() throws Exception { - SslContextBuilder builder = SslContextBuilder.forServer(sslCertificateFile, sslKeyFile, passPhrase); + private boolean isClientAuthenticationEnabled(final SslClientVerifyMode mode) { + return mode == SslClientVerifyMode.OPTIONAL || mode == SslClientVerifyMode.REQUIRED; + } + + public boolean isClientAuthenticationRequired() { + return verifyMode == SslClientVerifyMode.REQUIRED; + } + public SslContext build() throws Exception { if (logger.isDebugEnabled()) { - logger.debug("Available ciphers: " + SUPPORTED_CIPHERS); - logger.debug("Ciphers: " + Arrays.toString(ciphers)); + logger.debug("Available ciphers: {}", SUPPORTED_CIPHERS); + logger.debug("Ciphers: {}", Arrays.toString(ciphers)); } - builder.ciphers(Arrays.asList(ciphers)); + SslContextBuilder builder = SslContextBuilder + .forServer(sslCertificateFile, sslKeyFile, passphrase) + .ciphers(Arrays.asList(ciphers)) + .protocols(protocols); - if (requireClientAuth()) { - if (logger.isDebugEnabled()) - logger.debug("Certificate Authorities: " + Arrays.toString(certificateAuthorities)); + if (isClientAuthenticationEnabled(verifyMode)) { + if (logger.isDebugEnabled()) { + logger.debug("Certificate Authorities: {}", Arrays.toString(certificateAuthorities)); + } - builder.trustManager(loadCertificateCollection(certificateAuthorities)); + builder.clientAuth(verifyMode.toClientAuth()) + .trustManager(loadCertificateCollection(certificateAuthorities)); } return doBuild(builder); @@ -143,14 +193,12 @@ private X509Certificate[] loadCertificateCollection(String[] certificates) throw logger.debug("Load certificates collection"); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - List collections = new ArrayList(); + final List collections = new ArrayList<>(); - for(int i = 0; i < certificates.length; i++) { - String certificate = certificates[i]; + for (String certificate : certificates) { + logger.debug("Loading certificates from file {}", certificate); - logger.debug("Loading certificates from file " + certificate); - - try(InputStream in = new FileInputStream(certificate)) { + try (InputStream in = new FileInputStream(certificate)) { List certificatesChains = (List) certificateFactory.generateCertificates(in); collections.addAll(certificatesChains); } @@ -158,14 +206,6 @@ private X509Certificate[] loadCertificateCollection(String[] certificates) throw return collections.toArray(new X509Certificate[collections.size()]); } - private boolean requireClientAuth() { - if (certificateAuthorities != null) { - return true; - } - - return false; - } - public static String[] getDefaultCiphers() { if (isUnlimitedJCEAvailable()){ return DEFAULT_CIPHERS; @@ -185,4 +225,19 @@ public static boolean isUnlimitedJCEAvailable(){ } } + String[] getProtocols() { + return protocols != null ? protocols.clone() : null; + } + + String[] getCertificateAuthorities() { + return certificateAuthorities != null ? certificateAuthorities.clone() : null; + } + + String[] getCiphers() { + return ciphers != null ? ciphers.clone() : null; + } + + SslClientVerifyMode getVerifyMode() { + return verifyMode; + } } diff --git a/src/test/java/org/logstash/plugins/inputs/http/util/SslSimpleBuilderTest.java b/src/test/java/org/logstash/plugins/inputs/http/util/SslSimpleBuilderTest.java new file mode 100644 index 00000000..38659ff6 --- /dev/null +++ b/src/test/java/org/logstash/plugins/inputs/http/util/SslSimpleBuilderTest.java @@ -0,0 +1,250 @@ +package org.logstash.plugins.inputs.http.util; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLServerSocketFactory; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.isIn; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Every.everyItem; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.logstash.plugins.inputs.http.util.SslSimpleBuilder.SUPPORTED_CIPHERS; +import static org.logstash.plugins.inputs.http.util.SslSimpleBuilder.SslClientVerifyMode; +import static org.logstash.plugins.inputs.http.util.SslSimpleBuilder.getDefaultCiphers; +import static org.logstash.plugins.inputs.http.util.TestUtils.resourcePath; + +/** + * Unit test for {@link SslSimpleBuilder} + */ +class SslSimpleBuilderTest { + private static final String CERTIFICATE = resourcePath("host.crt"); + private static final String KEY = resourcePath("host.key"); + private static final String KEY_ENCRYPTED = resourcePath("host.enc.key"); + private static final String KEY_ENCRYPTED_PASS = "1234"; + private static final String CA = resourcePath("root-ca.crt"); + + @Test + void testConstructorShouldFailWhenCertificatePathIsInvalid() { + final IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> new SslSimpleBuilder("foo-bar.crt", KEY_ENCRYPTED, KEY_ENCRYPTED_PASS) + ); + + assertEquals( + "Certificate file cannot be read. Please confirm the user running Logstash has permissions to read: foo-bar.crt", + thrown.getMessage() + ); + } + + @Test + void testConstructorShouldFailWhenKeyPathIsInvalid() { + final IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> new SslSimpleBuilder(CERTIFICATE, "invalid.key", KEY_ENCRYPTED_PASS) + ); + + assertEquals( + "Private key file cannot be read. Please confirm the user running Logstash has permissions to read: invalid.key", + thrown.getMessage() + ); + } + + @Test + void testSetCipherSuitesShouldNotFailIfAllCiphersAreValid() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + assertDoesNotThrow(() -> sslSimpleBuilder.setCipherSuites(SUPPORTED_CIPHERS.toArray(new String[0]))); + } + + @Test + void testSetCipherSuitesShouldThrowIfAnyCiphersIsInValid() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + final String[] ciphers = SUPPORTED_CIPHERS + .toArray(new String[SUPPORTED_CIPHERS.size() + 1]); + + ciphers[ciphers.length - 1] = "TLS_INVALID_CIPHER"; + + final IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> sslSimpleBuilder.setCipherSuites(ciphers) + ); + + assertEquals("Cipher `TLS_INVALID_CIPHER` is not available", thrown.getMessage()); + } + + @Test + void testSetProtocols() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + assertArrayEquals(new String[]{"TLSv1.2", "TLSv1.3"}, sslSimpleBuilder.getProtocols()); + + sslSimpleBuilder.setProtocols(new String[]{"TLSv1.1"}); + assertArrayEquals(new String[]{"TLSv1.1"}, sslSimpleBuilder.getProtocols()); + + sslSimpleBuilder.setProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); + assertArrayEquals(new String[]{"TLSv1.1", "TLSv1.2"}, sslSimpleBuilder.getProtocols()); + } + + @Test + void testGetDefaultCiphers() { + final String[] defaultCiphers = getDefaultCiphers(); + assertTrue(defaultCiphers.length > 0); + + // Check that default ciphers is the subset of default ciphers of current Java version. + final SSLServerSocketFactory ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); + final List availableCiphers = Arrays.asList(ssf.getSupportedCipherSuites()); + assertThat(Arrays.asList(defaultCiphers), everyItem(isIn(availableCiphers))); + } + + @Test + void testSetClientAuthentication() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + final String[] certificateAuthorities = {CA}; + + sslSimpleBuilder.setClientAuthentication(SslClientVerifyMode.REQUIRED, certificateAuthorities); + + assertThat(sslSimpleBuilder.getVerifyMode(), is(SslClientVerifyMode.REQUIRED)); + assertThat(Arrays.asList(sslSimpleBuilder.getCertificateAuthorities()), everyItem(isIn(certificateAuthorities))); + } + + @Test + void testSetClientAuthenticationWithRequiredAndNoCertAuthorities() { + assertSetClientAuthenticationThrowsWhenCAIsNullOrEmpty(SslClientVerifyMode.REQUIRED); + } + + @Test + void testSetClientAuthenticationWithOptionalAndNoCertAuthorities() { + assertSetClientAuthenticationThrowsWhenCAIsNullOrEmpty(SslClientVerifyMode.OPTIONAL); + } + + @Test + void testSetClientAuthenticationWithNoneAndEmptyCA() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + sslSimpleBuilder.setClientAuthentication(SslClientVerifyMode.NONE, new String[0]); + assertThat(sslSimpleBuilder.getVerifyMode(), is(SslClientVerifyMode.NONE)); + assertThat(sslSimpleBuilder.getCertificateAuthorities(), arrayWithSize(0)); + } + + @Test + void testSetClientAuthenticationWithNoneAndNullCA() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + sslSimpleBuilder.setClientAuthentication(SslClientVerifyMode.NONE, null); + assertThat(sslSimpleBuilder.getVerifyMode(), is(SslClientVerifyMode.NONE)); + assertThat(sslSimpleBuilder.getCertificateAuthorities(), nullValue()); + } + + @Test + void testDefaultVerifyMode() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + assertThat(sslSimpleBuilder.getVerifyMode(), is(SslClientVerifyMode.NONE)); + } + + private void assertSetClientAuthenticationThrowsWhenCAIsNullOrEmpty(SslClientVerifyMode mode) { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + final String expectedMessage = "Certificate authorities are required to enable client authentication"; + + final IllegalArgumentException emptyThrown = assertThrows( + IllegalArgumentException.class, + () -> sslSimpleBuilder.setClientAuthentication(mode, new String[0]) + ); + + final IllegalArgumentException nullThrown = assertThrows(IllegalArgumentException.class, + () -> sslSimpleBuilder.setClientAuthentication(mode, null), + expectedMessage + ); + + assertEquals(expectedMessage, emptyThrown.getMessage()); + assertEquals(expectedMessage, nullThrown.getMessage()); + } + + @Test + void testSslClientVerifyModeToClientAuth() { + assertThat(SslClientVerifyMode.REQUIRED.toClientAuth(), is(ClientAuth.REQUIRE)); + assertThat(SslClientVerifyMode.OPTIONAL.toClientAuth(), is(ClientAuth.OPTIONAL)); + assertThat(SslClientVerifyMode.NONE.toClientAuth(), is(ClientAuth.NONE)); + } + + @Test + void testBuildContextWithNonEncryptedKey() { + final SslSimpleBuilder sslSimpleBuilder = new SslSimpleBuilder(CERTIFICATE, KEY, null); + assertDoesNotThrow(sslSimpleBuilder::build); + } + + @Test + void testBuildContextWithEncryptedKey() { + final SslSimpleBuilder sslSimpleBuilder = new SslSimpleBuilder(CERTIFICATE, KEY_ENCRYPTED, "1234"); + assertDoesNotThrow(sslSimpleBuilder::build); + } + + @Test + void testBuildContextWhenClientAuthenticationIsRequired() throws Exception { + final SSLEngine sslEngine = assertSSlEngineFromBuilder(createSslSimpleBuilder() + .setClientAuthentication(SslClientVerifyMode.REQUIRED, new String[]{CA}) + ); + + assertTrue(sslEngine.getNeedClientAuth()); + assertFalse(sslEngine.getWantClientAuth()); + } + + @Test + void testBuildContextWhenClientAuthenticationIsOptional() throws Exception { + final SSLEngine sslEngine = assertSSlEngineFromBuilder(createSslSimpleBuilder() + .setClientAuthentication(SslClientVerifyMode.OPTIONAL, new String[]{CA}) + ); + + assertFalse(sslEngine.getNeedClientAuth()); + assertTrue(sslEngine.getWantClientAuth()); + } + + @Test + void testBuildContextWhenClientAuthenticationIsNone() throws Exception { + final SSLEngine sslEngine = assertSSlEngineFromBuilder(createSslSimpleBuilder() + .setClientAuthentication(SslClientVerifyMode.NONE, new String[]{CA})); + + assertFalse(sslEngine.getNeedClientAuth()); + assertFalse(sslEngine.getWantClientAuth()); + } + + @Test + void testIsClientAuthenticationRequired() { + final SslSimpleBuilder sslSimpleBuilder = createSslSimpleBuilder(); + final String[] certificateAuthorities = {CA}; + + sslSimpleBuilder.setClientAuthentication(SslClientVerifyMode.NONE, certificateAuthorities); + assertFalse(sslSimpleBuilder.isClientAuthenticationRequired()); + + sslSimpleBuilder.setClientAuthentication(SslClientVerifyMode.OPTIONAL, certificateAuthorities); + assertFalse(sslSimpleBuilder.isClientAuthenticationRequired()); + + sslSimpleBuilder.setClientAuthentication(SslClientVerifyMode.REQUIRED, certificateAuthorities); + assertTrue(sslSimpleBuilder.isClientAuthenticationRequired()); + } + + private SSLEngine assertSSlEngineFromBuilder(SslSimpleBuilder sslSimpleBuilder) throws Exception { + final SslContext context = sslSimpleBuilder.build(); + assertTrue(context.isServer()); + + final SSLEngine sslEngine = context.newEngine(ByteBufAllocator.DEFAULT); + assertThat(sslEngine.getEnabledCipherSuites(), equalTo(sslSimpleBuilder.getCiphers())); + assertThat(sslEngine.getEnabledProtocols(), equalTo(sslSimpleBuilder.getProtocols())); + + return sslEngine; + } + + private SslSimpleBuilder createSslSimpleBuilder() { + return new SslSimpleBuilder(CERTIFICATE, KEY_ENCRYPTED, KEY_ENCRYPTED_PASS); + } +} \ No newline at end of file diff --git a/src/test/java/org/logstash/plugins/inputs/http/util/TestUtils.java b/src/test/java/org/logstash/plugins/inputs/http/util/TestUtils.java new file mode 100644 index 00000000..d2d9b8e7 --- /dev/null +++ b/src/test/java/org/logstash/plugins/inputs/http/util/TestUtils.java @@ -0,0 +1,20 @@ +package org.logstash.plugins.inputs.http.util; + +import java.nio.file.Path; +import java.nio.file.Paths; + +abstract class TestUtils { + + private static final Path RESOURCES = Paths.get("src/test/resources"); + + private TestUtils() { + } + + static Path resource(String name) { + return RESOURCES.resolve(name); + } + + static String resourcePath(String name) { + return resource(name).toString(); + } +} diff --git a/src/test/resources/host.crt b/src/test/resources/host.crt new file mode 100644 index 00000000..63c22df9 --- /dev/null +++ b/src/test/resources/host.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFezCCA2MCFAwv/gPfChqhnJU0fI/a3Ft3A8DTMA0GCSqGSIb3DQEBCwUAMHgx +CzAJBgNVBAYTAlVTMRUwEwYDVQQIDAxUaGUgSW50ZXJuZXQxFTATBgNVBAcMDFRo +ZSBJbnRlcm5ldDEUMBIGA1UECgwLTG9nc3Rhc2ggQ0ExETAPBgNVBAsMCExvZ3N0 +YXNoMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjMwMzMwMTEzOTIzWhgPMjI5NzAx +MTExMTM5MjNaMHoxCzAJBgNVBAYTAlVTMRUwEwYDVQQIDAxUaGUgSW50ZXJuZXQx +FTATBgNVBAcMDFRoZSBJbnRlcm5ldDEWMBQGA1UECgwNTG9nc3Rhc2ggSG9zdDER +MA8GA1UECwwITG9nc3Rhc2gxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBAOwLooFUlwzwae9GACzrlO/E+b0w1yAsRRYP +0yD3EfCogTjGibHEV/MM3y34ZSUUExZWERt1SXs3SPqfUTOkDhXjAzu8HbNjBJk3 +3R6Yzju+kJGxSxtheQLK+9tjP2O6/j6KXFhMwB7RqfN/aEB+BtmDBAdiTRuMMrWs +qViAcMt76of8lvnUXhvb7K1Um17eyc9wv7JLQN5TSG5EtB4x6zqvbfmTcCXLa7D8 +9PN3ayaBSLxAllv82RWts11i+PnVHYyU24qmdwmRIv6eKN9BRmgPqKw34yeCk4xT +T8pDXoiS9vGXxmrU1i1pZNUeVEAN+h4o8sWcseItUKsPSFFYga653XFtqzxyk4Cq +3gEPp05yH7s68T6T2H+CAyV0YuOgAL6gtt2Y97+oTwjJ1pxLjUaVz5nOjFZqxBQC +hjBItBtpO+wtngZbErfziWx2PTTSJ99lI1KSQbC8cBEUYXG3kdiiUHUm8meq7Mja +8OjUiSi2x2Qv1WBvHrbOpw5Vw1/v8QcW/OY1TwdByW4iJnBiqw+p1hA6bGANc+sP +Lcr5jz7VPHuAxRNecQmqvhjAHGIAkqCJecT9314lgehM3YpcsDVLXnGT96FF5Aq0 +LSEDKFTyC1OXDf2pfQOu9yVTz/SXSr91kj+3+ZeY9Ot9D8L1Xu6dwMsmf/CtV3+4 +lPRbQEEDAgMBAAEwDQYJKoZIhvcNAQELBQADggIBADygVFQGSy4R2htt6WX4DCb7 +FiB/KSxmhh2K+mRC77sqGDP3xRfHsWh2s1eejDde0Vp1OBjDXjsWidFuGzxhV3WC +svt5K8y47+wFboFKuIJUlK17mM3NlsArfwI2/58zIdMokJttSwX37M/jZamTG5kK +xqr3gpGjUxr+qPEizlsFlx1T4d2XIRu8/RZMoqHxI1IZSxlU6GYsvUa2T3vzz9/C +WCxj+CJ1kUnYf3SxeGX+IPVid55LOyWicRO5hKVfKfL1c9AwGL6+0lFYFdz+q5Ku +AMZ0KHk11up4c5oB8xrSet/WHRqIgEy78JxfolkIT5wAlx3sTcmo9mZOjv2tB2AD +1H95Sod/dAD84EWM5Epbt2wlldAGpMbxWH7Yp5gpH5PP8lob9ImNcf9v+wkj9alK +n+C/yhA+B8+hxPClHHgM6M4bdaXREjZ5iYa68riZtBQ3SuIWHJgOl3ZPInRs01kA +IvVFteKu5S4pnEXLAP5EpcM6Mpoxvf+H5z5sdxq3R7uVrbpmzRm7FtW188x0e/dB +ydA2Oz3kSWzwDHRc/WWglbHingf1472aNM02lPuUeN4ALILtIu38e/9IvhhyOkm9 +o7w0jVXCsB/hCcnfchgwG33NvWB2BZ/Zc5f2nTufxUYNODE2MZDaZYZeKd8LzerW +buJc6LvltjYly0HXsNf0 +-----END CERTIFICATE----- diff --git a/src/test/resources/host.enc.key b/src/test/resources/host.enc.key new file mode 100644 index 00000000..a7d420ef --- /dev/null +++ b/src/test/resources/host.enc.key @@ -0,0 +1,53 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +DR5g0lBs888AHpjmPQ== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/src/test/resources/host.key b/src/test/resources/host.key new file mode 100644 index 00000000..7c171495 --- /dev/null +++ b/src/test/resources/host.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +C8kebbjUgzIkPvLG87A6BCgFBj2p9UU= +-----END PRIVATE KEY----- diff --git a/src/test/resources/root-ca.crt b/src/test/resources/root-ca.crt new file mode 100644 index 00000000..f6c5a76c --- /dev/null +++ b/src/test/resources/root-ca.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +jMMZNVWQzQ== +-----END CERTIFICATE-----