Skip to content

Commit

Permalink
Merge pull request #11702 from rabbitmq/mqtt-extract-client-id-from-cert
Browse files Browse the repository at this point in the history
Extract MQTT client_id from client certificate and propagate to authnz backends
  • Loading branch information
michaelklishin authored Sep 3, 2024
2 parents e396dd1 + 606a651 commit 94baa7c
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 35 deletions.
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);
_ -> {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

0 comments on commit 94baa7c

Please sign in to comment.