1 |
|
%%%============================================================================= |
2 |
|
%%% @copyright (C) 1999-2018, Erlang Solutions Ltd |
3 |
|
%%% @author Denys Gonchar <denys.gonchar@erlang-solutions.com> |
4 |
|
%%% @doc TLS backend based on standard Erlang's SSL application |
5 |
|
%%% @end |
6 |
|
%%%============================================================================= |
7 |
|
-module(just_tls). |
8 |
|
-copyright("2018, Erlang Solutions Ltd."). |
9 |
|
-author('denys.gonchar@erlang-solutions.com'). |
10 |
|
|
11 |
|
-behaviour(mongoose_tls). |
12 |
|
|
13 |
|
-include_lib("public_key/include/public_key.hrl"). |
14 |
|
|
15 |
|
-record(tls_socket, {verify_results = [], |
16 |
|
ssl_socket |
17 |
|
}). |
18 |
|
|
19 |
|
-type tls_socket() :: #tls_socket{}. |
20 |
|
-export_type([tls_socket/0]). |
21 |
|
|
22 |
|
% mongoose_tls behaviour |
23 |
|
-export([tcp_to_tls/2, |
24 |
|
send/2, |
25 |
|
recv_data/2, |
26 |
|
controlling_process/2, |
27 |
|
sockname/1, |
28 |
|
peername/1, |
29 |
|
setopts/2, |
30 |
|
get_peer_certificate/1, |
31 |
|
close/1]). |
32 |
|
|
33 |
|
% API |
34 |
|
-export([make_ssl_opts/1, make_cowboy_ssl_opts/1]). |
35 |
|
|
36 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
37 |
|
%% APIs |
38 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
39 |
|
|
40 |
|
-spec tcp_to_tls(inet:socket(), mongoose_tls:options()) -> |
41 |
|
{ok, mongoose_tls:tls_socket()} | {error, any()}. |
42 |
|
tcp_to_tls(TCPSocket, Options) -> |
43 |
364 |
inet:setopts(TCPSocket, [{active, false}]), |
44 |
364 |
{Ref, SSLOpts} = format_opts_with_ref(Options, false), |
45 |
364 |
Ret = case Options of |
46 |
|
#{connect := true} -> |
47 |
|
% Currently unused as ejabberd_s2s_out uses fast_tls, |
48 |
|
% and outgoing pools use Erlang SSL directly |
49 |
:-( |
ssl:connect(TCPSocket, SSLOpts); |
50 |
|
#{} -> |
51 |
364 |
ssl:handshake(TCPSocket, SSLOpts, 5000) |
52 |
|
end, |
53 |
364 |
VerifyResults = receive_verify_results(Ref), |
54 |
364 |
case Ret of |
55 |
|
{ok, SSLSocket} -> |
56 |
77 |
{ok, #tls_socket{ssl_socket = SSLSocket, verify_results = VerifyResults}}; |
57 |
287 |
_ -> Ret |
58 |
|
end. |
59 |
|
|
60 |
|
%% -callback send(tls_socket(), binary()) -> ok | {error, any()}. |
61 |
263 |
send(#tls_socket{ssl_socket = SSLSocket}, Packet) -> ssl:send(SSLSocket, Packet). |
62 |
|
|
63 |
|
%% -callback recv_data(tls_socket(), binary()) -> {ok, binary()} | {error, any()}. |
64 |
|
recv_data(_, <<"">>) -> |
65 |
|
%% such call is required for fast_tls to accomplish |
66 |
|
%% tls handshake, for just_tls we can ignore it |
67 |
:-( |
{ok, <<"">>}; |
68 |
|
recv_data(#tls_socket{ssl_socket = SSLSocket}, Data1) -> |
69 |
:-( |
case ssl:recv(SSLSocket, 0, 0) of |
70 |
:-( |
{ok, Data2} -> {ok, <<Data1/binary, Data2/binary>>}; |
71 |
:-( |
_ -> {ok, Data1} |
72 |
|
end. |
73 |
|
|
74 |
|
%% -callback controlling_process(tls_socket(), pid()) -> ok | {error, any()}. |
75 |
|
controlling_process(#tls_socket{ssl_socket = SSLSocket}, Pid) -> |
76 |
:-( |
ssl:controlling_process(SSLSocket, Pid). |
77 |
|
|
78 |
|
|
79 |
|
%% -callback sockname(tls_socket()) -> {ok, {inet:ip_address(), inet:port_number()}} | |
80 |
|
%% {error, any()}. |
81 |
:-( |
sockname(#tls_socket{ssl_socket = SSLSocket}) -> ssl:sockname(SSLSocket). |
82 |
|
|
83 |
|
|
84 |
|
%% -callback peername(tls_socket()) -> {ok, {inet:ip_address(), inet:port_number()}} | |
85 |
|
%% {error, any()}. |
86 |
:-( |
peername(#tls_socket{ssl_socket = SSLSocket}) -> ssl:peername(SSLSocket). |
87 |
|
|
88 |
|
|
89 |
|
%% -callback setopts(tls_socket(), Opts::list()) -> ok | {error, any()}. |
90 |
287 |
setopts(#tls_socket{ssl_socket = SSLSocket}, Opts) -> ssl:setopts(SSLSocket, Opts). |
91 |
|
|
92 |
|
|
93 |
|
%% -callback get_peer_certificate(tls_socket()) -> {ok, Cert::any()} | |
94 |
|
%% {bad_cert, bitstring()} | |
95 |
|
%% no_peer_cert. |
96 |
|
get_peer_certificate(#tls_socket{verify_results = [], ssl_socket = SSLSocket}) -> |
97 |
117 |
case ssl:peercert(SSLSocket) of |
98 |
|
{ok, PeerCert} -> |
99 |
111 |
Cert = public_key:pkix_decode_cert(PeerCert, plain), |
100 |
111 |
{ok, Cert}; |
101 |
6 |
_ -> no_peer_cert |
102 |
|
end; |
103 |
|
get_peer_certificate(#tls_socket{verify_results = [Err | _]}) -> |
104 |
3 |
{bad_cert, error_to_list(Err)}. |
105 |
|
|
106 |
|
%% -callback close(tls_socket()) -> ok. |
107 |
77 |
close(#tls_socket{ssl_socket = SSLSocket}) -> ssl:close(SSLSocket). |
108 |
|
|
109 |
|
%% @doc Prepare SSL options for direct use of ssl:connect/2 or ssl:handshake/2 |
110 |
|
%% The `disconnect_on_failure' option is not supported |
111 |
|
-spec make_ssl_opts(mongoose_tls:options()) -> [ssl:tls_option()]. |
112 |
|
make_ssl_opts(Opts) -> |
113 |
526 |
{dummy_ref, SSLOpts} = format_opts_with_ref(Opts, false), |
114 |
526 |
SSLOpts. |
115 |
|
|
116 |
|
-spec make_cowboy_ssl_opts(mongoose_tls:options()) -> [ssl:tls_option()]. |
117 |
|
make_cowboy_ssl_opts(Opts) -> |
118 |
215 |
FailIfNoPeerCert = fail_if_no_peer_cert_opt(Opts), |
119 |
215 |
{dummy_ref, SSLOpts} = format_opts_with_ref(Opts, FailIfNoPeerCert), |
120 |
215 |
SSLOpts. |
121 |
|
|
122 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
123 |
|
%% local functions |
124 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
125 |
|
|
126 |
|
format_opts_with_ref(Opts, FailIfNoPeerCert) -> |
127 |
1105 |
Verify = verify_opt(Opts), |
128 |
1105 |
{Ref, VerifyFun} = verify_fun_opt(Opts), |
129 |
1105 |
SNIOpts = sni_opts(Opts), |
130 |
1105 |
SSLOpts = maps:to_list(maps:with(ssl_option_keys(), Opts)), |
131 |
1105 |
{Ref, [{fail_if_no_peer_cert, FailIfNoPeerCert}, {verify, Verify}, {verify_fun, VerifyFun}] ++ |
132 |
|
SNIOpts ++ SSLOpts}. |
133 |
|
|
134 |
|
ssl_option_keys() -> |
135 |
1105 |
[certfile, cacertfile, ciphers, keyfile, password, versions, dhfile]. |
136 |
|
|
137 |
|
sni_opts(#{server_name_indication := SNIOpts}) -> |
138 |
526 |
process_sni_opts(SNIOpts); |
139 |
|
sni_opts(#{}) -> |
140 |
579 |
[]. |
141 |
|
|
142 |
|
process_sni_opts(#{enabled := false}) -> |
143 |
525 |
[{server_name_indication, disable}]; |
144 |
|
process_sni_opts(#{enabled := true, host := SNIHost, protocol := https}) -> |
145 |
:-( |
[{server_name_indication, SNIHost}, |
146 |
|
{customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]}]; |
147 |
|
process_sni_opts(#{enabled := true, host := SNIHost, protocol := default}) -> |
148 |
:-( |
[{server_name_indication, SNIHost}]; |
149 |
|
process_sni_opts(#{enabled := true}) -> |
150 |
1 |
[]. |
151 |
|
|
152 |
|
error_to_list(_Error) -> |
153 |
|
%TODO: implement later if needed |
154 |
3 |
"verify_fun failed". |
155 |
|
|
156 |
178 |
verify_opt(#{verify_mode := none}) -> verify_none; |
157 |
927 |
verify_opt(#{}) -> verify_peer. |
158 |
|
|
159 |
25 |
fail_if_no_peer_cert_opt(#{verify_mode := peer}) -> true; |
160 |
13 |
fail_if_no_peer_cert_opt(#{verify_mode := selfsigned_peer}) -> true; |
161 |
177 |
fail_if_no_peer_cert_opt(#{}) -> false. |
162 |
|
|
163 |
|
%% This function translates TLS options to the function |
164 |
|
%% which will later be used when TCP socket is upgraded to TLS |
165 |
|
%% `verify_mode` is one of the following: |
166 |
|
%% none - no validation of the clients certificate - any cert is accepted. |
167 |
|
%% peer - standard verification of the certificate. |
168 |
|
%% selfsigned_peer - the same as peer but also accepts self-signed certificates |
169 |
|
%% `disconnect_on_failure` is a boolean parameter: |
170 |
|
%% true - drop connection if certificate verification failed |
171 |
|
%% false - connect anyway, but later return {bad_cert,Error} |
172 |
|
%% on certificate verification (the same as fast_tls do). |
173 |
|
verify_fun_opt(#{verify_mode := Mode, disconnect_on_failure := false}) -> |
174 |
20 |
Ref = erlang:make_ref(), |
175 |
20 |
{Ref, verify_fun(Ref, Mode)}; |
176 |
|
verify_fun_opt(#{verify_mode := Mode}) -> |
177 |
1085 |
{dummy_ref, verify_fun(Mode)}. |
178 |
|
|
179 |
|
verify_fun(Ref, Mode) when is_reference(Ref) -> |
180 |
20 |
{Fun, State} = verify_fun(Mode), |
181 |
20 |
{verify_fun_wrapper(Ref, Fun), State}. |
182 |
|
|
183 |
|
verify_fun_wrapper(Ref, Fun) when is_reference(Ref), is_function(Fun, 3) -> |
184 |
20 |
Pid = self(), |
185 |
20 |
fun(Cert, Event, UserState) -> |
186 |
91 |
Ret = Fun(Cert, Event, UserState), |
187 |
91 |
case {Ret, Event} of |
188 |
18 |
{{valid, _}, _} -> Ret; |
189 |
|
{{unknown, NewState}, {extension, #'Extension'{critical = true}}} -> |
190 |
:-( |
send_verification_failure(Pid, Ref, unknown_critical_extension), |
191 |
:-( |
{valid, NewState}; |
192 |
72 |
{{unknown, _}, {extension, _}} -> Ret; |
193 |
|
{_, _} -> %% {fail,Reason} = Ret |
194 |
1 |
send_verification_failure(Pid, Ref, Ret), |
195 |
1 |
{valid, UserState} %return the last valid user state |
196 |
|
end |
197 |
|
end. |
198 |
|
|
199 |
|
verify_fun(peer) -> |
200 |
894 |
{fun |
201 |
12 |
(_, {bad_cert, _} = R, _) -> {fail, R}; |
202 |
2312 |
(_, {extension, _}, S) -> {unknown, S}; |
203 |
:-( |
(_, valid, S) -> {valid, S}; |
204 |
578 |
(_, valid_peer, S) -> {valid, S} |
205 |
|
end, []}; |
206 |
|
verify_fun(selfsigned_peer) -> |
207 |
33 |
{fun |
208 |
21 |
(_, {bad_cert, selfsigned_peer}, S) -> {valid, S}; |
209 |
1 |
(_, {bad_cert, _} = R, _) -> {fail, R}; |
210 |
12 |
(_, {extension, _}, S) -> {unknown, S}; |
211 |
:-( |
(_, valid, S) -> {valid, S}; |
212 |
3 |
(_, valid_peer, S) -> {valid, S} |
213 |
|
end, []}; |
214 |
|
verify_fun(none) -> |
215 |
178 |
{fun(_, _, S) -> {valid, S} end, []}. |
216 |
|
|
217 |
|
|
218 |
|
send_verification_failure(Pid, Ref, Reason) -> |
219 |
1 |
Pid ! {cert_verification_failure, Ref, Reason}. |
220 |
|
|
221 |
344 |
receive_verify_results(dummy_ref) -> []; |
222 |
20 |
receive_verify_results(Ref) -> receive_verify_results(Ref, []). |
223 |
|
|
224 |
|
receive_verify_results(Ref, Acc) -> |
225 |
21 |
receive |
226 |
|
{cert_verification_failure, Ref, Reason} -> |
227 |
1 |
receive_verify_results(Ref, [Reason | Acc]) |
228 |
|
after 0 -> |
229 |
20 |
lists:reverse(Acc) |
230 |
|
end. |