aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorQijiang Fan <[email protected]>2015-04-16 22:25:57 +0800
committerIngela Anderton Andin <[email protected]>2015-05-12 14:04:06 +0200
commitdb509dd5debcd72d7f1d024d289315274f9b788b (patch)
tree9f8ccb37cf9ad162a2d1b90662109c1656d17017
parent181ceb12675b59de9bd7a881fe9b58995d03bac0 (diff)
downloadotp-db509dd5debcd72d7f1d024d289315274f9b788b.tar.gz
otp-db509dd5debcd72d7f1d024d289315274f9b788b.tar.bz2
otp-db509dd5debcd72d7f1d024d289315274f9b788b.zip
ssl: add option sni_fun
The newly added function sni_fun allows dynamic update of SSL options like keys and certificates depending on different SNI hostname, rather than a predefined rules of SSL options.
-rw-r--r--lib/ssl/doc/src/ssl.xml21
-rw-r--r--lib/ssl/src/ssl.erl19
-rw-r--r--lib/ssl/src/ssl_internal.hrl1
-rw-r--r--lib/ssl/src/tls_connection.erl20
-rw-r--r--lib/ssl/test/ssl_sni_SUITE.erl34
-rw-r--r--lib/ssl/test/ssl_to_openssl_SUITE.erl46
6 files changed, 128 insertions, 13 deletions
diff --git a/lib/ssl/doc/src/ssl.xml b/lib/ssl/doc/src/ssl.xml
index 77f63dcecf..8a0bf69be4 100644
--- a/lib/ssl/doc/src/ssl.xml
+++ b/lib/ssl/doc/src/ssl.xml
@@ -108,10 +108,12 @@
<p><c>| {log_alert, boolean()}</c></p>
<p><c>| {server_name_indication, hostname() | disable}</c></p>
<p><c>| {sni_hosts, [{hostname(), ssloptions()}]}</c></p>
+ <p><c>| {sni_fun, SNIfun::fun()}</c></p>
</item>
<tag><c>transportoption() =</c></tag>
<item><p><c>{cb_info, {CallbackModule::atom(), DataTag::atom(),
+
ClosedTag::atom(), ErrTag:atom()}}</c></p>
<p>Defaults to <c>{gen_tcp, tcp, tcp_closed, tcp_error}</c>. Can be used
to customize the transport layer. The callback module must implement a
@@ -185,6 +187,9 @@
<item><p><c>srp_1024 | srp_1536 | srp_2048 | srp_3072
| srp_4096 | srp_6144 | srp_8192</c></p></item>
+ <tag><c>SNIfun::fun()</c></tag>
+ <item><p><c>= fun(ServerName :: string()) -> ssloptions()</c></p></item>
+
</taglist>
</section>
@@ -630,7 +635,21 @@ fun(srp, Username :: string(), UserState :: term()) ->
<tag><c>{sni_hosts, [{hostname(), ssloptions()}]}</c></tag>
<item><p>If the server receives a SNI (Server Name Indication) from the client
matching a host listed in the <c>sni_hosts</c> option, the speicific options for
- that host will override previously specified options.</p></item>
+ that host will override previously specified options.
+
+ The option <c>sni_fun</c>, and <c>sni_hosts</c> are mutually exclusive.</p></item>
+
+ <tag><c>{sni_fun, SNIfun::fun()}</c></tag>
+ <item><p>If the server receives a SNI (Server Name Indication) from the client,
+ the given function will be called to retrive <c>ssloptions()</c> for indicated server.
+ These options will be merged into predefined <c>ssloptions()</c>.
+
+ The function should be defined as:
+ <c>fun(ServerName :: string()) -> ssloptions()</c>
+ and can be specified as a fun or as named <c>fun module:function/1</c>
+
+ The option <c>sni_fun</c>, and <c>sni_hosts</c> are mutually exclusive.</p></item>
+
</taglist>
</section>
diff --git a/lib/ssl/src/ssl.erl b/lib/ssl/src/ssl.erl
index 956c699c45..9f76612ee3 100644
--- a/lib/ssl/src/ssl.erl
+++ b/lib/ssl/src/ssl.erl
@@ -700,6 +700,7 @@ handle_options(Opts0) ->
log_alert = handle_option(log_alert, Opts, true),
server_name_indication = handle_option(server_name_indication, Opts, undefined),
sni_hosts = handle_option(sni_hosts, Opts, []),
+ sni_fun = handle_option(sni_fun, Opts, {}),
honor_cipher_order = handle_option(honor_cipher_order, Opts, false),
protocol = proplists:get_value(protocol, Opts, tls),
padding_check = proplists:get_value(padding_check, Opts, true),
@@ -716,7 +717,7 @@ handle_options(Opts0) ->
user_lookup_fun, psk_identity, srp_identity, ciphers,
reuse_session, reuse_sessions, ssl_imp,
cb_info, renegotiate_at, secure_renegotiate, hibernate_after,
- erl_dist, alpn_advertised_protocols, sni_hosts,
+ erl_dist, alpn_advertised_protocols, sni_hosts, sni_fun,
alpn_preferred_protocols, next_protocols_advertised,
client_preferred_next_protocols, log_alert,
server_name_indication, honor_cipher_order, padding_check, crl_check, crl_cache,
@@ -733,6 +734,18 @@ handle_options(Opts0) ->
inet_user = SockOpts, transport_info = CbInfo, connection_cb = ConnetionCb
}}.
+handle_option(sni_fun, Opts, Default) ->
+ OptFun = validate_option(sni_fun,
+ proplists:get_value(sni_fun, Opts, Default)),
+ OptHosts = proplists:get_value(sni_hosts, Opts, undefined),
+ case {OptFun, OptHosts} of
+ {Default, _} ->
+ Default;
+ {_, undefined} ->
+ OptFun;
+ _ ->
+ throw({error, {conflict_options, [sni_fun, sni_hosts]}})
+ end;
handle_option(OptionName, Opts, Default) ->
validate_option(OptionName,
proplists:get_value(OptionName, Opts, Default)).
@@ -920,6 +933,10 @@ validate_option(sni_hosts, [{Hostname, SSLOptions} | Tail]) when is_list(Hostnam
_ ->
throw({error, {options, {sni_hosts, RecursiveSNIOptions}}})
end;
+validate_option(sni_fun, {}) ->
+ {};
+validate_option(sni_fun, Fun) when is_function(Fun) ->
+ Fun;
validate_option(honor_cipher_order, Value) when is_boolean(Value) ->
Value;
validate_option(padding_check, Value) when is_boolean(Value) ->
diff --git a/lib/ssl/src/ssl_internal.hrl b/lib/ssl/src/ssl_internal.hrl
index e285f48202..b6b4fc44df 100644
--- a/lib/ssl/src/ssl_internal.hrl
+++ b/lib/ssl/src/ssl_internal.hrl
@@ -123,6 +123,7 @@
log_alert :: boolean(),
server_name_indication = undefined,
sni_hosts :: [{inet:hostname(), [tuple()]}],
+ sni_fun :: function(),
%% Should the server prefer its own cipher order over the one provided by
%% the client?
honor_cipher_order = false :: boolean(),
diff --git a/lib/ssl/src/tls_connection.erl b/lib/ssl/src/tls_connection.erl
index d804d7ad37..1ee47f28b1 100644
--- a/lib/ssl/src/tls_connection.erl
+++ b/lib/ssl/src/tls_connection.erl
@@ -400,12 +400,19 @@ initial_state(Role, Host, Port, Socket, {SSLOptions, SocketOptions, Tracker}, Us
update_ssl_options_from_sni(OrigSSLOptions, SNIHostname) ->
- case proplists:get_value(SNIHostname, OrigSSLOptions#ssl_options.sni_hosts) of
- undefined ->
- undefined;
- SSLOption ->
- ssl:handle_options(SSLOption, OrigSSLOptions)
- end.
+ SSLOption =
+ case OrigSSLOptions#ssl_options.sni_fun of
+ {} ->
+ proplists:get_value(SNIHostname, OrigSSLOptions#ssl_options.sni_hosts);
+ SNIFun ->
+ SNIFun(SNIHostname)
+ end,
+ case SSLOption of
+ undefined ->
+ undefined;
+ _ ->
+ ssl:handle_options(SSLOption, OrigSSLOptions)
+ end.
next_state(Current,_, #alert{} = Alert, #state{negotiated_version = Version} = State) ->
handle_own_alert(Alert, Version, Current, State);
@@ -454,7 +461,6 @@ next_state(Current, Next, #ssl_tls{type = ?HANDSHAKE, fragment = Data},
undefined ->
State0;
#sni{hostname = Hostname} ->
- OrigSSLOptions = State0#state.ssl_options,
NewOptions = update_ssl_options_from_sni(State0#state.ssl_options, Hostname),
case NewOptions of
undefined ->
diff --git a/lib/ssl/test/ssl_sni_SUITE.erl b/lib/ssl/test/ssl_sni_SUITE.erl
index 134e508b10..46cd644e4d 100644
--- a/lib/ssl/test/ssl_sni_SUITE.erl
+++ b/lib/ssl/test/ssl_sni_SUITE.erl
@@ -31,7 +31,7 @@
%%--------------------------------------------------------------------
suite() -> [{ct_hooks,[ts_install_cth]}].
-all() -> [no_sni_header, sni_match, sni_no_match].
+all() -> [no_sni_header, sni_match, sni_no_match] ++ [no_sni_header_fun, sni_match_fun, sni_no_match_fun].
init_per_suite(Config0) ->
catch crypto:stop(),
@@ -57,12 +57,20 @@ end_per_suite(_) ->
no_sni_header(Config) ->
run_handshake(Config, undefined, undefined, "server").
+no_sni_header_fun(Config) ->
+ run_sni_fun_handshake(Config, undefined, undefined, "server").
+
sni_match(Config) ->
run_handshake(Config, "a.server", "a.server", "a.server").
+sni_match_fun(Config) ->
+ run_sni_fun_handshake(Config, "a.server", "a.server", "a.server").
+
sni_no_match(Config) ->
run_handshake(Config, "c.server", undefined, "server").
+sni_no_match_fun(Config) ->
+ run_sni_fun_handshake(Config, "c.server", undefined, "server").
%%--------------------------------------------------------------------
@@ -112,6 +120,30 @@ recv_and_certificate(SSLSocket) ->
ct:log("Subject of certificate received from server: ~p", [Subject]),
rdn_to_string(rdnPart(Subject, ?'id-at-commonName')).
+run_sni_fun_handshake(Config, SNIHostname, ExpectedSNIHostname, ExpectedCN) ->
+ ct:log("Start running handshake for sni_fun, Config: ~p, SNIHostname: ~p, ExpectedSNIHostname: ~p, ExpectedCN: ~p", [Config, SNIHostname, ExpectedSNIHostname, ExpectedCN]),
+ [{sni_hosts, ServerSNIConf}] = ?config(sni_server_opts, Config),
+ SNIFun = fun(Domain) -> proplists:get_value(Domain, ServerSNIConf, undefined) end,
+ ServerOptions = ?config(server_opts, Config) ++ [{sni_fun, SNIFun}],
+ ClientOptions =
+ case SNIHostname of
+ undefined ->
+ ?config(client_opts, Config);
+ _ ->
+ [{server_name_indication, SNIHostname}] ++ ?config(client_opts, Config)
+ end,
+ ct:log("Options: ~p", [[ServerOptions, ClientOptions]]),
+ {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
+ Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
+ {from, self()}, {mfa, {?MODULE, send_and_hostname, []}},
+ {options, ServerOptions}]),
+ Port = ssl_test_lib:inet_port(Server),
+ Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port},
+ {host, Hostname}, {from, self()},
+ {mfa, {?MODULE, recv_and_certificate, []}},
+ {options, ClientOptions}]),
+ ssl_test_lib:check_result(Server, ExpectedSNIHostname, Client, ExpectedCN).
+
run_handshake(Config, SNIHostname, ExpectedSNIHostname, ExpectedCN) ->
ct:log("Start running handshake, Config: ~p, SNIHostname: ~p, ExpectedSNIHostname: ~p, ExpectedCN: ~p", [Config, SNIHostname, ExpectedSNIHostname, ExpectedCN]),
diff --git a/lib/ssl/test/ssl_to_openssl_SUITE.erl b/lib/ssl/test/ssl_to_openssl_SUITE.erl
index 3807a9983c..0413415e49 100644
--- a/lib/ssl/test/ssl_to_openssl_SUITE.erl
+++ b/lib/ssl/test/ssl_to_openssl_SUITE.erl
@@ -102,9 +102,12 @@ npn_tests() ->
erlang_client_openssl_server_npn_only_server].
sni_server_tests() ->
- [erlang_server_oepnssl_client_sni_match,
+ [erlang_server_openssl_client_sni_match,
+ erlang_server_openssl_client_sni_match_fun,
erlang_server_openssl_client_sni_no_match,
- erlang_server_openssl_client_sni_no_header].
+ erlang_server_openssl_client_sni_no_match_fun,
+ erlang_server_openssl_client_sni_no_header,
+ erlang_server_openssl_client_sni_no_header_fun].
init_per_suite(Config0) ->
@@ -230,7 +233,10 @@ special_init(TestCase, Config)
special_init(TestCase, Config)
when TestCase == erlang_server_openssl_client_sni_match;
TestCase == erlang_server_openssl_client_sni_no_match;
- TestCase == erlang_server_openssl_client_sni_no_header ->
+ TestCase == erlang_server_openssl_client_sni_no_header;
+ TestCase == erlang_server_openssl_client_sni_match_fun;
+ TestCase == erlang_server_openssl_client_sni_no_match_fun;
+ TestCase == erlang_server_openssl_client_sni_no_header_fun ->
check_openssl_sni_support(Config);
special_init(_, Config) ->
@@ -1196,12 +1202,21 @@ erlang_server_openssl_client_npn_only_client(Config) when is_list(Config) ->
erlang_server_openssl_client_sni_no_header(Config) when is_list(Config) ->
erlang_server_openssl_client_sni_test(Config, undefined, undefined, "server").
+erlang_server_openssl_client_sni_no_header_fun(Config) when is_list(Config) ->
+ erlang_server_openssl_client_sni_test_sni_fun(Config, undefined, undefined, "server").
+
erlang_server_openssl_client_sni_match(Config) when is_list(Config) ->
erlang_server_openssl_client_sni_test(Config, "a.server", "a.server", "a.server").
+erlang_server_openssl_client_sni_match_fun(Config) when is_list(Config) ->
+ erlang_server_openssl_client_sni_test_sni_fun(Config, "a.server", "a.server", "a.server").
+
erlang_server_openssl_client_sni_no_match(Config) when is_list(Config) ->
erlang_server_openssl_client_sni_test(Config, "c.server", undefined, "server").
+erlang_server_openssl_client_sni_no_match_fun(Config) when is_list(Config) ->
+ erlang_server_openssl_client_sni_test_sni_fun(Config, "c.server", undefined, "server").
+
%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
@@ -1285,6 +1300,31 @@ erlang_server_openssl_client_sni_test(Config, SNIHostname, ExpectedSNIHostname,
ok.
+erlang_server_openssl_client_sni_test_sni_fun(Config, SNIHostname, ExpectedSNIHostname, ExpectedCN) ->
+ ct:log("Start running handshake for sni_fun, Config: ~p, SNIHostname: ~p, ExpectedSNIHostname: ~p, ExpectedCN: ~p", [Config, SNIHostname, ExpectedSNIHostname, ExpectedCN]),
+ [{sni_hosts, ServerSNIConf}] = ?config(sni_server_opts, Config),
+ SNIFun = fun(Domain) -> proplists:get_value(Domain, ServerSNIConf, undefined) end,
+ ServerOptions = ?config(server_opts, Config) ++ [{sni_fun, SNIFun}],
+ {_, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
+ Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
+ {from, self()}, {mfa, {?MODULE, send_and_hostname, []}},
+ {options, ServerOptions}]),
+ Port = ssl_test_lib:inet_port(Server),
+ ClientCommand = case SNIHostname of
+ undefined ->
+ "openssl s_client -connect " ++ Hostname ++ ":" ++ integer_to_list(Port);
+ _ ->
+ "openssl s_client -connect " ++ Hostname ++ ":" ++ integer_to_list(Port) ++ " -servername " ++ SNIHostname
+ end,
+ ct:log("Options: ~p", [[ServerOptions, ClientCommand]]),
+ ClientPort = open_port({spawn, ClientCommand}, [stderr_to_stdout]),
+ ssl_test_lib:check_result(Server, ExpectedSNIHostname),
+ ExpectedClientOutput = ["OK", "/CN=" ++ ExpectedCN ++ "/"],
+ ok = client_read_bulk(ClientPort, ExpectedClientOutput),
+ ssl_test_lib:close_port(ClientPort),
+ ssl_test_lib:close(Server),
+ ok.
+
cipher(CipherSuite, Version, Config, ClientOpts, ServerOpts) ->
process_flag(trap_exit, true),