Jelajahi Sumber

Merge pull request #5732 from tigercl/feat/upload-certs

feat(upload certs): save certs to file
tigercl 4 tahun lalu
induk
melakukan
c4403e886d

+ 103 - 5
apps/emqx/src/emqx_authentication.erl

@@ -73,6 +73,11 @@
         , code_change/3
         , code_change/3
         ]).
         ]).
 
 
+-ifdef(TEST).
+-compile(export_all).
+-compile(nowarn_export_all).
+-endif.
+
 -define(CHAINS_TAB, emqx_authn_chains).
 -define(CHAINS_TAB, emqx_authn_chains).
 
 
 -define(VER_1, <<"1">>).
 -define(VER_1, <<"1">>).
@@ -193,20 +198,31 @@ pre_config_update(UpdateReq, OldConfig) ->
     end.
     end.
 
 
 do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) ->
 do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) ->
-    {ok, OldConfig ++ [Config]};
+    try convert_certs(Config) of
+        NConfig ->
+            {ok, OldConfig ++ [NConfig]}
+    catch
+        error:{save_cert_to_file, _} = Reason ->
+            {error, Reason}
+    end;
 do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
 do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
     NewConfig = lists:filter(fun(OldConfig0) ->
     NewConfig = lists:filter(fun(OldConfig0) ->
                                 AuthenticatorID =/= generate_id(OldConfig0)
                                 AuthenticatorID =/= generate_id(OldConfig0)
                              end, OldConfig),
                              end, OldConfig),
     {ok, NewConfig};
     {ok, NewConfig};
 do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) ->
 do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) ->
-    NewConfig = lists:map(fun(OldConfig0) ->
+    try lists:map(fun(OldConfig0) ->
                               case AuthenticatorID =:= generate_id(OldConfig0) of
                               case AuthenticatorID =:= generate_id(OldConfig0) of
-                                  true -> maps:merge(OldConfig0, Config);
+                                  true -> convert_certs(Config, OldConfig0);
                                   false -> OldConfig0
                                   false -> OldConfig0
                               end
                               end
-                          end, OldConfig),
-    {ok, NewConfig};
+                          end, OldConfig) of
+        NewConfig ->
+            {ok, NewConfig}
+    catch
+        error:{save_cert_to_file, _} = Reason ->
+            {error, Reason}
+    end;
 do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
 do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
     case split_by_id(AuthenticatorID, OldConfig) of
     case split_by_id(AuthenticatorID, OldConfig) of
         {error, Reason} -> {error, Reason};
         {error, Reason} -> {error, Reason};
@@ -600,6 +616,85 @@ reply(Reply, State) ->
 %% Internal functions
 %% Internal functions
 %%------------------------------------------------------------------------------
 %%------------------------------------------------------------------------------
 
 
