From 97e1cf65b747737ecf3c5ef921d9f89ef93cc5f8 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 21 Sep 2021 20:27:29 +0200 Subject: [PATCH] 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(),