[rabbitmq-discuss] Authenticate client using certificate only

jiri at krutil.com jiri at krutil.com
Thu Aug 19 09:51:08 BST 2010


> Just take the output of 'hg diff' and post it here or somewhere else  
> we can see. Thanks.

Attached is a patch against the default branch. I'm new to Erlang so  
please forgive my coding style and feel free to change the code as you  
see fit.

Please let me know if this could become official.

Cheers
Jiri
-------------- next part --------------
diff -r a11bf1f62fbb include/rabbit.hrl
--- a/include/rabbit.hrl	Wed Aug 18 17:40:19 2010 +0100
+++ b/include/rabbit.hrl	Thu Aug 19 09:49:35 2010 +0100
@@ -68,7 +68,7 @@
 -record(basic_message, {exchange_name, routing_key, content, guid,
                         is_persistent}).
 
--record(ssl_socket, {tcp, ssl}).
+-record(ssl_socket, {tcp, ssl, cn}).
 -record(delivery, {mandatory, immediate, txn, sender, message}).
 -record(amqp_error, {name, explanation, method = none}).
 
diff -r a11bf1f62fbb src/rabbit_access_control.erl
--- a/src/rabbit_access_control.erl	Wed Aug 18 17:40:19 2010 +0100
+++ b/src/rabbit_access_control.erl	Thu Aug 19 09:49:35 2010 +0100
@@ -33,7 +33,7 @@
 -include_lib("stdlib/include/qlc.hrl").
 -include("rabbit.hrl").
 