+convert_certs(#{<<"ssl">> := SSLOpts} = Config) ->
+    NSSLOPts = lists:foldl(fun(K, Acc) ->
+                               case maps:get(K, Acc, undefined) of
+                                   undefined -> Acc;
+                                   PemBin ->
+                                       CertFile = generate_filename(K),
+                                       ok = save_cert_to_file(CertFile, PemBin),
+                                       Acc#{K => CertFile}
+                               end
+                           end, SSLOpts, [<<"certfile">>, <<"keyfile">>, <<"cacertfile">>]),
+    Config#{<<"ssl">> => NSSLOPts};
+convert_certs(Config) ->
+    Config.
+
+convert_certs(#{<<"ssl">> := NewSSLOpts} = NewConfig, OldConfig) ->
+    OldSSLOpts = maps:get(<<"ssl">>, OldConfig, #{}),
+    Diff = diff_certs(NewSSLOpts, OldSSLOpts),
+    NSSLOpts = lists:foldl(fun({identical, K}, Acc) ->
+                               Acc#{K => maps:get(K, OldSSLOpts)};
+                              ({_, K}, Acc) ->
+                               CertFile = generate_filename(K),
+                               ok = save_cert_to_file(CertFile, maps:get(K, NewSSLOpts)),
+                               Acc#{K => CertFile}
+                           end, NewSSLOpts, Diff),
+    NewConfig#{<<"ssl">> => NSSLOpts};
+convert_certs(NewConfig, _OldConfig) ->
+    NewConfig.
+
+save_cert_to_file(Filename, PemBin) ->
+    case public_key:pem_decode(PemBin) =/= [] of
+        true ->
+            case filelib:ensure_dir(Filename) of
+                ok ->
+                    case file:write_file(Filename, PemBin) of
+                        ok -> ok;
+                        {error, Reason} -> error({save_cert_to_file, {write_file, Reason}})
+                    end;
+                {error, Reason} ->
+                    error({save_cert_to_file, {ensure_dir, Reason}})
+            end;
+        false ->
+            error({save_cert_to_file, invalid_certificate})
+    end.
+
+generate_filename(Key) ->
+    Prefix = case Key of
+                 <<"keyfile">> -> "key-";
+                 <<"certfile">> -> "cert-";
+                 <<"cacertfile">> -> "cacert-"
+             end,
+    to_bin(filename:join([emqx:get_config([node, data_dir]), "certs/authn", Prefix ++ emqx_misc:gen_id() ++ ".pem"])).
+
+diff_certs(NewSSLOpts, OldSSLOpts) ->
+    Keys = [<<"cacertfile">>, <<"certfile">>, <<"keyfile">>],
+    CertPems = maps:with(Keys, NewSSLOpts),
+    CertFiles = maps:with(Keys, OldSSLOpts),
+    Diff = lists:foldl(fun({K, CertFile}, Acc) ->
+                    case maps:find(K, CertPems) of
+                        error -> Acc;
+                        {ok, PemBin1} ->
+                            {ok, PemBin2} = file:read_file(CertFile),
+                            case diff_cert(PemBin1, PemBin2) of
+                                true ->
+                                    [{changed, K} | Acc];
+                                false ->
+                                    [{identical, K} | Acc]
+                            end
+                    end
+                end,
+                [], maps:to_list(CertFiles)),
+    Added = [{added, K} || K <- maps:keys(maps:without(maps:keys(CertFiles), CertPems))],
+    Diff ++ Added.
+
+diff_cert(Pem1, Pem2) ->
+    cal_md5_for_cert(Pem1) =/= cal_md5_for_cert(Pem2).
+
+cal_md5_for_cert(Pem) ->
+    crypto:hash(md5, term_to_binary(public_key:pem_decode(Pem))).
+
 split_by_id(ID, AuthenticatorsConfig) ->
 split_by_id(ID, AuthenticatorsConfig) ->
     case lists:foldl(
     case lists:foldl(
              fun(C, {P1, P2, F0}) ->
              fun(C, {P1, P2, F0}) ->
@@ -777,3 +872,6 @@ to_list(M) when is_map(M) ->
     [M];
     [M];
 to_list(L) when is_list(L) ->
 to_list(L) when is_list(L) ->
     L.
     L.
+
+to_bin(B) when is_binary(B) -> B;
+to_bin(L) when is_list(L) -> list_to_binary(L).

+ 37 - 0
apps/emqx/src/emqx_misc.erl

@@ -45,6 +45,8 @@
         , index_of/2
         , index_of/2
         , maybe_parse_ip/1
         , maybe_parse_ip/1
         , ipv6_probe/1
         , ipv6_probe/1
+        , gen_id/0
+        , gen_id/1
         ]).
         ]).
 
 
 -export([ bin2hexstr_A_F/1
 -export([ bin2hexstr_A_F/1
@@ -52,6 +54,8 @@
         , hexstr2bin/1
         , hexstr2bin/1
         ]).
         ]).
 
 
+-define(SHORT, 8).
+
 %% @doc Parse v4 or v6 string format address to tuple.
 %% @doc Parse v4 or v6 string format address to tuple.
 %% `Host' itself is returned if it's not an ip string.
 %% `Host' itself is returned if it's not an ip string.
 maybe_parse_ip(Host) ->
 maybe_parse_ip(Host) ->
@@ -298,6 +302,39 @@ hexchar2int(I) when I >= $0 andalso I =< $9 -> I - $0;
 hexchar2int(I) when I >= $A andalso I =< $F -> I - $A + 10;
 hexchar2int(I) when I >= $A andalso I =< $F -> I - $A + 10;
 hexchar2int(I) when I >= $a andalso I =< $f -> I - $a + 10.
 hexchar2int(I) when I >= $a andalso I =< $f -> I - $a + 10.
 
 
+-spec(gen_id() -> list()).
+gen_id() ->
+    gen_id(?SHORT).
+
+-spec(gen_id(integer()) -> list()).
+gen_id(Len) ->
+    BitLen = Len * 4,
+    <<R:BitLen>> = crypto:strong_rand_bytes(Len div 2),
+    int_to_hex(R, Len).
+
+%%------------------------------------------------------------------------------
+%% Internal Functions
+%%------------------------------------------------------------------------------
+
+int_to_hex(I, N) when is_integer(I), I >= 0 ->
+    int_to_hex([], I, 1, N).
+
+int_to_hex(L, I, Count, N)
+    when I < 16 ->
+    pad([int_to_hex(I) | L], N - Count);
+int_to_hex(L, I, Count, N) ->
+    int_to_hex([int_to_hex(I rem 16) | L], I div 16, Count + 1, N).
+
+int_to_hex(I) when 0 =< I, I =< 9 ->
+    I + $0;
+int_to_hex(I) when 10 =< I, I =< 15 ->
+    (I - 10) + $a.
+
+pad(L, 0) ->
+    L;
+pad(L, Count) ->
+    pad([$0 | L], Count - 1).
+
 -ifdef(TEST).
 -ifdef(TEST).
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("eunit/include/eunit.hrl").
 
 

+ 51 - 2
apps/emqx/test/emqx_authentication_SUITE.erl

@@ -92,6 +92,19 @@ end_per_suite(_) ->
     emqx_ct_helpers:stop_apps([]),
     emqx_ct_helpers:stop_apps([]),
     ok.
     ok.
 
 
+init_per_testcase(_, Config) ->
+    meck:new(emqx, [non_strict, passthrough, no_history, no_link]),
+    meck:expect(emqx, get_config, fun([node, data_dir]) ->
+                                          {data_dir, Data} = lists:keyfind(data_dir, 1, Config),
+                                          Data;
+                                     (C) -> meck:passthrough([C])
+                                  end),
+    Config.
+
+end_per_testcase(_, _Config) ->
+    meck:unload(emqx),
+    ok.
+
 t_chain(_) ->
 t_chain(_) ->
     % CRUD of authentication chain
     % CRUD of authentication chain
     ChainName = 'test',
     ChainName = 'test',
@@ -203,7 +216,7 @@ t_update_config(_) ->
     ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig2})),
     ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig2})),
     ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID2)),
     ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID2)),
 
 
