%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2008-2015. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% %% %% -module(ssh_protocol_SUITE). -include_lib("common_test/include/ct.hrl"). -include_lib("kernel/include/inet.hrl"). -include_lib("ssh/src/ssh.hrl"). % ?UINT32, ?BYTE, #ssh{} ... -include_lib("ssh/src/ssh_transport.hrl"). -include_lib("ssh/src/ssh_auth.hrl"). %% Note: This directive should only be used in test suites. -compile(export_all). -define(NEWLINE, <<"\r\n">>). -define(REKEY_DATA_TMO, 65000). -define(v(Key, Config), proplists:get_value(Key, Config)). -define(v(Key, Config, Default), proplists:get_value(Key, Config, Default)). %%-------------------------------------------------------------------- %% Common Test interface functions ----------------------------------- %%-------------------------------------------------------------------- suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [{group,tool_tests}, {group,kex}, {group,service_requests}, {group,packet_size_error}, {group,field_size_error} ]. groups() -> [{tool_tests, [], [lib_works_as_client, lib_works_as_server, lib_match, lib_no_match ]}, {packet_size_error, [], [packet_length_too_large, packet_length_too_short]}, {field_size_error, [], [service_name_length_too_large, service_name_length_too_short]}, {kex, [], [no_common_alg_server_disconnects, no_common_alg_client_disconnects, gex_client_init_option_groups, gex_server_gex_limit, gex_client_init_option_groups_moduli_file, gex_client_init_option_groups_file, gex_client_old_request_exact, gex_client_old_request_noexact ]}, {service_requests, [], [bad_service_name, bad_long_service_name, bad_very_long_service_name, empty_service_name, bad_service_name_then_correct ]} ]. init_per_suite(Config) -> start_std_daemon( setup_dirs( start_apps(Config))). end_per_suite(Config) -> stop_apps(Config). init_per_testcase(no_common_alg_server_disconnects, Config) -> start_std_daemon(Config, [{preferred_algorithms,[{public_key,['ssh-rsa']}]}]); init_per_testcase(TC, Config) when TC == gex_client_init_option_groups ; TC == gex_client_init_option_groups_moduli_file ; TC == gex_client_init_option_groups_file ; TC == gex_server_gex_limit ; TC == gex_client_old_request_exact ; TC == gex_client_old_request_noexact -> Opts = case TC of gex_client_init_option_groups -> [{dh_gex_groups, [{2345, 3, 41}]}]; gex_client_init_option_groups_file -> DataDir = ?config(data_dir, Config), F = filename:join(DataDir, "dh_group_test"), [{dh_gex_groups, {file,F}}]; gex_client_init_option_groups_moduli_file -> DataDir = ?config(data_dir, Config), F = filename:join(DataDir, "dh_group_test.moduli"), [{dh_gex_groups, {ssh_moduli_file,F}}]; _ when TC == gex_server_gex_limit ; TC == gex_client_old_request_exact ; TC == gex_client_old_request_noexact -> [{dh_gex_groups, [{ 500, 3, 17}, {1000, 7, 91}, {3000, 5, 61}]}, {dh_gex_limits,{500,1500}} ]; _ -> [] end, start_std_daemon(Config, [{preferred_algorithms, ssh:default_algorithms()} | Opts]); init_per_testcase(_TestCase, Config) -> check_std_daemon_works(Config, ?LINE). end_per_testcase(no_common_alg_server_disconnects, Config) -> stop_std_daemon(Config); end_per_testcase(TC, Config) when TC == gex_client_init_option_groups ; TC == gex_client_init_option_groups_moduli_file ; TC == gex_client_init_option_groups_file ; TC == gex_server_gex_limit ; TC == gex_client_old_request_exact ; TC == gex_client_old_request_noexact -> stop_std_daemon(Config); end_per_testcase(_TestCase, Config) -> check_std_daemon_works(Config, ?LINE). %%%-------------------------------------------------------------------- %%% Test Cases -------------------------------------------------------- %%%-------------------------------------------------------------------- %%%-------------------------------------------------------------------- %%% Connect to an erlang server and check that the testlib acts as a client. lib_works_as_client(Config) -> %% Connect and negotiate keys {ok,InitialState} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_seqnums, print_messages]}] ), {ok,AfterKexState} = connect_and_kex(Config, InitialState), %% Do the authentcation {User,Pwd} = server_user_password(Config), {ok,EndState} = ssh_trpt_test_lib:exec( [{send, #ssh_msg_service_request{name = "ssh-userauth"}}, {match, #ssh_msg_service_accept{name = "ssh-userauth"}, receive_msg}, {send, #ssh_msg_userauth_request{user = User, service = "ssh-connection", method = "password", data = <> }}, {match, #ssh_msg_userauth_success{_='_'}, receive_msg} ], AfterKexState), %% Disconnect {ok,_} = ssh_trpt_test_lib:exec( [{send, #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION, description = "End of the fun", language = "" }}, close_socket ], EndState). %%-------------------------------------------------------------------- %%% Connect an erlang client and check that the testlib can act as a server. lib_works_as_server(Config) -> {User,_Pwd} = server_user_password(Config), %% Create a listening socket as server socket: {ok,InitialState} = ssh_trpt_test_lib:exec(listen), HostPort = ssh_trpt_test_lib:server_host_port(InitialState), %% Start a process handling one connection on the server side: spawn_link( fun() -> {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_messages]}, {accept, [{system_dir, system_dir(Config)}, {user_dir, user_dir(Config)}]}, receive_hello, {send, hello}, {send, ssh_msg_kexinit}, {match, #ssh_msg_kexinit{_='_'}, receive_msg}, {match, #ssh_msg_kexdh_init{_='_'}, receive_msg}, {send, ssh_msg_kexdh_reply}, {send, #ssh_msg_newkeys{}}, {match, #ssh_msg_newkeys{_='_'}, receive_msg}, {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg}, {send, #ssh_msg_service_accept{name="ssh-userauth"}}, {match, #ssh_msg_userauth_request{service="ssh-connection", method="none", user=User, _='_'}, receive_msg}, {send, #ssh_msg_userauth_failure{authentications = "password", partial_success = false}}, {match, #ssh_msg_userauth_request{service="ssh-connection", method="password", user=User, _='_'}, receive_msg}, {send, #ssh_msg_userauth_success{}}, close_socket, print_state ], InitialState) end), %% and finally connect to it with a regular Erlang SSH client: {ok,_} = std_connect(HostPort, Config, [{preferred_algorithms,[{kex,['diffie-hellman-group1-sha1']}]}] ). %%-------------------------------------------------------------------- %%% Matching lib_match(_Config) -> {ok,_} = ssh_trpt_test_lib:exec([{set_options, [print_ops]}, {match, abc, abc}, {match, '$a', {cde,fgh}}, {match, {cde,fgh}, '$a'}, {match, '_', {cde,fgh}}, {match, [a,'$a',b], [a,{cde,fgh},b]}, {match, [a,'$a'|'$b'], [a,{cde,fgh},b,c]}, {match, '$b', [b,c]} ]). %%-------------------------------------------------------------------- %%% Not matching lib_no_match(_Config) -> case ssh_trpt_test_lib:exec([{set_options, [print_ops]}, {match, '$x', b}, {match, a, '$x'}]) of {ok,_} -> {fail,"Unexpected match"}; {error, {_Op,{expected,a,b},_State}} -> ok end. %%-------------------------------------------------------------------- %%% Algo negotiation fail. This should result in a ssh_msg_disconnect %%% being sent from the server. no_common_alg_server_disconnects(Config) -> {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, {print_messages,detail}]}, {connect, server_host(Config),server_port(Config), [{silently_accept_hosts, true}, {user_dir, user_dir(Config)}, {user_interaction, false}, {preferred_algorithms,[{public_key,['ssh-dss']}]} ]}, receive_hello, {send, hello}, {match, #ssh_msg_kexinit{_='_'}, receive_msg}, {send, ssh_msg_kexinit}, % with server unsupported 'ssh-dss' ! {match, {'or',[#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, _='_'}, tcp_closed, {tcp_error,econnaborted} ]}, receive_msg} ] ). %%-------------------------------------------------------------------- %%% Algo negotiation fail. This should result in a ssh_msg_disconnect %%% being sent from the client. no_common_alg_client_disconnects(Config) -> %% Create a listening socket as server socket: {ok,InitialState} = ssh_trpt_test_lib:exec(listen), HostPort = ssh_trpt_test_lib:server_host_port(InitialState), Parent = self(), %% Start a process handling one connection on the server side: Pid = spawn_link( fun() -> Parent ! {result,self(), ssh_trpt_test_lib:exec( [{set_options, [print_ops, {print_messages,detail}]}, {accept, [{system_dir, system_dir(Config)}, {user_dir, user_dir(Config)}]}, receive_hello, {send, hello}, {match, #ssh_msg_kexinit{_='_'}, receive_msg}, {send, #ssh_msg_kexinit{ % with unsupported "SOME-UNSUPPORTED" cookie = 247381486335508958743193106082599558706, kex_algorithms = ["diffie-hellman-group1-sha1"], server_host_key_algorithms = ["SOME-UNSUPPORTED"], % SIC! encryption_algorithms_client_to_server = ["aes128-ctr"], encryption_algorithms_server_to_client = ["aes128-ctr"], mac_algorithms_client_to_server = ["hmac-sha2-256"], mac_algorithms_server_to_client = ["hmac-sha2-256"], compression_algorithms_client_to_server = ["none"], compression_algorithms_server_to_client = ["none"], languages_client_to_server = [], languages_server_to_client = [], first_kex_packet_follows = false, reserved = 0 }}, {match, {'or',[#ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, _='_'}, tcp_closed]}, receive_msg} ], InitialState) } end), %% and finally connect to it with a regular Erlang SSH client %% which of course does not support SOME-UNSUPPORTED as pub key algo: Result = std_connect(HostPort, Config, [{preferred_algorithms,[{public_key,['ssh-dss']}]}]), ct:log("Result of connect is ~p",[Result]), receive {result,Pid,{ok,_}} -> ok; {result,Pid,{error,{Op,ExecResult,S}}} -> ct:log("ERROR!~nOp = ~p~nExecResult = ~p~nState =~n~s", [Op,ExecResult,ssh_trpt_test_lib:format_msg(S)]), {fail, ExecResult}; X -> ct:log("¤¤¤¤¤"), ct:fail(X) after 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. %%%-------------------------------------------------------------------- gex_client_init_option_groups(Config) -> do_gex_client_init(Config, {2000, 2048, 4000}, {3,41}). gex_client_init_option_groups_file(Config) -> do_gex_client_init(Config, {2000, 2048, 4000}, {5,61}). gex_client_init_option_groups_moduli_file(Config) -> do_gex_client_init(Config, {2000, 2048, 4000}, {5,16#B7}). gex_server_gex_limit(Config) -> do_gex_client_init(Config, {1000, 3000, 4000}, {7,91}). do_gex_client_init(Config, {Min,N,Max}, {G,P}) -> {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_seqnums, print_messages]}, {connect, server_host(Config),server_port(Config), [{silently_accept_hosts, true}, {user_dir, user_dir(Config)}, {user_interaction, false}, {preferred_algorithms,[{kex,['diffie-hellman-group-exchange-sha1']}]} ]}, receive_hello, {send, hello}, {send, ssh_msg_kexinit}, {match, #ssh_msg_kexinit{_='_'}, receive_msg}, {send, #ssh_msg_kex_dh_gex_request{min = Min, n = N, max = Max}}, {match, #ssh_msg_kex_dh_gex_group{p=P, g=G, _='_'}, receive_msg} ] ). %%%-------------------------------------------------------------------- gex_client_old_request_exact(Config) -> do_gex_client_init_old(Config, 500, {3,17}). gex_client_old_request_noexact(Config) -> do_gex_client_init_old(Config, 800, {7,91}). do_gex_client_init_old(Config, N, {G,P}) -> {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_seqnums, print_messages]}, {connect, server_host(Config),server_port(Config), [{silently_accept_hosts, true}, {user_dir, user_dir(Config)}, {user_interaction, false}, {preferred_algorithms,[{kex,['diffie-hellman-group-exchange-sha1']}]} ]}, receive_hello, {send, hello}, {send, ssh_msg_kexinit}, {match, #ssh_msg_kexinit{_='_'}, receive_msg}, {send, #ssh_msg_kex_dh_gex_request_old{n = N}}, {match, #ssh_msg_kex_dh_gex_group{p=P, g=G, _='_'}, receive_msg} ] ). %%%-------------------------------------------------------------------- bad_service_name(Config) -> bad_service_name(Config, "kfglkjf"). bad_long_service_name(Config) -> bad_service_name(Config, lists:duplicate(?SSH_MAX_PACKET_SIZE div 2, $a)). bad_very_long_service_name(Config) -> bad_service_name(Config, lists:duplicate(4*?SSH_MAX_PACKET_SIZE, $a)). empty_service_name(Config) -> bad_service_name(Config, ""). bad_service_name_then_correct(Config) -> {ok,InitialState} = connect_and_kex(Config), {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_seqnums, print_messages]}, {send, #ssh_msg_service_request{name = "kdjglkfdjgkldfjglkdfjglkfdjglkj"}}, {send, #ssh_msg_service_request{name = "ssh-connection"}}, {match, {'or',[#ssh_msg_disconnect{_='_'}, tcp_closed ]}, receive_msg} ], InitialState). bad_service_name(Config, Name) -> {ok,InitialState} = connect_and_kex(Config), {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_seqnums, print_messages]}, {send, #ssh_msg_service_request{name = Name}}, {match, {'or',[#ssh_msg_disconnect{_='_'}, tcp_closed ]}, receive_msg} ], InitialState). %%%-------------------------------------------------------------------- packet_length_too_large(Config) -> bad_packet_length(Config, +4). packet_length_too_short(Config) -> bad_packet_length(Config, -4). bad_packet_length(Config, LengthExcess) -> PacketFun = fun(Msg, Ssh) -> BinMsg = ssh_message:encode(Msg), ssh_transport:pack(BinMsg, Ssh, LengthExcess) end, {ok,InitialState} = connect_and_kex(Config), {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_seqnums, print_messages]}, {send, {special, #ssh_msg_service_request{name="ssh-userauth"}, PacketFun}}, %% Prohibit remote decoder starvation: {send, #ssh_msg_service_request{name="ssh-userauth"}}, {match, {'or',[#ssh_msg_disconnect{_='_'}, tcp_closed, {tcp_error,econnaborted} ]}, receive_msg} ], InitialState). %%%-------------------------------------------------------------------- service_name_length_too_large(Config) -> bad_service_name_length(Config, +4). service_name_length_too_short(Config) -> bad_service_name_length(Config, -4). bad_service_name_length(Config, LengthExcess) -> PacketFun = fun(#ssh_msg_service_request{name=Service}, Ssh) -> BinName = list_to_binary(Service), BinMsg = <>, ssh_transport:pack(BinMsg, Ssh) end, {ok,InitialState} = connect_and_kex(Config), {ok,_} = ssh_trpt_test_lib:exec( [{set_options, [print_ops, print_seqnums, print_messages]}, {send, {special, #ssh_msg_service_request{name="ssh-userauth"}, PacketFun} }, %% Prohibit remote decoder starvation: {send, #ssh_msg_service_request{name="ssh-userauth"}}, {match, {'or',[#ssh_msg_disconnect{_='_'}, tcp_closed, {tcp_error,econnaborted} ]}, receive_msg} ], InitialState). %%%================================================================ %%%==== Internal functions ======================================== %%%================================================================ %%%---- init_suite and end_suite --------------------------------------- start_apps(Config) -> catch crypto:stop(), case catch crypto:start() of ok -> catch ssh:stop(), ok = ssh:start(), [{stop_apps, fun() -> ssh:stop(), crypto:stop() end} | Config]; _Else -> {skip, "Crypto could not be started!"} end. stop_apps(Config) -> (?v(stop_apps, Config, fun()-> ok end))(), ssh:stop(). setup_dirs(Config) -> DataDir = ?config(data_dir, Config), PrivDir = ?config(priv_dir, Config), ssh_test_lib:setup_rsa(DataDir, PrivDir), Config. system_dir(Config) -> filename:join(?config(priv_dir, Config), system). user_dir(Config) -> ?config(priv_dir, Config). %%%---------------------------------------------------------------- start_std_daemon(Config) -> start_std_daemon(Config, []). start_std_daemon(Config, ExtraOpts) -> PrivDir = ?config(priv_dir, Config), UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth file:make_dir(UserDir), UserPasswords = [{"user1","pwd1"}], Options = [%%{preferred_algorithms,[{public_key,['ssh-rsa']}]}, %% For some test cases {system_dir, system_dir(Config)}, {user_dir, UserDir}, {user_passwords, UserPasswords}, {failfun, fun ssh_test_lib:failfun/2} | ExtraOpts], Ref = {Server, Host, Port} = ssh_test_lib:daemon(Options), ct:log("Std server ~p started at ~p:~p~nOptions=~p",[Server, Host, Port, Options]), [{server,Ref}, {user_passwords, UserPasswords} | Config]. stop_std_daemon(Config) -> ssh:stop_daemon(server_pid(Config)), ct:log("Std server ~p at ~p:~p stopped", [server_pid(Config), server_host(Config), server_port(Config)]), lists:keydelete(server, 1, Config). check_std_daemon_works(Config, Line) -> case std_connect(Config) of {ok,C} -> ct:log("Server ~p:~p ~p is ok at line ~p", [server_host(Config), server_port(Config), server_pid(Config), Line]), ok = ssh:close(C), Config; Error = {error,_} -> ct:fail("Standard server ~p:~p ~p is ill at line ~p: ~p", [server_host(Config), server_port(Config), server_pid(Config), Line, Error]) end. server_pid(Config) -> element(1,?v(server,Config)). server_host(Config) -> element(2,?v(server,Config)). server_port(Config) -> element(3,?v(server,Config)). server_user_password(Config) -> server_user_password(1, Config). server_user_password(N, Config) -> lists:nth(N, ?v(user_passwords,Config)). std_connect(Config) -> std_connect({server_host(Config), server_port(Config)}, Config). std_connect({Host,Port}, Config) -> std_connect({Host,Port}, Config, []). std_connect({Host,Port}, Config, Opts) -> std_connect(Host, Port, Config, Opts). std_connect(Host, Port, Config, Opts) -> {User,Pwd} = server_user_password(Config), ssh:connect(Host, Port, %% Prefere User's Opts to the default opts [O || O = {Tag,_} <- [{user,User},{password,Pwd}, {silently_accept_hosts, true}, {user_dir, user_dir(Config)}, {user_interaction, false}], not lists:keymember(Tag, 1, Opts) ] ++ Opts, 30000). %%%---------------------------------------------------------------- connect_and_kex(Config) -> connect_and_kex(Config, ssh_trpt_test_lib:exec([]) ). connect_and_kex(Config, InitialState) -> ssh_trpt_test_lib:exec( [{connect, server_host(Config),server_port(Config), [{preferred_algorithms,[{kex,['diffie-hellman-group1-sha1']}]}, {silently_accept_hosts, true}, {user_dir, user_dir(Config)}, {user_interaction, false}]}, receive_hello, {send, hello}, {send, ssh_msg_kexinit}, {match, #ssh_msg_kexinit{_='_'}, receive_msg}, {send, ssh_msg_kexdh_init}, {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg}, {send, #ssh_msg_newkeys{}}, {match, #ssh_msg_newkeys{_='_'}, receive_msg} ], InitialState).