Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract MQTT client_id from client certificate and propagate to authnz backends #11702

Merged
merged 2 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions deps/rabbit/src/rabbit_ssl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
-include_lib("public_key/include/public_key.hrl").

-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,
cipher_suites_openssl/2, cipher_suites_openssl/1,
cipher_suites/1]).
-export([info/2, cert_info/2]).

%%--------------------------------------------------------------------------

-export_type([certificate/0]).
-export_type([certificate/0, ssl_cert_login_type/0]).

% Due to API differences between OTP releases.
-dialyzer(no_missing_calls).
Expand Down Expand Up @@ -109,28 +109,51 @@ peer_cert_subject_alternative_names(Cert, Type) ->
peer_cert_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);
MarcialRosales marked this conversation as resolved.
Show resolved Hide resolved
_ -> {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
-spec peer_cert_auth_name(certificate()) -> binary() | 'not_found' | 'unsafe'.
peer_cert_auth_name(Cert) ->
{ok, Mode} = application:get_env(rabbit, ssl_cert_login_from),
peer_cert_auth_name(Mode, Cert).
case extract_ssl_cert_login_settings() of
none -> 'not_found';
Settings -> peer_cert_auth_name(Settings, Cert)
end.

-spec peer_cert_auth_name(atom(), certificate()) -> binary() | 'not_found' | 'unsafe'.
peer_cert_auth_name(distinguished_name, Cert) ->
-spec peer_cert_auth_name(ssl_cert_login_type(), certificate()) -> binary() | 'not_found' | 'unsafe'.
peer_cert_auth_name({distinguished_name, _, _}, Cert) ->
case auth_config_sane() of
true -> iolist_to_binary(peer_cert_subject(Cert));
false -> unsafe
end;

peer_cert_auth_name(subject_alt_name, Cert) ->
peer_cert_auth_name(subject_alternative_name, Cert);
peer_cert_auth_name({subject_alt_name, Type, Index0}, 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
true ->
Type = application:get_env(rabbit, ssl_cert_login_san_type, dns),
%% 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)),
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
Expand All @@ -152,7 +175,7 @@ peer_cert_auth_name(subject_alternative_name, Cert) ->
false -> unsafe
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
%% vaguely DN-like way. But this is more just so we do something
%% more intelligent than crashing, if you actually want to escape
Expand Down
4 changes: 4 additions & 0 deletions deps/rabbitmq_ct_helpers/tools/tls-certs/openssl.cnf.in
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ keyUsage = keyCertSign, cRLSign
[ client_ca_extensions ]
basicConstraints = CA:false
keyUsage = digitalSignature,keyEncipherment
subjectAltName = @client_alt_names

[ server_ca_extensions ]
basicConstraints = CA:false
Expand All @@ -59,3 +60,6 @@ subjectAltName = @server_alt_names
[ server_alt_names ]
DNS.1 = @HOSTNAME@
DNS.2 = localhost

[ client_alt_names ]
DNS.1 = rabbit_client_id
2 changes: 1 addition & 1 deletion deps/rabbitmq_mqtt/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ rabbitmq_integration_suite(
"test/rabbit_auth_backend_mqtt_mock.beam",
"test/util.beam",
],
shard_count = 14,
shard_count = 18,
runtime_deps = [
"@emqtt//:erlang_app",
"@meck//:erlang_app",
Expand Down
14 changes: 14 additions & 0 deletions deps/rabbitmq_mqtt/priv/schema/rabbitmq_mqtt.schema
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@ end}.
{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_listen_options, [{backlog, 128},
Expand Down
68 changes: 56 additions & 12 deletions deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ process_connect(
Result0 =
maybe
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),

{VHostPickedUsing, {VHost, Username2}} = get_vhost(Username1, SslLoginName, Port),
?LOG_DEBUG("MQTT connection ~s picked vhost using ~s", [ConnName0, VHostPickedUsing]),
ok ?= check_vhost_exists(VHost, Username2, PeerIp),
Expand Down Expand Up @@ -642,6 +642,26 @@ check_credentials(Username, Password, SslLoginName, PeerIp) ->
{error, ?RC_BAD_USER_NAME_OR_PASSWORD}
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};
Client0 ->
{ok, Client0};
Other ->
?LOG_ERROR(
"MQTT login failed: client_id in the certificate (~tp) does not match the client-provided ID (~p)",
[Other, Client0]),
{error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}
end
end.

-spec ensure_client_id(client_id(), boolean(), protocol_version()) ->
{ok, client_id()} | {error, reason_code()}.
ensure_client_id(<<>>, _CleanStart = false, ProtoVer)
Expand Down Expand Up @@ -1029,16 +1049,9 @@ check_vhost_alive(VHost) ->
end.

check_user_login(VHost, Username, Password, ClientId, PeerIp, ConnName) ->
AuthProps = case Password of
none ->
%% SSL user name provided.
%% Authenticating using username only.
[];
_ ->
[{password, Password},
{vhost, VHost},
{client_id, ClientId}]
end,
AuthProps = [{vhost, VHost},
{client_id, ClientId},
{password, Password}],
case rabbit_access_control:check_user_login(Username, AuthProps) of
{ok, User = #user{username = Username1}} ->
notify_auth_result(user_authentication_success, Username1, ConnName),
Expand Down Expand Up @@ -2292,6 +2305,37 @@ ssl_login_name(Sock) ->
nossl -> none
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().
proto_integer_to_atom(3) ->
?MQTT_PROTO_V3;
Expand Down
Loading
Loading