From fbd57019892f2e88af9f1045389a0d4ddba984be Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 21 Sep 2021 01:02:48 +0200 Subject: [PATCH] fix(emqx_schema): make ssl config schema right --- apps/emqx/etc/emqx.conf | 41 ++-- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_schema.erl | 196 +++++++++++++----- apps/emqx/test/emqx_schema_tests.erl | 108 ++++++++++ .../src/emqx_connector_schema_lib.erl | 17 ++ .../src/emqx_dashboard_schema.erl | 4 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 65 ++---- apps/emqx_machine/src/emqx_machine_schema.erl | 2 +- rebar.config | 2 +- 9 files changed, 302 insertions(+), 135 deletions(-) create mode 100644 apps/emqx/test/emqx_schema_tests.erl diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 42d9305c8..267f9a7ec 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -194,12 +194,17 @@ listeners.ssl.default { mountpoint = "" ## SSL options - ## See ${example_common_ssl_options} for more information - ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" 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"] + # 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" + # NOTE: If PSK cipher-suites are intended, tlsv1.3 should not be enabled in 'versions' config + # ssl.ciphers = "" + ## TCP options ## See ${example_common_tcp_options} for more information tcp.backlog = 1024 @@ -1345,12 +1350,13 @@ example_common_ssl_options { ## Default: true ssl.honor_cipher_order = true - ## TLS versions only to protect from POODLE attack. - ## - ## @doc listeners..ssl.versions - ## ValueType: Array - ## Default: ["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" + # NOTE: If PSK cipher-suites are intended, tlsv1.3 should not be enabled in 'versions' config + # NOTE: by default, ALL ciphers are enabled + # ssl.ciphers = "" ## TLS Handshake timeout. ## @@ -1446,27 +1452,6 @@ example_common_ssl_options { ## Default: true ssl.fail_if_no_peer_cert = false - ## This is the single most important configuration option of an Erlang SSL - ## application. Ciphers (and their ordering) define the way the client and - ## server encrypt information over the wire, from the initial Diffie-Helman - ## key exchange, the session key encryption ## algorithm and the message - ## digest algorithm. Selecting a good cipher suite is critical for the - ## application’s data security, confidentiality and performance. - ## - ## The cipher list above offers: - ## - ## A good balance between compatibility with older browsers. - ## It can get stricter for Machine-To-Machine scenarios. - ## Perfect Forward Secrecy. - ## No old/insecure encryption and HMAC algorithms - ## - ## Most of it was copied from Mozilla’s Server Side TLS article - ## - ## @doc listeners..ssl.ciphers - ## ValueType: Array - ## Default: [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-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] - ssl.ciphers = [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-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA] - } ## Socket options for websocket connections diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 54735360b..da55f3fd0 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -16,7 +16,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.3"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 66db17e81..56e16dec7 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -23,6 +23,7 @@ -dialyzer(no_fail_call). -include_lib("typerefl/include/types.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -type duration() :: integer(). -type duration_s() :: integer(). @@ -71,7 +72,7 @@ -export([namespace/0, roots/0, roots/1, fields/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). --export([ssl/1]). +-export([ssl_opts_schema/2, ciphers_schema/1, default_ciphers/1]). namespace() -> undefined. @@ -461,7 +462,7 @@ fields("mqtt_ssl_listener") -> #{}) } , {"ssl", - sc(ref("listener_ssl_opts"), + sc(ref("ssl_opts"), #{}) } ] ++ mqtt_listener(); @@ -483,7 +484,7 @@ fields("mqtt_wss_listener") -> #{}) } , {"ssl", - sc(ref("listener_ssl_opts"), + sc(ref("wss_ssl_opts"), #{}) } , {"websocket", @@ -498,6 +499,7 @@ fields("mqtt_quic_listener") -> #{ default => true }) } + %% TODO: ensure cacertfile is configurable , {"certfile", sc(string(), #{}) @@ -506,11 +508,7 @@ fields("mqtt_quic_listener") -> sc(string(), #{}) } - , {"ciphers", - sc(comma_separated_list(), - #{ default => "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256," - "TLS_CHACHA20_POLY1305_SHA256" - })} + , {"ciphers", ciphers_schema(quic)} , {"idle_timeout", sc(duration(), #{ default => "15s" @@ -633,13 +631,21 @@ fields("tcp_opts") -> } ]; -fields("listener_ssl_opts") -> - ssl(#{handshake_timeout => "15s" - , depth => 10 - , reuse_sessions => true - , versions => default_tls_vsns() - , ciphers => default_ciphers() - }); +fields("ssl_opts") -> + ssl_opts_schema( + #{ depth => 10 + , reuse_sessions => true + , versions => tcp + , ciphers => tcp_all + }, false); + +fields("wss_ssl_opts") -> + ssl_opts_schema( + #{ depth => 10 + , reuse_sessions => true + , versions => tcp + , ciphers => tcp_all + }, true); fields("deflate_opts") -> [ {"level", @@ -902,7 +908,10 @@ conf_get(Key, Conf, Default) -> filter(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined]. -ssl(Defaults) -> +%% @doc This function defines the SSL opts only for TLS server (listners). +%% When it's for ranch listener, an extra field `handshake_timeout' is added. +-spec ssl_opts_schema(map(), boolean()) -> hocon_schema:field_schema(). +ssl_opts_schema(Defaults, IsRanchListener) -> D = fun (Field) -> maps:get(to_atom(Field), Defaults, undefined) end, Df = fun (Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end, [ {"enable", @@ -933,6 +942,14 @@ ssl(Defaults) -> , {"fail_if_no_peer_cert", sc(boolean(), #{ default => Df("fail_if_no_peer_cert", false) + , desc => +""" +Used together with {verify, verify_peer} by an TLS/DTLS server. +If set to true, the server fails if the client does not have a +certificate to send, that is, sends an empty certificate. +If set to false, it fails only if the client sends an invalid +certificate (an empty certificate is considered valid). +""" }) } , {"secure_renegotiate", @@ -971,11 +988,6 @@ the number of messages the underlying cipher suite can encipher. #{ default => Df("honor_cipher_order", true) }) } - , {"handshake_timeout", - sc(duration(), - #{ default => Df("handshake_timeout", "15s") - }) - } , {"depth", sc(integer(), #{default => Df("depth", 10) @@ -983,50 +995,118 @@ the number of messages the underlying cipher suite can encipher. } , {"password", sc(string(), - #{ default => D("key_password") - , sensitive => true + #{ sensitive => true + , nullable => true + , desc => +"""String containing the user's password. Only used if the private +keyfile is password-protected.""" }) } , {"dhfile", sc(string(), #{ default => D("dhfile") - }) - } - , {"server_name_indication", - sc(hoconsc:union([disable, string()]), - #{ default => D("server_name_indication") + , nullable => true + , desc => +"""Path to a file containing PEM-encoded Diffie Hellman parameters +to be used by the server if a cipher suite using Diffie Hellman +key exchange is negotiated. If not specified, default parameters +are used.
+NOTE: The dhfile option is not supported by TLS 1.3.""" }) } , {"versions", - sc(typerefl:alias("string", list(atom())), - #{ default => maps:get(versions, Defaults, default_tls_vsns()) - , converter => fun (Vsns) -> [tls_vsn(iolist_to_binary(V)) || V <- Vsns] end + sc(hoconsc:array(typerefl:atom()), + #{ default => default_tls_vsns(maps:get(versions, Defaults, tcp)) + , 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
. +""" }) } - , {"ciphers", - sc(hoconsc:array(string()), - #{ default => D("ciphers") - }) - } - , {"user_lookup_fun", + , {"ciphers", ciphers_schema(D("ciphers"))} + , {user_lookup_fun, sc(typerefl:alias("string", any()), #{ default => "emqx_psk:lookup" , converter => fun ?MODULE:parse_user_lookup_fun/1 }) } + | [ {"handshake_timeout", + sc(duration(), + #{ default => Df("handshake_timeout", "15s") + , desc => "Maximum time duration allowed for the handshake to complete" + })} + || IsRanchListener] ]. -%% on erl23.2.7.2-emqx-2, sufficient_crypto_support('tlsv1.3') -> false -default_tls_vsns() -> [<<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. +default_tls_vsns(dtls) -> + [<<"dtlsv1.2">>, <<"dtlsv1">>]; +default_tls_vsns(tcp) -> + [<<"tlsv1.3">>, <<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. -tls_vsn(<<"tlsv1.3">>) -> 'tlsv1.3'; -tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2'; -tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1'; -tls_vsn(<<"tlsv1">>) -> 'tlsv1'. +-spec ciphers_schema(quic | dtls | tcp_all | undefined) -> hocon_schema:field_schema(). +ciphers_schema(Default) -> + sc(hoconsc:union([string(), hoconsc:array(string())]), + #{ default => default_ciphers(Default) + , converter => fun(Ciphers) when is_binary(Ciphers) -> + binary:split(Ciphers, <<",">>, [global]); + (Ciphers) when is_list(Ciphers) -> + Ciphers + end + , validator => fun validate_ciphers/1 + , 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 +[\"TLS_AES_256_GCM_SHA384\",\"TLS_AES_128_GCM_SHA256\"] +Ciphers (and their ordering) define the way in which the +client and server encrypts information over the wire. +Selecting a good cipher suite is critical for the +application's data security, confidentiality and performance. +The names should be in OpenSSL sting format (not RFC format). +Default values and examples proveded by EMQ X config +documentation are all in OpenSSL format.
-default_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", "ECDHE-ECDSA-AES256-GCM-SHA384", +NOTE: Certain cipher suites are only compatible with +specific TLS versions ('tlsv1.1', 'tlsv1.2' or 'tlsv1.3') +incompatible cipher suites will be silently dropped. +For instance, if only 'tlsv1.3' is given in the versions, +configuring cipher suites for other versions will have no effect. +
+ +NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config
+If PSK cipher suites are intended, 'tlsv1.3' should be disabled from versions.
+PSK cipher suites: \"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, +RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA\"
+""" ++ case Default of + quic -> "NOTE: QUIC listener supports only 'tlsv1.3' ciphers
"; + _ -> "" + end}). + +default_ciphers(undefined) -> + default_ciphers(tcp_all); +default_ciphers(quic) -> [ + "TLS_AES_256_GCM_SHA384", + "TLS_AES_128_GCM_SHA256", + "TLS_CHACHA20_POLY1305_SHA256" + ]; +default_ciphers(tcp_all) -> + default_ciphers('tlsv1.3') ++ + default_ciphers('tlsv1.2') ++ + default_ciphers(psk); +default_ciphers(dtls) -> + %% 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'); +default_ciphers('tlsv1.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", @@ -1039,10 +1119,12 @@ default_ciphers() -> [ "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_ciphers(). - -psk_ciphers() -> [ - "PSK-AES128-CBC-SHA", "PSK-AES256-CBC-SHA", "PSK-3DES-EDE-CBC-SHA", "PSK-RC4-SHA" + ]; +default_ciphers(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", + "RSA-PSK-DES-CBC3-SHA","RSA-PSK-RC4-SHA" ]. %% @private return a list of keys in a parent field @@ -1160,3 +1242,17 @@ parse_user_lookup_fun(StrConf) -> Mod = list_to_atom(ModStr), Fun = list_to_atom(FunStr), {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 + lists:foreach( + fun(Cipher) -> + case lists:member(Cipher, All) of + true -> + ok; + false -> + ?tp(error, bad_tls_cipher_suite, #{ciphers => Cipher}), + error({bad_tls_cipher_suite, Cipher}) + end + end, Ciphers). diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl new file mode 100644 index 000000000..e8a5d41d6 --- /dev/null +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -0,0 +1,108 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_schema_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +ssl_opts_dtls_test() -> + Sc = emqx_schema:ssl_opts_schema(#{versions => dtls, + ciphers => dtls}, false), + Checked = validate(Sc, #{<<"versions">> => [<<"dtlsv1.2">>, <<"dtlsv1">>]}), + ?assertMatch(#{versions := ['dtlsv1.2', 'dtlsv1'], + ciphers := ["ECDHE-ECDSA-AES256-GCM-SHA384" | _] + }, Checked). + +ssl_opts_tls_1_3_test() -> + Sc = emqx_schema:ssl_opts_schema(#{}, false), + Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>]}), + ?assertNot(maps:is_key(handshake_timeout, Checked)), + ?assertMatch(#{versions := ['tlsv1.3'], + ciphers := [_ | _] + }, Checked). + +ssl_opts_tls_for_ranch_test() -> + Sc = emqx_schema:ssl_opts_schema(#{}, true), + Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>]}), + ?assertMatch(#{versions := ['tlsv1.3'], + ciphers := [_ | _], + handshake_timeout := _ + }, Checked). + +ssl_opts_cipher_array_test() -> + Sc = emqx_schema:ssl_opts_schema(#{}, false), + Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>], + <<"ciphers">> => [<<"TLS_AES_256_GCM_SHA384">>, + <<"ECDHE-ECDSA-AES256-GCM-SHA384">>]}), + ?assertMatch(#{versions := ['tlsv1.3'], + ciphers := ["TLS_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"] + }, Checked). + +ssl_opts_cipher_comma_separated_string_test() -> + Sc = emqx_schema:ssl_opts_schema(#{}, false), + Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>], + <<"ciphers">> => <<"TLS_AES_256_GCM_SHA384,ECDHE-ECDSA-AES256-GCM-SHA384">>}), + ?assertMatch(#{versions := ['tlsv1.3'], + ciphers := ["TLS_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"] + }, Checked). + +ssl_opts_tls_psk_test() -> + Sc = emqx_schema:ssl_opts_schema(#{}, false), + Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>]}), + ?assertMatch(#{versions := ['tlsv1.2']}, Checked), + #{ciphers := Ciphers} = Checked, + PskCiphers = emqx_schema:default_ciphers(psk), + lists:foreach(fun(Cipher) -> + ?assert(lists:member(Cipher, Ciphers)) + end, PskCiphers). + +bad_cipher_test() -> + ok = snabbkaffe:start_trace(), + Sc = emqx_schema:ssl_opts_schema(#{}, false), + ?assertThrow({_Sc, [{validation_error, _Error}]}, + [validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>], + <<"ciphers">> => [<<"foo">>]})]), + Trace = snabbkaffe:collect_trace(), + ?assertEqual(1, length(?of_kind(bad_tls_cipher_suite, Trace))), + snabbkaffe:stop(), + ok. + +validate(Schema, Data0) -> + Sc = #{ roots => [ssl_opts] + , fields => #{ssl_opts => Schema} + }, + Data = Data0#{ cacertfile => <<"cacertfile">> + , certfile => <<"certfile">> + , keyfile => <<"keyfile">> + }, + #{ssl_opts := Checked} = + hocon_schema:check_plain(Sc, #{<<"ssl_opts">> => Data}, + #{atom_key => true}), + Checked. + +ciperhs_schema_test() -> + Sc = emqx_schema:ciphers_schema(undefined), + ?assertMatch( + #{type := {union, [_, {array, _}]}, + default := [_ | _], + converter := Converter, + validator := Validator + } when is_function(Converter) andalso is_function(Validator), + Sc), + WSc = #{roots => [{ciphers, Sc}]}, + ?assertThrow({_, [{validation_error, _}]}, + hocon_schema:check_plain(WSc, #{<<"ciphers">> => <<"foo,bar">>})). diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index ecdfb1416..59e7b87d3 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -61,6 +61,7 @@ fields("ssl") -> , {keyfile, fun keyfile/1} , {certfile, fun certfile/1} , {verify, fun verify/1} + , {server_name_indicator, fun server_name_indicator/1} ]. ssl_fields() -> @@ -150,3 +151,19 @@ to_servers(Str) -> [{host, Ip}, {port, list_to_integer(Port)}] end end, string:tokens(Str, " , "))}. + +server_name_indicator(type) -> string(); +server_name_indicator(default) -> disable; +server_name_indicator(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 +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 +to establish the connection, unless it is IP addressed used.
+The host name is then also used in the host name verification of the peer +certificate.
The special value 'disable' prevents the Server Name +Indication extension from being sent and disables the hostname +verification check."""; +server_name_indicator(_) -> undefined. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 94cfaddad..bd0c28b92 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -45,7 +45,9 @@ fields("http") -> ]; fields("https") -> - proplists:delete("fail_if_no_peer_cert", emqx_schema:ssl(#{})) ++ fields("http"). + fields("http") ++ + proplists:delete("fail_if_no_peer_cert", + emqx_schema:ssl_opts_schema(#{}, true)). default_username(type) -> string(); default_username(default) -> "admin"; diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 3811d56c6..5540f7387 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -163,7 +163,9 @@ fields(tcp_listener) -> fields(ssl_listener) -> fields(tcp_listener) ++ - ssl_opts(); + [{ssl, sc_meta(hoconsc:ref(emqx_schema, "ssl_opts"), + #{desc => "SSL listener options"})}]; + fields(udp_listener) -> [ @@ -174,7 +176,8 @@ fields(udp_listener) -> fields(dtls_listener) -> fields(udp_listener) ++ - dtls_opts(); + [{dtls, sc_meta(ref(dtls_opts), + #{desc => "DTLS listener options"})}]; fields(udp_opts) -> [ {active_n, sc(integer(), 100)} @@ -184,45 +187,13 @@ fields(udp_opts) -> , {reuseaddr, sc(boolean(), true)} ]; -fields(dtls_listener_ssl_opts) -> - Base = emqx_schema:fields("listener_ssl_opts"), - DtlsVers = hoconsc:mk( - typerefl:alias("string", list(atom())), - #{ default => default_dtls_vsns(), - converter => fun (Vsns) -> - [dtls_vsn(iolist_to_binary(V)) || V <- Vsns] - end - }), - Ciphers = sc(hoconsc:array(string()), default_ciphers()), - lists:keydelete( - "handshake_timeout", 1, - lists:keyreplace( - "ciphers", 1, - lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}), - {"ciphers", Ciphers} - ) - ). - -default_ciphers() -> - ["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_ciphers(). - -psk_ciphers() -> - ["PSK-AES128-CBC-SHA", "PSK-AES256-CBC-SHA", - "PSK-3DES-EDE-CBC-SHA", "PSK-RC4-SHA" - ]. +fields(dtls_opts) -> + emqx_schema:ssl_opts_schema( + #{ depth => 10 + , reuse_sessions => true + , versions => dtls + , ciphers => dtls + }, false). % authentication() -> % hoconsc:union( @@ -270,23 +241,11 @@ tcp_opts() -> udp_opts() -> [{udp, sc_meta(ref(udp_opts), #{})}]. -ssl_opts() -> - [{ssl, sc_meta(ref(emqx_schema, "listener_ssl_opts"), #{})}]. - -dtls_opts() -> - [{dtls, sc_meta(ref(dtls_listener_ssl_opts), #{})}]. - proxy_protocol_opts() -> [ {proxy_protocol, sc(boolean())} , {proxy_protocol_timeout, sc(duration())} ]. -default_dtls_vsns() -> - [<<"dtlsv1.2">>, <<"dtlsv1">>]. - -dtls_vsn(<<"dtlsv1.2">>) -> 'dtlsv1.2'; -dtls_vsn(<<"dtlsv1">>) -> 'dtlsv1'. - sc(Type) -> sc_meta(Type, #{}). diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 5d1acb02e..faa8a7621 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -216,7 +216,7 @@ fields(cluster_etcd) -> ]; fields(etcd_ssl_opts) -> - emqx_schema:ssl(#{}); + emqx_schema:ssl_opts_schema(#{}, false); fields(cluster_k8s) -> [ {"apiserver", diff --git a/rebar.config b/rebar.config index 54e6d23f8..35774298b 100644 --- a/rebar.config +++ b/rebar.config @@ -61,7 +61,7 @@ , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.3"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.1"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}