aboutsummaryrefslogtreecommitdiff
path: root/src/xmpp_stream_pkix.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/xmpp_stream_pkix.erl')
-rw-r--r--src/xmpp_stream_pkix.erl176
1 files changed, 176 insertions, 0 deletions
diff --git a/src/xmpp_stream_pkix.erl b/src/xmpp_stream_pkix.erl
new file mode 100644
index 000000000..5d64c5eb6
--- /dev/null
+++ b/src/xmpp_stream_pkix.erl
@@ -0,0 +1,176 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 13 Dec 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(xmpp_stream_pkix).
+
+%% API
+-export([authenticate/1, authenticate/2, format_error/1]).
+
+-include("xmpp.hrl").
+-include_lib("public_key/include/public_key.hrl").
+-include("XmppAddr.hrl").
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+-spec authenticate(xmpp_stream_in:state() | xmpp_stream_out:state())
+ -> {ok, binary()} | {error, atom(), binary()}.
+authenticate(State) ->
+ authenticate(State, <<"">>).
+
+-spec authenticate(xmpp_stream_in:state() | xmpp_stream_out:state(), binary())
+ -> {ok, binary()} | {error, atom(), binary()}.
+authenticate(#{xmlns := ?NS_SERVER, sockmod := SockMod,
+ socket := Socket} = State, Authzid) ->
+ Peer = try maps:get(remote_server, State)
+ catch _:{badkey, _} -> Authzid
+ end,
+ case SockMod:get_peer_certificate(Socket) of
+ {ok, Cert} ->
+ case SockMod:get_verify_result(Socket) of
+ 0 ->
+ case ejabberd_idna:domain_utf8_to_ascii(Peer) of
+ false ->
+ {error, idna_failed, Peer};
+ AsciiPeer ->
+ case lists:any(
+ fun(D) -> match_domain(AsciiPeer, D) end,
+ get_cert_domains(Cert)) of
+ true ->
+ {ok, Peer};
+ false ->
+ {error, hostname_mismatch, Peer}
+ end
+ end;
+ VerifyRes ->
+ %% TODO: return atomic errors
+ %% This should be improved in fast_tls
+ Reason = fast_tls:get_cert_verify_string(VerifyRes, Cert),
+ {error, erlang:binary_to_atom(Reason, utf8), Peer}
+ end;
+ {error, _Reason} ->
+ {error, get_cert_failed, Peer};
+ error ->
+ {error, get_cert_failed, Peer}
+ end;
+authenticate(_State, _Authzid) ->
+ %% TODO: client PKIX authentication
+ {error, client_not_supported, <<"">>}.
+
+format_error(idna_failed) ->
+ {'bad-protocol', <<"Remote domain is not an IDN hostname">>};
+format_error(hostname_mismatch) ->
+ {'not-authorized', <<"Certificate host name mismatch">>};
+format_error(get_cert_failed) ->
+ {'bad-protocol', <<"Failed to get peer certificate">>};
+format_error(client_not_supported) ->
+ {'invalid-mechanism', <<"Client certificate verification is not supported">>};
+format_error(Other) ->
+ {'not-authorized', erlang:atom_to_binary(Other, utf8)}.
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+get_cert_domains(Cert) ->
+ TBSCert = Cert#'Certificate'.tbsCertificate,
+ Subject = case TBSCert#'TBSCertificate'.subject of
+ {rdnSequence, Subj} -> lists:flatten(Subj);
+ _ -> []
+ end,
+ Extensions = case TBSCert#'TBSCertificate'.extensions of
+ Exts when is_list(Exts) -> Exts;
+ _ -> []
+ end,
+ lists:flatmap(
+ fun(#'AttributeTypeAndValue'{type = ?'id-at-commonName',value = Val}) ->
+ case 'OTP-PUB-KEY':decode('X520CommonName', Val) of
+ {ok, {_, D1}} ->
+ D = if is_binary(D1) -> D1;
+ is_list(D1) -> list_to_binary(D1);
+ true -> error
+ end,
+ if D /= error ->
+ case jid:from_string(D) of
+ #jid{luser = <<"">>, lserver = LD,
+ lresource = <<"">>} ->
+ [LD];
+ _ -> []
+ end;
+ true -> []
+ end;
+ _ -> []
+ end;
+ (_) -> []
+ end, Subject) ++
+ lists:flatmap(
+ fun(#'Extension'{extnID = ?'id-ce-subjectAltName',
+ extnValue = Val}) ->
+ BVal = if is_list(Val) -> list_to_binary(Val);
+ true -> Val
+ end,
+ case 'OTP-PUB-KEY':decode('SubjectAltName', BVal) of
+ {ok, SANs} ->
+ lists:flatmap(
+ fun({otherName, #'AnotherName'{'type-id' = ?'id-on-xmppAddr',
+ value = XmppAddr}}) ->
+ case 'XmppAddr':decode('XmppAddr', XmppAddr) of
+ {ok, D} when is_binary(D) ->
+ case jid:from_string(D) of
+ #jid{luser = <<"">>,
+ lserver = LD,
+ lresource = <<"">>} ->
+ case ejabberd_idna:domain_utf8_to_ascii(LD) of
+ false ->
+ [];
+ PCLD ->
+ [PCLD]
+ end;
+ _ -> []
+ end;
+ _ -> []
+ end;
+ ({dNSName, D}) when is_list(D) ->
+ case jid:from_string(list_to_binary(D)) of
+ #jid{luser = <<"">>,
+ lserver = LD,
+ lresource = <<"">>} ->
+ [LD];
+ _ -> []
+ end;
+ (_) -> []
+ end, SANs);
+ _ -> []
+ end;
+ (_) -> []
+ end, Extensions).
+
+match_domain(Domain, Domain) -> true;
+match_domain(Domain, Pattern) ->
+ DLabels = str:tokens(Domain, <<".">>),
+ PLabels = str:tokens(Pattern, <<".">>),
+ match_labels(DLabels, PLabels).
+
+match_labels([], []) -> true;
+match_labels([], [_ | _]) -> false;
+match_labels([_ | _], []) -> false;
+match_labels([DL | DLabels], [PL | PLabels]) ->
+ case lists:all(fun (C) ->
+ $a =< C andalso C =< $z orelse
+ $0 =< C andalso C =< $9 orelse
+ C == $- orelse C == $*
+ end,
+ binary_to_list(PL))
+ of
+ true ->
+ Regexp = ejabberd_regexp:sh_to_awk(PL),
+ case ejabberd_regexp:run(DL, Regexp) of
+ match -> match_labels(DLabels, PLabels);
+ nomatch -> false
+ end;
+ false -> false
+ end.