From e7e8b8c77bf31b332f2efff15e2e11c449b09136 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Sun, 26 Sep 2021 23:15:17 +0200 Subject: [PATCH 1/4] fix(schema): check tlsv1.3 availability --- apps/emqx/src/emqx_schema.erl | 54 ++++++++++++------- apps/emqx/test/emqx_schema_tests.erl | 15 ++++-- apps/emqx_gateway/src/emqx_gateway_schema.erl | 4 +- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 53e9fe835..09b642108 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -637,16 +637,16 @@ fields("listener_ssl_opts") -> server_ssl_opts_schema( #{ depth => 10 , reuse_sessions => true - , versions => tcp - , ciphers => tcp_all + , versions => tls_all_available + , ciphers => tls_all_available }, false); fields("listener_wss_opts") -> server_ssl_opts_schema( #{ depth => 10 , reuse_sessions => true - , versions => tcp - , ciphers => tcp_all + , versions => tls_all_available + , ciphers => tls_all_available }, true); fields(ssl_client_opts) -> client_ssl_opts_schema(#{}); @@ -987,13 +987,14 @@ keyfile is password-protected.""" } , {"versions", sc(hoconsc:array(typerefl:atom()), - #{ default => default_tls_vsns(maps:get(versions, Defaults, tcp)) + #{ default => default_tls_vsns(maps:get(versions, Defaults, tls_all_available)) , desc => """All TLS/DTLS versions to be supported.
NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config
In case PSK cipher suites are intended, make sure to configured ['tlsv1.2', 'tlsv1.1'] here. """ + , validator => fun validate_tls_versions/1 }) } , {"ciphers", ciphers_schema(D("ciphers"))} @@ -1086,7 +1087,7 @@ client_ssl_opts_schema(Defaults) -> , desc => """Specify the host name to be used in TLS Server Name Indication extension.
For instance, when connecting to \"server.example.net\", the genuine server -which accedpts the connection and performs TSL handshake may differ from the +which accedpts the connection and performs TLS handshake may differ from the host the TLS client initially connects to, e.g. when connecting to an IP address or when the host has multiple resolvable DNS records
If not specified, it will default to the host name string which is used @@ -1099,12 +1100,12 @@ verification check.""" ]. -default_tls_vsns(dtls) -> - [<<"dtlsv1.2">>, <<"dtlsv1">>]; -default_tls_vsns(tcp) -> - [<<"tlsv1.3">>, <<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. +default_tls_vsns(dtls_all_available) -> + proplists:get_value(available_dtls, ssl:versions()); +default_tls_vsns(tls_all_available) -> + proplists:get_value(available, ssl:versions()). --spec ciphers_schema(quic | dtls | tcp_all | undefined) -> hocon_schema:field_schema(). +-spec ciphers_schema(quic | dtls_all_available | tls_all_available | undefined) -> hocon_schema:field_schema(). ciphers_schema(Default) -> sc(hoconsc:array(string()), #{ default => default_ciphers(Default) @@ -1146,24 +1147,24 @@ RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA\"
end}). default_ciphers(undefined) -> - default_ciphers(tcp_all); + default_ciphers(tls_all_available); default_ciphers(quic) -> [ "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256", "TLS_CHACHA20_POLY1305_SHA256" ]; -default_ciphers(tcp_all) -> +default_ciphers(tls_all_available) -> default_ciphers('tlsv1.3') ++ default_ciphers('tlsv1.2') ++ default_ciphers(psk); -default_ciphers(dtls) -> +default_ciphers(dtls_all_available) -> %% as of now, dtls does not support tlsv1.3 ciphers default_ciphers('tlsv1.2') ++ default_ciphers('psk'); default_ciphers('tlsv1.3') -> - ["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"] - ++ default_ciphers('tlsv1.2'); + case is_tlsv13_available() of + true -> ssl:cipher_suites(exclusive, 'tlsv1.3', openssl); + false -> [] + end ++ default_ciphers('tlsv1.2'); default_ciphers('tlsv1.2') -> [ "ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA384", @@ -1314,9 +1315,22 @@ parse_user_lookup_fun(StrConf) -> {fun Mod:Fun/3, <<>>}. validate_ciphers(Ciphers) -> - All = ssl:cipher_suites(all, 'tlsv1.3', openssl) ++ - ssl:cipher_suites(all, 'tlsv1.2', openssl), %% includes older version ciphers + All = case is_tlsv13_available() of + true -> ssl:cipher_suites(all, 'tlsv1.3', openssl); + false -> [] + end ++ ssl:cipher_suites(all, 'tlsv1.2', openssl), case lists:filter(fun(Cipher) -> not lists:member(Cipher, All) 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()), + case lists:filter(fun(V) -> not lists:member(V, AvailableVersions) end, Versions) of + [] -> ok; + Vs -> {error, {unsupported_ssl_versions, Vs}} + end. + +is_tlsv13_available() -> + lists:member('tlsv1.3', proplists:get_value(available, ssl:versions())). diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl index 3fb0c0130..4585089e2 100644 --- a/apps/emqx/test/emqx_schema_tests.erl +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -19,8 +19,8 @@ -include_lib("eunit/include/eunit.hrl"). ssl_opts_dtls_test() -> - Sc = emqx_schema:server_ssl_opts_schema(#{versions => dtls, - ciphers => dtls}, false), + Sc = emqx_schema:server_ssl_opts_schema(#{versions => dtls_all_available, + ciphers => dtls_all_available}, false), Checked = validate(Sc, #{<<"versions">> => [<<"dtlsv1.2">>, <<"dtlsv1">>]}), ?assertMatch(#{versions := ['dtlsv1.2', 'dtlsv1'], ciphers := ["ECDHE-ECDSA-AES256-GCM-SHA384" | _] @@ -73,8 +73,8 @@ bad_cipher_test() -> Sc = emqx_schema:server_ssl_opts_schema(#{}, false), Reason = {bad_ciphers, ["foo"]}, ?assertThrow({_Sc, [{validation_error, #{reason := Reason}}]}, - [validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>], - <<"ciphers">> => [<<"foo">>]})]), + validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>], + <<"ciphers">> => [<<"foo">>]})), ok. validate(Schema, Data0) -> @@ -95,3 +95,10 @@ ciperhs_schema_test() -> WSc = #{roots => [{ciphers, Sc}]}, ?assertThrow({_, [{validation_error, _}]}, hocon_schema:check_plain(WSc, #{<<"ciphers">> => <<"foo,bar">>})). + +bad_tls_version_test() -> + Sc = emqx_schema:server_ssl_opts_schema(#{}, false), + Reason = {unsupported_ssl_versions, [foo]}, + ?assertThrow({_Sc, [{validation_error, #{reason := Reason}}]}, + validate(Sc, #{<<"versions">> => [<<"foo">>]})), + ok. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index bdc92bf57..bb0bf9dbe 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -193,8 +193,8 @@ fields(dtls_opts) -> emqx_schema:server_ssl_opts_schema( #{ depth => 10 , reuse_sessions => true - , versions => dtls - , ciphers => dtls + , versions => dtls_all_available + , ciphers => dtls_all_available }, false). authentication() -> From d376c0f9fc344e76aec9d7f0a6f9d9caa0414eed Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Mon, 27 Sep 2021 08:50:16 +0200 Subject: [PATCH 2/4] refactor(schema): call emqx_tls_lib for default tls versions --- apps/emqx/src/emqx_schema.erl | 2 +- apps/emqx/src/emqx_tls_lib.erl | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 09b642108..7426925d8 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1103,7 +1103,7 @@ verification check.""" default_tls_vsns(dtls_all_available) -> proplists:get_value(available_dtls, ssl:versions()); default_tls_vsns(tls_all_available) -> - proplists:get_value(available, ssl:versions()). + emqx_tls_lib:default_versions(). -spec ciphers_schema(quic | dtls_all_available | tls_all_available | undefined) -> hocon_schema:field_schema(). ciphers_schema(Default) -> diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index 24a9a15cf..683166e87 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -31,9 +31,7 @@ %% @doc Returns the default supported tls versions. -spec default_versions() -> [atom()]. -default_versions() -> - OtpRelease = list_to_integer(erlang:system_info(otp_release)), - integral_versions(default_versions(OtpRelease)). +default_versions() -> available_versions(). %% @doc Validate a given list of desired tls versions. %% raise an error exception if non of them are available. @@ -51,7 +49,7 @@ integral_versions(Desired) when ?IS_STRING(Desired) -> integral_versions(Desired) when is_binary(Desired) -> integral_versions(parse_versions(Desired)); integral_versions(Desired) -> - {_, Available} = lists:keyfind(available, 1, ssl:versions()), + Available = available_versions(), case lists:filter(fun(V) -> lists:member(V, Available) end, Desired) of [] -> erlang:error(#{ reason => no_available_tls_version , desired => Desired @@ -103,11 +101,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). + %% tlsv1.3 is available from OTP-22 but we do not want to use until 23. default_versions(OtpRelease) when OtpRelease >= 23 -> - ['tlsv1.3' | default_versions(22)]; + proplists:get_value(available, ssl:versions()); default_versions(_) -> - ['tlsv1.2', 'tlsv1.1', tlsv1]. + lists:delete('tlsv1.3', proplists:get_value(available, ssl:versions())). %% Deduplicate a list without re-ordering the elements. dedup([]) -> []; From 58ffc4651f66f41643554aac060899f1cd5988a4 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Mon, 27 Sep 2021 11:47:02 +0200 Subject: [PATCH 3/4] fix(config): use default value for tls versions default value for ssl.versions is dynamically resolved based on otp version and underlying openssl installation --- apps/emqx/etc/emqx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 267f9a7ec..df5ae9034 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -198,7 +198,7 @@ listeners.ssl.default { ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" - ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + # ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] # TLS 1.3: "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" # TLS 1-1.2 "ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,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,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" # PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" @@ -1350,7 +1350,7 @@ example_common_ssl_options { ## Default: true ssl.honor_cipher_order = true - ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + # ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] # TLS 1.3: "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" # TLS 1-1.2 "ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,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,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" # PSK: "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA" From 5417eb328a8d7e32b3da0cc24adedd71f3b023d6 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Mon, 27 Sep 2021 13:46:45 +0200 Subject: [PATCH 4/4] fix(schema): no ciphers validator for quic listener --- apps/emqx/src/emqx_schema.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 7426925d8..344a1aa45 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1114,7 +1114,10 @@ ciphers_schema(Default) -> (Ciphers) when is_list(Ciphers) -> Ciphers end - , validator => fun validate_ciphers/1 + , validator => case Default =:= quic of + true -> undefined; %% quic has openssl statically linked + false -> fun validate_ciphers/1 + end , desc => """TLS cipher suite names separated by comma, or as an array of strings \"TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256\" or