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
This commit is contained in:
Zaiming Shi 2021-09-21 20:27:29 +02:00
parent fbd5701989
commit 97e1cf65b7
6 changed files with 115 additions and 123 deletions

View File

@ -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.<br>
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.<br>
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.<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."""
})}
].
default_tls_vsns(dtls) ->
[<<"dtlsv1.2">>, <<"dtlsv1">>];
default_tls_vsns(tcp) ->

View File

@ -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">>]})]),

View File

@ -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.<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.""";
server_name_indicator(_) -> undefined.

View File

@ -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";

View File

@ -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

View File

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