From 8503d3c6dd408b50ea4a91097a5881fe5ef68014 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 3 May 2023 14:34:03 +0200 Subject: [PATCH] test(tls-partial-chains): renewed intermediate_cacert --- changes/v4.4.18-en.md | 3 +- changes/v4.4.18-zh.md | 4 +- src/emqx_tls_lib.erl | 4 +- ...istener_tls_verify_partial_chain_SUITE.erl | 47 ++++++++++++- test/emqx_test_tls_certs_helper.erl | 70 +++++++++++-------- 5 files changed, 94 insertions(+), 34 deletions(-) diff --git a/changes/v4.4.18-en.md b/changes/v4.4.18-en.md index edfedd621..1b26c5522 100644 --- a/changes/v4.4.18-en.md +++ b/changes/v4.4.18-en.md @@ -8,8 +8,7 @@ Prior to the improvement, the `key` in `${key}` could only contain letters, numbers, and underscores. Now the `key` supports any UTF8 character after the improvement. - Adds a new feature to enable partial certificate chain validation for TLS listeners[#10553](https://github.com/emqx/emqx/pull/10553). - If TLS listener has `partial_chain` set to `cacert_from_cacertfile`, - the certificate in the `cacertfile` will be used as the `cacert` for chain path validation. If the `cacertfile` has a chain of certificates, the cert at the end of the file will be used as the `cacert` for path validation. + If partial_chain is set to 'true', the last certificate in cacertfile is treated as the terminal of the certificate trust-chain. That is, the TLS handshake does not require full trust-chain, and EMQX will not try to validate the chain all the way up to the root CA. ## Bug fixes diff --git a/changes/v4.4.18-zh.md b/changes/v4.4.18-zh.md index f78bd5246..714572012 100644 --- a/changes/v4.4.18-zh.md +++ b/changes/v4.4.18-zh.md @@ -8,8 +8,8 @@ 改进前,`${key}` 中的 `key` 只能包含字母、数字和下划线。改进后 `key` 支持任意的 UTF8 字符了。 - 增加了一个新的功能,为TLS监听器启用部分证书链验证[#10553](https://github.com/emqx/emqx/pull/10553)。 - 如果TLS监听器的 `partial_chain` 设置为 `cacert_from_cacertfile`, - `cacertfile` 中的证书将被用作链式路径验证的 `cacert` 。如果 `cacertfile` 文件有一连串的证书,文件末尾的证书将被用作路径验证的 `cacert`。 + 如果 partial_chain 设置为“true”,cacertfile 中的最后一个证书将被视为证书信任链的顶端证书。 也就是说,TLS 握手不需要完整的链,并且 EMQX 不会尝试一直验证链直到根 CA。 + ## 修复 diff --git a/src/emqx_tls_lib.erl b/src/emqx_tls_lib.erl index e7383b05c..eaa46d3f5 100644 --- a/src/emqx_tls_lib.erl +++ b/src/emqx_tls_lib.erl @@ -186,7 +186,9 @@ opt_partial_chain(SslOpts) -> case proplists:get_value(partial_chain, SslOpts, undefined) of undefined -> SslOpts; - cacert_from_cacertfile -> + false -> + SslOpts; + V when V =:= cacert_from_cacertfile orelse V == true -> replace(SslOpts, partial_chain, cacert_from_cacertfile(SslOpts)) end. diff --git a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl index e97f11cf8..615709a44 100644 --- a/test/emqx_listener_tls_verify_partial_chain_SUITE.erl +++ b/test/emqx_listener_tls_verify_partial_chain_SUITE.erl @@ -70,6 +70,51 @@ t_conn_success_with_intermediate_cacert_bundle(Config) -> fail_when_ssl_error(Socket), ssl:close(Socket). +t_conn_success_with_renewed_intermediate_cacert(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.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, "client1.key")}, + {certfile, filename:join(DataDir, "client1.pem")} + ], 1000), + fail_when_ssl_error(Socket), + ssl:close(Socket). + +t_conn_fail_with_renewed_intermediate_cacert_and_client_using_old_complete_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.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-complete-bundle.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + +t_conn_fail_with_renewed_intermediate_cacert_other_client(Config) -> + Port = emqx_test_tls_certs_helper:select_free_port(ssl), + DataDir = ?config(data_dir, Config), + Options = [{ssl_options, [ {cacertfile, filename:join(DataDir, "intermediate1_renewed.pem")} + , {certfile, filename:join(DataDir, "server1.pem")} + , {keyfile, filename:join(DataDir, "server1.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.pem")} + ], 1000), + fail_when_no_ssl_alert(Socket, unknown_ca), + ssl:close(Socket). + t_conn_fail_with_intermediate_cacert_bundle_but_incorrect_order(Config) -> Port = emqx_test_tls_certs_helper:select_free_port(ssl), DataDir = ?config(data_dir, Config), @@ -208,5 +253,5 @@ t_conn_success_with_server_intermediate_and_client_root_chain(Config) -> ssl_config_verify_partial_chain() -> [ {verify, verify_peer} , {fail_if_no_peer_cert, true} - , {partial_chain, cacert_from_cacertfile} + , {partial_chain, true} ]. diff --git a/test/emqx_test_tls_certs_helper.erl b/test/emqx_test_tls_certs_helper.erl index 82b343fdb..e51875272 100644 --- a/test/emqx_test_tls_certs_helper.erl +++ b/test/emqx_test_tls_certs_helper.erl @@ -73,7 +73,9 @@ gen_host_cert(H, CaName, Path, Opts) -> CN = str(H), HKey = filename(Path, "~s.key", [H]), HCSR = filename(Path, "~s.csr", [H]), + HCSR2 = filename(Path, "~s.csr", [H]), HPEM = filename(Path, "~s.pem", [H]), + HPEM2 = filename(Path, "~s_renewed.pem", [H]), HEXT = filename(Path, "~s.extfile", [H]), PasswordArg = case maps:get(password, Opts, undefined) of @@ -82,44 +84,56 @@ gen_host_cert(H, CaName, Path, Opts) -> Password -> io_lib:format(" -passout pass:'~s' ", [Password]) end, - CSR_Cmd = - lists:flatten( - io_lib:format( - "openssl req -new ~s -newkey ec:~s " - "-keyout ~s -out ~s " - "-addext \"subjectAltName=DNS:~s\" " - "-addext basicConstraints=CA:TRUE " - "-addext keyUsage=digitalSignature,keyAgreement,keyCertSign " - "-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"", - [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] - ) - ), + create_file( - HEXT, - "keyUsage=digitalSignature,keyAgreement,keyCertSign\n" - "basicConstraints=CA:TRUE \n" - "subjectAltName=DNS:~s\n", - [CN] + HEXT, + "keyUsage=digitalSignature,keyAgreement,keyCertSign\n" + "basicConstraints=CA:TRUE \n" + "subjectAltName=DNS:~s\n", + [CN] ), - CERT_Cmd = - lists:flatten( + + CSR_Cmd = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN), + CSR_Cmd2 = csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR2, CN), + + CERT_Cmd = cert_sign_cmd(HEXT, HCSR, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM), + %% 2nd cert for testing renewed cert. + CERT_Cmd2 = cert_sign_cmd(HEXT, HCSR, ca_cert_name(Path, CaName), ca_key_name(Path, CaName), HPEM2), + ct:pal(os:cmd(CSR_Cmd)), + ct:pal(os:cmd(CSR_Cmd2)), + ct:pal(os:cmd(CERT_Cmd)), + ct:pal(os:cmd(CERT_Cmd2)), + file:delete(HEXT). + +cert_sign_cmd(ExtFile, CSRFile, CACert, CAKey, OutputCert)-> + lists:flatten( io_lib:format( "openssl x509 -req " "-extfile ~s " "-in ~s -CA ~s -CAkey ~s -CAcreateserial " "-out ~s -days 500", [ - HEXT, - HCSR, - ca_cert_name(Path, CaName), - ca_key_name(Path, CaName), - HPEM + ExtFile, + CSRFile, + CACert, + CAKey, + OutputCert ] ) - ), - ct:pal(os:cmd(CSR_Cmd)), - ct:pal(os:cmd(CERT_Cmd)), - file:delete(HEXT). + ). + +csr_cmd(PasswordArg, ECKeyFile, HKey, HCSR, CN) -> + lists:flatten( + io_lib:format( + "openssl req -new ~s -newkey ec:~s " + "-keyout ~s -out ~s " + "-addext \"subjectAltName=DNS:~s\" " + "-addext basicConstraints=CA:TRUE " + "-addext keyUsage=digitalSignature,keyAgreement,keyCertSign " + "-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"", + [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN] + ) + ). filename(Path, F, A) -> filename:join(Path, str(io_lib:format(F, A))).