fix(emqx_schema): make ssl config schema right

This commit is contained in:
Zaiming Shi 2021-09-21 01:02:48 +02:00
parent 22e8c83a52
commit fbd5701989
9 changed files with 302 additions and 135 deletions

View File

@ -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.<name>.ssl.versions
## ValueType: Array<TLSVersion>
## 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
## applications 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 Mozillas Server Side TLS article
##
## @doc listeners.<name>.ssl.ciphers
## ValueType: Array<Cipher>
## 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

View File

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

View File

@ -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.<br>
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.<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<br>.
"""
})
}
, {"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
<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>
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 <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 +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).

View File

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

View File

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

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

View File

@ -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, #{}).

View File

@ -216,7 +216,7 @@ fields(cluster_etcd) ->
];
fields(etcd_ssl_opts) ->
emqx_schema:ssl(#{});
emqx_schema:ssl_opts_schema(#{}, false);
fields(cluster_k8s) ->
[ {"apiserver",

View File

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