-    ?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, #{}})),
+    ?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, AuthenticatorConfig1#{enable => false}})),
     ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
     ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
 
 
     ?assertMatch({ok, _}, update_config([authentication], {move_authenticator, Global, ID2, top})),
     ?assertMatch({ok, _}, update_config([authentication], {move_authenticator, Global, ID2, top})),
@@ -220,7 +233,7 @@ t_update_config(_) ->
     ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig2})),
     ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig2})),
     ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID2)),
     ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID2)),
 
 
-    ?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, #{}})),
+    ?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, AuthenticatorConfig1#{enable => false}})),
     ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)),
     ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)),
 
 
     ?assertMatch({ok, _}, update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, top})),
     ?assertMatch({ok, _}, update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, top})),
@@ -234,5 +247,41 @@ t_update_config(_) ->
     ?AUTHN:remove_provider(AuthNType2),
     ?AUTHN:remove_provider(AuthNType2),
     ok.
     ok.
 
 
+t_convert_cert_options(_) ->
+    Certs = certs([ {<<"keyfile">>, "key.pem"}
+                  , {<<"certfile">>, "cert.pem"}
+                  , {<<"cacertfile">>, "cacert.pem"}
+                  ]),
+    #{<<"ssl">> := NCerts} = ?AUTHN:convert_certs(#{<<"ssl">> => Certs}),
+    ?assertEqual(false, diff_cert(maps:get(<<"keyfile">>, NCerts), maps:get(<<"keyfile">>, Certs))),
+
+    Certs2 = certs([ {<<"keyfile">>, "key.pem"}
+                   , {<<"certfile">>, "cert.pem"}
+                   ]),
+    #{<<"ssl">> := NCerts2} = ?AUTHN:convert_certs(#{<<"ssl">> => Certs2}, #{<<"ssl">> => NCerts}),
+    ?assertEqual(false, diff_cert(maps:get(<<"keyfile">>, NCerts2), maps:get(<<"keyfile">>, Certs2))),
+    ?assertEqual(maps:get(<<"keyfile">>, NCerts), maps:get(<<"keyfile">>, NCerts2)),
+    ?assertEqual(maps:get(<<"certfile">>, NCerts), maps:get(<<"certfile">>, NCerts2)),
+
+    Certs3 = certs([ {<<"keyfile">>, "client-key.pem"}
+                   , {<<"certfile">>, "client-cert.pem"}
+                   , {<<"cacertfile">>, "cacert.pem"}
+                   ]),
+    #{<<"ssl">> := NCerts3} = ?AUTHN:convert_certs(#{<<"ssl">> => Certs3}, #{<<"ssl">> => NCerts2}),
+    ?assertEqual(false, diff_cert(maps:get(<<"keyfile">>, NCerts3), maps:get(<<"keyfile">>, Certs3))),
+    ?assertNotEqual(maps:get(<<"keyfile">>, NCerts2), maps:get(<<"keyfile">>, NCerts3)),
+    ?assertNotEqual(maps:get(<<"certfile">>, NCerts2), maps:get(<<"certfile">>, NCerts3)).
+
 update_config(Path, ConfigRequest) ->
 update_config(Path, ConfigRequest) ->
     emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
     emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
