Skip to content

Commit

Permalink
Fix SSL keystore and truststore support
Browse files Browse the repository at this point in the history
  • Loading branch information
edmocosta committed Nov 21, 2023
1 parent 67e9bfe commit a482f27
Show file tree
Hide file tree
Showing 24 changed files with 916 additions and 350 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation "org.apache.logging.log4j:log4j-core:${log4jVersion}"
testImplementation 'org.elasticsearch:securemock:1.2'

implementation "io.netty:netty-buffer:${nettyVersion}"
implementation "io.netty:netty-codec:${nettyVersion}"
Expand Down
43 changes: 42 additions & 1 deletion docs/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ This plugin supports the following configuration options plus the <<plugins-{typ
| <<plugins-{type}s-{plugin}-ssl_key_passphrase>> |<<password,password>>|No
| <<plugins-{type}s-{plugin}-ssl_keystore_password>> |<<password,password>>|No
| <<plugins-{type}s-{plugin}-ssl_keystore_path>> |<<path,path>>|No
| <<plugins-{type}s-{plugin}-ssl_keystore_type>> |<<string,string>>|No
| <<plugins-{type}s-{plugin}-ssl_supported_protocols>> |<<array,array>>|No
| <<plugins-{type}s-{plugin}-ssl_truststore_password>> |<<password,password>>|No
| <<plugins-{type}s-{plugin}-ssl_truststore_path>> |<<path,path>>|No
| <<plugins-{type}s-{plugin}-ssl_truststore_type>> |<<string,string>>|No
| <<plugins-{type}s-{plugin}-ssl_verify_mode>> |<<string,string>>, one of `["none", "peer", "force_peer"]`|__Deprecated__
| <<plugins-{type}s-{plugin}-threads>> |<<number,number>>|No
| <<plugins-{type}s-{plugin}-tls_max_version>> |<<number,number>>|__Deprecated__
Expand Down Expand Up @@ -405,7 +409,18 @@ SSL key passphrase to use.
* Value type is <<path,path>>
* There is no default value for this setting.

The JKS keystore to validate the client's certificates
The path for the keystore file that contains a private key and certificate.
It must be either a Java keystore (jks) or a PKCS#12 file.

NOTE: You cannot use this setting and <<plugins-{type}s-{plugin}-ssl_certificate>> at the same time.

[id="plugins-{type}s-{plugin}-ssl_keystore_type"]
===== `ssl_keystore_type`

* Value can be any of: `jks`, `pkcs12`
* If not provided, the value will be inferred from the keystore filename.

The format of the keystore file. It must be either `jks` or `pkcs12`.

[id="plugins-{type}s-{plugin}-ssl_keystore_password"]
===== `ssl_keystore_password`
Expand All @@ -432,6 +447,32 @@ NOTE: If you configure the plugin to use `'TLSv1.1'` on any recent JVM, such as
the protocol is disabled by default and needs to be enabled manually by changing `jdk.tls.disabledAlgorithms` in
the *$JDK_HOME/conf/security/java.security* configuration file. That is, `TLSv1.1` needs to be removed from the list.

[id="plugins-{type}s-{plugin}-ssl_truststore_password"]
===== `ssl_truststore_password`

* Value type is <<password,password>>
* There is no default value for this setting.

Set the truststore password

[id="plugins-{type}s-{plugin}-ssl_truststore_path"]
===== `ssl_truststore_path`

* Value type is <<path,path>>
* There is no default value for this setting.

The path for the keystore that contains the certificates to trust. It must be either a Java keystore (jks) or a PKCS#12 file.

NOTE: You cannot use this setting and <<plugins-{type}s-{plugin}-ssl_certificate_authorities>> at the same time.

[id="plugins-{type}s-{plugin}-ssl_truststore_type"]
===== `ssl_truststore_type`

* Value can be any of: `jks`, `pkcs12`
* If not provided, the value will be inferred from the truststore filename.

The format of the truststore file. It must be either `jks` or `pkcs12`.

[id="plugins-{type}s-{plugin}-ssl_verify_mode"]
===== `ssl_verify_mode`
deprecated[3.7.0, Replaced by <<plugins-{type}s-{plugin}-ssl_client_authentication>>]
Expand Down
104 changes: 66 additions & 38 deletions lib/logstash/inputs/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,24 @@ class LogStash::Inputs::Http < LogStash::Inputs::Base
# The JKS keystore password
config :ssl_keystore_password, :validate => :password

# The JKS keystore to validate the client's certificates
# The path for the keystore file that contains a private key and certificate
config :ssl_keystore_path, :validate => :path

# The format of the keystore file. It must be either jks or pkcs12
config :ssl_keystore_type, :validate => %w[pkcs12 jks]

# SSL key passphrase to use.
config :ssl_key_passphrase, :validate => :password

# Set the truststore password
config :ssl_truststore_password, :validate => :password

# The path for the keystore that contains the certificates to trust. It must be either a Java keystore (jks) or a PKCS#12 file
config :ssl_truststore_path, :validate => :path

# The format of the truststore file. It must be either jks or pkcs12
config :ssl_truststore_type, :validate => %w[pkcs12 jks]

# 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_client_authentication`
Expand Down Expand Up @@ -301,18 +313,31 @@ def validate_ssl_settings!
raise LogStash::ConfigurationError, 'An `ssl_certificate` is required when using an `ssl_key`'
end

unless ssl_key_configured? || ssl_jks_configured?
unless ssl_certificate_configured? || ssl_keystore_configured?
raise LogStash::ConfigurationError, "Either an `ssl_certificate` or `ssl_keystore_path` is required when SSL is enabled `#{ssl_config_name} => true`"
end

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`"
if ssl_certificate_configured? && ssl_keystore_configured?
raise LogStash::ConfigurationError, 'Use either an `ssl_certificate` or an `ssl_keystore_path`'
end

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}'"
if ssl_certificate_authorities_configured? && ssl_truststore_configured?
raise LogStash::ConfigurationError, 'Use either an `ssl_certificate_authorities` or an `ssl_truststore_path`'
end

cli_auth_config_name, cli_auth_optional_val, cli_auth_required_val = provided_ssl_client_authentication_config([SSL_CLIENT_AUTH_OPTIONAL, SSL_CLIENT_AUTH_REQUIRED])
if ssl_client_authentication_enabled?
# Ensure any CA is configured. By default, the keystore can also be used as CA
unless ssl_certificate_authorities_configured? || ssl_truststore_configured? || ssl_keystore_configured?
raise LogStash::ConfigurationError, "Using `#{cli_auth_config_name}` set to `#{cli_auth_optional_val}` or `#{cli_auth_required_val}`, requires the configuration of `ssl_certificate_authorities` or `ssl_truststore_path`"
end
else
if ssl_truststore_configured?
raise LogStash::ConfigurationError, "The configuration of `ssl_truststore_path` requires setting `#{cli_auth_config_name}` to `#{cli_auth_optional_val}` or '#{cli_auth_required_val}'"
end
if ssl_certificate_authorities_configured?
raise LogStash::ConfigurationError, "The configuration of `ssl_certificate_authorities` requires setting `#{cli_auth_config_name}` to `#{cli_auth_optional_val}` or '#{cli_auth_required_val}'"
end
end
end

Expand Down Expand Up @@ -372,73 +397,76 @@ def create_http_server(message_handler)
def build_ssl_params
return nil unless @ssl_enabled

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
ssl_builder = new_ssl_simple_builder
end

new_ssl_handshake_provider(ssl_builder)
new_ssl_handshake_provider(new_ssl_simple_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 ssl_keystore_configured?
ssl_context_builder = SslSimpleBuilder.withKeyStore(@ssl_keystore_type, @ssl_keystore_path, @ssl_keystore_password&.value)
else
ssl_context_builder = SslSimpleBuilder.withPemCertificate(@ssl_certificate, @ssl_key, @ssl_key_passphrase&.value)
end

if client_authentication_enabled?
ssl_context_builder.setClientAuthentication(ssl_simple_builder_verify_mode, @ssl_certificate_authorities)
ssl_context_builder.setProtocols(@ssl_supported_protocols)
.setCipherSuites(normalized_cipher_suites)
.setClientAuthentication(ssl_simple_builder_verify_mode)

if ssl_client_authentication_enabled?
if ssl_certificate_authorities_configured?
ssl_context_builder.setCertificateAuthorities(@ssl_certificate_authorities)
elsif ssl_truststore_configured?
ssl_context_builder.setTrustStore(@ssl_truststore_type, @ssl_truststore_path, @ssl_truststore_password&.value)
end
end

ssl_context_builder
rescue java.lang.IllegalArgumentException => e
rescue => e
@logger.error("SSL configuration invalid", error_details(e))
raise LogStash::ConfigurationError, e
end
end

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?
return SslSimpleBuilder::SslClientVerifyMode::OPTIONAL if ssl_client_authentication_optional?
return SslSimpleBuilder::SslClientVerifyMode::REQUIRED if ssl_client_authentication_required?
return SslSimpleBuilder::SslClientVerifyMode::NONE if ssl_client_authentication_none?
raise LogStash::ConfigurationError, "Invalid `ssl_client_authentication` value #{@ssl_client_authentication}"
end

def ssl_key_configured?
!!(@ssl_certificate && @ssl_key)
def ssl_certificate_configured?
!(@ssl_certificate.nil? || @ssl_certificate.empty?)
end

def ssl_jks_configured?
!!(@ssl_keystore_path && @ssl_keystore_password)
def ssl_keystore_configured?
!(@ssl_keystore_path.nil? || @ssl_keystore_path.empty?)
end

def client_authentication_enabled?
client_authentication_optional? || client_authentication_required?
def ssl_truststore_configured?
!(@ssl_truststore_path.nil? || @ssl_truststore_path.empty?)
end

def require_certificate_authorities?
client_authentication_required? || client_authentication_optional?
def ssl_client_authentication_enabled?
ssl_client_authentication_optional? || ssl_client_authentication_required?
end

def certificate_authorities_configured?
def ssl_certificate_authorities_configured?
@ssl_certificate_authorities && @ssl_certificate_authorities.size > 0
end

def client_authentication_required?
def ssl_client_authentication_required?
@ssl_client_authentication && @ssl_client_authentication.downcase == SSL_CLIENT_AUTH_REQUIRED
end

def client_authentication_none?
def ssl_client_authentication_none?
@ssl_client_authentication && @ssl_client_authentication.downcase == SSL_CLIENT_AUTH_NONE
end

def client_authentication_optional?
def ssl_client_authentication_optional?
@ssl_client_authentication && @ssl_client_authentication.downcase == SSL_CLIENT_AUTH_OPTIONAL
end

def provided_client_authentication_config(values = [@ssl_client_authentication])
def provided_ssl_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')
Expand Down
2 changes: 2 additions & 0 deletions spec/fixtures/certs/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ echo "DO NOT USE THESE CERTIFICATES IN PRODUCTION" >> ./README.txt
# certificate authority
openssl genrsa -out root.key 4096
openssl req -new -x509 -days 1826 -extensions ca -key root.key -out root.crt -subj "/C=LS/ST=NA/L=Http Input/O=Logstash/CN=root" -config ../openssl.cnf
keytool -import -file root.crt -alias rootCA -keystore truststore.jks -noprompt -storepass 12345678

# server certificate from root
openssl genrsa -out server_from_root.key 4096
openssl req -new -key server_from_root.key -out server_from_root.csr -subj "/C=LS/ST=NA/L=Http Input/O=Logstash/CN=server" -config ../openssl.cnf
openssl x509 -req -extensions server_cert -extfile ../openssl.cnf -days 1096 -in server_from_root.csr -CA root.crt -CAkey root.key -set_serial 03 -out server_from_root.crt
openssl pkcs12 -export -out server_from_root.p12 -inkey server_from_root.key -in server_from_root.crt -certfile root.crt -password pass:12345678

# client certificate from root
openssl genrsa -out client_from_root.key 4096
Expand Down
Binary file not shown.
Binary file added spec/fixtures/certs/generated/truststore.jks
Binary file not shown.
86 changes: 83 additions & 3 deletions spec/inputs/http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,16 @@ def setup_server_client(url = self.url)
end
end

context "and with :ssl_keystore_path" do
let(:config) do
super().merge('ssl_keystore_path' => certificate_path( 'server_from_root.p12'), 'ssl_enabled' => true )
end

it "should raise a configuration error" do
expect { subject.register }.to raise_error LogStash::ConfigurationError, /Use either an `ssl_certificate` or an `ssl_keystore_path`/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") }
Expand Down Expand Up @@ -766,7 +776,7 @@ def setup_server_client(url = self.url)
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`")
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` or `ssl_truststore_path`")
end
end
end
Expand All @@ -786,13 +796,21 @@ def setup_server_client(url = self.url)
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

context "with ssl_truststore_path set" do
let(:config) { super().merge("ssl_truststore_path" => [certificate_path( 'server_from_root.p12')], "ssl_truststore_password" => "12345678") }

it "raise a configuration error" do
expect {subject.register}.to raise_error(LogStash::ConfigurationError, "The configuration of `ssl_truststore_path` 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`")
expect {subject.register}.to raise_error(LogStash::ConfigurationError, "Using `ssl_client_authentication` set to `optional` or `required`, requires the configuration of `ssl_certificate_authorities` or `ssl_truststore_path`")
end

context "with ssl_certificate_authorities set" do
Expand All @@ -802,13 +820,21 @@ def setup_server_client(url = self.url)
expect {subject.register}.not_to raise_error
end
end

context "with ssl_truststore_path set" do
let(:config) { super().merge("ssl_truststore_path" => [certificate_path( 'server_from_root.p12')], "ssl_truststore_password" => "12345678") }

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`")
expect {subject.register}.to raise_error(LogStash::ConfigurationError, "Using `ssl_client_authentication` set to `optional` or `required`, requires the configuration of `ssl_certificate_authorities` or `ssl_truststore_path`")
end

context "with certificate_authorities set" do
Expand All @@ -818,9 +844,63 @@ def setup_server_client(url = self.url)
expect {subject.register}.not_to raise_error
end
end

context "with ssl_truststore_path set" do
let(:config) { super().merge("ssl_truststore_path" => [certificate_path( 'server_from_root.p12')], "ssl_truststore_password" => "12345678") }

it "doesn't raise a configuration error" do
expect {subject.register}.not_to raise_error
end
end
end
end
end
context "with :ssl_keystore_path" do
let(:config) do
{
"port" => port,
"ssl_enabled" => true,
"ssl_keystore_path" => certificate_path( 'server_from_root.p12'),
"ssl_keystore_password" => "12345678"
}
end

subject { LogStash::Inputs::Http.new(config) }

it "should not raise exception" do
expect { subject.register }.to_not raise_exception
end
end
context "with :ssl_truststore_path" do
let(:config) do
{
"port" => port,
"ssl_enabled" => true,
"ssl_client_authentication" => "optional",
"ssl_keystore_path" => certificate_path( 'server_from_root.p12'),
"ssl_keystore_password" => "12345678",
"ssl_truststore_path" => certificate_path( 'truststore.jks'),
"ssl_truststore_password" => "12345678"
}
end

subject { LogStash::Inputs::Http.new(config) }

it "should not raise exception" do
expect { subject.register }.to_not raise_exception
end

context "and with :ssl_certificate_authorities configured" do
let(:config) do
super().merge('ssl_certificate_authorities' => [certificate_path( 'root.crt')], 'ssl_enabled' => true )
end

it "should raise a configuration error" do
expect { subject.register }.to raise_error LogStash::ConfigurationError, /Use either an `ssl_certificate_authorities` or an `ssl_truststore_path`/i
end
end
end

end
end

Expand Down

0 comments on commit a482f27

Please sign in to comment.