فهرست منبع

Merge pull request #5777 from zmstone/fix-ssl-listener-config

fix(emqx_schema): make ssl config schema right
zhongwencool 4 سال پیش
والد
کامیت
357456880e

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 13 - 28
apps/emqx/etc/emqx.conf


+ 2 - 2
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.0"}}}
+    , {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"}}}

+ 2 - 1
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() ->

+ 247 - 98
apps/emqx/src/emqx_schema.erl

@@ -71,7 +71,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([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1, default_ciphers/1]).
 
 namespace() -> undefined.
 
@@ -87,23 +87,26 @@ roots(high) ->
       }
     , {"zones",
        sc(map("name", ref("zone")),
-          #{ desc => "A zone is a set of configs grouped by the zone <code>name</code>. <br>"
-                     "For flexible configuration mapping, the <code>name</code> "
-                     "can be set to a listener's <code>zone</code> config.<br>"
-                     "NOTE: A builtin zone named <code>default</code> is auto created "
-                     "and can not be deleted."
+          #{ desc =>
+"""A zone is a set of configs grouped by the zone <code>name</code>.<br>
+For flexible configuration mapping, the <code>name</code>
+can be set to a listener's <code>zone</code> config.<br>
+NOTE: A builtin zone named <code>default</code> is auto created
+and can not be deleted."""
            })}
     , {"mqtt",
        sc(ref("mqtt"),
-         #{ desc => "Global MQTT configuration.<br>"
-                    "The configs here work as default values which can be overriden "
-                    "in <code>zone</code> configs"
+         #{ desc =>
+"""Global MQTT configuration.<br>
+The configs here work as default values which can be overriden
+in <code>zone</code> configs"""
           })}
     , {"authentication",
       sc(hoconsc:lazy(hoconsc:array(map())),
-         #{ desc => "Default authentication configs for all MQTT listeners.<br>"
-                    "For per-listener overrides see <code>authentication</code> "
-                    "in listener configs"
+         #{ desc =>
+"""Default authentication configs for all MQTT listeners.<br>
+For per-listener overrides see <code>authentication</code>
+in listener configs"""
           })}
     , {"authorization",
        sc(ref("authorization"),
@@ -483,7 +486,7 @@ fields("mqtt_wss_listener") ->
           #{})
       }
     , {"ssl",
-       sc(ref("listener_ssl_opts"),
+       sc(ref("listener_wss_opts"),
           #{})
       }
     , {"websocket",
@@ -498,6 +501,7 @@ fields("mqtt_quic_listener") ->
           #{ default => true
            })
       }
+      %% TODO: ensure cacertfile is configurable
     , {"certfile",
        sc(string(),
           #{})
@@ -506,11 +510,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"
@@ -634,12 +634,22 @@ fields("tcp_opts") ->
     ];
 
 fields("listener_ssl_opts") ->
-    ssl(#{handshake_timeout => "15s"
-        , depth => 10
-        , reuse_sessions => true
-        , versions => default_tls_vsns()
-        , ciphers => default_ciphers()
-        });
+    server_ssl_opts_schema(
+      #{ depth => 10
+       , reuse_sessions => true
+       , versions => tcp
+       , ciphers => tcp_all
+       }, false);
+
+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",
@@ -902,7 +912,10 @@ conf_get(Key, Conf, Default) ->
 filter(Opts) ->
     [{K, V} || {K, V} <- Opts, V =/= undefined].
 
-ssl(Defaults) ->
+%% @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",
@@ -913,16 +926,39 @@ ssl(Defaults) ->
     , {"cacertfile",
        sc(string(),
           #{ default => D("cacertfile")
+           , nullable => true
+           , desc =>
+"""Trusted PEM format CA certificates bundle file.<br>
+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).<br>
+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.<br>
+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.<br>
+"""
            })
       }
     , {"verify",
@@ -930,52 +966,11 @@ ssl(Defaults) ->
           #{ default => Df("verify", verify_none)
            })
       }
-    , {"fail_if_no_peer_cert",
-       sc(boolean(),
-          #{ default => Df("fail_if_no_peer_cert", false)
-           })
-      }
-    , {"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)
-           })
-      }
-    , {"handshake_timeout",
-       sc(duration(),
-          #{ default => Df("handshake_timeout", "15s")
-           })
-      }
     , {"depth",
        sc(integer(),
           #{default => Df("depth", 10)
@@ -983,50 +978,194 @@ 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")
+    , {"versions",
+       sc(hoconsc:array(typerefl:atom()),
+          #{ default => default_tls_vsns(maps:get(versions, Defaults, tcp))
+           , desc =>
+"""All TLS/DTLS versions to be supported.<br>
+NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config<br>
+In case PSK cipher suites are intended, make sure to configured
+<code>['tlsv1.2', 'tlsv1.1']</code> here.
+"""
            })
       }
-    , {"server_name_indication",
-       sc(hoconsc:union([disable, string()]),
-          #{ default => D("server_name_indication")
+    , {"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
            })
       }
-    , {"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
+    , {"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.
+"""
            })
       }
-    , {"ciphers",
-       sc(hoconsc:array(string()),
-          #{ default => D("ciphers")
+    ].
+
+%% @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.<br>
+NOTE: The dhfile option is not supported by TLS 1.3."""
            })
       }
