From fbd57019892f2e88af9f1045389a0d4ddba984be Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 21 Sep 2021 01:02:48 +0200 Subject: [PATCH 1/7] 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"}}} From 97e1cf65b747737ecf3c5ef921d9f89ef93cc5f8 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 21 Sep 2021 20:27:29 +0200 Subject: [PATCH 2/7] refactor(schema): make a client ssl options schema client and server ssl options share some common fields this commit make an abstraction for the common fields then export server_ssl_options_schema/2 and client_ssl_options_schema/1 for other schema modules to call --- apps/emqx/src/emqx_schema.erl | 168 +++++++++++------- apps/emqx/test/emqx_schema_tests.erl | 14 +- .../src/emqx_connector_schema_lib.erl | 45 +---- .../src/emqx_dashboard_schema.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 4 +- apps/emqx_machine/src/emqx_machine_schema.erl | 5 +- 6 files changed, 115 insertions(+), 123 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 56e16dec7..dcce6e438 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -72,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_opts_schema/2, ciphers_schema/1, default_ciphers/1]). +-export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1, default_ciphers/1]). namespace() -> undefined. @@ -462,7 +462,7 @@ fields("mqtt_ssl_listener") -> #{}) } , {"ssl", - sc(ref("ssl_opts"), + sc(ref("listener_ssl_opts"), #{}) } ] ++ mqtt_listener(); @@ -484,7 +484,7 @@ fields("mqtt_wss_listener") -> #{}) } , {"ssl", - sc(ref("wss_ssl_opts"), + sc(ref("listener_wss_opts"), #{}) } , {"websocket", @@ -631,21 +631,23 @@ fields("tcp_opts") -> } ]; -fields("ssl_opts") -> - ssl_opts_schema( +fields("listener_ssl_opts") -> + server_ssl_opts_schema( #{ depth => 10 , reuse_sessions => true , versions => tcp , ciphers => tcp_all }, false); -fields("wss_ssl_opts") -> - ssl_opts_schema( +fields("listener_wss_opts") -> + server_ssl_opts_schema( #{ depth => 10 , reuse_sessions => true , versions => tcp , ciphers => tcp_all }, true); +fields(ssl_client_opts) -> + client_ssl_opts_schema(#{}); fields("deflate_opts") -> [ {"level", @@ -908,10 +910,10 @@ conf_get(Key, Conf, Default) -> filter(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined]. -%% @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) -> +%% @private This function defines the SSL opts which are commonly used by +%% SSL listener and client. +-spec common_ssl_opts_schema(map()) -> hocon_schema:field_schema(). +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, [ {"enable", @@ -939,55 +941,11 @@ ssl_opts_schema(Defaults, IsRanchListener) -> #{ default => Df("verify", verify_none) }) } - , {"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", - sc(boolean(), - #{ default => Df("secure_renegotiate", true) - , desc => """ -SSL parameter renegotiation is a feature that allows a client and a server -to renegotiate the parameters of the SSL connection on the fly. -RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, -you drop support for the insecure renegotiation, prone to MitM attacks. -""" - }) - } - , {"client_renegotiation", - sc(boolean(), - #{ default => Df("client_renegotiation", true) - , desc => """ -In protocols that support client-initiated renegotiation, -the cost of resources of such an operation is higher for the server than the client. -This can act as a vector for denial of service attacks. -The SSL application already takes measures to counter-act such attempts, -but client-initiated renegotiation can be strictly disabled by setting this option to false. -The default value is true. Note that disabling renegotiation can result in -long-lived connections becoming unusable due to limits on -the number of messages the underlying cipher suite can encipher. -""" - }) - } , {"reuse_sessions", sc(boolean(), #{ default => Df("reuse_sessions", true) }) } - , {"honor_cipher_order", - sc(boolean(), - #{ default => Df("honor_cipher_order", true) - }) - } , {"depth", sc(integer(), #{default => Df("depth", 10) @@ -1002,18 +960,6 @@ the number of messages the underlying cipher suite can encipher. keyfile is password-protected.""" }) } - , {"dhfile", - sc(string(), - #{ default => D("dhfile") - , 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(hoconsc:array(typerefl:atom()), #{ default => default_tls_vsns(maps:get(versions, Defaults, tcp)) @@ -1032,6 +978,71 @@ In case PSK cipher suites are intended, make sure to configured , converter => fun ?MODULE:parse_user_lookup_fun/1 }) } + , {"secure_renegotiate", + sc(boolean(), + #{ default => Df("secure_renegotiate", true) + , desc => """ +SSL parameter renegotiation is a feature that allows a client and a server +to renegotiate the parameters of the SSL connection on the fly. +RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, +you drop support for the insecure renegotiation, prone to MitM attacks. +""" + }) + } + ]. + +%% @doc Make schema for SSL listener options. +%% When it's for ranch listener, an extra field `handshake_timeout' is added. +-spec server_ssl_opts_schema(map(), boolean()) -> hocon_schema:field_schema(). +server_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, + common_ssl_opts_schema(Defaults) ++ + [ {"dhfile", + sc(string(), + #{ default => D("dhfile") + , 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.""" + }) + } + , {"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). +""" + }) + } + , {"honor_cipher_order", + sc(boolean(), + #{ default => Df("honor_cipher_order", true) + }) + } + , {"client_renegotiation", + sc(boolean(), + #{ default => Df("client_renegotiation", true) + , desc => """ +In protocols that support client-initiated renegotiation, +the cost of resources of such an operation is higher for the server than the client. +This can act as a vector for denial of service attacks. +The SSL application already takes measures to counter-act such attempts, +but client-initiated renegotiation can be strictly disabled by setting this option to false. +The default value is true. Note that disabling renegotiation can result in +long-lived connections becoming unusable due to limits on +the number of messages the underlying cipher suite can encipher. +""" + }) + } | [ {"handshake_timeout", sc(duration(), #{ default => Df("handshake_timeout", "15s") @@ -1040,6 +1051,29 @@ In case PSK cipher suites are intended, make sure to configured || IsRanchListener] ]. +%% @doc Make schema for SSL client. +-spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema(). +client_ssl_opts_schema(Defaults) -> + common_ssl_opts_schema(Defaults) ++ + [ { "server_name_indication", + sc(hoconsc:union([disable, string()]), + #{ default => disable + , 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.""" + })} + ]. + + default_tls_vsns(dtls) -> [<<"dtlsv1.2">>, <<"dtlsv1">>]; default_tls_vsns(tcp) -> diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl index e8a5d41d6..87d243405 100644 --- a/apps/emqx/test/emqx_schema_tests.erl +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -20,7 +20,7 @@ -include_lib("snabbkaffe/include/snabbkaffe.hrl"). ssl_opts_dtls_test() -> - Sc = emqx_schema:ssl_opts_schema(#{versions => dtls, + Sc = emqx_schema:server_ssl_opts_schema(#{versions => dtls, ciphers => dtls}, false), Checked = validate(Sc, #{<<"versions">> => [<<"dtlsv1.2">>, <<"dtlsv1">>]}), ?assertMatch(#{versions := ['dtlsv1.2', 'dtlsv1'], @@ -28,7 +28,7 @@ ssl_opts_dtls_test() -> }, Checked). ssl_opts_tls_1_3_test() -> - Sc = emqx_schema:ssl_opts_schema(#{}, false), + Sc = emqx_schema:server_ssl_opts_schema(#{}, false), Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>]}), ?assertNot(maps:is_key(handshake_timeout, Checked)), ?assertMatch(#{versions := ['tlsv1.3'], @@ -36,7 +36,7 @@ ssl_opts_tls_1_3_test() -> }, Checked). ssl_opts_tls_for_ranch_test() -> - Sc = emqx_schema:ssl_opts_schema(#{}, true), + Sc = emqx_schema:server_ssl_opts_schema(#{}, true), Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>]}), ?assertMatch(#{versions := ['tlsv1.3'], ciphers := [_ | _], @@ -44,7 +44,7 @@ ssl_opts_tls_for_ranch_test() -> }, Checked). ssl_opts_cipher_array_test() -> - Sc = emqx_schema:ssl_opts_schema(#{}, false), + Sc = emqx_schema:server_ssl_opts_schema(#{}, false), Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.3">>], <<"ciphers">> => [<<"TLS_AES_256_GCM_SHA384">>, <<"ECDHE-ECDSA-AES256-GCM-SHA384">>]}), @@ -53,7 +53,7 @@ ssl_opts_cipher_array_test() -> }, Checked). ssl_opts_cipher_comma_separated_string_test() -> - Sc = emqx_schema:ssl_opts_schema(#{}, false), + Sc = emqx_schema:server_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'], @@ -61,7 +61,7 @@ ssl_opts_cipher_comma_separated_string_test() -> }, Checked). ssl_opts_tls_psk_test() -> - Sc = emqx_schema:ssl_opts_schema(#{}, false), + Sc = emqx_schema:server_ssl_opts_schema(#{}, false), Checked = validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>]}), ?assertMatch(#{versions := ['tlsv1.2']}, Checked), #{ciphers := Ciphers} = Checked, @@ -72,7 +72,7 @@ ssl_opts_tls_psk_test() -> bad_cipher_test() -> ok = snabbkaffe:start_trace(), - Sc = emqx_schema:ssl_opts_schema(#{}, false), + Sc = emqx_schema:server_ssl_opts_schema(#{}, false), ?assertThrow({_Sc, [{validation_error, _Error}]}, [validate(Sc, #{<<"versions">> => [<<"tlsv1.2">>], <<"ciphers">> => [<<"foo">>]})]), diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 59e7b87d3..9ecfb56b3 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -53,19 +53,12 @@ -export([roots/0, fields/1]). -roots() -> ["ssl"]. +roots() -> []. -fields("ssl") -> - [ {enable, #{type => boolean(), default => false}} - , {cacertfile, fun cacertfile/1} - , {keyfile, fun keyfile/1} - , {certfile, fun certfile/1} - , {verify, fun verify/1} - , {server_name_indicator, fun server_name_indicator/1} - ]. +fields(_) -> []. ssl_fields() -> - [ {ssl, #{type => hoconsc:ref(?MODULE, "ssl"), + [ {ssl, #{type => hoconsc:ref(emqx_schema, ssl_client_opts), default => #{<<"enable">> => false} } } @@ -107,22 +100,6 @@ auto_reconnect(type) -> boolean(); auto_reconnect(default) -> true; auto_reconnect(_) -> undefined. -cacertfile(type) -> string(); -cacertfile(nullable) -> true; -cacertfile(_) -> undefined. - -keyfile(type) -> string(); -keyfile(nullable) -> true; -keyfile(_) -> undefined. - -certfile(type) -> string(); -certfile(nullable) -> true; -certfile(_) -> undefined. - -verify(type) -> boolean(); -verify(default) -> false; -verify(_) -> undefined. - servers(type) -> servers(); servers(validator) -> [?NOT_EMPTY("the value of the field 'servers' cannot be empty")]; servers(_) -> undefined. @@ -151,19 +128,3 @@ 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 bd0c28b92..ff3be9320 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -47,7 +47,7 @@ fields("http") -> fields("https") -> fields("http") ++ proplists:delete("fail_if_no_peer_cert", - emqx_schema:ssl_opts_schema(#{}, true)). + emqx_schema:server_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 5540f7387..abef053cb 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -163,7 +163,7 @@ fields(tcp_listener) -> fields(ssl_listener) -> fields(tcp_listener) ++ - [{ssl, sc_meta(hoconsc:ref(emqx_schema, "ssl_opts"), + [{ssl, sc_meta(hoconsc:ref(emqx_schema, "listener_ssl_opts"), #{desc => "SSL listener options"})}]; @@ -188,7 +188,7 @@ fields(udp_opts) -> ]; fields(dtls_opts) -> - emqx_schema:ssl_opts_schema( + emqx_schema:server_ssl_opts_schema( #{ depth => 10 , reuse_sessions => true , versions => dtls diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index faa8a7621..e610088bb 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -211,13 +211,10 @@ fields(cluster_etcd) -> #{ default => "1m" })} , {"ssl", - sc(ref(etcd_ssl_opts), + sc(hoconsc:ref(emqx_schema, ssl_client_opts), #{})} ]; -fields(etcd_ssl_opts) -> - emqx_schema:ssl_opts_schema(#{}, false); - fields(cluster_k8s) -> [ {"apiserver", sc(string(), From b1cf5bc1d4b708e179644e67cc04bd6129aa6a4e Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 21 Sep 2021 20:42:18 +0200 Subject: [PATCH 3/7] fix(schema): do not validate etc_dir --- apps/emqx_machine/src/emqx_machine_schema.erl | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index e610088bb..1637ca0e6 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -309,24 +309,8 @@ fields("node") -> )} , {"etc_dir", sc(string(), - #{ - converter => fun(EtcDir) -> - case filename:absname(EtcDir) =:= EtcDir of - true -> - unicode:characters_to_list(EtcDir); - false -> - unicode:characters_to_list(filename:join([code:lib_dir(), "..", EtcDir])) - end - end, - validator => fun(Path) -> - case filelib:is_dir(Path) of - true -> - ok; - false -> - error({not_dir, Path}) - end - end - } + #{ desc => "`etc` dir for the node" + } )} ]; From 5bd67a49de5ffb03f9eaef098a2a0393237af2fe Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Wed, 22 Sep 2021 10:36:38 +0200 Subject: [PATCH 4/7] chore(schema): reformat schema fields descriptions --- apps/emqx/src/emqx_schema.erl | 99 ++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index dcce6e438..a50f3a5f6 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -88,23 +88,26 @@ roots(high) -> } , {"zones", sc(map("name", ref("zone")), - #{ desc => "A zone is a set of configs grouped by the zone name.
" - "For flexible configuration mapping, the name " - "can be set to a listener's zone config.
" - "NOTE: A builtin zone named default is auto created " - "and can not be deleted." + #{ desc => +"""A zone is a set of configs grouped by the zone name.
+For flexible configuration mapping, the name +can be set to a listener's zone config.
+NOTE: A builtin zone named default is auto created +and can not be deleted.""" })} , {"mqtt", sc(ref("mqtt"), - #{ desc => "Global MQTT configuration.
" - "The configs here work as default values which can be overriden " - "in zone configs" + #{ desc => +"""Global MQTT configuration.
+The configs here work as default values which can be overriden +in zone configs""" })} , {"authentication", sc(hoconsc:lazy(hoconsc:array(map())), - #{ desc => "Default authentication configs for all MQTT listeners.
" - "For per-listener overrides see authentication " - "in listener configs" + #{ desc => +"""Default authentication configs for all MQTT listeners.
+For per-listener overrides see authentication +in listener configs""" })} , {"authorization", sc(ref("authorization"), @@ -956,7 +959,7 @@ common_ssl_opts_schema(Defaults) -> #{ sensitive => true , nullable => true , desc => -"""String containing the user's password. Only used if the private +"""String containing the user's password. Only used if the private keyfile is password-protected.""" }) } @@ -967,7 +970,7 @@ keyfile is password-protected.""" """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
. +['tlsv1.2', 'tlsv1.1'] here. """ }) } @@ -982,9 +985,9 @@ In case PSK cipher suites are intended, make sure to configured sc(boolean(), #{ default => Df("secure_renegotiate", true) , desc => """ -SSL parameter renegotiation is a feature that allows a client and a server -to renegotiate the parameters of the SSL connection on the fly. -RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, +SSL parameter renegotiation is a feature that allows a client and a server +to renegotiate the parameters of the SSL connection on the fly. +RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, you drop support for the insecure renegotiation, prone to MitM attacks. """ }) @@ -1003,9 +1006,9 @@ server_ssl_opts_schema(Defaults, IsRanchListener) -> #{ default => D("dhfile") , 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 +"""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.""" }) @@ -1015,10 +1018,10 @@ NOTE: The dhfile option is not supported by TLS 1.3.""" #{ 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 +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). """ }) @@ -1032,13 +1035,13 @@ certificate (an empty certificate is considered valid). sc(boolean(), #{ default => Df("client_renegotiation", true) , desc => """ -In protocols that support client-initiated renegotiation, -the cost of resources of such an operation is higher for the server than the client. -This can act as a vector for denial of service attacks. -The SSL application already takes measures to counter-act such attempts, -but client-initiated renegotiation can be strictly disabled by setting this option to false. -The default value is true. Note that disabling renegotiation can result in -long-lived connections becoming unusable due to limits on +In protocols that support client-initiated renegotiation, +the cost of resources of such an operation is higher for the server than the client. +This can act as a vector for denial of service attacks. +The SSL application already takes measures to counter-act such attempts, +but client-initiated renegotiation can be strictly disabled by setting this option to false. +The default value is true. Note that disabling renegotiation can result in +long-lived connections becoming unusable due to limits on the number of messages the underlying cipher suite can encipher. """ }) @@ -1060,15 +1063,15 @@ client_ssl_opts_schema(Defaults) -> #{ default => disable , 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 +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 +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 +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 +Indication extension from being sent and disables the hostname verification check.""" })} ]. @@ -1090,22 +1093,22 @@ ciphers_schema(Default) -> 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 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 +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.
-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, +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.
From 4f638b8242f93116c5deb163b6818cab02f021ae Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Wed, 22 Sep 2021 15:09:46 +0200 Subject: [PATCH 5/7] fix(schema): upgrade to hocon 0.19.5 renamed no_conversion option to only_fill_defaults --- apps/emqx/rebar.config | 4 ++-- apps/emqx/src/emqx_config.erl | 3 ++- apps/emqx_authn/src/emqx_authn_api.erl | 7 ++++--- rebar.config | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index da55f3fd0..cf86338f9 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -10,13 +10,13 @@ %% `git_subdir` dependency in other projects. {deps, [ {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} - , {typerefl, {git, "https://github.com/k32/typerefl", {tag, "0.8.4"}}} + , {typerefl, {git, "https://github.com/k32/typerefl", {tag, "0.8.5"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.3"}}} , {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.3"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.5"}}} , {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_config.erl b/apps/emqx/src/emqx_config.erl index 98466d3df..67af08eba 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -294,7 +294,8 @@ fill_defaults(RawConf) -> -spec fill_defaults(module(), raw_config()) -> map(). fill_defaults(SchemaMod, RawConf) -> hocon_schema:check_plain(SchemaMod, RawConf, - #{nullable => true, no_conversion => true}, root_names_from_conf(RawConf)). + #{nullable => true, only_fill_defaults => true}, + root_names_from_conf(RawConf)). -spec read_override_conf() -> raw_config(). read_override_conf() -> diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 540cf86e3..d115131a5 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1970,8 +1970,9 @@ find_config(AuthenticatorID, AuthenticatorsConfig) -> end. fill_defaults(Config) -> - #{<<"authentication">> := CheckedConfig} = hocon_schema:check_plain( - ?AUTHN, #{<<"authentication">> => Config}, #{no_conversion => true}), + #{<<"authentication">> := CheckedConfig} = + hocon_schema:check_plain(?AUTHN, #{<<"authentication">> => Config}, + #{only_fill_defaults => true}), CheckedConfig. convert_certs(#{<<"ssl">> := SSLOpts} = Config) -> @@ -2070,4 +2071,4 @@ to_list(L) when is_list(L) -> to_atom(B) when is_binary(B) -> binary_to_atom(B); to_atom(A) when is_atom(A) -> - A. \ No newline at end of file + A. diff --git a/rebar.config b/rebar.config index 35774298b..9b374d107 100644 --- a/rebar.config +++ b/rebar.config @@ -44,7 +44,7 @@ {deps, [ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps - , {typerefl, {git, "https://github.com/k32/typerefl", {tag, "0.8.4"}}} + , {typerefl, {git, "https://github.com/k32/typerefl", {tag, "0.8.5"}}} , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.9"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} @@ -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.3"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.19.5"}}} , {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"}}} From 4392357877178ceaeff676fb02c5bbd6326ec4ad Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Wed, 22 Sep 2021 22:01:45 +0200 Subject: [PATCH 6/7] fix(schema): simplify ssl ciphers filed schema --- apps/emqx/src/emqx_schema.erl | 17 +++++------------ apps/emqx/test/emqx_schema_tests.erl | 15 ++------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index a50f3a5f6..63eff5f61 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -23,7 +23,6 @@ -dialyzer(no_fail_call). -include_lib("typerefl/include/types.hrl"). --include_lib("snabbkaffe/include/snabbkaffe.hrl"). -type duration() :: integer(). -type duration_s() :: integer(). @@ -1084,7 +1083,7 @@ default_tls_vsns(tcp) -> -spec ciphers_schema(quic | dtls | tcp_all | undefined) -> hocon_schema:field_schema(). ciphers_schema(Default) -> - sc(hoconsc:union([string(), hoconsc:array(string())]), + sc(hoconsc:array(string()), #{ default => default_ciphers(Default) , converter => fun(Ciphers) when is_binary(Ciphers) -> binary:split(Ciphers, <<",">>, [global]); @@ -1283,13 +1282,7 @@ parse_user_lookup_fun(StrConf) -> 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). + case lists:filter(fun(Cipher) -> not lists:member(Cipher, All) end, Ciphers) of + [] -> ok; + Bad -> {error, {bad_ciphers, Bad}} + end. diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl index 87d243405..3fb0c0130 100644 --- a/apps/emqx/test/emqx_schema_tests.erl +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -17,7 +17,6 @@ -module(emqx_schema_tests). -include_lib("eunit/include/eunit.hrl"). --include_lib("snabbkaffe/include/snabbkaffe.hrl"). ssl_opts_dtls_test() -> Sc = emqx_schema:server_ssl_opts_schema(#{versions => dtls, @@ -71,14 +70,11 @@ ssl_opts_tls_psk_test() -> end, PskCiphers). bad_cipher_test() -> - ok = snabbkaffe:start_trace(), Sc = emqx_schema:server_ssl_opts_schema(#{}, false), - ?assertThrow({_Sc, [{validation_error, _Error}]}, + Reason = {bad_ciphers, ["foo"]}, + ?assertThrow({_Sc, [{validation_error, #{reason := Reason}}]}, [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) -> @@ -96,13 +92,6 @@ validate(Schema, Data0) -> 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">>})). From 3027bc3a0c0bcba6fa85f84427de760252b8bb48 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Wed, 22 Sep 2021 23:47:36 +0200 Subject: [PATCH 7/7] fix(schema): sll key and cert files are nullable --- apps/emqx/src/emqx_schema.erl | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 63eff5f61..27688a868 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -926,16 +926,39 @@ common_ssl_opts_schema(Defaults) -> , {"cacertfile", sc(string(), #{ default => D("cacertfile") + , nullable => true + , desc => +"""Trusted PEM format CA certificates bundle file.
+The certificates in this file are used to verify the TLS peer's certificates. +Append new certificates to the file if new CAs are to be trusted. +There is no need to restart EMQ X to have the updated file loaded, because +the system regularly checks if file has been updated (and reload).
+NOTE: invalidating (deleting) a certificate from the file will not affect +already established connections. +""" }) } , {"certfile", sc(string(), #{ default => D("certfile") + , nullable => true + , desc => +"""PEM format certificates chain file.
+The certificates in this file should be in reversed order of the certificate +issue chain. That is, the host's certificate should be placed in the beginning +of the file, followed by the immediate issuer certificate and so on. +Although the root CA certificate is optional, it should placed at the end of +the file if it is to be added. +""" }) } , {"keyfile", sc(string(), #{ default => D("keyfile") + , nullable => true + , desc => +"""PEM format private key file.
+""" }) } , {"verify",