diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 90cd9cbff..1079443f3 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -2071,7 +2071,13 @@ common_ssl_opts_schema(Defaults) -> %% @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) -> +server_ssl_opts_schema(Defaults1, IsRanchListener) -> + Defaults0 = #{ + cacertfile => emqx:cert_file("cacert.pem"), + certfile => emqx:cert_file("cert.pem"), + keyfile => emqx:cert_file("key.pem") + }, + Defaults = maps:merge(Defaults0, Defaults1), 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) ++ @@ -2148,7 +2154,15 @@ server_ssl_opts_schema(Defaults, IsRanchListener) -> %% @doc Make schema for SSL client. -spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema(). -client_ssl_opts_schema(Defaults) -> +client_ssl_opts_schema(Defaults1) -> + %% assert + true = lists:all(fun(K) -> is_atom(K) end, maps:keys(Defaults1)), + Defaults0 = #{ + cacertfile => emqx:cert_file("cacert.pem"), + certfile => emqx:cert_file("client-cert.pem"), + keyfile => emqx:cert_file("client-key.pem") + }, + Defaults = maps:merge(Defaults0, Defaults1), common_ssl_opts_schema(Defaults) ++ [ {"server_name_indication", diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index c3748cbd4..6235b61e8 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -31,7 +31,8 @@ -export([ ensure_ssl_files/2, delete_ssl_files/3, - file_content_as_options/1 + drop_invalid_certs/1, + is_valid_pem_file/1 ]). -export([ @@ -40,10 +41,11 @@ -include("logger.hrl"). --define(IS_TRUE(Val), ((Val =:= true) or (Val =:= <<"true">>))). --define(IS_FALSE(Val), ((Val =:= false) or (Val =:= <<"false">>))). +-define(IS_TRUE(Val), ((Val =:= true) orelse (Val =:= <<"true">>))). +-define(IS_FALSE(Val), ((Val =:= false) orelse (Val =:= <<"false">>))). -define(SSL_FILE_OPT_NAMES, [<<"keyfile">>, <<"certfile">>, <<"cacertfile">>]). +-define(SSL_FILE_OPT_NAMES_A, [keyfile, certfile, cacertfile]). %% non-empty string -define(IS_STRING(L), (is_list(L) andalso L =/= [] andalso is_integer(hd(L)))). @@ -398,35 +400,37 @@ pem_file_name(Dir, Key, Pem) -> hex_str(Bin) -> iolist_to_binary([io_lib:format("~2.16.0b", [X]) || <> <= Bin]). +%% @doc Returns 'true' when the file is a valid pem, otherwise {error, Reason}. is_valid_pem_file(Path) -> case file:read_file(Path) of {ok, Pem} -> is_pem(Pem) orelse {error, not_pem}; {error, Reason} -> {error, Reason} end. -%% @doc This is to return SSL file content in management APIs. -file_content_as_options(undefined) -> - undefined; -file_content_as_options(#{<<"enable">> := False} = SSL) when ?IS_FALSE(False) -> - {ok, maps:without(?SSL_FILE_OPT_NAMES, SSL)}; -file_content_as_options(#{<<"enable">> := True} = SSL) when ?IS_TRUE(True) -> - file_content_as_options(?SSL_FILE_OPT_NAMES, SSL). +%% @doc Input and output are both HOCON-checked maps, with invalid SSL +%% file options dropped. +%% This is to give a feedback to the front-end or management API caller +%% so they are forced to upload a cert file, or use an existing file path. +-spec drop_invalid_certs(map()) -> map(). +drop_invalid_certs(#{enable := False} = SSL) when ?IS_FALSE(False) -> + maps:without(?SSL_FILE_OPT_NAMES_A, SSL); +drop_invalid_certs(#{<<"enable">> := False} = SSL) when ?IS_FALSE(False) -> + maps:without(?SSL_FILE_OPT_NAMES, SSL); +drop_invalid_certs(#{enable := True} = SSL) when ?IS_TRUE(True) -> + drop_invalid_certs(?SSL_FILE_OPT_NAMES_A, SSL); +drop_invalid_certs(#{<<"enable">> := True} = SSL) when ?IS_TRUE(True) -> + drop_invalid_certs(?SSL_FILE_OPT_NAMES, SSL). -file_content_as_options([], SSL) -> - {ok, SSL}; -file_content_as_options([Key | Keys], SSL) -> +drop_invalid_certs([], SSL) -> + SSL; +drop_invalid_certs([Key | Keys], SSL) -> case maps:get(Key, SSL, undefined) of undefined -> - file_content_as_options(Keys, SSL); + drop_invalid_certs(Keys, SSL); Path -> - case file:read_file(Path) of - {ok, Bin} -> - file_content_as_options(Keys, SSL#{Key => Bin}); - {error, Reason} -> - {error, #{ - file_path => Path, - reason => Reason - }} + case is_valid_pem_file(Path) of + true -> SSL; + {error, _} -> maps:without([Key], SSL) end end. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index ed0117b96..878e879ed 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1202,25 +1202,8 @@ fill_defaults(Configs) when is_list(Configs) -> fill_defaults(Config) -> emqx_authn:check_config(Config, #{only_fill_defaults => true}). -convert_certs(#{ssl := #{enable := true} = SSLOpts} = Config) -> - NSSLOpts = lists:foldl( - fun(K, Acc) -> - case maps:get(K, Acc, undefined) of - undefined -> - Acc; - Filename -> - case file:read_file(Filename) of - {ok, Bin} -> - Acc#{K => Bin}; - {error, _} -> - Acc#{K => Filename} - end - end - end, - SSLOpts, - [certfile, keyfile, cacertfile] - ), - Config#{ssl => NSSLOpts}; +convert_certs(#{ssl := SSL} = Config) when SSL =/= undefined -> + Config#{ssl := emqx_tls_lib:drop_invalid_certs(SSL)}; convert_certs(Config) -> Config. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 759fd92f8..5529bbfca 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -214,7 +214,7 @@ sources(get, _) -> ]) end; (Source, AccIn) -> - lists:append(AccIn, [read_certs(Source)]) + lists:append(AccIn, [drop_invalid_certs(Source)]) end, [], get_raw_sources() @@ -248,7 +248,7 @@ source(get, #{bindings := #{type := Type}}) -> }} end; [Source] -> - {200, read_certs(Source)} + {200, drop_invalid_certs(Source)} end; source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>} = Body}) -> update_authz_file(Body); @@ -498,15 +498,9 @@ update_config(Cmd, Sources) -> }} end. -read_certs(#{<<"ssl">> := SSL} = Source) -> - case emqx_tls_lib:file_content_as_options(SSL) of - {error, Reason} -> - ?SLOG(error, Reason#{msg => failed_to_read_ssl_file}), - throw(failed_to_read_ssl_file); - {ok, NewSSL} -> - Source#{<<"ssl">> => NewSSL} - end; -read_certs(Source) -> +drop_invalid_certs(#{<<"ssl">> := SSL} = Source) when SSL =/= undefined -> + Source#{<<"ssl">> => emqx_tls_lib:drop_invalid_certs(SSL)}; +drop_invalid_certs(Source) -> Source. parameters_field() -> diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index fe445fbf0..e78e273e0 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -94,9 +94,6 @@ >> }). --define(MATCH_RSA_KEY, <<"-----BEGIN RSA PRIVATE KEY", _/binary>>). --define(MATCH_CERT, <<"-----BEGIN CERTIFICATE", _/binary>>). - all() -> emqx_common_test_helpers:all(?MODULE). @@ -279,9 +276,9 @@ t_api(_) -> <<"type">> := <<"mongodb">>, <<"ssl">> := #{ <<"enable">> := <<"true">>, - <<"cacertfile">> := ?MATCH_CERT, - <<"certfile">> := ?MATCH_CERT, - <<"keyfile">> := ?MATCH_RSA_KEY, + <<"cacertfile">> := _, + <<"certfile">> := _, + <<"keyfile">> := _, <<"verify">> := <<"verify_none">> } }, @@ -313,9 +310,9 @@ t_api(_) -> <<"type">> := <<"mongodb">>, <<"ssl">> := #{ <<"enable">> := <<"true">>, - <<"cacertfile">> := ?MATCH_CERT, - <<"certfile">> := ?MATCH_CERT, - <<"keyfile">> := ?MATCH_RSA_KEY, + <<"cacertfile">> := _, + <<"certfile">> := _, + <<"keyfile">> := _, <<"verify">> := <<"verify_none">> } },