feat(tls-partial-chain): support CAcert renewal
The listener could support two versions of CAcerts if partial_chain is set to `two_cacerts_from_cacertfile`
This commit is contained in:
parent
151176a6be
commit
285d3dabc7
|
@ -21,7 +21,25 @@
|
||||||
-export([ make_tls_root_fun/2
|
-export([ make_tls_root_fun/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
make_tls_root_fun(cacert_from_cacertfile, CADer) ->
|
%% @doc Build a root fun for verify TLS partial_chain.
|
||||||
fun(_InputChain) ->
|
%% The `InputChain' is composed by OTP SSL with local cert store
|
||||||
{trusted_ca, CADer}
|
%% AND the cert (chain if any) from the client.
|
||||||
end.
|
%% @end
|
||||||
|
make_tls_root_fun(cacert_from_cacertfile, [Trusted]) ->
|
||||||
|
%% Allow only one trusted ca cert, and just return the defined trusted CA cert,
|
||||||
|
fun(_InputChain) ->
|
||||||
|
%% Note, returing `trusted_ca` doesn't really mean it accepts the connection
|
||||||
|
%% OTP SSL app will do the path validation, signature validation subsequently.
|
||||||
|
{trusted_ca, Trusted}
|
||||||
|
end;
|
||||||
|
make_tls_root_fun(cacert_from_cacertfile, [TrustedOne, TrustedTwo]) ->
|
||||||
|
%% Allow two trusted CA certs in case of CA cert renewal
|
||||||
|
%% This is a little expensive call as it compares the binaries.
|
||||||
|
fun(InputChain) ->
|
||||||
|
case lists:member(TrustedOne, InputChain) of
|
||||||
|
true ->
|
||||||
|
{trusted_ca, TrustedOne};
|
||||||
|
false ->
|
||||||
|
{trusted_ca, TrustedTwo}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
|
@ -191,26 +191,31 @@ opt_partial_chain(SslOpts) ->
|
||||||
false ->
|
false ->
|
||||||
SslOpts;
|
SslOpts;
|
||||||
V when V =:= cacert_from_cacertfile orelse V == true ->
|
V when V =:= cacert_from_cacertfile orelse V == true ->
|
||||||
replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(SslOpts))
|
replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(1, SslOpts));
|
||||||
|
V when V =:= two_cacerts_from_cacertfile -> %% for certificate rotations
|
||||||
|
replace(SslOpts, partial_chain, rootfun_trusted_ca_from_cacertfile(2, SslOpts))
|
||||||
end.
|
end.
|
||||||
|
|
||||||
replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].
|
replace(Opts, Key, Value) -> [{Key, Value} | proplists:delete(Key, Opts)].
|
||||||
|
|
||||||
%% @doc Helper, make TLS root_fun
|
%% @doc Helper, make TLS root_fun
|
||||||
rootfun_trusted_ca_from_cacertfile(SslOpts) ->
|
rootfun_trusted_ca_from_cacertfile(NumOfCerts, SslOpts) ->
|
||||||
Cacertfile = proplists:get_value(cacertfile, SslOpts, undefined),
|
Cacertfile = proplists:get_value(cacertfile, SslOpts, undefined),
|
||||||
try do_rootfun_trusted_ca_from_cacertfile(Cacertfile)
|
try do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, Cacertfile)
|
||||||
catch _Error:_Info:ST ->
|
catch _Error:_Info:ST ->
|
||||||
%% The cacertfile will be checked by OTP SSL as well and OTP choice to be silent on this.
|
%% The cacertfile will be checked by OTP SSL as well and OTP choice to be silent on this.
|
||||||
%% We are touching security sutffs, don't leak extra info..
|
%% We are touching security sutffs, don't leak extra info..
|
||||||
?LOG(error, "Failed to look for trusted cacert from cacertfile. Stacktrace: ~p", [ST]),
|
?LOG(error, "Failed to look for trusted cacert from cacertfile. Stacktrace: ~p", [ST]),
|
||||||
throw({error, ?FUNCTION_NAME})
|
throw({error, ?FUNCTION_NAME})
|
||||||
end.
|
end.
|
||||||
do_rootfun_trusted_ca_from_cacertfile(Cacertfile) ->
|
do_rootfun_trusted_ca_from_cacertfile(NumOfCerts, Cacertfile) ->
|
||||||
{ok, PemBin} = file:read_file(Cacertfile),
|
{ok, PemBin} = file:read_file(Cacertfile),
|
||||||
%% The last one should be the top parent in the chain if it is a chain
|
%% The last one or two should be the top parent in the chain if it is a chain
|
||||||
{'Certificate', CADer, _} = lists:last(public_key:pem_decode(PemBin)),
|
Certs = public_key:pem_decode(PemBin),
|
||||||
emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, CADer).
|
Pos = length(Certs) - NumOfCerts + 1,
|
||||||
|
Trusted = [ CADer || {'Certificate', CADer, _} <-
|
||||||
|
lists:sublist(public_key:pem_decode(PemBin), Pos, NumOfCerts)],
|
||||||
|
emqx_const_v2:make_tls_root_fun(cacert_from_cacertfile, Trusted).
|
||||||
|
|
||||||
-if(?OTP_RELEASE > 22).
|
-if(?OTP_RELEASE > 22).
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
|
|
|
@ -115,7 +115,56 @@ t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_bundle(Config)
|
||||||
fail_when_no_ssl_alert(Socket, unknown_ca),
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
ssl:close(Socket).
|
ssl:close(Socket).
|
||||||
|
|
||||||
%%@TODO limitation: EMQX is not able to check if the trusted CAcert and the old CAcert belongs to same CA.
|
t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_renewed_client_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
, {partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2_renewed.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% Note, this is good to have for usecase coverage
|
||||||
|
t_conn_success_with_new_intermediate_cacert_and_client_provides_renewed_client_cert_signed_by_old_intermediate(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2_renewed.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc server should build a partial_chain with old version of ca cert.
|
||||||
|
t_conn_success_with_old_and_renewed_intermediate_cacert_and_client_provides_client_cert(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
, {partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc verify when config does not allow two versions of certs from same trusted CA.
|
||||||
t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle(Config) ->
|
t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle(Config) ->
|
||||||
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
DataDir = ?config(data_dir, Config),
|
DataDir = ?config(data_dir, Config),
|
||||||
|
@ -131,6 +180,44 @@ t_conn_fail_with_renewed_and_old_intermediate_cacert_and_client_using_old_bundle
|
||||||
fail_when_no_ssl_alert(Socket, unknown_ca),
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
ssl:close(Socket).
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc verify when config (two_cacerts_from_cacertfile) allows two versions of certs from same trusted CA.
|
||||||
|
t_conn_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_old_bundle(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
, {partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client2.key")},
|
||||||
|
{certfile, filename:join(DataDir, "client2-intermediate2-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_ssl_error(Socket),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
|
%% @doc: verify even if listener has old/new intermediate2 certs,
|
||||||
|
%% client1 should not able to connect with old intermediate2 cert.
|
||||||
|
%% In this case, listener verify_fun returns {trusted_ca, Oldintermediate2Cert} but OTP should still fail the validation
|
||||||
|
%% since the client1 cert is not signed by Oldintermediate2Cert (trusted CA cert).
|
||||||
|
%% @end
|
||||||
|
t_fail_success_with_old_and_renewed_intermediate_cacert_bundle_and_client_using_all_CAcerts(Config) ->
|
||||||
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
|
DataDir = ?config(data_dir, Config),
|
||||||
|
Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "intermediate2_renewed_old-bundle.pem")}
|
||||||
|
, {certfile, filename:join(DataDir, "server2.pem")}
|
||||||
|
, {keyfile, filename:join(DataDir, "server2.key")}
|
||||||
|
, {partial_chain, two_cacerts_from_cacertfile}
|
||||||
|
| ?config(ssl_config, Config)
|
||||||
|
]}],
|
||||||
|
emqx_listeners:start_listener(ssl, Port, Options),
|
||||||
|
{ok, Socket} = ssl:connect({127, 0, 0, 1}, Port, [{keyfile, filename:join(DataDir, "client1.key")},
|
||||||
|
{certfile, filename:join(DataDir, "all-CAcerts-bundle.pem")}
|
||||||
|
], 1000),
|
||||||
|
fail_when_no_ssl_alert(Socket, unknown_ca),
|
||||||
|
ssl:close(Socket).
|
||||||
|
|
||||||
t_conn_fail_with_renewed_intermediate_cacert_other_client(Config) ->
|
t_conn_fail_with_renewed_intermediate_cacert_other_client(Config) ->
|
||||||
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
Port = emqx_test_tls_certs_helper:select_free_port(ssl),
|
||||||
DataDir = ?config(data_dir, Config),
|
DataDir = ?config(data_dir, Config),
|
||||||
|
|
|
@ -214,6 +214,8 @@ generate_tls_certs(Config) ->
|
||||||
gen_host_cert("client1", "intermediate1", DataDir),
|
gen_host_cert("client1", "intermediate1", DataDir),
|
||||||
gen_host_cert("server2", "intermediate2", DataDir),
|
gen_host_cert("server2", "intermediate2", DataDir),
|
||||||
gen_host_cert("client2", "intermediate2", DataDir),
|
gen_host_cert("client2", "intermediate2", DataDir),
|
||||||
|
|
||||||
|
%% Build bundles below
|
||||||
os:cmd(io_lib:format("cat ~p ~p ~p > ~p", [filename:join(DataDir, "client2.pem"),
|
os:cmd(io_lib:format("cat ~p ~p ~p > ~p", [filename:join(DataDir, "client2.pem"),
|
||||||
filename:join(DataDir, "intermediate2.pem"),
|
filename:join(DataDir, "intermediate2.pem"),
|
||||||
filename:join(DataDir, "root.pem"),
|
filename:join(DataDir, "root.pem"),
|
||||||
|
|
Loading…
Reference in New Issue