Explorar o código

Merge pull request #10077 from qzhuyan/dev/william/quic-cert-password

feat(quic): support TLS password protected keyfile
William Yang %!s(int64=2) %!d(string=hai) anos
pai
achega
65ef9c9086

+ 5 - 1
apps/emqx/src/emqx_listeners.erl

@@ -388,7 +388,11 @@ do_start_listener(quic, ListenerName, #{bind := Bind} = Opts) ->
                 ] ++
                     case maps:get(cacertfile, SSLOpts, undefined) of
                         undefined -> [];
-                        CaCertFile -> [{cacertfile, binary_to_list(CaCertFile)}]
+                        CaCertFile -> [{cacertfile, str(CaCertFile)}]
+                    end ++
+                    case maps:get(password, SSLOpts, undefined) of
+                        undefined -> [];
+                        Password -> [{password, str(Password)}]
                     end ++
                     optional_quic_listener_opts(Opts),
             ConnectionOpts = #{

+ 2 - 2
apps/emqx/src/emqx_schema.erl

@@ -3023,9 +3023,9 @@ is_quic_ssl_opts(Name) ->
         "cacertfile",
         "certfile",
         "keyfile",
-        "verify"
+        "verify",
+        "password"
         %% Followings are planned
-        %% , "password"
         %% , "hibernate_after"
         %% , "fail_if_no_peer_cert"
         %% , "handshake_timeout"

+ 106 - 0
apps/emqx/test/emqx_common_test_helpers.erl

@@ -85,6 +85,13 @@
     reset_proxy/2
 ]).
 
+%% TLS certs API
+-export([
+    gen_ca/2,
+    gen_host_cert/3,
+    gen_host_cert/4
+]).
+
 -define(CERTS_PATH(CertName), filename:join(["etc", "certs", CertName])).
 
 -define(MQTT_SSL_CLIENT_CERTS, [
@@ -562,6 +569,7 @@ ensure_quic_listener(Name, UdpPort, ExtraSettings) ->
         mountpoint => <<>>,
         zone => default
     },
+
     Conf2 = maps:merge(Conf, ExtraSettings),
     emqx_config:put([listeners, quic, Name], Conf2),
     case emqx_listeners:start_listener(emqx_listeners:listener_id(quic, Name)) of
@@ -1075,6 +1083,104 @@ latency_up_proxy(off, Name, ProxyHost, ProxyPort) ->
     ).
 
 %%-------------------------------------------------------------------------------
+%% TLS certs
+%%-------------------------------------------------------------------------------
+gen_ca(Path, Name) ->
+    %% Generate ca.pem and ca.key which will be used to generate certs
+    %% for hosts server and clients
+    ECKeyFile = filename(Path, "~s-ec.key", [Name]),
+    filelib:ensure_dir(ECKeyFile),
+    os:cmd("openssl ecparam -name secp256r1 > " ++ ECKeyFile),
+    Cmd = lists:flatten(
+        io_lib:format(
+            "openssl req -new -x509 -nodes "
+            "-newkey ec:~s "
+            "-keyout ~s -out ~s -days 3650 "
+            "-subj \"/C=SE/O=Internet Widgits Pty Ltd CA\"",
+            [
+                ECKeyFile,
+                ca_key_name(Path, Name),
+                ca_cert_name(Path, Name)
+            ]
+        )
+    ),
+    os:cmd(Cmd).
+
+ca_cert_name(Path, Name) ->
+    filename(Path, "~s.pem", [Name]).
+ca_key_name(Path, Name) ->
+    filename(Path, "~s.key", [Name]).
+
+gen_host_cert(H, CaName, Path) ->
+    gen_host_cert(H, CaName, Path, #{}).
+
+gen_host_cert(H, CaName, Path, Opts) ->
+    ECKeyFile = filename(Path, "~s-ec.key", [CaName]),
+    CN = str(H),
+    HKey = filename(Path, "~s.key", [H]),
+    HCSR = filename(Path, "~s.csr", [H]),
+    HPEM = filename(Path, "~s.pem", [H]),
+    HEXT = filename(Path, "~s.extfile", [H]),
+    PasswordArg =
+        case maps:get(password, Opts, undefined) of
+            undefined ->
+                " -nodes ";
+            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 keyUsage=digitalSignature,keyAgreement "
+                "-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"",
+                [PasswordArg, ECKeyFile, HKey, HCSR, CN, CN]
+            )
+        ),
+    create_file(
+        HEXT,
+        "keyUsage=digitalSignature,keyAgreement\n"
+        "subjectAltName=DNS:~s\n",
+        [CN]
+    ),
+    CERT_Cmd =
+        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
+                ]
+            )
+        ),
+    ct:pal(os:cmd(CSR_Cmd)),
+    ct:pal(os:cmd(CERT_Cmd)),
+    file:delete(HEXT).
+
+filename(Path, F, A) ->
+    filename:join(Path, str(io_lib:format(F, A))).
+
+str(Arg) ->
+    binary_to_list(iolist_to_binary(Arg)).
+
+create_file(Filename, Fmt, Args) ->
+    filelib:ensure_dir(Filename),
+    {ok, F} = file:open(Filename, [write]),
+    try
+        io:format(F, Fmt, Args)
+    after
+        file:close(F)
+    end,
+    ok.
+%%-------------------------------------------------------------------------------
 %% Testcase teardown utilities
 %%-------------------------------------------------------------------------------
 

+ 54 - 5
apps/emqx/test/emqx_listeners_SUITE.erl

@@ -26,6 +26,8 @@
 
 -define(CERTS_PATH(CertName), filename:join(["../../lib/emqx/etc/certs/", CertName])).
 