-    , {"user_lookup_fun",
-       sc(typerefl:alias("string", any()),
-          #{ default => "emqx_psk:lookup"
-           , converter => fun ?MODULE:parse_user_lookup_fun/1
+    , {"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")
+             , 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">>].
+%% @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.<br>
+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 <br>
+If not specified, it will default to the host name string which is used
+to establish the connection, unless it is IP addressed used.<br>
+The host name is then also used in the host name verification of the peer
+certificate.<br> The special value 'disable' prevents the Server Name
+Indication extension from being sent and disables the hostname
+verification check."""
+            })}
+    ].
 
-tls_vsn(<<"tlsv1.3">>) -> 'tlsv1.3';
-tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2';
-tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1';
-tls_vsn(<<"tlsv1">>) -> 'tlsv1'.
 
-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",
+default_tls_vsns(dtls) ->
+    [<<"dtlsv1.2">>, <<"dtlsv1">>];
+default_tls_vsns(tcp) ->
+    [<<"tlsv1.3">>, <<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>].
+
+-spec ciphers_schema(quic | dtls | tcp_all | undefined) -> hocon_schema:field_schema().
+ciphers_schema(Default) ->
+    sc(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
+<code>\"TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256\"</code> or
+<code>[\"TLS_AES_256_GCM_SHA384\",\"TLS_AES_128_GCM_SHA256\"]</code].
+<br>
+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.<br>
+
+NOTE: Certain cipher suites are only compatible with
+specific TLS <code>versions</code> ('tlsv1.1', 'tlsv1.2' or 'tlsv1.3')
+incompatible cipher suites will be silently dropped.
+For instance, if only 'tlsv1.3' is given in the <code>versions</code>,
+configuring cipher suites for other versions will have no effect.
+<br>
+
+NOTE: PSK ciphers are suppresed by 'tlsv1.3' version config<br>
+If PSK cipher suites are intended, 'tlsv1.3' should be disabled from <code>versions</code>.<br>
+PSK cipher suites: <code>\"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\"</code><br>
+""" ++ case Default of
+           quic -> "NOTE: QUIC listener supports only 'tlsv1.3' ciphers<br>";
+           _ -> ""
+       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 +1178,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 +1301,11 @@ 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
+    case lists:filter(fun(Cipher) -> not lists:member(Cipher, All) end, Ciphers) of
+        [] -> ok;
+        Bad -> {error, {bad_ciphers, Bad}}
+    end.

+ 97 - 0
apps/emqx/test/emqx_schema_tests.erl

@@ -0,0 +1,97 @@
+%%--------------------------------------------------------------------
+%% 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").
+
+ssl_opts_dtls_test() ->
+    Sc = emqx_schema:server_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:server_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:server_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: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'],
+                   ciphers := ["TLS_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"]
+                  }, Checked).
+
+ssl_opts_cipher_comma_separated_string_test() ->
+    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'],
+                   ciphers := ["TLS_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"]
+                  }, Checked).
+
+ssl_opts_tls_psk_test() ->
+    Sc = emqx_schema:server_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() ->
+    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">>]})]),
+    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),
+    WSc = #{roots => [{ciphers, Sc}]},
+    ?assertThrow({_, [{validation_error, _}]},
+                 hocon_schema:check_plain(WSc, #{<<"ciphers">> => <<"foo,bar">>})).

+ 4 - 3
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.
+    A.

+ 4 - 26
apps/emqx_connector/src/emqx_connector_schema_lib.erl

@@ -53,18 +53,12 @@
 
 -export([roots/0, fields/1]).
 
-roots() -> ["ssl"].
-
-fields("ssl") ->
-    [ {enable, #{type => boolean(), default => false}}
-    , {cacertfile, fun cacertfile/1}
-    , {keyfile, fun keyfile/1}
-    , {certfile, fun certfile/1}
-    , {verify, fun verify/1}
-    ].
+roots() -> [].
+
+fields(_) -> [].
 
 ssl_fields() ->
-    [ {ssl, #{type => hoconsc:ref(?MODULE, "ssl"),
+    [ {ssl, #{type => hoconsc:ref(emqx_schema, ssl_client_opts),
               default => #{<<"enable">> => false}
              }
       }
@@ -106,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.

+ 3 - 1
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:server_ssl_opts_schema(#{}, true)).
 
 default_username(type) -> string();
 default_username(default) -> "admin";

+ 12 - 53
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, "listener_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:server_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, #{}).
 

+ 3 - 22
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(#{});
-
 fields(cluster_k8s) ->
     [ {"apiserver",
        sc(string(),
@@ -312,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"
+           }
          )}
     ];
 

+ 2 - 2
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.0"}}}
+    , {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"}}}