+
+certs(Certs) ->
+    CertsPath = emqx_ct_helpers:deps_path(emqx, "etc/certs"),
+    lists:foldl(fun({Key, Filename}, Acc) ->
+                    {ok, Bin} = file:read_file(filename:join([CertsPath, Filename])),
+                    Acc#{Key => Bin}
+                end, #{}, Certs).
+
+diff_cert(CertFile, CertPem2) ->
+    {ok, CertPem1} = file:read_file(CertFile),
+    ?AUTHN:diff_cert(CertPem1, CertPem2).

+ 27 - 4
apps/emqx_authn/src/emqx_authn_api.erl

@@ -1841,14 +1841,14 @@ create_authenticator(ConfKeyPath, ChainName0, Config) ->
         {ok, #{post_config_update := #{?AUTHN := #{id := ID}},
         {ok, #{post_config_update := #{?AUTHN := #{id := ID}},
                raw_config := AuthenticatorsConfig}} ->
                raw_config := AuthenticatorsConfig}} ->
             {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
             {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
-            {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))};
+            {200, maps:put(id, ID, convert_certs(fill_defaults(AuthenticatorConfig)))};
         {error, {_, _, Reason}} ->
         {error, {_, _, Reason}} ->
             serialize_error(Reason)
             serialize_error(Reason)
     end.
     end.
 
 
 list_authenticators(ConfKeyPath) ->
 list_authenticators(ConfKeyPath) ->
     AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath),
     AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath),
-    NAuthenticators = [maps:put(id, ?AUTHN:generate_id(AuthenticatorConfig), AuthenticatorConfig)
+    NAuthenticators = [maps:put(id, ?AUTHN:generate_id(AuthenticatorConfig), convert_certs(AuthenticatorConfig))
                         || AuthenticatorConfig <- AuthenticatorsConfig],
                         || AuthenticatorConfig <- AuthenticatorsConfig],
     {200, NAuthenticators}.
     {200, NAuthenticators}.
 
 
@@ -1856,7 +1856,7 @@ list_authenticator(ConfKeyPath, AuthenticatorID) ->
     AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath),
     AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath),
     case find_config(AuthenticatorID, AuthenticatorsConfig) of
     case find_config(AuthenticatorID, AuthenticatorsConfig) of
         {ok, AuthenticatorConfig} ->
         {ok, AuthenticatorConfig} ->
