Extract client_id from client cert

This commit is contained in:
Marcial Rosales 2024-08-30 11:35:28 +01:00
parent 818edefa43
commit 1abc4ed02f
7 changed files with 212 additions and 35 deletions

View File

@ -10,7 +10,7 @@
-include_lib("public_key/include/public_key.hrl"). -include_lib("public_key/include/public_key.hrl").
-export([peer_cert_issuer/1, peer_cert_subject/1, peer_cert_validity/1]). -export([peer_cert_issuer/1, peer_cert_subject/1, peer_cert_validity/1]).
-export([peer_cert_subject_items/2, peer_cert_auth_name/1]). -export([peer_cert_subject_items/2, peer_cert_auth_name/1, peer_cert_auth_name/2]).
-export([cipher_suites_erlang/2, cipher_suites_erlang/1, -export([cipher_suites_erlang/2, cipher_suites_erlang/1,
cipher_suites_openssl/2, cipher_suites_openssl/1, cipher_suites_openssl/2, cipher_suites_openssl/1,
cipher_suites/1]). cipher_suites/1]).
@ -18,7 +18,7 @@
%%-------------------------------------------------------------------------- %%--------------------------------------------------------------------------
-export_type([certificate/0]). -export_type([certificate/0, ssl_cert_login_type/0]).
% Due to API differences between OTP releases. % Due to API differences between OTP releases.
-dialyzer(no_missing_calls). -dialyzer(no_missing_calls).
@ -109,28 +109,51 @@ peer_cert_subject_alternative_names(Cert, Type) ->
peer_cert_validity(Cert) -> peer_cert_validity(Cert) ->
rabbit_cert_info:validity(Cert). rabbit_cert_info:validity(Cert).
-type ssl_cert_login_type() ::
{subject_alternative_name | subject_alt_name, atom(), integer()} |
{distinguished_name | common_name, undefined, undefined }.
-spec extract_ssl_cert_login_settings() -> none | ssl_cert_login_type().
extract_ssl_cert_login_settings() ->
case application:get_env(rabbit, ssl_cert_login_from) of
{ok, Mode} ->
case Mode of
subject_alternative_name -> extract_san_login_type(Mode);
subject_alt_name -> extract_san_login_type(Mode);
_ -> {Mode, undefined, undefined}
end;
undefined -> none
end.
extract_san_login_type(Mode) ->
{Mode,
application:get_env(rabbit, ssl_cert_login_san_type, dns),
application:get_env(rabbit, ssl_cert_login_san_index, 0)
}.
%% Extract a username from the certificate %% Extract a username from the certificate
-spec peer_cert_auth_name(certificate()) -> binary() | 'not_found' | 'unsafe'. -spec peer_cert_auth_name(certificate()) -> binary() | 'not_found' | 'unsafe'.
peer_cert_auth_name(Cert) -> peer_cert_auth_name(Cert) ->
{ok, Mode} = application:get_env(rabbit, ssl_cert_login_from), case extract_ssl_cert_login_settings() of
peer_cert_auth_name(Mode, Cert). none -> 'not_found';
Settings -> peer_cert_auth_name(Settings, Cert)
end.
-spec peer_cert_auth_name(atom(), certificate()) -> binary() | 'not_found' | 'unsafe'. -spec peer_cert_auth_name(ssl_cert_login_type(), certificate()) -> binary() | 'not_found' | 'unsafe'.
peer_cert_auth_name(distinguished_name, Cert) -> peer_cert_auth_name({distinguished_name, _, _}, Cert) ->
case auth_config_sane() of case auth_config_sane() of
true -> iolist_to_binary(peer_cert_subject(Cert)); true -> iolist_to_binary(peer_cert_subject(Cert));
false -> unsafe false -> unsafe
end; end;
peer_cert_auth_name(subject_alt_name, Cert) -> peer_cert_auth_name({subject_alt_name, Type, Index0}, Cert) ->
peer_cert_auth_name(subject_alternative_name, Cert); peer_cert_auth_name({subject_alternative_name, Type, Index0}, Cert);
peer_cert_auth_name(subject_alternative_name, Cert) -> peer_cert_auth_name({subject_alternative_name, Type, Index0}, Cert) ->
case auth_config_sane() of case auth_config_sane() of
true -> true ->
Type = application:get_env(rabbit, ssl_cert_login_san_type, dns),
%% lists:nth/2 is 1-based %% lists:nth/2 is 1-based
Index = application:get_env(rabbit, ssl_cert_login_san_index, 0) + 1, Index = Index0 + 1,
OfType = peer_cert_subject_alternative_names(Cert, otp_san_type(Type)), OfType = peer_cert_subject_alternative_names(Cert, otp_san_type(Type)),
rabbit_log:debug("Peer certificate SANs of type ~ts: ~tp, index to use with lists:nth/2: ~b", [Type, OfType, Index]), rabbit_log:debug("Peer certificate SANs of type ~ts: ~tp, index to use with lists:nth/2: ~b", [Type, OfType, Index]),
case length(OfType) of case length(OfType) of
@ -152,7 +175,7 @@ peer_cert_auth_name(subject_alternative_name, Cert) ->
false -> unsafe false -> unsafe
end; end;
peer_cert_auth_name(common_name, Cert) -> peer_cert_auth_name({common_name, _, _}, Cert) ->
%% If there is more than one CN then we join them with "," in a %% If there is more than one CN then we join them with "," in a
%% vaguely DN-like way. But this is more just so we do something %% vaguely DN-like way. But this is more just so we do something
%% more intelligent than crashing, if you actually want to escape %% more intelligent than crashing, if you actually want to escape

