From db509dd5debcd72d7f1d024d289315274f9b788b Mon Sep 17 00:00:00 2001 From: Qijiang Fan Date: Thu, 16 Apr 2015 22:25:57 +0800 Subject: 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. --- lib/ssl/doc/src/ssl.xml | 21 +++++++++++++++- lib/ssl/src/ssl.erl | 19 ++++++++++++++- lib/ssl/src/ssl_internal.hrl | 1 + lib/ssl/src/tls_connection.erl | 20 +++++++++------ lib/ssl/test/ssl_sni_SUITE.erl | 34 +++++++++++++++++++++++++- lib/ssl/test/ssl_to_openssl_SUITE.erl | 46 ++++++++++++++++++++++++++++++++--- 6 files changed, 128 insertions(+), 13 deletions(-) (limited to 'lib') 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 @@

| {log_alert, boolean()}

| {server_name_indication, hostname() | disable}

| {sni_hosts, [{hostname(), ssloptions()}]}

+

| {sni_fun, SNIfun::fun()}

transportoption() =

{cb_info, {CallbackModule::atom(), DataTag::atom(), + ClosedTag::atom(), ErrTag:atom()}}

Defaults to {gen_tcp, tcp, tcp_closed, tcp_error}. Can be used to customize the transport layer. The callback module must implement a @@ -185,6 +187,9 @@

srp_1024 | srp_1536 | srp_2048 | srp_3072 | srp_4096 | srp_6144 | srp_8192

+ SNIfun::fun() +

= fun(ServerName :: string()) -> ssloptions()

+ @@ -630,7 +635,21 @@ fun(srp, Username :: string(), UserState :: term()) -> {sni_hosts, [{hostname(), ssloptions()}]}

If the server receives a SNI (Server Name Indication) from the client matching a host listed in the sni_hosts option, the speicific options for - that host will override previously specified options.

+ that host will override previously specified options. + + The option sni_fun, and sni_hosts are mutually exclusive.

+ + {sni_fun, SNIfun::fun()} +

If the server receives a SNI (Server Name Indication) from the client, + the given function will be called to retrive ssloptions() for indicated server. + These options will be merged into predefined ssloptions(). + + The function should be defined as: + fun(ServerName :: string()) -> ssloptions() + and can be specified as a fun or as named fun module:function/1 + + The option sni_fun, and sni_hosts are mutually exclusive.

+ 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), -- cgit v1.2.3