-            {200, AuthenticatorConfig#{id => AuthenticatorID}};
+            {200, maps:put(id, AuthenticatorID, convert_certs(AuthenticatorConfig))};
         {error, Reason} ->
         {error, Reason} ->
             serialize_error(Reason)
             serialize_error(Reason)
     end.
     end.
@@ -1867,7 +1867,7 @@ update_authenticator(ConfKeyPath, ChainName0, AuthenticatorID, Config) ->
         {ok, #{post_config_update := #{?AUTHN := #{id := ID}},
         {ok, #{post_config_update := #{?AUTHN := #{id := ID}},
                raw_config := AuthenticatorsConfig}} ->
                raw_config := AuthenticatorsConfig}} ->
             {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
             {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
-            {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))};
+            {200, maps:put(id, ID, convert_certs(fill_defaults(AuthenticatorConfig)))};
         {error, {_, _, Reason}} ->
         {error, {_, _, Reason}} ->
             serialize_error(Reason)
             serialize_error(Reason)
     end.
     end.
@@ -1971,6 +1971,19 @@ fill_defaults(Config) ->
         ?AUTHN, #{<<"authentication">> => Config}, #{nullable => true, no_conversion => true}),
         ?AUTHN, #{<<"authentication">> => Config}, #{nullable => true, no_conversion => true}),
     CheckedConfig.
     CheckedConfig.
 
 
+convert_certs(#{<<"ssl">> := SSLOpts} = Config) ->
+    NSSLOpts = lists:foldl(fun(K, Acc) ->
+                               case maps:get(K, Acc, undefined) of
+                                   undefined -> Acc;
+                                   Filename ->
+                                       {ok, Bin} = file:read_file(Filename),
+                                       Acc#{K => Bin}
+                               end
+                           end, SSLOpts, [<<"certfile">>, <<"keyfile">>, <<"cacertfile">>]),
+    Config#{<<"ssl">> => NSSLOpts};
+convert_certs(Config) ->
+    Config.
+
 serialize_error({not_found, {authenticator, ID}}) ->
 serialize_error({not_found, {authenticator, ID}}) ->
     {404, #{code => <<"NOT_FOUND">>,
     {404, #{code => <<"NOT_FOUND">>,
             message => list_to_binary(
             message => list_to_binary(
@@ -2011,6 +2024,16 @@ serialize_error(unsupported_operation) ->
     {400, #{code => <<"BAD_REQUEST">>,
     {400, #{code => <<"BAD_REQUEST">>,
             message => <<"Operation not supported in this authentication type">>}};
             message => <<"Operation not supported in this authentication type">>}};
 
 
+serialize_error({save_cert_to_file, invalid_certificate}) ->
+    {400, #{code => <<"BAD_REQUEST">>,
+            message => <<"Invalid certificate">>}};
+
+serialize_error({save_cert_to_file, {_, Reason}}) ->
+    {500, #{code => <<"INTERNAL_SERVER_ERROR">>,
+            message => list_to_binary(
+                io_lib:format("Cannot save certificate to file due to '~p'", [Reason])
+            )}};
+
 serialize_error({missing_parameter, Name}) ->
 serialize_error({missing_parameter, Name}) ->
     {400, #{code => <<"MISSING_PARAMETER">>,
     {400, #{code => <<"MISSING_PARAMETER">>,
             message => list_to_binary(
             message => list_to_binary(

+ 3 - 3
apps/emqx_connector/src/emqx_connector_schema_lib.erl

@@ -107,15 +107,15 @@ auto_reconnect(default) -> true;
 auto_reconnect(_) -> undefined.
 auto_reconnect(_) -> undefined.
 
 
 cacertfile(type) -> string();
 cacertfile(type) -> string();
-cacertfile(default) -> "";
+cacertfile(nullable) -> true;
 cacertfile(_) -> undefined.
 cacertfile(_) -> undefined.
 
 
 keyfile(type) -> string();
 keyfile(type) -> string();
-keyfile(default) -> "";
+keyfile(nullable) -> true;
 keyfile(_) -> undefined.
 keyfile(_) -> undefined.
 
 
 certfile(type) -> string();
 certfile(type) -> string();
-certfile(default) -> "";
+certfile(nullable) -> true;
 certfile(_) -> undefined.
 certfile(_) -> undefined.
 
 
 verify(type) -> boolean();
 verify(type) -> boolean();