+-define(SERVER_KEY_PASSWORD, "sErve7r8Key$!").
+
 all() -> emqx_common_test_helpers:all(?MODULE).
 
 init_per_suite(Config) ->
@@ -33,6 +35,7 @@ init_per_suite(Config) ->
     application:ensure_all_started(esockd),
     application:ensure_all_started(quicer),
     application:ensure_all_started(cowboy),
+    generate_tls_certs(Config),
     lists:foreach(fun set_app_env/1, NewConfig),
     Config.
 
@@ -45,11 +48,6 @@ init_per_testcase(Case, Config) when
 ->
     catch emqx_config_handler:stop(),
     {ok, _} = emqx_config_handler:start_link(),
-    case emqx_config:get([listeners], undefined) of
-        undefined -> ok;
-        Listeners -> emqx_config:put([listeners], maps:remove(quic, Listeners))
-    end,
-
     PrevListeners = emqx_config:get([listeners], #{}),
     PureListeners = remove_default_limiter(PrevListeners),
     PureListeners2 = PureListeners#{
@@ -185,6 +183,50 @@ t_wss_conn(_) ->
     {ok, Socket} = ssl:connect({127, 0, 0, 1}, 9998, [{verify, verify_none}], 1000),
     ok = ssl:close(Socket).
 
+t_quic_conn(Config) ->
+    Port = 24568,
+    DataDir = ?config(data_dir, Config),
+    SSLOpts = #{
+        password => ?SERVER_KEY_PASSWORD,
+        certfile => filename:join(DataDir, "server-password.pem"),
+        cacertfile => filename:join(DataDir, "ca.pem"),
+        keyfile => filename:join(DataDir, "server-password.key")
+    },
+    emqx_common_test_helpers:ensure_quic_listener(?FUNCTION_NAME, Port, #{ssl_options => SSLOpts}),
+    ct:pal("~p", [emqx_listeners:list()]),
+    {ok, Conn} = quicer:connect(
+        {127, 0, 0, 1},
+        Port,
+        [
+            {verify, verify_none},
+            {alpn, ["mqtt"]}
+        ],
+        1000
+    ),
+    ok = quicer:close_connection(Conn),
+    emqx_listeners:stop_listener(quic, ?FUNCTION_NAME, #{bind => Port}).
+
+t_ssl_password_cert(Config) ->
+    Port = 24568,
+    DataDir = ?config(data_dir, Config),
+    SSLOptsPWD = #{
+        password => ?SERVER_KEY_PASSWORD,
+        certfile => filename:join(DataDir, "server-password.pem"),
+        cacertfile => filename:join(DataDir, "ca.pem"),
+        keyfile => filename:join(DataDir, "server-password.key")
+    },
+    LConf = #{
+        enabled => true,
+        bind => {{127, 0, 0, 1}, Port},
+        mountpoint => <<>>,
+        zone => default,
+        ssl_options => SSLOptsPWD
+    },
+    ok = emqx_listeners:start_listener(ssl, ?FUNCTION_NAME, LConf),
+    {ok, SSLSocket} = ssl:connect("127.0.0.1", Port, [{verify, verify_none}]),
+    ssl:close(SSLSocket),
+    emqx_listeners:stop_listener(ssl, ?FUNCTION_NAME, LConf).
+
 t_format_bind(_) ->
     ?assertEqual(
         ":1883",
@@ -269,3 +311,10 @@ remove_default_limiter(Listeners) ->
         end,
         Listeners
     ).
+
+generate_tls_certs(Config) ->
+    DataDir = ?config(data_dir, Config),
+    emqx_common_test_helpers:gen_ca(DataDir, "ca"),
+    emqx_common_test_helpers:gen_host_cert("server-password", "ca", DataDir, #{
+        password => ?SERVER_KEY_PASSWORD
+    }).

+ 7 - 6
apps/emqx/test/emqx_quic_multistreams_SUITE.erl

@@ -1569,7 +1569,7 @@ t_multi_streams_remote_shutdown(Config) ->
 
     ok = stop_emqx(),
     %% Client should be closed
-    assert_client_die(C).
+    assert_client_die(C, 100, 50).
 
 t_multi_streams_remote_shutdown_with_reconnect(Config) ->
     erlang:process_flag(trap_exit, true),
@@ -2047,14 +2047,15 @@ via_stream({quic, _Conn, Stream}) ->
 assert_client_die(C) ->
     assert_client_die(C, 100, 10).
 assert_client_die(C, _, 0) ->
-    ct:fail("Client ~p did not die", [C]);
+    ct:fail("Client ~p did not die: stacktrace: ~p", [C, process_info(C, current_stacktrace)]);
 assert_client_die(C, Delay, Retries) ->
-    case catch emqtt:info(C) of
-        {'EXIT', {noproc, {gen_statem, call, [_, info, infinity]}}} ->
-            ok;
-        _Other ->
+    try emqtt:info(C) of
+        Info when is_list(Info) ->
             timer:sleep(Delay),
             assert_client_die(C, Delay, Retries - 1)
+    catch
+        exit:Error ->
+            ct:comment("client die with ~p", [Error])
     end.
 
 %% BUILD_WITHOUT_QUIC

+ 2 - 0
changes/ce/feat-10077.en.md

@@ -0,0 +1,2 @@
+Add support for QUIC TLS password protected certificate file.
+

+ 1 - 0
changes/ce/feat-10077.zh.md

@@ -0,0 +1 @@
+增加对 QUIC TLS 密码保护证书文件的支持。