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(),