diff --git a/CHANGES-5.0.md b/CHANGES-5.0.md index 4856b09f1..66529953a 100644 --- a/CHANGES-5.0.md +++ b/CHANGES-5.0.md @@ -6,6 +6,10 @@ * Fix `$queue` topic name error in management API return. [#8728](https://github.com/emqx/emqx/pull/8728) * Fix sometimes `client.connected` and `client.disconnected` could be in wrong order. [#8625](https://github.com/emqx/emqx/pull/8625) +## Enhancements + +* Do not auto-populate default SSL cipher suites, so that the configs are less bloated. [#8769](https://github.com/emqx/emqx/pull/8769) + # 5.0.5 ## Bug fixes diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 326661e75..aa2a2e0f9 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -583,11 +583,7 @@ enable_authn(Opts) -> maps:get(enable_authn, Opts, true). ssl_opts(Opts) -> - maps:to_list( - emqx_tls_lib:drop_tls13_for_old_otp( - maps:get(ssl_options, Opts, #{}) - ) - ). + emqx_tls_lib:to_server_opts(tls, maps:get(ssl_options, Opts, #{})). tcp_opts(Opts) -> maps:to_list( diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 574305a4f..75818ab2b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -102,7 +102,7 @@ -export([namespace/0, roots/0, roots/1, fields/1, desc/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). --export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1, default_ciphers/1]). +-export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1]). -export([sc/2, map/2]). -elvis([{elvis_style, god_modules, disable}]). @@ -1843,6 +1843,8 @@ filter(Opts) -> common_ssl_opts_schema(Defaults) -> D = fun(Field) -> maps:get(to_atom(Field), Defaults, undefined) end, Df = fun(Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end, + Collection = maps:get(versions, Defaults, tls_all_available), + AvailableVersions = default_tls_vsns(Collection), [ {"cacertfile", sc( @@ -1909,9 +1911,9 @@ common_ssl_opts_schema(Defaults) -> sc( hoconsc:array(typerefl:atom()), #{ - default => default_tls_vsns(maps:get(versions, Defaults, tls_all_available)), + default => AvailableVersions, desc => ?DESC(common_ssl_opts_schema_versions), - validator => fun validate_tls_versions/1 + validator => fun(Inputs) -> validate_tls_versions(AvailableVersions, Inputs) end } )}, {"ciphers", ciphers_schema(D("ciphers"))}, @@ -2022,9 +2024,9 @@ client_ssl_opts_schema(Defaults) -> ]. default_tls_vsns(dtls_all_available) -> - proplists:get_value(available_dtls, ssl:versions()); + emqx_tls_lib:available_versions(dtls); default_tls_vsns(tls_all_available) -> - emqx_tls_lib:default_versions(). + emqx_tls_lib:available_versions(tls). -spec ciphers_schema(quic | dtls_all_available | tls_all_available | undefined) -> hocon_schema:field_schema(). @@ -2039,6 +2041,10 @@ ciphers_schema(Default) -> #{ default => default_ciphers(Default), converter => fun + (<<>>) -> + []; + ("") -> + []; (Ciphers) when is_binary(Ciphers) -> binary:split(Ciphers, <<",">>, [global]); (Ciphers) when is_list(Ciphers) -> @@ -2060,19 +2066,15 @@ default_ciphers(Which) -> do_default_ciphers(Which) ). -do_default_ciphers(undefined) -> - do_default_ciphers(tls_all_available); do_default_ciphers(quic) -> [ "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", "TLS_CHACHA20_POLY1305_SHA256" ]; -do_default_ciphers(dtls_all_available) -> - %% as of now, dtls does not support tlsv1.3 ciphers - emqx_tls_lib:selected_ciphers(['dtlsv1.2', 'dtlsv1']); -do_default_ciphers(tls_all_available) -> - emqx_tls_lib:default_ciphers(). +do_default_ciphers(_) -> + %% otherwise resolve default ciphers list at runtime + []. %% @private return a list of keys in a parent field -spec keys(string(), hocon:config()) -> [string()]. @@ -2246,19 +2248,16 @@ parse_user_lookup_fun(StrConf) -> {fun Mod:Fun/3, undefined}. validate_ciphers(Ciphers) -> - All = emqx_tls_lib:all_ciphers(), - case lists:filter(fun(Cipher) -> not lists:member(Cipher, All) end, Ciphers) of + Set = emqx_tls_lib:all_ciphers_set_cached(), + case lists:filter(fun(Cipher) -> not sets:is_element(Cipher, Set) end, Ciphers) of [] -> ok; Bad -> {error, {bad_ciphers, Bad}} end. -validate_tls_versions(Versions) -> - AvailableVersions = - proplists:get_value(available, ssl:versions()) ++ - proplists:get_value(available_dtls, ssl:versions()), +validate_tls_versions(AvailableVersions, Versions) -> case lists:filter(fun(V) -> not lists:member(V, AvailableVersions) end, Versions) of [] -> ok; - Vs -> {error, {unsupported_ssl_versions, Vs}} + Vs -> {error, {unsupported_tls_versions, Vs}} end. validations() -> diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index b08270df9..28bda5cfa 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -18,13 +18,12 @@ %% version & cipher suites -export([ - default_versions/0, - integral_versions/1, + available_versions/1, + integral_versions/2, default_ciphers/0, selected_ciphers/1, integral_ciphers/2, - drop_tls13_for_old_otp/1, - all_ciphers/0 + all_ciphers_set_cached/0 ]). %% SSL files @@ -38,7 +37,9 @@ ]). -export([ - to_client_opts/1 + to_server_opts/2, + to_client_opts/1, + to_client_opts/2 ]). -include("logger.hrl"). @@ -54,27 +55,80 @@ %% non-empty list of strings -define(IS_STRING_LIST(L), (is_list(L) andalso L =/= [] andalso ?IS_STRING(hd(L)))). -%% @doc Returns the default supported tls versions. --spec default_versions() -> [atom()]. -default_versions() -> available_versions(). +%% The ciphers that ssl:cipher_suites(exclusive, 'tlsv1.3', openssl) +%% should return when running on otp 23. +%% But we still have to hard-code them because tlsv1.3 on otp 22 is +%% not trustworthy. +-define(TLSV13_EXCLUSIVE_CIPHERS, [ + "TLS_AES_256_GCM_SHA384", + "TLS_AES_128_GCM_SHA256", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_CCM_SHA256", + "TLS_AES_128_CCM_8_SHA256" +]). + +-define(SELECTED_CIPHERS, [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA384", + "ECDH-ECDSA-AES256-GCM-SHA384", + "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384", + "ECDH-RSA-AES256-SHA384", + "DHE-DSS-AES256-GCM-SHA384", + "DHE-DSS-AES256-SHA256", + "AES256-GCM-SHA384", + "AES256-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-SHA256", + "ECDHE-RSA-AES128-SHA256", + "ECDH-ECDSA-AES128-GCM-SHA256", + "ECDH-RSA-AES128-GCM-SHA256", + "ECDH-ECDSA-AES128-SHA256", + "ECDH-RSA-AES128-SHA256", + "DHE-DSS-AES128-GCM-SHA256", + "DHE-DSS-AES128-SHA256", + "AES128-GCM-SHA256", + "AES128-SHA256", + "ECDHE-ECDSA-AES256-SHA", + "ECDHE-RSA-AES256-SHA", + "DHE-DSS-AES256-SHA", + "ECDH-ECDSA-AES256-SHA", + "ECDH-RSA-AES256-SHA", + "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", + "DHE-DSS-AES128-SHA", + "ECDH-ECDSA-AES128-SHA", + "ECDH-RSA-AES128-SHA", + + %% psk + "RSA-PSK-AES256-GCM-SHA384", + "RSA-PSK-AES256-CBC-SHA384", + "RSA-PSK-AES128-GCM-SHA256", + "RSA-PSK-AES128-CBC-SHA256", + "RSA-PSK-AES256-CBC-SHA", + "RSA-PSK-AES128-CBC-SHA" +]). %% @doc Validate a given list of desired tls versions. %% raise an error exception if non of them are available. %% The input list can be a string/binary of comma separated versions. --spec integral_versions(undefined | string() | binary() | [ssl:tls_version()]) -> +-spec integral_versions(tls | dtls, undefined | string() | binary() | [ssl:tls_version()]) -> [ssl:tls_version()]. -integral_versions(undefined) -> - integral_versions(default_versions()); -integral_versions([]) -> - integral_versions(default_versions()); -integral_versions(<<>>) -> - integral_versions(default_versions()); -integral_versions(Desired) when ?IS_STRING(Desired) -> - integral_versions(iolist_to_binary(Desired)); -integral_versions(Desired) when is_binary(Desired) -> - integral_versions(parse_versions(Desired)); -integral_versions(Desired) -> - Available = available_versions(), +integral_versions(Type, undefined) -> + available_versions(Type); +integral_versions(Type, []) -> + available_versions(Type); +integral_versions(Type, <<>>) -> + available_versions(Type); +integral_versions(Type, Desired) when ?IS_STRING(Desired) -> + integral_versions(Type, iolist_to_binary(Desired)); +integral_versions(Type, Desired) when is_binary(Desired) -> + integral_versions(Type, parse_versions(Desired)); +integral_versions(Type, Desired) -> + Available = available_versions(Type), case lists:filter(fun(V) -> lists:member(V, Available) end, Desired) of [] -> erlang:error(#{ @@ -86,33 +140,36 @@ integral_versions(Desired) -> Filtered end. -%% @doc Return a list of all supported ciphers. -all_ciphers() -> all_ciphers(default_versions()). +%% @doc Return a set of all ciphers +all_ciphers_set_cached() -> + case persistent_term:get(?FUNCTION_NAME, false) of + false -> + S = sets:from_list(all_ciphers()), + persistent_term:put(?FUNCTION_NAME, S); + Set -> + Set + end. -%% @doc Return a list of (openssl string format) cipher suites. +%% @hidden Return a list of all supported ciphers. +all_ciphers() -> + all_ciphers(available_versions(all)). + +%% @hidden Return a list of (openssl string format) cipher suites. -spec all_ciphers([ssl:tls_version()]) -> [string()]. all_ciphers(['tlsv1.3']) -> %% When it's only tlsv1.3 wanted, use 'exclusive' here %% because 'all' returns legacy cipher suites too, %% which does not make sense since tlsv1.3 can not use %% legacy cipher suites. - ssl:cipher_suites(exclusive, 'tlsv1.3', openssl); + ?TLSV13_EXCLUSIVE_CIPHERS; all_ciphers(Versions) -> %% assert non-empty List = lists:append([ssl:cipher_suites(all, V, openssl) || V <- Versions]), [_ | _] = dedup(List). %% @doc All Pre-selected TLS ciphers. -%% ssl:cipher_suites(all, V, openssl) is too slow. so we cache default ciphers. default_ciphers() -> - case persistent_term:get(default_ciphers, undefined) of - undefined -> - Default = selected_ciphers(available_versions()), - persistent_term:put(default_ciphers, Default), - Default; - Default -> - Default - end. + selected_ciphers(available_versions(all)). %% @doc Pre-selected TLS ciphers for given versions.. selected_ciphers(Vsns) -> @@ -126,54 +183,11 @@ selected_ciphers(Vsns) -> do_selected_ciphers('tlsv1.3') -> case lists:member('tlsv1.3', proplists:get_value(available, ssl:versions())) of - true -> ssl:cipher_suites(exclusive, 'tlsv1.3', openssl); + true -> ?TLSV13_EXCLUSIVE_CIPHERS; false -> [] end ++ do_selected_ciphers('tlsv1.2'); do_selected_ciphers(_) -> - [ - "ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES256-SHA384", - "ECDHE-RSA-AES256-SHA384", - "ECDH-ECDSA-AES256-GCM-SHA384", - "ECDH-RSA-AES256-GCM-SHA384", - "ECDH-ECDSA-AES256-SHA384", - "ECDH-RSA-AES256-SHA384", - "DHE-DSS-AES256-GCM-SHA384", - "DHE-DSS-AES256-SHA256", - "AES256-GCM-SHA384", - "AES256-SHA256", - "ECDHE-ECDSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-ECDSA-AES128-SHA256", - "ECDHE-RSA-AES128-SHA256", - "ECDH-ECDSA-AES128-GCM-SHA256", - "ECDH-RSA-AES128-GCM-SHA256", - "ECDH-ECDSA-AES128-SHA256", - "ECDH-RSA-AES128-SHA256", - "DHE-DSS-AES128-GCM-SHA256", - "DHE-DSS-AES128-SHA256", - "AES128-GCM-SHA256", - "AES128-SHA256", - "ECDHE-ECDSA-AES256-SHA", - "ECDHE-RSA-AES256-SHA", - "DHE-DSS-AES256-SHA", - "ECDH-ECDSA-AES256-SHA", - "ECDH-RSA-AES256-SHA", - "ECDHE-ECDSA-AES128-SHA", - "ECDHE-RSA-AES128-SHA", - "DHE-DSS-AES128-SHA", - "ECDH-ECDSA-AES128-SHA", - "ECDH-RSA-AES128-SHA", - - %% psk - "RSA-PSK-AES256-GCM-SHA384", - "RSA-PSK-AES256-CBC-SHA384", - "RSA-PSK-AES128-GCM-SHA256", - "RSA-PSK-AES128-CBC-SHA256", - "RSA-PSK-AES256-CBC-SHA", - "RSA-PSK-AES128-CBC-SHA" - ]. + ?SELECTED_CIPHERS. %% @doc Ensure version & cipher-suites integrity. -spec integral_ciphers([ssl:tls_version()], binary() | string() | [string()]) -> [string()]. @@ -201,17 +215,17 @@ ensure_tls13_cipher(true, Ciphers) -> ensure_tls13_cipher(false, Ciphers) -> Ciphers. -%% default ssl versions based on available versions. --spec available_versions() -> [atom()]. -available_versions() -> - OtpRelease = list_to_integer(erlang:system_info(otp_release)), - default_versions(OtpRelease). +%% @doc Returns the default available tls/dtls versions. +available_versions(Type) -> + All = ssl:versions(), + available_versions(Type, All). -%% tlsv1.3 is available from OTP-22 but we do not want to use until 23. -default_versions(OtpRelease) when OtpRelease >= 23 -> - proplists:get_value(available, ssl:versions()); -default_versions(_) -> - lists:delete('tlsv1.3', proplists:get_value(available, ssl:versions())). +available_versions(tls, All) -> + proplists:get_value(available, All); +available_versions(dtls, All) -> + proplists:get_value(available_dtls, All); +available_versions(all, All) -> + available_versions(tls, All) ++ available_versions(dtls, All). %% Deduplicate a list without re-ordering the elements. dedup([]) -> @@ -244,6 +258,8 @@ do_parse_versions([V | More], Acc) -> do_parse_versions(More, [Parsed | Acc]) end. +parse_version(<<"dtlsv1.2">>) -> 'dtlsv1.2'; +parse_version(<<"dtlsv1">>) -> dtlsv1; parse_version(<<"tlsv", Vsn/binary>>) -> parse_version(Vsn); parse_version(<<"v", Vsn/binary>>) -> parse_version(Vsn); parse_version(<<"1.3">>) -> 'tlsv1.3'; @@ -259,36 +275,6 @@ split_by_comma(Bin) -> trim_space(Bin) -> hd([I || I <- binary:split(Bin, <<" ">>), I =/= <<>>]). -%% @doc Drop tlsv1.3 version and ciphers from ssl options -%% if running on otp 22 or earlier. -drop_tls13_for_old_otp(SslOpts) -> - case list_to_integer(erlang:system_info(otp_release)) < 23 of - true -> drop_tls13(SslOpts); - false -> SslOpts - end. - -%% The ciphers that ssl:cipher_suites(exclusive, 'tlsv1.3', openssl) -%% should return when running on otp 23. -%% But we still have to hard-code them because tlsv1.3 on otp 22 is -%% not trustworthy. --define(TLSV13_EXCLUSIVE_CIPHERS, [ - "TLS_AES_256_GCM_SHA384", - "TLS_AES_128_GCM_SHA256", - "TLS_CHACHA20_POLY1305_SHA256", - "TLS_AES_128_CCM_SHA256", - "TLS_AES_128_CCM_8_SHA256" -]). -drop_tls13(SslOpts0) -> - SslOpts1 = - case maps:find(versions, SslOpts0) of - error -> SslOpts0; - {ok, Vsns} -> SslOpts0#{versions => (Vsns -- ['tlsv1.3'])} - end, - case maps:find(ciphers, SslOpts1) of - error -> SslOpts1; - {ok, Ciphers} -> SslOpts1#{ciphers => Ciphers -- ?TLSV13_EXCLUSIVE_CIPHERS} - end. - %% @doc The input map is a HOCON decoded result of a struct defined as %% emqx_schema:server_ssl_opts_schema. (NOTE: before schema-checked). %% `keyfile', `certfile' and `cacertfile' can be either pem format key or certificates, @@ -498,27 +484,54 @@ do_drop_invalid_certs([Key | Keys], SSL) -> end end. -%% @doc Convert hocon-checked ssl client options (map()) to +%% @doc Convert hocon-checked ssl server options (map()) to %% proplist accepted by ssl library. +-spec to_server_opts(tls | dtls, map()) -> [{atom(), term()}]. +to_server_opts(Type, Opts) -> + Versions = integral_versions(Type, maps:get(versions, Opts, undefined)), + Ciphers = integral_ciphers(Versions, maps:get(ciphers, Opts, undefined)), + maps:to_list(Opts#{ + ciphers => Ciphers, + versions => Versions + }). + +%% @doc Convert hocon-checked tls client options (map()) to +%% proplist accepted by ssl library. +-spec to_client_opts(map()) -> [{atom(), term()}]. to_client_opts(Opts) -> + to_client_opts(tls, Opts). + +%% @doc Convert hocon-checked tls or dtls client options (map()) to +%% proplist accepted by ssl library. +-spec to_client_opts(tls | dtls, map()) -> [{atom(), term()}]. +to_client_opts(Type, Opts) -> GetD = fun(Key, Default) -> fuzzy_map_get(Key, Opts, Default) end, Get = fun(Key) -> GetD(Key, undefined) end, - KeyFile = ensure_str(Get(keyfile)), - CertFile = ensure_str(Get(certfile)), - CAFile = ensure_str(Get(cacertfile)), - Verify = GetD(verify, verify_none), - SNI = ensure_sni(Get(server_name_indication)), - Versions = integral_versions(Get(versions)), - Ciphers = integral_ciphers(Versions, Get(ciphers)), - filter([ - {keyfile, KeyFile}, - {certfile, CertFile}, - {cacertfile, CAFile}, - {verify, Verify}, - {server_name_indication, SNI}, - {versions, Versions}, - {ciphers, Ciphers} - ]). + case GetD(enable, false) of + true -> + KeyFile = ensure_str(Get(keyfile)), + CertFile = ensure_str(Get(certfile)), + CAFile = ensure_str(Get(cacertfile)), + Verify = GetD(verify, verify_none), + SNI = ensure_sni(Get(server_name_indication)), + Versions = integral_versions(Type, Get(versions)), + Ciphers = integral_ciphers(Versions, Get(ciphers)), + filter([ + {keyfile, KeyFile}, + {certfile, CertFile}, + {cacertfile, CAFile}, + {verify, Verify}, + {server_name_indication, SNI}, + {versions, Versions}, + {ciphers, Ciphers}, + {reuse_sessions, Get(reuse_sessions)}, + {depth, Get(depth)}, + {password, ensure_str(Get(password))}, + {secure_renegotiate, Get(secure_renegotiate)} + ]); + false -> + [] + end. filter([]) -> []; filter([{_, undefined} | T]) -> filter(T); @@ -556,28 +569,3 @@ ensure_ssl_file_key(SSL, RequiredKeys) -> [] -> ok; Miss -> {error, #{reason => ssl_file_option_not_found, which_options => Miss}} end. - --if(?OTP_RELEASE > 22). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). - -drop_tls13_test() -> - Versions = default_versions(), - ?assert(lists:member('tlsv1.3', Versions)), - Ciphers = all_ciphers(), - ?assert(has_tlsv13_cipher(Ciphers)), - Opts0 = #{versions => Versions, ciphers => Ciphers, other => true}, - Opts = drop_tls13(Opts0), - ?assertNot(lists:member('tlsv1.3', maps:get(versions, Opts, undefined))), - ?assertNot(has_tlsv13_cipher(maps:get(ciphers, Opts, undefined))). - -drop_tls13_no_versions_cipers_test() -> - Opts0 = #{other => 0, bool => true}, - Opts = drop_tls13(Opts0), - ?_assertEqual(Opts0, Opts). - -has_tlsv13_cipher(Ciphers) -> - lists:any(fun(C) -> lists:member(C, Ciphers) end, ?TLSV13_EXCLUSIVE_CIPHERS). - --endif. --endif. diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl index a40026d4c..9118ac226 100644 --- a/apps/emqx/test/emqx_schema_tests.erl +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -21,8 +21,7 @@ ssl_opts_dtls_test() -> Sc = emqx_schema:server_ssl_opts_schema( #{ - versions => dtls_all_available, - ciphers => dtls_all_available + versions => dtls_all_available }, false ), @@ -30,7 +29,7 @@ ssl_opts_dtls_test() -> ?assertMatch( #{ versions := ['dtlsv1.2', 'dtlsv1'], - ciphers := ["ECDHE-ECDSA-AES256-GCM-SHA384" | _] + ciphers := [] }, Checked ). @@ -42,7 +41,7 @@ ssl_opts_tls_1_3_test() -> ?assertMatch( #{ versions := ['tlsv1.3'], - ciphers := [_ | _] + ciphers := [] }, Checked ). @@ -53,7 +52,7 @@ ssl_opts_tls_for_ranch_test() -> ?assertMatch( #{ versions := ['tlsv1.3'], - ciphers := [_ | _], + ciphers := [], handshake_timeout := _ }, Checked @@ -125,7 +124,7 @@ validate(Schema, Data0) -> ), Checked. -ciperhs_schema_test() -> +ciphers_schema_test() -> Sc = emqx_schema:ciphers_schema(undefined), WSc = #{roots => [{ciphers, Sc}]}, ?assertThrow( @@ -135,7 +134,7 @@ ciperhs_schema_test() -> bad_tls_version_test() -> Sc = emqx_schema:server_ssl_opts_schema(#{}, false), - Reason = {unsupported_ssl_versions, [foo]}, + Reason = {unsupported_tls_versions, [foo]}, ?assertThrow( {_Sc, [#{kind := validation_error, reason := Reason}]}, validate(Sc, #{<<"versions">> => [<<"foo">>]}) diff --git a/apps/emqx/test/emqx_tls_lib_tests.erl b/apps/emqx/test/emqx_tls_lib_tests.erl index 22c480637..647e7c3e2 100644 --- a/apps/emqx/test/emqx_tls_lib_tests.erl +++ b/apps/emqx/test/emqx_tls_lib_tests.erl @@ -51,24 +51,34 @@ test_cipher_format(Input) -> ?assertEqual([?TLS_13_CIPHER, ?TLS_12_CIPHER], Ciphers). tls_versions_test() -> - ?assert(lists:member('tlsv1.3', emqx_tls_lib:default_versions())). + ?assert(lists:member('tlsv1.3', emqx_tls_lib:available_versions(tls))). -tls_version_unknown_test() -> - ?assertEqual( - emqx_tls_lib:default_versions(), - emqx_tls_lib:integral_versions([]) - ), - ?assertEqual( - emqx_tls_lib:default_versions(), - emqx_tls_lib:integral_versions(<<>>) - ), - ?assertEqual( - emqx_tls_lib:default_versions(), - emqx_tls_lib:integral_versions("foo") - ), - ?assertError( - #{reason := no_available_tls_version}, - emqx_tls_lib:integral_versions([foo]) +tls_version_unknown_test_() -> + lists:flatmap( + fun(Type) -> + [ + ?_assertEqual( + emqx_tls_lib:available_versions(Type), + emqx_tls_lib:integral_versions(Type, []) + ), + ?_assertEqual( + emqx_tls_lib:available_versions(Type), + emqx_tls_lib:integral_versions(Type, <<>>) + ), + ?_assertEqual( + emqx_tls_lib:available_versions(Type), + %% unknown version dropped + emqx_tls_lib:integral_versions(Type, "foo") + ), + fun() -> + ?assertError( + #{reason := no_available_tls_version}, + emqx_tls_lib:integral_versions(Type, [foo]) + ) + end + ] + end, + [tls, dtls] ). cipher_suites_no_duplication_test() -> diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index dfe937024..8850fa462 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -365,8 +365,7 @@ fields(ssl_server_opts) -> #{ depth => 10, reuse_sessions => true, - versions => tls_all_available, - ciphers => tls_all_available + versions => tls_all_available }, true ); @@ -502,8 +501,7 @@ fields(dtls_opts) -> #{ depth => 10, reuse_sessions => true, - versions => dtls_all_available, - ciphers => dtls_all_available + versions => dtls_all_available }, false ). diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 15359dea6..3a6de2031 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -455,14 +455,12 @@ esockd_access_rules(StrRules) -> [Access(R) || R <- StrRules]. ssl_opts(Name, Opts) -> - maps:to_list( - emqx_tls_lib:drop_tls13_for_old_otp( - maps:without( - [enable], - maps:get(Name, Opts, #{}) - ) - ) - ). + Type = + case Name of + ssl -> tls; + dtls -> dtls + end, + emqx_tls_lib:to_server_opts(Type, maps:get(Name, Opts, #{})). sock_opts(Name, Opts) -> maps:to_list( diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index aac779c8e..66f780d3f 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -483,8 +483,8 @@ ssl_opts() -> maps:merge( Certs, #{ - versions => emqx_tls_lib:default_versions(), - ciphers => emqx_tls_lib:default_ciphers(), + versions => emqx_tls_lib:available_versions(tls), + ciphers => [], verify => verify_peer, fail_if_no_peer_cert => true, secure_renegotiate => false, diff --git a/rel/emqx_conf.template.en.md b/rel/emqx_conf.template.en.md index 48fa01b61..76d25680b 100644 --- a/rel/emqx_conf.template.en.md +++ b/rel/emqx_conf.template.en.md @@ -233,3 +233,76 @@ authentication=[{enable=true, backend="built_in_database", mechanism="password_b authentication=[{enable=true}] ``` ::: + +#### TLS/SSL ciphers + +Starting from v5.0.6, EMQX no longer pre-populate the ciphers list with a default +set of cipher suite names. +Instead, the default ciphers are applyed at runtime when starting the listener +for servers, or when establishing a TLS connection as a client. + +Below are the default ciphers selected by EMQX. + +For tlsv1.3: +``` +ciphers = + [ "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", + "TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_128_CCM_SHA256", + "TLS_AES_128_CCM_8_SHA256" + ] +``` + +For tlsv1.2 or earlier + +``` +ciphers = + [ "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA384", + "ECDH-ECDSA-AES256-GCM-SHA384", + "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384", + "ECDH-RSA-AES256-SHA384", + "DHE-DSS-AES256-GCM-SHA384", + "DHE-DSS-AES256-SHA256", + "AES256-GCM-SHA384", + "AES256-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-SHA256", + "ECDHE-RSA-AES128-SHA256", + "ECDH-ECDSA-AES128-GCM-SHA256", + "ECDH-RSA-AES128-GCM-SHA256", + "ECDH-ECDSA-AES128-SHA256", + "ECDH-RSA-AES128-SHA256", + "DHE-DSS-AES128-GCM-SHA256", + "DHE-DSS-AES128-SHA256", + "AES128-GCM-SHA256", + "AES128-SHA256", + "ECDHE-ECDSA-AES256-SHA", + "ECDHE-RSA-AES256-SHA", + "DHE-DSS-AES256-SHA", + "ECDH-ECDSA-AES256-SHA", + "ECDH-RSA-AES256-SHA", + "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", + "DHE-DSS-AES128-SHA", + "ECDH-ECDSA-AES128-SHA", + "ECDH-RSA-AES128-SHA" + ] +``` + +For PSK enabled listeners + +``` +ciphers = + [ "RSA-PSK-AES256-GCM-SHA384", + "RSA-PSK-AES256-CBC-SHA384", + "RSA-PSK-AES128-GCM-SHA256", + "RSA-PSK-AES128-CBC-SHA256", + "RSA-PSK-AES256-CBC-SHA", + "RSA-PSK-AES128-CBC-SHA" + ] +``` + diff --git a/rel/emqx_conf.template.zh.md b/rel/emqx_conf.template.zh.md index b4a56b28f..ac4c5ce39 100644 --- a/rel/emqx_conf.template.zh.md +++ b/rel/emqx_conf.template.zh.md @@ -216,3 +216,73 @@ authentication=[{enable=true, backend="built_in_database", mechanism="password_b authentication=[{enable=true}] ``` ::: + +#### TLS/SSL ciphers + +从 v5.0.6 开始 EMQX 不在配置文件中详细列出所有默认的密码套件名称。 +而是在配置文件中使用一个空列表,然后在运行时替换成默认的密码套件。 + +下面这些密码套件是 EMQX 默认支持的: + +tlsv1.3: +``` +ciphers = + [ "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", + "TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_128_CCM_SHA256", + "TLS_AES_128_CCM_8_SHA256" + ] +``` + +tlsv1.2 或更早 + +``` +ciphers = + [ "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA384", + "ECDH-ECDSA-AES256-GCM-SHA384", + "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384", + "ECDH-RSA-AES256-SHA384", + "DHE-DSS-AES256-GCM-SHA384", + "DHE-DSS-AES256-SHA256", + "AES256-GCM-SHA384", + "AES256-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-SHA256", + "ECDHE-RSA-AES128-SHA256", + "ECDH-ECDSA-AES128-GCM-SHA256", + "ECDH-RSA-AES128-GCM-SHA256", + "ECDH-ECDSA-AES128-SHA256", + "ECDH-RSA-AES128-SHA256", + "DHE-DSS-AES128-GCM-SHA256", + "DHE-DSS-AES128-SHA256", + "AES128-GCM-SHA256", + "AES128-SHA256", + "ECDHE-ECDSA-AES256-SHA", + "ECDHE-RSA-AES256-SHA", + "DHE-DSS-AES256-SHA", + "ECDH-ECDSA-AES256-SHA", + "ECDH-RSA-AES256-SHA", + "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", + "DHE-DSS-AES128-SHA", + "ECDH-ECDSA-AES128-SHA", + "ECDH-RSA-AES128-SHA" + ] +``` + +配置 PSK 认证的监听器 + +``` +ciphers = [ + [ "RSA-PSK-AES256-GCM-SHA384", + "RSA-PSK-AES256-CBC-SHA384", + "RSA-PSK-AES128-GCM-SHA256", + "RSA-PSK-AES128-CBC-SHA256", + "RSA-PSK-AES256-CBC-SHA", + "RSA-PSK-AES128-CBC-SHA" + ] +```