1 |
|
%%%============================================================================= |
2 |
|
%%% @copyright (C) 1999-2020, Erlang Solutions Ltd |
3 |
|
%%% @doc SASL EXTERNAL implementation (XEP178) |
4 |
|
%%% |
5 |
|
%%% SASL EXTERNAL mechanism requires client's SSL certificate. the purpose of |
6 |
|
%%% this module is to parse the certificate and get authorization identity (if |
7 |
|
%%% any provided by the client). this module doesn't make authorization, it |
8 |
|
%%% only prepares all the data and provides it to auth. backend. |
9 |
|
%%% |
10 |
|
%%% the next extra fields are added to the mongoose_credentials record by this |
11 |
|
%%% module: |
12 |
|
%%% * pem_cert - certificate in PEM format |
13 |
|
%%% * der_cert - certificate in DER format |
14 |
|
%%% * common_name - CN field (bitstring) of the client's cert (if available) |
15 |
|
%%% * xmpp_addresses - list of provided "xmppAddr" fields (bare jids) provided in |
16 |
|
%%% the client's certificate (empty list if not available) |
17 |
|
%%% * auth_id - authorization identity (bare jid, if provided by the client) |
18 |
|
%%% |
19 |
|
%%% @end |
20 |
|
%%%============================================================================= |
21 |
|
-module(cyrsasl_external). |
22 |
|
|
23 |
|
-xep([{xep, 178}, {version, "1.2"}, {status, partial}]). |
24 |
|
|
25 |
|
-include("mongoose.hrl"). |
26 |
|
-include("jlib.hrl"). |
27 |
|
|
28 |
|
-export([mechanism/0, mech_new/3, mech_step/2]). |
29 |
|
|
30 |
|
-ignore_xref([mech_new/3, behaviour_info/1]). |
31 |
|
|
32 |
|
-behaviour(cyrsasl). |
33 |
|
|
34 |
|
-callback verify_creds(Creds :: mongoose_credentials:t()) -> |
35 |
|
{ok, Username :: binary()} | {error, Error :: binary()}. |
36 |
|
|
37 |
|
-record(state, {creds :: mongoose_credentials:t()}). |
38 |
|
-type sasl_external_state() :: #state{}. |
39 |
|
|
40 |
|
-spec mechanism() -> cyrsasl:mechanism(). |
41 |
|
mechanism() -> |
42 |
282 |
<<"EXTERNAL">>. |
43 |
|
|
44 |
|
-spec mech_new(Host :: jid:server(), |
45 |
|
Creds :: mongoose_credentials:t(), |
46 |
|
Socket :: term()) -> {ok, sasl_external_state()}. |
47 |
|
mech_new(_Host, Creds, #{socket := Socket, listener_opts := LOpts}) -> |
48 |
72 |
Cert = case mongoose_c2s_socket:get_peer_certificate(Socket, LOpts) of |
49 |
67 |
{ok, C} -> C; |
50 |
5 |
_ -> no_cert |
51 |
|
end, |
52 |
72 |
maybe_extract_certs(Cert, Creds). |
53 |
|
|
54 |
|
maybe_extract_certs(no_cert, Creds) -> |
55 |
5 |
{ok, #state{creds = mongoose_credentials:extend(Creds, [{cert_file, false}])}}; |
56 |
|
maybe_extract_certs(Cert, Creds) -> |
57 |
67 |
DerCert = public_key:pkix_encode('Certificate', Cert, plain), |
58 |
67 |
PemCert = public_key:pem_encode([{'Certificate', DerCert, not_encrypted}]), |
59 |
67 |
CertFields = get_common_name(Cert) ++ get_xmpp_addresses(Cert), |
60 |
67 |
SaslExternalCredentials = [{cert_file, true}, {pem_cert, PemCert}, {der_cert, DerCert} | CertFields], |
61 |
67 |
{ok, #state{creds = mongoose_credentials:extend(Creds, SaslExternalCredentials)}}. |
62 |
|
|
63 |
|
-spec mech_step(State :: sasl_external_state(), |
64 |
|
ClientIn :: binary()) -> {ok, mongoose_credentials:t()} | {error, binary()}. |
65 |
|
mech_step(#state{creds = Creds}, User) -> |
66 |
72 |
case mongoose_credentials:get(Creds, cert_file) of |
67 |
|
false -> |
68 |
5 |
{error, <<"not-authorized">>}; |
69 |
|
true -> |
70 |
67 |
NewCreds = maybe_add_auth_id(Creds, User), |
71 |
67 |
do_mech_step(NewCreds) |
72 |
|
end. |
73 |
|
|
74 |
|
%%%============================================================================= |
75 |
|
%%% local functions |
76 |
|
%%%============================================================================= |
77 |
|
get_common_name(Cert) -> |
78 |
67 |
case cert_utils:get_common_name(Cert) of |
79 |
:-( |
error -> []; |
80 |
67 |
CN -> [{common_name, CN}] |
81 |
|
end. |
82 |
|
|
83 |
|
get_xmpp_addresses(Cert) -> |
84 |
67 |
XmmpAddresses = cert_utils:get_xmpp_addresses(Cert), |
85 |
67 |
[{xmpp_addresses, XmmpAddresses}]. |
86 |
|
|
87 |
|
maybe_add_auth_id(Creds, <<"">>) -> |
88 |
40 |
Creds; |
89 |
|
maybe_add_auth_id(Creds, User) -> |
90 |
27 |
mongoose_credentials:set(Creds, auth_id, User). |
91 |
|
|
92 |
|
do_mech_step(Creds) -> |
93 |
67 |
VerificationList = get_verification_list(Creds), |
94 |
67 |
case verification_loop(VerificationList, Creds) of |
95 |
|
{error, Error} -> |
96 |
27 |
{error, Error}; |
97 |
|
{ok, Name} -> |
98 |
40 |
NewCreds = mongoose_credentials:extend(Creds, [{username, Name}]), |
99 |
40 |
authorize(NewCreds) |
100 |
|
end. |
101 |
|
|
102 |
|
authorize(Creds) -> |
103 |
40 |
case ejabberd_auth:authorize(Creds) of |
104 |
37 |
{ok, NewCreds} -> {ok, NewCreds}; |
105 |
3 |
{error, not_authorized} -> {error, <<"not-authorized">>} |
106 |
|
end. |
107 |
|
|
108 |
|
get_verification_list(Creds) -> |
109 |
67 |
HostType = mongoose_credentials:host_type(Creds), |
110 |
67 |
mongoose_config:get_opt([{auth, HostType}, sasl_external]). |
111 |
|
|
112 |
|
verification_loop([VerificationFn | T], Creds) -> |
113 |
79 |
case verify_creds(VerificationFn, Creds) of |
114 |
|
{error, Error} when T =:= [] -> |
115 |
27 |
{error, Error}; |
116 |
|
{error, _} -> |
117 |
12 |
verification_loop(T, Creds); |
118 |
|
{ok, Name} -> |
119 |
40 |
{ok, Name} |
120 |
|
end. |
121 |
|
|
122 |
|
verify_creds(standard, Creds) -> |
123 |
49 |
standard_verification(Creds); |
124 |
|
verify_creds(common_name, Creds) -> |
125 |
9 |
common_name_verification(Creds); |
126 |
|
verify_creds(auth_id, Creds) -> |
127 |
3 |
auth_id_verification(Creds); |
128 |
|
verify_creds({mod, Mod}, Creds) -> |
129 |
18 |
custom_verification(Mod, Creds). |
130 |
|
|
131 |
|
|
132 |
|
standard_verification(Creds) -> |
133 |
49 |
Server = mongoose_credentials:lserver(Creds), |
134 |
49 |
XmppAddrs = get_credentials(Creds, xmpp_addresses), |
135 |
49 |
AuthId = get_credentials(Creds, auth_id), |
136 |
49 |
XmppAddr = case {XmppAddrs, AuthId} of |
137 |
|
{[OneXmppAddr], undefined} -> |
138 |
16 |
OneXmppAddr; |
139 |
|
{[_], _} -> |
140 |
6 |
{error, <<"invalid-authzid">>}; |
141 |
|
{[_, _ | _], undefined} -> |
142 |
3 |
{error, <<"invalid-authzid">>}; |
143 |
|
_ -> |
144 |
24 |
case lists:member(AuthId, XmppAddrs) of |
145 |
|
true -> |
146 |
3 |
AuthId; |
147 |
|
_ -> |
148 |
21 |
{error, <<"not-authorized">>} |
149 |
|
end |
150 |
|
end, |
151 |
49 |
verify_server(XmppAddr, Server). |
152 |
|
|
153 |
|
common_name_verification(Creds) -> |
154 |
9 |
Server = mongoose_credentials:lserver(Creds), |
155 |
9 |
AuthId = get_credentials(Creds, auth_id), |
156 |
9 |
CommonName = get_credentials(Creds, common_name), |
157 |
9 |
case AuthId of |
158 |
|
undefined when is_binary(CommonName) -> |
159 |
3 |
{ok, CommonName}; |
160 |
|
_ -> |
161 |
6 |
case verify_server(AuthId, Server) of |
162 |
3 |
{ok, CommonName} -> {ok, CommonName}; |
163 |
3 |
_ -> {error, <<"invalid-authzid">>} |
164 |
|
end |
165 |
|
end. |
166 |
|
|
167 |
|
auth_id_verification(Creds) -> |
168 |
3 |
Server = mongoose_credentials:lserver(Creds), |
169 |
3 |
AuthId = get_credentials(Creds, auth_id), |
170 |
3 |
verify_server(AuthId, Server). |
171 |
|
|
172 |
|
custom_verification(Module, Creds) -> |
173 |
18 |
try |
174 |
18 |
case apply(Module, verify_creds, [Creds]) of |
175 |
|
{ok, Username} when is_binary(Username) -> |
176 |
12 |
{ok, Username}; |
177 |
|
{error, Error} when is_binary(Error) -> |
178 |
6 |
{error, Error}; |
179 |
|
InvalidReturnValue -> |
180 |
:-( |
?LOG_ERROR(#{what => sasl_external_failed, sasl_module => Module, |
181 |
:-( |
reason => invalid_return_value, return_value => InvalidReturnValue}), |
182 |
:-( |
{error, <<"not-authorized">>} |
183 |
|
end |
184 |
|
catch |
185 |
|
Class:Exception -> |
186 |
:-( |
?LOG_ERROR(#{what => sasl_external_failed, sasl_module => Module, |
187 |
:-( |
class => Class, reason => Exception}), |
188 |
:-( |
{error, <<"not-authorized">>} |
189 |
|
end. |
190 |
|
|
191 |
|
get_credentials(Cred, Key) -> |
192 |
119 |
mongoose_credentials:get(Cred, Key, undefined). |
193 |
|
|
194 |
|
verify_server(undefined, _Server) -> |
195 |
:-( |
{error, <<"not-authorized">>}; |
196 |
|
verify_server({error, Error}, _Server) -> |
197 |
30 |
{error, Error}; |
198 |
|
verify_server(Jid, Server) -> |
199 |
28 |
JidRecord = jid:binary_to_bare(Jid), |
200 |
28 |
case JidRecord#jid.lserver of |
201 |
|
Server -> |
202 |
28 |
{ok, JidRecord#jid.luser}; |
203 |
|
_ -> |
204 |
:-( |
{error, <<"not-authorized">>} |
205 |
|
end. |