From 751f7a24e9c6368e82a349ae4f1a449b73233d2a Mon Sep 17 00:00:00 2001 From: zmstone Date: Tue, 11 Jun 2024 19:31:11 +0200 Subject: [PATCH] feat(authn): support ${cert_pem} placeholder --- apps/emqx/include/emqx_placeholder.hrl | 2 ++ apps/emqx/src/emqx_channel.erl | 22 +++++++++++++++---- .../src/emqx_authn/emqx_authn_utils.erl | 3 +++ .../test/emqx_authn_http_SUITE.erl | 12 +++++++--- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/apps/emqx/include/emqx_placeholder.hrl b/apps/emqx/include/emqx_placeholder.hrl index 31ce6a070..7c538f4cb 100644 --- a/apps/emqx/include/emqx_placeholder.hrl +++ b/apps/emqx/include/emqx_placeholder.hrl @@ -28,8 +28,10 @@ %% cert -define(VAR_CERT_SUBJECT, "cert_subject"). -define(VAR_CERT_CN_NAME, "cert_common_name"). +-define(VAR_CERT_PEM, "cert_pem"). -define(PH_CERT_SUBJECT, ?PH(?VAR_CERT_SUBJECT)). -define(PH_CERT_CN_NAME, ?PH(?VAR_CERT_CN_NAME)). +-define(PH_CERT_PEM, ?PH(?VAR_CERT_PEM)). %% MQTT/Gateway -define(VAR_PASSWORD, "password"). diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index eb54f6ba1..acc4538bd 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1722,6 +1722,16 @@ count_flapping_event(_ConnPkt, #channel{clientinfo = ClientInfo}) -> %%-------------------------------------------------------------------- %% Authenticate +%% If peercert exists, add it as `cert_pem` credential field. +maybe_add_cert(Map, #channel{conninfo = ConnInfo}) -> + maybe_add_cert(Map, ConnInfo); +maybe_add_cert(Map, #{peercert := PeerCert}) when is_binary(PeerCert) -> + %% NOTE: it's raw binary at this point, + %% encoding to PEM (base64) is done lazy in emqx_authn_utils:render_var + Map#{cert_pem => PeerCert}; +maybe_add_cert(Map, _) -> + Map. + authenticate( ?CONNECT_PACKET( #mqtt_packet_connect{ @@ -1734,20 +1744,23 @@ authenticate( auth_cache = AuthCache } = Channel ) -> + %% Auth with CONNECT packet for MQTT v5 AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), - do_authenticate( + Credential0 = ClientInfo#{ auth_method => AuthMethod, auth_data => AuthData, auth_cache => AuthCache }, - Channel - ); + Credential = maybe_add_cert(Credential0, Channel), + do_authenticate(Credential, Channel); authenticate( ?CONNECT_PACKET(#mqtt_packet_connect{password = Password}), #channel{clientinfo = ClientInfo} = Channel ) -> - do_authenticate(ClientInfo#{password => Password}, Channel); + %% Auth with CONNECT packet for MQTT v3 + Credential = maybe_add_cert(ClientInfo#{password => Password}, Channel), + do_authenticate(Credential, Channel); authenticate( ?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properties), #channel{ @@ -1756,6 +1769,7 @@ authenticate( auth_cache = AuthCache } = Channel ) -> + %% Enhanced auth case emqx_mqtt_props:get('Authentication-Method', ConnProps, undefined) of AuthMethod -> AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined), diff --git a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl index a2050fbf0..8d4d245da 100644 --- a/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl +++ b/apps/emqx_auth/src/emqx_authn/emqx_authn_utils.erl @@ -55,6 +55,7 @@ ?VAR_PEERHOST, ?VAR_CERT_SUBJECT, ?VAR_CERT_CN_NAME, + ?VAR_CERT_PEM, ?VAR_NS_CLIENT_ATTRS ]). @@ -357,6 +358,8 @@ render_var(_, undefined) -> % Any allowed but undefined binding will be replaced with empty string, even when % rendering SQL values. <<>>; +render_var(?VAR_CERT_PEM, Value) -> + base64:encode(Value); render_var(?VAR_PEERHOST, Value) -> inet:ntoa(Value); render_var(_Name, Value) -> diff --git a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl index 54e1f4b11..864aa6e0e 100644 --- a/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl +++ b/apps/emqx_auth_http/test/emqx_authn_http_SUITE.erl @@ -37,6 +37,7 @@ protocol => mqtt, cert_subject => <<"cert_subject_data">>, cert_common_name => <<"cert_common_name_data">>, + cert_pem => <<"fake_raw_cert_to_be_base64_encoded">>, client_attrs => #{<<"group">> => <<"g1">>} }). @@ -222,7 +223,8 @@ t_no_value_for_placeholder(_Config) -> {ok, RawBody, Req1} = cowboy_req:read_body(Req0), #{ <<"cert_subject">> := <<"">>, - <<"cert_common_name">> := <<"">> + <<"cert_common_name">> := <<"">>, + <<"cert_pem">> := <<"">> } = emqx_utils_json:decode(RawBody, [return_maps]), Req = cowboy_req:reply( 200, @@ -238,7 +240,8 @@ t_no_value_for_placeholder(_Config) -> <<"headers">> => #{<<"content-type">> => <<"application/json">>}, <<"body">> => #{ <<"cert_subject">> => ?PH_CERT_SUBJECT, - <<"cert_common_name">> => ?PH_CERT_CN_NAME + <<"cert_common_name">> => ?PH_CERT_CN_NAME, + <<"cert_pem">> => ?PH_CERT_PEM } }, @@ -251,7 +254,7 @@ t_no_value_for_placeholder(_Config) -> ok = emqx_authn_http_test_server:set_handler(Handler), - Credentials = maps:without([cert_subject, cert_common_name], ?CREDENTIALS), + Credentials = maps:without([cert_subject, cert_common_name, cert_pem], ?CREDENTIALS), ?assertMatch({ok, _}, emqx_access_control:authenticate(Credentials)), @@ -656,8 +659,10 @@ samples() -> <<"peerhost">> := <<"127.0.0.1">>, <<"cert_subject">> := <<"cert_subject_data">>, <<"cert_common_name">> := <<"cert_common_name_data">>, + <<"cert_pem">> := CertPem, <<"the_group">> := <<"g1">> } = emqx_utils_json:decode(RawBody, [return_maps]), + <<"fake_raw_cert_to_be_base64_encoded">> = base64:decode(CertPem), Req = cowboy_req:reply( 200, #{<<"content-type">> => <<"application/json">>}, @@ -676,6 +681,7 @@ samples() -> <<"peerhost">> => ?PH_PEERHOST, <<"cert_subject">> => ?PH_CERT_SUBJECT, <<"cert_common_name">> => ?PH_CERT_CN_NAME, + <<"cert_pem">> => ?PH_CERT_PEM, <<"the_group">> => <<"${client_attrs.group}">> } },