rabbitmq-server/deps/rabbitmq_trust_store/test/system_SUITE.erl

1010 lines
46 KiB
Erlang

%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(system_SUITE).
-compile([export_all]).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("amqp_client/include/amqp_client.hrl").
-include_lib("public_key/include/public_key.hrl").
-define(SERVER_REJECT_CLIENT, {tls_alert, "unknown ca"}).
-define(SERVER_REJECT_CLIENT_NEW, {tls_alert, {unknown_ca, _}}).
-define(SERVER_REJECT_CLIENT_ERLANG24,
{tls_alert,
{handshake_failure,
"TLS client: In state cipher received SERVER ALERT: Fatal - "
"Handshake Failure\n"}}).
-define(SERVER_REJECT_CONNECTION_ERLANG23,
{{socket_error,
{tls_alert,
{handshake_failure,
"TLS client: In state connection received SERVER ALERT: Fatal - "
"Handshake Failure\n"}}},
{expecting,'connection.start'}}).
all() ->
[
{group, http_provider_tests},
{group, file_provider_tests}
].
groups() ->
CommonTests = [
validate_chain,
validate_longer_chain,
validate_chain_without_whitelisted,
whitelisted_certificate_accepted_from_AMQP_client_regardless_of_validation_to_root,
removed_certificate_denied_from_AMQP_client,
installed_certificate_accepted_from_AMQP_client,
whitelist_directory_DELTA,
replaced_whitelisted_certificate_should_be_accepted,
ensure_configuration_using_binary_strings_is_handled,
ignore_corrupt_cert,
ignore_same_cert_with_different_name,
list],
[
{file_provider_tests, [], [
library,
invasive_SSL_option_change,
validation_success_for_AMQP_client,
validation_failure_for_AMQP_client,
disabled_provider_removes_certificates,
enabled_provider_adds_cerificates |
CommonTests
]},
{http_provider_tests, [], CommonTests}
].
suite() ->
[{timetrap, {seconds, 180}}].
%% -------------------------------------------------------------------
%% Testsuite setup/teardown.
%% -------------------------------------------------------------------
set_up_node(Config) ->
rabbit_ct_helpers:log_environment(),
Config1 = rabbit_ct_helpers:set_config(Config, [
{rmq_nodename_suffix, ?MODULE},
{rmq_extra_tcp_ports, [tcp_port_amqp_tls_extra]}
]),
rabbit_ct_helpers:run_setup_steps(Config1,
rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_client_helpers:setup_steps()).
tear_down_node(Config) ->
rabbit_ct_helpers:run_teardown_steps(Config,
rabbit_ct_client_helpers:teardown_steps() ++
rabbit_ct_broker_helpers:teardown_steps()).
init_per_group(file_provider_tests, Config) ->
case set_up_node(Config) of
{skip, _} = Error -> Error;
Config1 ->
WhitelistDir = filename:join([?config(rmq_certsdir, Config1),
"trust_store", "file_provider_tests"]),
Config2 = init_whitelist_dir(Config1, WhitelistDir),
ok = rabbit_ct_broker_helpers:rpc(Config2, 0,
?MODULE, change_configuration,
[rabbitmq_trust_store, [{directory, WhitelistDir},
{refresh_interval, interval()},
{providers, [rabbit_trust_store_file_provider]}]]),
Config2
end;
init_per_group(http_provider_tests, Config) ->
case set_up_node(Config) of
{skip, _} = Error -> Error;
Config1 ->
WhitelistDir = filename:join([?config(rmq_certsdir, Config1),
"trust_store", "http_provider_tests"]),
Config2 = init_whitelist_dir(Config1, WhitelistDir),
Config3 = init_provider_server(Config2, WhitelistDir),
Url = ?config(trust_store_server_url, Config3),
ok = rabbit_ct_broker_helpers:rpc(Config3, 0,
?MODULE, change_configuration,
[rabbitmq_trust_store, [{url, Url},
{refresh_interval, interval()},
{providers, [rabbit_trust_store_http_provider]}]]),
Config3
end.
init_provider_server(Config, WhitelistDir) ->
%% Assume we don't have more than 100 ports allocated for tests
PortBase = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_ports_base),
CertServerPort = PortBase + 100,
Url = "http://127.0.0.1:" ++ integer_to_list(CertServerPort) ++ "/",
application:load(trust_store_http),
ok = application:set_env(trust_store_http, directory, WhitelistDir),
ok = application:set_env(trust_store_http, port, CertServerPort),
ok = application:unset_env(trust_store_http, ssl_options),
application:ensure_all_started(trust_store_http),
rabbit_ct_helpers:set_config(Config, [{trust_store_server_port, CertServerPort},
{trust_store_server_url, Url}]).
end_per_group(file_provider_tests, Config) ->
Config1 = tear_down_node(Config),
tear_down_whitelist_dir(Config1),
Config;
end_per_group(http_provider_tests, Config) ->
Config1 = tear_down_node(Config),
application:stop(trust_store_http),
Config1;
end_per_group(_, Config) ->
tear_down_node(Config).
init_whitelist_dir(Config, WhitelistDir) ->
ok = filelib:ensure_dir(WhitelistDir),
ok = file:make_dir(WhitelistDir),
rabbit_ct_helpers:set_config(Config, {whitelist_dir, WhitelistDir}).
tear_down_whitelist_dir(Config) ->
WhitelistDir = ?config(whitelist_dir, Config),
ok = rabbit_file:recursive_delete([WhitelistDir]).
init_per_testcase(Testcase, Config) ->
WhitelistDir = ?config(whitelist_dir, Config),
ok = rabbit_file:recursive_delete([WhitelistDir]),
ok = file:make_dir(WhitelistDir),
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, change_configuration,
[rabbitmq_trust_store, []]),
rabbit_ct_helpers:testcase_started(Config, Testcase).
end_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_finished(Config, Testcase).
%% -------------------------------------------------------------------
%% Testsuite cases
%% -------------------------------------------------------------------
library(_) ->
%% Given: Makefile.
{_Root, _Certificate, _Key} = ct_helper:make_certs(),
ok.
invasive_SSL_option_change(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, invasive_SSL_option_change1, []).
invasive_SSL_option_change1() ->
%% Given: Rabbit is started with the boot-steps in the
%% Trust-Store's OTP Application file.
%% When: we get Rabbit's SSL options.
{ok, Options} = application:get_env(rabbit, ssl_options),
%% Then: all necessary settings are correct.
verify_peer = proplists:get_value(verify, Options),
true = proplists:get_value(fail_if_no_peer_cert, Options),
{Verifyfun, _UserState} = proplists:get_value(verify_fun, Options),
{module, rabbit_trust_store} = erlang:fun_info(Verifyfun, module),
ok.
validation_success_for_AMQP_client(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, validation_success_for_AMQP_client1, [Config]).
validation_success_for_AMQP_client1(Config) ->
%% This test intentionally doesn't whitelist any certificates.
%% Both the client and the server use certificate/key pairs signed by
%% the same root CA. This exercises a verify_fun clause that no ther tests hit.
%% Note that when this test is executed together with the HTTP provider group
%% it runs into unexpected interference and fails, even if TLS app PEM cache is force
%% cleared. That's why originally each group was made to use a separate node.
RootCert = #{cert := Root} = public_key:pkix_test_root_cert("RootCA", []),
{Certificate, Key} = chain(RootCert),
{Certificate2, Key2} = chain(RootCert),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
%% When: Rabbit accepts just this one authority's certificate
%% (i.e. these are options that'd be in the configuration
%% file).
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, [Root]},
{cert, Certificate2},
{key, Key2} | cfg()], 1, 1),
%% Then: a client presenting a certifcate rooted at the same
%% authority connects successfully.
{ok, Con} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, [Root]},
{cert, Certificate},
{key, Key},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% Clean: client & server TLS/TCP.
ok = amqp_connection:close(Con),
ok = rabbit_networking:stop_tcp_listener(Port).
validation_failure_for_AMQP_client(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, validation_failure_for_AMQP_client1, [Config]).
validation_failure_for_AMQP_client1(Config) ->
%% Given: a root certificate and a certificate rooted with another
%% authority.
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertOther, KeyOther} = ct_helper:make_certs(),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
%% When: Rabbit accepts certificates rooted with just one
%% particular authority.
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% Then: a client presenting a certificate rooted with another
%% authority is REJECTED.
{error, Error} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertOther},
{key, KeyOther},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
case Error of
%% Expected error from amqp_client.
?SERVER_REJECT_CLIENT -> ok;
?SERVER_REJECT_CLIENT_NEW -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok;
%% ssl:setopts/2 hangs indefinitely on occasion
{timeout, {gen_server,call,[_,post_init|_]}} -> ssl_setopts_hangs_occassionally
end,
%% Clean: server TLS/TCP.
ok = rabbit_networking:stop_tcp_listener(Port).
validate_chain(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, validate_chain1, [Config]).
validate_chain1(Config) ->
%% Given: a whitelisted certificate `CertTrusted` AND a CA `RootTrusted`
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertTrusted, KeyTrusted} = ct_helper:make_certs(),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
ok = whitelist(Config, "alice", CertTrusted),
rabbit_trust_store:refresh(),
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% When: a client connects and present `RootTrusted` as well as the `CertTrusted`
%% Then: the connection is successful.
{ok, Con} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertTrusted},
{key, KeyTrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% Clean: client & server TLS/TCP
ok = amqp_connection:close(Con),
ok = rabbit_networking:stop_tcp_listener(Port).
validate_longer_chain(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, validate_longer_chain1, [Config]).
validate_longer_chain1(Config) ->
{ServerCACerts, Cert, Key} = ct_helper:make_certs(),
%% Given: a whitelisted certificate `CertTrusted`
%% AND a certificate `CertUntrusted` that is not whitelisted with the same root as `CertTrusted`
%% AND `CertInter` intermediate CA
%% AND `RootTrusted` CA
KeyInterDec = public_key:generate_key({rsa, 2048, 17}),
KeyInter = {'RSAPrivateKey', public_key:der_encode('RSAPrivateKey', KeyInterDec)},
TestDataTrusted = public_key:pkix_test_data(#{
root => [],
intermediates => [[{digest, sha256}, {key, KeyInterDec}]],
peer => [{digest, sha256}, {key, {rsa, 2048, 17}}]
}),
CertTrusted = proplists:get_value(cert, TestDataTrusted),
KeyTrusted = proplists:get_value(key, TestDataTrusted),
[RootCA, CertInter, RootCA] = proplists:get_value(cacerts, TestDataTrusted),
TestDataUntrusted = public_key:pkix_test_data(#{
root => #{cert => CertInter, key => KeyInterDec},
peer => [{digest, sha256}, {key, {rsa, 2048, 17}}]
}),
CertUntrusted = proplists:get_value(cert, TestDataUntrusted),
KeyUntrusted = proplists:get_value(key, TestDataUntrusted),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
ok = whitelist(Config, "alice", CertTrusted),
rabbit_trust_store:refresh(),
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, ServerCACerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% @todo The following 3 connection tests are actually the same test: the cacerts option
%% of the client is only used so the client can verify that the server certificate
%% is correct. The cacerts are not sent to the server. When the client verification
%% is enabled they must include the correct CA for the server, that's all there is
%% to it.
%% When: a client connects and present `CertInter` as well as the `CertTrusted`
%% Then: the connection is successful.
{ok, Con} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, [CertInter|ServerCACerts]},
{cert, CertTrusted},
{key, KeyTrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% When: a client connects and present `RootTrusted` and `CertInter` as well as the `CertTrusted`
%% Then: the connection is successful.
{ok, Con2} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, [RootCA, CertInter|ServerCACerts]},
{cert, CertTrusted},
{key, KeyTrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% When: a client connects and present `CertInter` and `RootCA` as well as the `CertTrusted`
%% Then: the connection is successful.
{ok, Con3} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, [CertInter, RootCA|ServerCACerts]},
{cert, CertTrusted},
{key, KeyTrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
% %% When: a client connects and present `CertInter` and `RootCA` but NOT `CertTrusted`
% %% Then: the connection is not succcessful
{error, Error1} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, [RootCA|ServerCACerts]},
{cert, CertInter},
{key, KeyInter},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
case Error1 of
%% Expected error from amqp_client.
?SERVER_REJECT_CLIENT -> ok;
?SERVER_REJECT_CLIENT_NEW -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok;
%% ssl:setopts/2 hangs indefinitely on occasion
{timeout, {gen_server,call,[_,post_init|_]}} -> ssl_setopts_hangs_occassionally
end,
%% When: a client connects and present `CertUntrusted` and `RootCA` and `CertInter`
%% Then: the connection is not succcessful
%% TODO: for some reason this returns `bad certifice` rather than `unknown ca`
{error, Error2} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, [RootCA, CertInter|ServerCACerts]},
{cert, CertUntrusted},
{key, KeyUntrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
case Error2 of
%% Expected error from amqp_client.
{tls_alert, "bad certificate"} -> ok;
{tls_alert, {bad_certificate, _}} -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok;
%% ssl:setopts/2 hangs indefinitely on occasion
{timeout, {gen_server,call,[_,post_init|_]}} -> ssl_setopts_hangs_occassionally
end,
%% Clean: client & server TLS/TCP
ok = amqp_connection:close(Con),
ok = amqp_connection:close(Con2),
ok = amqp_connection:close(Con3),
ok = rabbit_networking:stop_tcp_listener(Port).
validate_chain_without_whitelisted(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, validate_chain_without_whitelisted1, [Config]).
validate_chain_without_whitelisted1(Config) ->
%% Given: a certificate `CertUntrusted` that is NOT whitelisted.
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertUntrusted, KeyUntrusted} = ct_helper:make_certs(),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
rabbit_trust_store:refresh(),
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% When: Rabbit validates paths
%% Then: a client presenting the non-whitelisted certificate `CertUntrusted` and `RootUntrusted`
%% is rejected
{error, Error} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertUntrusted},
{key, KeyUntrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
case Error of
%% Expected error from amqp_client.
?SERVER_REJECT_CLIENT -> ok;
?SERVER_REJECT_CLIENT_NEW -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok;
%% ssl:setopts/2 hangs indefinitely on occasion
{timeout, {gen_server,call,[_,post_init|_]}} -> ssl_setopts_hangs_occassionally
end,
ok = rabbit_networking:stop_tcp_listener(Port).
whitelisted_certificate_accepted_from_AMQP_client_regardless_of_validation_to_root(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, whitelisted_certificate_accepted_from_AMQP_client_regardless_of_validation_to_root1, [Config]).
whitelisted_certificate_accepted_from_AMQP_client_regardless_of_validation_to_root1(Config) ->
%% Given: a certificate `CertTrusted` AND that it is whitelisted.
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertTrusted, KeyTrusted} = ct_helper:make_certs(),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
ok = whitelist(Config, "alice", CertTrusted),
rabbit_trust_store:refresh(),
%% When: Rabbit validates paths with a different root `R` than
%% that of the certificate `CertTrusted`.
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% Then: a client presenting the whitelisted certificate `C`
%% is allowed.
{ok, Con} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertTrusted},
{key, KeyTrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% Clean: client & server TLS/TCP
ok = amqp_connection:close(Con),
ok = rabbit_networking:stop_tcp_listener(Port).
removed_certificate_denied_from_AMQP_client(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, removed_certificate_denied_from_AMQP_client1, [Config]).
removed_certificate_denied_from_AMQP_client1(Config) ->
%% Given: a certificate `CertOther` AND that it is whitelisted.
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertOther, KeyOther} = ct_helper:make_certs(),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
ok = whitelist(Config, "bob", CertOther),
rabbit_trust_store:refresh(),
%% When: we wait for at least one second (the accuracy of the
%% file system's time), remove the whitelisted certificate,
%% then wait for the trust-store to refresh the whitelist.
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
wait_for_file_system_time(),
ok = delete("bob.pem", Config),
wait_for_trust_store_refresh(),
%% Then: a client presenting the removed whitelisted
%% certificate `CertOther` is denied.
{error, Error} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertOther},
{key, KeyOther},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
case Error of
%% Expected error from amqp_client.
?SERVER_REJECT_CLIENT -> ok;
?SERVER_REJECT_CLIENT_NEW -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok;
%% ssl:setopts/2 hangs indefinitely on occasion
{timeout, {gen_server,call,[_,post_init|_]}} -> ssl_setopts_hangs_occassionally
end,
%% Clean: server TLS/TCP
ok = rabbit_networking:stop_tcp_listener(Port).
installed_certificate_accepted_from_AMQP_client(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, installed_certificate_accepted_from_AMQP_client1, [Config]).
installed_certificate_accepted_from_AMQP_client1(Config) ->
%% Given: a certificate `CertOther` which is NOT yet whitelisted.
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertOther, KeyOther} = ct_helper:make_certs(),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
rabbit_trust_store:refresh(),
%% When: we wait for at least one second (the accuracy of the
%% file system's time), add a certificate to the directory,
%% then wait for the trust-store to refresh the whitelist.
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
wait_for_file_system_time(),
ok = whitelist(Config, "charlie", CertOther),
wait_for_trust_store_refresh(),
%% Then: a client presenting the whitelisted certificate `CertOther`
%% is allowed.
{ok, Con} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertOther},
{key, KeyOther},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% Clean: Client & server TLS/TCP
ok = amqp_connection:close(Con),
ok = rabbit_networking:stop_tcp_listener(Port).
whitelist_directory_DELTA(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, whitelist_directory_DELTA1, [Config]).
whitelist_directory_DELTA1(Config) ->
%% Given: a certificate `Root` which Rabbit can use as a
%% root certificate to validate agianst AND three
%% certificates which clients can present (the first two
%% of which are whitelisted).
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertListed1, KeyListed1} = ct_helper:make_certs(),
{_, CertRevoked, KeyRevoked} = ct_helper:make_certs(),
{_, CertListed2, KeyListed2} = ct_helper:make_certs(),
ok = whitelist(Config, "foo", CertListed1),
ok = whitelist(Config, "bar", CertRevoked),
rabbit_trust_store:refresh(),
%% When: we wait for at least one second (the accuracy
%% of the file system's time), delete a certificate and
%% a certificate to the directory, then wait for the
%% trust-store to refresh the whitelist.
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
wait_for_file_system_time(),
ok = delete("bar.pem", Config),
ok = whitelist(Config, "baz", CertListed2),
wait_for_trust_store_refresh(),
%% Then: connectivity to Rabbit is as it should be.
{ok, Conn1} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertListed1},
{key, KeyListed1},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
{error, Error} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertRevoked},
{key, KeyRevoked},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
case Error of
%% Expected error from amqp_client.
?SERVER_REJECT_CLIENT -> ok;
?SERVER_REJECT_CLIENT_NEW -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok;
%% ssl:setopts/2 hangs indefinitely on occasion
{timeout, {gen_server,call,[_,post_init|_]}} -> ssl_setopts_hangs_occassionally
end,
{ok, Conn2} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertListed2},
{key, KeyListed2},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% Clean: delete certificate file, close client & server
%% TLS/TCP
ok = amqp_connection:close(Conn1),
ok = amqp_connection:close(Conn2),
ok = rabbit_networking:stop_tcp_listener(Port).
replaced_whitelisted_certificate_should_be_accepted(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, replaced_whitelisted_certificate_should_be_accepted1, [Config]).
replaced_whitelisted_certificate_should_be_accepted1(Config) ->
%% Given: a root certificate and a 2 other certificates
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertFirst, KeyFirst} = ct_helper:make_certs(),
{_, CertUpdated, KeyUpdated} = ct_helper:make_certs(),
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% And: the first certificate has been whitelisted
ok = whitelist(Config, "bart", CertFirst),
rabbit_trust_store:refresh(),
wait_for_trust_store_refresh(),
%% verify that the first cert can be used to connect
{ok, Con} =
amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertFirst},
{key, KeyFirst},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% verify the other certificate is not accepted
{error, Error1} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertUpdated},
{key, KeyUpdated},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
case Error1 of
%% Expected error from amqp_client.
?SERVER_REJECT_CLIENT -> ok;
?SERVER_REJECT_CLIENT_NEW -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok;
%% ssl:setopts/2 hangs indefinitely on occasion
{timeout, {gen_server,call,[_,post_init|_]}} -> ssl_setopts_hangs_occassionally
end,
ok = amqp_connection:close(Con),
%% When: a whitelisted certicate is replaced with one with the same name
ok = whitelist(Config, "bart", CertUpdated),
wait_for_trust_store_refresh(),
%% Then: the first certificate should be rejected
{error, Error2} = amqp_connection:start(
#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertFirst},
{key, KeyFirst},
{verify, verify_none},
{versions, ['tlsv1.2']},
%% disable ssl session caching
%% as this ensures the cert
%% will be re-verified by the
%% server
{reuse_sessions, false}]}),
case Error2 of
%% Expected error from amqp_client.
?SERVER_REJECT_CLIENT -> ok;
?SERVER_REJECT_CLIENT_NEW -> ok;
?SERVER_REJECT_CLIENT_ERLANG24 -> ok;
?SERVER_REJECT_CONNECTION_ERLANG23 -> ok;
%% Sometimes the TCP socket gets closed before the client receives the alert.
closed -> ok
end,
%% And: the updated certificate should allow the user to connect
{ok, Con2} =
amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertUpdated},
{key, KeyUpdated},
{verify, verify_none},
{versions, ['tlsv1.2']},
{reuse_sessions, false}]}),
ok = amqp_connection:close(Con2),
%% Clean: server TLS/TCP.
ok = rabbit_networking:stop_tcp_listener(Port).
ensure_configuration_using_binary_strings_is_handled(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, ensure_configuration_using_binary_strings_is_handled1, [Config]).
ensure_configuration_using_binary_strings_is_handled1(Config) ->
ok = change_configuration(rabbitmq_trust_store, [{directory, list_to_binary(whitelist_dir(Config))},
{refresh_interval,
{seconds, interval()}}]).
ignore_corrupt_cert(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, ignore_corrupt_cert1, [Config]).
ignore_corrupt_cert1(Config) ->
%% Given: a certificate `CertTrusted` AND that it is whitelisted.
%% Given: a corrupt certificate.
Port = port(Config),
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertTrusted, KeyTrusted} = ct_helper:make_certs(),
rabbit_trust_store:refresh(),
ok = whitelist(Config, "alice", CertTrusted),
%% When: Rabbit tries to whitelist the corrupt certificate.
ok = whitelist(Config, "corrupt", <<48>>),
rabbit_trust_store:refresh(),
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% Then: the trust store should keep functioning
%% And: a client presenting the whitelisted certificate `CertTrusted`
%% is allowed.
{ok, Con} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertTrusted},
{key, KeyTrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% Clean: client & server TLS/TCP
ok = amqp_connection:close(Con),
ok = rabbit_networking:stop_tcp_listener(Port).
ignore_same_cert_with_different_name(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, ignore_same_cert_with_different_name1, [Config]).
ignore_same_cert_with_different_name1(Config) ->
%% Given: a certificate `CertTrusted` AND that it is whitelisted.
%% Given: the same certificate saved with a different filename.
Host = rabbit_ct_helpers:get_config(Config, rmq_hostname),
Port = port(Config),
{RootCerts, Cert, Key} = ct_helper:make_certs(),
{_, CertTrusted, KeyTrusted} = ct_helper:make_certs(),
rabbit_trust_store:refresh(),
ok = whitelist(Config, "alice", CertTrusted),
%% When: Rabbit tries to insert the duplicate certificate
ok = whitelist(Config, "malice", CertTrusted),
rabbit_trust_store:refresh(),
catch rabbit_networking:stop_tcp_listener(Port),
ok = rabbit_networking:start_ssl_listener(Port, [{cacerts, RootCerts},
{cert, Cert},
{key, Key} | cfg()], 1, 1),
%% Then: the trust store should keep functioning.
%% And: a client presenting the whitelisted certificate `CertTrusted`
%% is allowed.
{ok, Con} = amqp_connection:start(#amqp_params_network{host = Host,
port = Port,
ssl_options = [{cacerts, RootCerts},
{cert, CertTrusted},
{key, KeyTrusted},
{verify, verify_none},
{versions, ['tlsv1.2']}]}),
%% Clean: client & server TLS/TCP
ok = amqp_connection:close(Con),
ok = rabbit_networking:stop_tcp_listener(Port).
list(Config) ->
%% FIXME: The file provider calls stat(2) on the certificate
%% directory to detect any new certificates. Unfortunately, the
%% modification time has a resolution of one second. Thus, it can
%% miss certificates added within the same second after a refresh.
%% To workaround this, we force a refresh, wait for two seconds,
%% write the test certificate and call refresh again.
%%
%% Once this is fixed, the two lines below can be removed.
%%
%% See rabbitmq/rabbitmq-trust-store#58.
ok = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_trust_store, refresh, []),
timer:sleep(2000),
{_Root, Cert, _Key} = ct_helper:make_certs(),
ok = whitelist(Config, "alice", Cert),
% wait_for_trust_store_refresh(),
ok = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_trust_store, refresh, []),
Certs = rabbit_ct_broker_helpers:rpc(Config, 0,
rabbit_trust_store, list, []),
% only really tests it isn't totally broken.
{match, _} = re:run(Certs, ".*alice\.pem.*").
disabled_provider_removes_certificates(Config) ->
{_Root, Cert, _Key} = ct_helper:make_certs(),
ok = whitelist(Config, "alice", Cert),
ok = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_trust_store, refresh, []),
%% Certificate is there
Certs = rabbit_ct_broker_helpers:rpc(Config, 0,
rabbit_trust_store, list, []),
{match, _} = re:run(Certs, ".*alice\.pem.*"),
rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env,
[rabbitmq_trust_store, providers, []]),
wait_for_trust_store_refresh(),
%% Certificate is not there anymore
CertsAfterDelete = rabbit_ct_broker_helpers:rpc(Config, 0,
rabbit_trust_store, list, []),
nomatch = re:run(CertsAfterDelete, ".*alice\.pem.*").
enabled_provider_adds_cerificates(Config) ->
{_Root, Cert, _Key} = ct_helper:make_certs(),
ok = whitelist(Config, "alice", Cert),
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
?MODULE, change_configuration,
[rabbitmq_trust_store, [{directory, whitelist_dir(Config)},
{providers, []}]]),
%% Certificate is not there yet
Certs = rabbit_ct_broker_helpers:rpc(Config, 0,
rabbit_trust_store, list, []),
nomatch = re:run(Certs, ".*alice\.pem.*"),
rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env,
[rabbitmq_trust_store, providers, [rabbit_trust_store_file_provider]]),
wait_for_trust_store_refresh(),
%% Certificate is there
CertsAfterAdd = rabbit_ct_broker_helpers:rpc(Config, 0,
rabbit_trust_store, list, []),
{match, _} = re:run(CertsAfterAdd, ".*alice\.pem.*").
%% Test Constants
port(Config) ->
rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp_tls_extra).
whitelist_dir(Config) ->
?config(whitelist_dir, Config).
interval() ->
1.
wait_for_file_system_time() ->
timer:sleep(timer:seconds(1)).
wait_for_trust_store_refresh() ->
timer:sleep(5 * timer:seconds(interval())).
cfg() ->
{ok, Cfg} = application:get_env(rabbit, ssl_options),
Cfg.
%% Ancillary
chain(Issuer) ->
%% These are DER encoded.
TestData = public_key:pkix_test_data(#{
root => Issuer,
peer => [{digest, sha256}, {key, {rsa, 2048, 17}}, {extensions, [
#'Extension'{
extnID = ?'id-ce-subjectAltName',
extnValue = [{dNSName, "localhost"}],
critical = true}
]}]}),
{proplists:get_value(cert, TestData), proplists:get_value(key, TestData)}.
change_configuration(App, Props) ->
ok = application:stop(App),
ok = change_cfg(App, Props),
application:start(App).
change_cfg(_, []) ->
ok;
change_cfg(App, [{Name,Value}|Rest]) ->
ok = application:set_env(App, Name, Value),
change_cfg(App, Rest).
whitelist(Config, Filename, Certificate) ->
Path = whitelist_dir(Config),
ok = file:write_file(filename:join(Path, Filename ++ ".pem"),
public_key:pem_encode([{'Certificate', Certificate, not_encrypted}])),
ok.
delete(Name, Config) ->
F = filename:join([whitelist_dir(Config), Name]),
file:delete(F).