View File

@ -49,6 +49,7 @@ keyUsage = keyCertSign, cRLSign
[ client_ca_extensions ] [ client_ca_extensions ]
basicConstraints = CA:false basicConstraints = CA:false
keyUsage = digitalSignature,keyEncipherment keyUsage = digitalSignature,keyEncipherment
subjectAltName = @client_alt_names
[ server_ca_extensions ] [ server_ca_extensions ]
basicConstraints = CA:false basicConstraints = CA:false
@ -59,3 +60,6 @@ subjectAltName = @server_alt_names
[ server_alt_names ] [ server_alt_names ]
DNS.1 = @HOSTNAME@ DNS.1 = @HOSTNAME@
DNS.2 = localhost DNS.2 = localhost
[ client_alt_names ]
DNS.1 = rabbit_client_id

View File

@ -135,7 +135,7 @@ rabbitmq_integration_suite(
"test/rabbit_auth_backend_mqtt_mock.beam", "test/rabbit_auth_backend_mqtt_mock.beam",
"test/util.beam", "test/util.beam",
], ],
shard_count = 14, shard_count = 18,
runtime_deps = [ runtime_deps = [
"@emqtt//:erlang_app", "@emqtt//:erlang_app",
"@meck//:erlang_app", "@meck//:erlang_app",

View File

@ -156,6 +156,20 @@ end}.
{datatype, {enum, [true, false]}}]}. {datatype, {enum, [true, false]}}]}.
{mapping, "mqtt.ssl_cert_client_id_from", "rabbitmq_mqtt.ssl_cert_client_id_from", [
{datatype, {enum, [distinguished_name, subject_alternative_name]}}
]}.
{mapping, "mqtt.ssl_cert_login_san_type", "rabbitmq_mqtt.ssl_cert_login_san_type", [
{datatype, {enum, [dns, ip, email, uri, other_name]}}
]}.
{mapping, "mqtt.ssl_cert_login_san_index", "rabbitmq_mqtt.ssl_cert_login_san_index", [
{datatype, integer}, {validators, ["non_negative_integer"]}
]}.
%% TCP/Socket options (as per the broker configuration). %% TCP/Socket options (as per the broker configuration).
%% %%
%% {tcp_listen_options, [{backlog, 128}, %% {tcp_listen_options, [{backlog, 128},

View File

@ -182,9 +182,9 @@ process_connect(
Result0 = Result0 =
maybe maybe
ok ?= check_extended_auth(ConnectProps), ok ?= check_extended_auth(ConnectProps),
{ok, ClientId} ?= ensure_client_id(ClientId0, CleanStart, ProtoVer), {ok, ClientId1} ?= extract_client_id_from_certificate(ClientId0, Socket),
{ok, ClientId} ?= ensure_client_id(ClientId1, CleanStart, ProtoVer),
{ok, Username1, Password} ?= check_credentials(Username0, Password0, SslLoginName, PeerIp), {ok, Username1, Password} ?= check_credentials(Username0, Password0, SslLoginName, PeerIp),
{VHostPickedUsing, {VHost, Username2}} = get_vhost(Username1, SslLoginName, Port), {VHostPickedUsing, {VHost, Username2}} = get_vhost(Username1, SslLoginName, Port),
?LOG_DEBUG("MQTT connection ~s picked vhost using ~s", [ConnName0, VHostPickedUsing]), ?LOG_DEBUG("MQTT connection ~s picked vhost using ~s", [ConnName0, VHostPickedUsing]),
ok ?= check_vhost_exists(VHost, Username2, PeerIp), ok ?= check_vhost_exists(VHost, Username2, PeerIp),
@ -642,6 +642,26 @@ check_credentials(Username, Password, SslLoginName, PeerIp) ->
{error, ?RC_BAD_USER_NAME_OR_PASSWORD} {error, ?RC_BAD_USER_NAME_OR_PASSWORD}
end. end.
%% Extract client_id from the certificate provided it was configured to do so and
%% it is possible to extract it else returns the client_id passed as parameter
-spec extract_client_id_from_certificate(client_id(), rabbit_net:socket()) -> {ok, client_id()} | {error, reason_code()}.
extract_client_id_from_certificate(Client0, Socket) ->
case extract_ssl_cert_client_id_settings() of
none -> {ok, Client0};
SslClientIdSettings ->
case ssl_client_id(Socket, SslClientIdSettings) of
none ->
{ok, Client0};
V when V == Client0 ->
{ok, Client0};
V ->
?LOG_ERROR(
"MQTT login failed: client_id in cert (~p) does not match client_id in protocol (~p)",
[V, Client0]),
{error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}
end
end.
-spec ensure_client_id(client_id(), boolean(), protocol_version()) -> -spec ensure_client_id(client_id(), boolean(), protocol_version()) ->
{ok, client_id()} | {error, reason_code()}. {ok, client_id()} | {error, reason_code()}.
ensure_client_id(<<>>, _CleanStart = false, ProtoVer) ensure_client_id(<<>>, _CleanStart = false, ProtoVer)
@ -1029,16 +1049,9 @@ check_vhost_alive(VHost) ->
end. end.
check_user_login(VHost, Username, Password, ClientId, PeerIp, ConnName) -> check_user_login(VHost, Username, Password, ClientId, PeerIp, ConnName) ->
AuthProps = case Password of AuthProps = [{vhost, VHost},
none -> {client_id, ClientId},
%% SSL user name provided. {password, Password}],
%% Authenticating using username only.
[];
_ ->
[{password, Password},
{vhost, VHost},
{client_id, ClientId}]
end,
case rabbit_access_control:check_user_login(Username, AuthProps) of case rabbit_access_control:check_user_login(Username, AuthProps) of
{ok, User = #user{username = Username1}} -> {ok, User = #user{username = Username1}} ->
notify_auth_result(user_authentication_success, Username1, ConnName), notify_auth_result(user_authentication_success, Username1, ConnName),
@ -2292,6 +2305,37 @@ ssl_login_name(Sock) ->
nossl -> none nossl -> none
end. end.
-spec extract_ssl_cert_client_id_settings() -> none | rabbit_ssl:ssl_cert_login_type().
extract_ssl_cert_client_id_settings() ->
case application:get_env(?APP_NAME, ssl_cert_client_id_from) of
{ok, Mode} ->
case Mode of
subject_alternative_name -> extract_client_id_san_type(Mode);
_ -> {Mode, undefined, undefined}
end;
undefined -> none
end.
extract_client_id_san_type(Mode) ->
{Mode,
application:get_env(?APP_NAME, ssl_cert_client_id_san_type, dns),
application:get_env(?APP_NAME, ssl_cert_client_id_san_index, 0)
}.
-spec ssl_client_id(rabbit_net:socket(), rabbit_ssl:ssl_cert_login_type()) ->
none | binary().
ssl_client_id(Sock, SslClientIdSettings) ->
case rabbit_net:peercert(Sock) of
{ok, C} -> case rabbit_ssl:peer_cert_auth_name(SslClientIdSettings, C) of
unsafe -> none;
not_found -> none;
Name -> Name
end;
{error, no_peercert} -> none;
nossl -> none
end.
-spec proto_integer_to_atom(protocol_version()) -> protocol_version_atom(). -spec proto_integer_to_atom(protocol_version()) -> protocol_version_atom().
proto_integer_to_atom(3) -> proto_integer_to_atom(3) ->
?MQTT_PROTO_V3; ?MQTT_PROTO_V3;

View File

@ -68,6 +68,13 @@ sub_groups() ->
ssl_user_vhost_parameter_mapping_vhost_does_not_exist, ssl_user_vhost_parameter_mapping_vhost_does_not_exist,
ssl_user_cert_vhost_mapping_takes_precedence_over_port_vhost_mapping ssl_user_cert_vhost_mapping_takes_precedence_over_port_vhost_mapping
]}, ]},
{ssl_user_with_client_id_in_cert_san_dns, [],
[client_id_from_cert_san_dns,
invalid_client_id_from_cert_san_dns
]},
{ssl_user_with_client_id_in_cert_dn, [],
[client_id_from_cert_dn
]},
{no_ssl_user, [shuffle], {no_ssl_user, [shuffle],
[anonymous_auth_failure, [anonymous_auth_failure,
user_credentials_auth, user_credentials_auth,
@ -194,14 +201,27 @@ mqtt_config(no_ssl_user) ->
mqtt_config(client_id_propagation) -> mqtt_config(client_id_propagation) ->
{rabbitmq_mqtt, [{ssl_cert_login, true}, {rabbitmq_mqtt, [{ssl_cert_login, true},
{allow_anonymous, true}]}; {allow_anonymous, true}]};
mqtt_config(ssl_user_with_client_id_in_cert_san_dns) ->
{rabbitmq_mqtt, [{ssl_cert_login, true},
{allow_anonymous, false},
{ssl_cert_client_id_from, subject_alternative_name},
{ssl_cert_client_id_san_type, dns}]};
mqtt_config(ssl_user_with_client_id_in_cert_dn) ->
{rabbitmq_mqtt, [{ssl_cert_login, true},
{allow_anonymous, false},
{ssl_cert_client_id_from, distinguished_name}
]};
mqtt_config(_) -> mqtt_config(_) ->
undefined. undefined.
auth_config(client_id_propagation) -> auth_config(T) when T == client_id_propagation;
T == ssl_user_with_client_id_in_cert_san_dns;
T == ssl_user_with_client_id_in_cert_dn ->
{rabbit, [ {rabbit, [
{auth_backends, [rabbit_auth_backend_mqtt_mock]} {auth_backends, [rabbit_auth_backend_mqtt_mock]}
] ]
}; };
auth_config(_) -> auth_config(_) ->
undefined. undefined.
@ -292,9 +312,24 @@ init_per_testcase(T, Config)
v4 -> {skip, "Will Delay Interval is an MQTT 5.0 feature"}; v4 -> {skip, "Will Delay Interval is an MQTT 5.0 feature"};
v5 -> testcase_started(Config, T) v5 -> testcase_started(Config, T)
end; end;
init_per_testcase(T, Config)
when T =:= client_id_propagation;
T =:= invalid_client_id_from_cert_san_dns;
T =:= client_id_from_cert_san_dns;
T =:= client_id_from_cert_dn ->
SetupProcess = setup_rabbit_auth_backend_mqtt_mock(Config),
rabbit_ct_helpers:set_config(Config, {mock_setup_process, SetupProcess});
init_per_testcase(Testcase, Config) -> init_per_testcase(Testcase, Config) ->
testcase_started(Config, Testcase). testcase_started(Config, Testcase).
get_client_cert_subject(Config) ->
CertsDir = ?config(rmq_certsdir, Config),
CertFile = filename:join([CertsDir, "client", "cert.pem"]),
{ok, CertBin} = file:read_file(CertFile),
[{'Certificate', Cert, not_encrypted}] = public_key:pem_decode(CertBin),
iolist_to_binary(rpc(Config, 0, rabbit_ssl, peer_cert_subject, [Cert])).
set_cert_user_on_default_vhost(Config) -> set_cert_user_on_default_vhost(Config) ->
CertsDir = ?config(rmq_certsdir, Config), CertsDir = ?config(rmq_certsdir, Config),
CertFile = filename:join([CertsDir, "client", "cert.pem"]), CertFile = filename:join([CertsDir, "client", "cert.pem"]),
@ -404,6 +439,15 @@ end_per_testcase(T, Config) when T == queue_bind_permission;
file:write_file(?config(log_location, Config), <<>>), file:write_file(?config(log_location, Config), <<>>),
rabbit_ct_helpers:testcase_finished(Config, T); rabbit_ct_helpers:testcase_finished(Config, T);
end_per_testcase(T, Config)
when T =:= client_id_propagation;
T =:= invalid_client_id_from_cert_san_dns;
T =:= client_id_from_cert_san_dns;
T =:= client_id_from_cert_dn ->
SetupProcess = ?config(mock_setup_process, Config),
SetupProcess ! stop;
end_per_testcase(Testcase, Config) -> end_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_finished(Config, Testcase). rabbit_ct_helpers:testcase_finished(Config, Testcase).
@ -455,6 +499,36 @@ user_credentials_auth(Config) ->
fun(Conf) -> connect_user(<<"non-existing-vhost:guest">>, <<"guest">>, Conf) end, fun(Conf) -> connect_user(<<"non-existing-vhost:guest">>, <<"guest">>, Conf) end,
Config). Config).
client_id_from_cert_san_dns(Config) ->
ExpectedClientId = <<"rabbit_client_id">>, % Found in the client's certificate as SAN type CLIENT_ID
MqttClientId = ExpectedClientId,
{ok, C} = connect_ssl(MqttClientId, Config),
{ok, _} = emqtt:connect(C),
[{authentication, AuthProps}] = rpc(Config, 0,
rabbit_auth_backend_mqtt_mock,
get,
[authentication]),
?assertEqual(ExpectedClientId, proplists:get_value(client_id, AuthProps)),
ok = emqtt:disconnect(C).
client_id_from_cert_dn(Config) ->
ExpectedClientId = get_client_cert_subject(Config), % subject = distinguished_name
MqttClientId = ExpectedClientId,
{ok, C} = connect_ssl(MqttClientId, Config),
{ok, _} = emqtt:connect(C),
[{authentication, AuthProps}] = rpc(Config, 0,
rabbit_auth_backend_mqtt_mock,
get,
[authentication]),
?assertEqual(ExpectedClientId, proplists:get_value(client_id, AuthProps)),
ok = emqtt:disconnect(C).
invalid_client_id_from_cert_san_dns(Config) ->
MqttClientId = <<"other_client_id">>,
{ok, C} = connect_ssl(MqttClientId, Config),
?assertMatch({error, _}, emqtt:connect(C)),
unlink(C).
ssl_user_vhost_parameter_mapping_success(Config) -> ssl_user_vhost_parameter_mapping_success(Config) ->
expect_successful_connection(fun connect_ssl/1, Config). expect_successful_connection(fun connect_ssl/1, Config).
@ -506,6 +580,9 @@ connect_anonymous(Config, ClientId) ->
{proto_ver, ?config(mqtt_version, Config)}]). {proto_ver, ?config(mqtt_version, Config)}]).
connect_ssl(Config) -> connect_ssl(Config) ->
connect_ssl(<<"simpleClient">>, Config).
connect_ssl(ClientId, Config) ->
CertsDir = ?config(rmq_certsdir, Config), CertsDir = ?config(rmq_certsdir, Config),
SSLConfig = [{cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])}, SSLConfig = [{cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])},
{certfile, filename:join([CertsDir, "client", "cert.pem"])}, {certfile, filename:join([CertsDir, "client", "cert.pem"])},
@ -514,12 +591,12 @@ connect_ssl(Config) ->
P = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mqtt_tls), P = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mqtt_tls),
emqtt:start_link([{host, "localhost"}, emqtt:start_link([{host, "localhost"},
{port, P}, {port, P},
{clientid, <<"simpleClient">>}, {clientid, ClientId},
{proto_ver, ?config(mqtt_version, Config)}, {proto_ver, ?config(mqtt_version, Config)},
{ssl, true}, {ssl, true},
{ssl_opts, SSLConfig}]). {ssl_opts, SSLConfig}]).
client_id_propagation(Config) -> setup_rabbit_auth_backend_mqtt_mock(Config) ->
ok = rabbit_ct_broker_helpers:add_code_path_to_all_nodes(Config, ok = rabbit_ct_broker_helpers:add_code_path_to_all_nodes(Config,
rabbit_auth_backend_mqtt_mock), rabbit_auth_backend_mqtt_mock),
%% setup creates the ETS table required for the mqtt auth mock %% setup creates the ETS table required for the mqtt auth mock
@ -530,11 +607,13 @@ client_id_propagation(Config) ->
rpc(Config, 0, rabbit_auth_backend_mqtt_mock, setup, [Self]) rpc(Config, 0, rabbit_auth_backend_mqtt_mock, setup, [Self])
end), end),
%% the setup process will notify us %% the setup process will notify us
SetupProcess = receive receive
{ok, SP} -> SP {ok, SP} -> SP
after after
3000 -> ct:fail("timeout waiting for rabbit_auth_backend_mqtt_mock:setup/1") 3000 -> ct:fail("timeout waiting for rabbit_auth_backend_mqtt_mock:setup/1")
end, end.
client_id_propagation(Config) ->
ClientId = <<"client-id-propagation">>, ClientId = <<"client-id-propagation">>,
{ok, C} = connect_user(<<"fake-user">>, <<"fake-password">>, {ok, C} = connect_user(<<"fake-user">>, <<"fake-password">>,
Config, ClientId), Config, ClientId),
@ -565,11 +644,8 @@ client_id_propagation(Config) ->
VariableMap = maps:get(variable_map, TopicContext), VariableMap = maps:get(variable_map, TopicContext),
?assertEqual(ClientId, maps:get(<<"client_id">>, VariableMap)), ?assertEqual(ClientId, maps:get(<<"client_id">>, VariableMap)),
ok = emqtt:disconnect(C), emqtt:disconnect(C).
SetupProcess ! stop,
ok.
%% These tests try to cover all operations that are listed in the %% These tests try to cover all operations that are listed in the
%% table in https://www.rabbitmq.com/access-control.html#authorisation %% table in https://www.rabbitmq.com/access-control.html#authorisation