--export([check_login/2, user_pass_login/2,
+-export([check_login/3, user_pass_login/2,
          check_vhost_access/2, check_resource_access/3]).
 -export([add_user/2, delete_user/1, change_password/2, list_users/0,
          lookup_user/1]).
@@ -52,10 +52,11 @@
 -type(password() :: binary()).
 -type(regexp() :: binary()).
 -type(scope() :: binary()).
+-type(socket() :: rabbit_networking:ip_port() | rabbit_types:ssl_socket()).
 
--spec(check_login/2 ::
-        (binary(), binary()) -> rabbit_types:user() |
-                                rabbit_types:channel_exit()).
+-spec(check_login/3 ::
+        (binary(), binary(), socket()) -> rabbit_types:user() |
+                                          rabbit_types:channel_exit()).
 -spec(user_pass_login/2 ::
         (username(), password())
         -> rabbit_types:user() | rabbit_types:channel_exit()).
@@ -95,33 +96,54 @@
 
 %% SASL PLAIN, as used by the Qpid Java client and our clients. Also,
 %% apparently, by OpenAMQ.
-check_login(<<"PLAIN">>, Response) ->
-    [User, Pass] = [list_to_binary(T) ||
-                       T <- string:tokens(binary_to_list(Response), [0])],
-    user_pass_login(User, Pass);
+check_login(<<"PLAIN">>, Response, Sock) ->
+    case is_record(Sock, ssl_socket) andalso Sock#ssl_socket.cn /= none of
+        true ->
+            certificate_login(Sock);
+        false ->
+            [User, Pass] = [list_to_binary(T) ||
+                               T <- string:tokens(binary_to_list(Response), [0])],
+            user_pass_login(User, Pass)
+    end;
 %% AMQPLAIN, as used by Qpid Python test suite. The 0-8 spec actually
 %% defines this as PLAIN, but in 0-9 that definition is gone, instead
 %% referring generically to "SASL security mechanism", i.e. the above.
-check_login(<<"AMQPLAIN">>, Response) ->
-    LoginTable = rabbit_binary_parser:parse_table(Response),
-    case {lists:keysearch(<<"LOGIN">>, 1, LoginTable),
-          lists:keysearch(<<"PASSWORD">>, 1, LoginTable)} of
-        {{value, {_, longstr, User}},
-         {value, {_, longstr, Pass}}} ->
-            user_pass_login(User, Pass);
-        _ ->
-            %% Is this an information leak?
-            rabbit_misc:protocol_error(
-              access_refused,
-              "AMQPPLAIN auth info ~w is missing LOGIN or PASSWORD field",
-              [LoginTable])
+check_login(<<"AMQPLAIN">>, Response, Sock) ->
+    case is_record(Sock, ssl_socket) andalso Sock#ssl_socket.cn /= none of
+        true ->
+            certificate_login(Sock);
+        false ->
+            LoginTable = rabbit_binary_parser:parse_table(Response),
+            case {lists:keysearch(<<"LOGIN">>, 1, LoginTable),
+                  lists:keysearch(<<"PASSWORD">>, 1, LoginTable)} of
+                {{value, {_, longstr, User}},
+                 {value, {_, longstr, Pass}}} ->
+                    user_pass_login(User, Pass);
+                _ ->
+                    %% Is this an information leak?
+                    rabbit_misc:protocol_error(
+                      access_refused,
+                      "AMQPPLAIN auth info ~w is missing LOGIN or PASSWORD field",
+                      [LoginTable])
+            end
     end;
 
-check_login(Mechanism, _Response) ->
+check_login(Mechanism, _Response, _Sock) ->
     rabbit_misc:protocol_error(
       access_refused, "unsupported authentication mechanism '~s'",
       [Mechanism]).
 
+certificate_login(Sock) ->
+    CN = list_to_binary(Sock#ssl_socket.cn),
+    ?LOGDEBUG("Login with cert (CN=~s)~n", [CN]),
+    case lookup_user(CN) of
+        {ok, U} ->
+            U;
+        {error, not_found} ->
+            rabbit_misc:protocol_error(
+              access_refused, "certificate login refused for Common Name '~s'", [CN])
+    end.
+
 user_pass_login(User, Pass) ->
     ?LOGDEBUG("Login with user ~p pass ~p~n", [User, Pass]),
     case lookup_user(User) of
diff -r a11bf1f62fbb src/rabbit_networking.erl
--- a/src/rabbit_networking.erl	Wed Aug 18 17:40:19 2010 +0100
+++ b/src/rabbit_networking.erl	Thu Aug 19 09:49:35 2010 +0100
@@ -45,6 +45,7 @@
          start_client/1, start_ssl_client/2]).
 
 -include("rabbit.hrl").
+-include_lib("public_key/include/OTP-PUB-KEY.hrl").
 -include_lib("kernel/include/inet.hrl").
 
 -define(RABBIT_TCP_OPTS, [
@@ -216,19 +217,65 @@
     start_client(
       Sock,
       fun (Sock1) ->
-              case catch ssl:ssl_accept(Sock1, SslOpts, ?SSL_TIMEOUT * 1000) of
-                  {ok, SslSock} ->
-                      rabbit_log:info("upgraded TCP connection ~p to SSL~n",
-                                      [self()]),
-                      {ok, #ssl_socket{tcp = Sock1, ssl = SslSock}};
-                  {error, Reason} ->
-                      {error, {ssl_upgrade_error, Reason}};
-                  {'EXIT', Reason} ->
-                      {error, {ssl_upgrade_failure, Reason}}
+         case catch ssl:ssl_accept(Sock1, SslOpts, ?SSL_TIMEOUT * 1000) of
+             {ok, SslSock} ->
+                 % authenticate with certificate or password?
+                 case proplists:get_value(authentication, SslOpts, password) of
+                     cert ->
+                         % retrieve client certificate and extract subject Common Name
+                         case ssl:peercert(SslSock, [ssl]) of
+                             {ok, Cert} ->
+                                 case extract_common_name(Cert) of
+                                     error ->
+                                         {error, {ssl_upgrade_error,
+                                             "Could not extract CommonName from client cert"}};
+                                     CommonName ->
+                                         rabbit_log:info(
+                                             "upgraded TCP connection ~p to SSL (CN=~p)~n",
+                                             [self(), CommonName]),
+                                         {ok, #ssl_socket{
+                                             tcp = Sock1, ssl = SslSock, cn = CommonName}}
+                                 end;
+                             {error, Reason} ->
+                                 {error, {ssl_upgrade_error, Reason}}
+                         end;
+                     _ ->
+                         rabbit_log:info("upgraded TCP connection ~p to SSL~n",
+                                         [self()]),
+                         {ok, #ssl_socket{tcp = Sock1, ssl = SslSock, cn = none}}
+                 end;
+             {error, Reason} ->
+                 {error, {ssl_upgrade_error, Reason}};
+             {'EXIT', Reason} ->
+                 {error, {ssl_upgrade_failure, Reason}}
 
-              end
+         end
       end).
 
+extract_common_name(Cert) ->
+    case is_record(Cert, 'OTPCertificate') of
+        true ->
+            TbsCert = Cert#'OTPCertificate'.tbsCertificate,
+            case is_record(TbsCert, 'OTPTBSCertificate') of
+                true ->
+                    Subject = TbsCert#'OTPTBSCertificate'.subject,
+                    case Subject of
+                        {rdnSequence, Attributes} ->
+                            find_attribute(Attributes, ?'id-at-commonName');
+                        _ -> error
+                    end;
+                false -> error
+            end;
+        false -> error
+    end.
+
+find_attribute([], _) -> error;
+find_attribute([Attr | Tail], Key) ->
+    case Attr of
+        [{'AttributeTypeAndValue', Key, {printableString, Value}}] -> Value;
+        _ -> find_attribute(Tail, Key)
+    end.
+
 connections() ->
     [Pid || {_, Pid, _, _} <- supervisor:which_children(
                                 rabbit_tcp_client_sup)].
diff -r a11bf1f62fbb src/rabbit_reader.erl
--- a/src/rabbit_reader.erl	Wed Aug 18 17:40:19 2010 +0100
+++ b/src/rabbit_reader.erl	Thu Aug 19 09:49:35 2010 +0100
@@ -719,7 +719,7 @@
                            connection = Connection =
                                #connection{protocol = Protocol},
                            sock = Sock}) ->
-    User = rabbit_access_control:check_login(Mechanism, Response),
+    User = rabbit_access_control:check_login(Mechanism, Response, Sock),
     Tune = #'connection.tune'{channel_max = 0,
                               frame_max = ?FRAME_MAX,
                               heartbeat = 0},


More information about the rabbitmq-discuss mailing list