View File

@ -95,6 +95,22 @@
"ssl_cert_login_from = common_name", "ssl_cert_login_from = common_name",
[{rabbit,[{ssl_cert_login_from,common_name}]}], [{rabbit,[{ssl_cert_login_from,common_name}]}],
[rabbitmq_mqtt]}, [rabbitmq_mqtt]},
{ssl_cert_client_id_from_common_name,
"mqtt.ssl_cert_client_id_from = distinguished_name",
[{rabbitmq_mqtt,[{ssl_cert_client_id_from,distinguished_name}]}],
[rabbitmq_mqtt]},
{ssl_cert_login_dns_san_type,
"mqtt.ssl_cert_login_san_type = dns",
[{rabbitmq_mqtt,[{ssl_cert_login_san_type,dns}]}],
[rabbitmq_mqtt]},
{ssl_cert_login_other_name_san_type,
"mqtt.ssl_cert_login_san_type = other_name",
[{rabbitmq_mqtt,[{ssl_cert_login_san_type,other_name}]}],
[rabbitmq_mqtt]},
{ssl_cert_login_san_index,
"mqtt.ssl_cert_login_san_index = 0",
[{rabbitmq_mqtt,[{ssl_cert_login_san_index,0}]}],
[rabbitmq_mqtt]},
{proxy_protocol, {proxy_protocol,
"listeners.tcp.default = 5672 "listeners.tcp.default = 5672
mqtt.allow_anonymous = true mqtt.allow_anonymous = true