From e4b56a9f4a511aa9990dd86c16c61439c828df83 Mon Sep 17 00:00:00 2001 From: Jakub Witczak Date: Tue, 6 May 2025 17:01:29 +0200 Subject: [PATCH] ssh: KEX strict implementation fixes - fixed KEX strict implementation - draft-miller-sshm-strict-kex-01.txt - ssh_dbg added to ssh_fsm_kexinit module - CVE-2025-46712 --- lib/ssh/src/ssh_connection_handler.erl | 24 ++-- lib/ssh/src/ssh_fsm_kexinit.erl | 129 ++++++++++++++++++-- lib/ssh/src/ssh_transport.erl | 13 +- lib/ssh/test/ssh_protocol_SUITE.erl | 158 ++++++++++++++++++++++--- lib/ssh/test/ssh_trpt_test_lib.erl | 39 +++++- 5 files changed, 313 insertions(+), 50 deletions(-) diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index 5ddafa997567..15f98dfb5c31 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -34,7 +34,6 @@ -include("ssh_transport.hrl"). -include("ssh_auth.hrl"). -include("ssh_connect.hrl"). - -include("ssh_fsm.hrl"). %%==================================================================== @@ -728,16 +727,6 @@ handle_event(internal, #ssh_msg_disconnect{description=Desc} = Msg, StateName, D disconnect_fun("Received disconnect: "++Desc, D), {stop_and_reply, {shutdown,Desc}, Actions, D}; -handle_event(internal, #ssh_msg_ignore{}, {_StateName, _Role, init}, - #data{ssh_params = #ssh{kex_strict_negotiated = true, - send_sequence = SendSeq, - recv_sequence = RecvSeq}}) -> - ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - io_lib:format("strict KEX violation: unexpected SSH_MSG_IGNORE " - "send_sequence = ~p recv_sequence = ~p", - [SendSeq, RecvSeq]) - ); - handle_event(internal, #ssh_msg_ignore{}, _StateName, _) -> keep_state_and_data; @@ -1141,11 +1130,14 @@ handle_event(info, {Proto, Sock, NewData}, StateName, of {packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} -> D1 = D0#data{ssh_params = - Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)}, - decrypted_data_buffer = <<>>, - undecrypted_packet_length = undefined, - aead_data = <<>>, - encrypted_data_buffer = EncryptedDataRest}, + Ssh1#ssh{recv_sequence = + ssh_transport:next_seqnum(StateName, + Ssh1#ssh.recv_sequence, + SshParams)}, + decrypted_data_buffer = <<>>, + undecrypted_packet_length = undefined, + aead_data = <<>>, + encrypted_data_buffer = EncryptedDataRest}, try ssh_message:decode(set_kex_overload_prefix(DecryptedBytes,D1)) of diff --git a/lib/ssh/src/ssh_fsm_kexinit.erl b/lib/ssh/src/ssh_fsm_kexinit.erl index 05f7bdf22f16..b8fdc29079e8 100644 --- a/lib/ssh/src/ssh_fsm_kexinit.erl +++ b/lib/ssh/src/ssh_fsm_kexinit.erl @@ -43,6 +43,11 @@ -export([callback_mode/0, handle_event/4, terminate/3, format_status/2, code_change/4]). +-behaviour(ssh_dbg). +-export([ssh_dbg_trace_points/0, ssh_dbg_flags/1, + ssh_dbg_on/1, ssh_dbg_off/1, + ssh_dbg_format/2]). + %%==================================================================== %% gen_statem callbacks %%==================================================================== @@ -53,8 +58,13 @@ callback_mode() -> %%-------------------------------------------------------------------- -%%% ######## {kexinit, client|server, init|renegotiate} #### +handle_event(Type, Event = prepare_next_packet, StateName, D) -> + ssh_connection_handler:handle_event(Type, Event, StateName, D); +handle_event(Type, Event = {send_disconnect, _, _, _, _}, StateName, D) -> + ssh_connection_handler:handle_event(Type, Event, StateName, D); + +%%% ######## {kexinit, client|server, init|renegotiate} #### handle_event(internal, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg}, D = #data{key_exchange_init_msg = OwnKex}) -> Ssh1 = ssh_transport:key_init(peer_role(Role), D#data.ssh_params, Payload), @@ -67,11 +77,10 @@ handle_event(internal, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg}, end, {next_state, {key_exchange,Role,ReNeg}, D#data{ssh_params=Ssh}}; - %%% ######## {key_exchange, client|server, init|renegotiate} #### - %%%---- diffie-hellman handle_event(internal, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(KexdhReply, D), {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), @@ -81,6 +90,7 @@ handle_event(internal, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(internal, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, NewKeys, Ssh1} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(NewKeys, D), {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), @@ -89,24 +99,28 @@ handle_event(internal, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg} %%%---- diffie-hellman group exchange handle_event(internal, #ssh_msg_kex_dh_gex_request{} = Msg, {key_exchange,server,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(GexGroup, D), Ssh = ssh_transport:parallell_gen_key(Ssh1), {next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(internal, #ssh_msg_kex_dh_gex_request_old{} = Msg, {key_exchange,server,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, GexGroup, Ssh1} = ssh_transport:handle_kex_dh_gex_request(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(GexGroup, D), Ssh = ssh_transport:parallell_gen_key(Ssh1), {next_state, {key_exchange_dh_gex_init,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(internal, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, KexGexInit, Ssh} = ssh_transport:handle_kex_dh_gex_group(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(KexGexInit, D), {next_state, {key_exchange_dh_gex_reply,client,ReNeg}, D#data{ssh_params=Ssh}}; %%%---- elliptic curve diffie-hellman handle_event(internal, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(KexEcdhReply, D), {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), @@ -116,16 +130,25 @@ handle_event(internal, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNe {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(internal, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(NewKeys, D), {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), ssh_connection_handler:send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; +%%% ######## handle KEX strict +handle_event(internal, _Event, {key_exchange,_Role,init}, + #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true}, + send_sequence = SendSeq, + recv_sequence = RecvSeq}}) -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p", + [SendSeq, RecvSeq])); %%% ######## {key_exchange_dh_gex_init, server, init|renegotiate} #### - handle_event(internal, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,server,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(KexGexReply, D), {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), @@ -133,20 +156,33 @@ handle_event(internal, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_in {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), ssh_connection_handler:send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; - +%%% ######## handle KEX strict +handle_event(internal, _Event, {key_exchange_dh_gex_init,_Role,init}, + #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true}, + send_sequence = SendSeq, + recv_sequence = RecvSeq}}) -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p", + [SendSeq, RecvSeq])); %%% ######## {key_exchange_dh_gex_reply, client, init|renegotiate} #### - handle_event(internal, #ssh_msg_kex_dh_gex_reply{} = Msg, {key_exchange_dh_gex_reply,client,ReNeg}, D) -> + ok = check_kex_strict(Msg, D), {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, D#data.ssh_params), ssh_connection_handler:send_bytes(NewKeys, D), {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), ssh_connection_handler:send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; - +%%% ######## handle KEX strict +handle_event(internal, _Event, {key_exchange_dh_gex_reply,_Role,init}, + #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true}, + send_sequence = SendSeq, + recv_sequence = RecvSeq}}) -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p", + [SendSeq, RecvSeq])); %%% ######## {new_keys, client|server} #### - %% First key exchange round: handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,client,init}, D0) -> {ok, Ssh1} = ssh_transport:handle_new_keys(Msg, D0#data.ssh_params), @@ -162,6 +198,15 @@ handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,server,init}, D) -> %% ssh_connection_handler:send_bytes(ExtInfo, D), {next_state, {ext_info,server,init}, D#data{ssh_params=Ssh}}; +%%% ######## handle KEX strict +handle_event(internal, _Event, {new_keys,_Role,init}, + #data{ssh_params = #ssh{algorithms = #alg{kex_strict_negotiated = true}, + send_sequence = SendSeq, + recv_sequence = RecvSeq}}) -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("KEX strict violation (send_sequence = ~p recv_sequence = ~p)", + [SendSeq, RecvSeq])); + %% Subsequent key exchange rounds (renegotiation): handle_event(internal, #ssh_msg_newkeys{} = Msg, {new_keys,Role,renegotiate}, D) -> {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), @@ -183,7 +228,6 @@ handle_event(internal, #ssh_msg_ext_info{}=Msg, {ext_info,Role,renegotiate}, D0) handle_event(internal, #ssh_msg_newkeys{}=Msg, {ext_info,_Role,renegotiate}, D) -> {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), {keep_state, D#data{ssh_params = Ssh}}; - handle_event(internal, Msg, {ext_info,Role,init}, D) when is_tuple(Msg) -> %% If something else arrives, goto next state and handle the event in that one @@ -217,3 +261,70 @@ code_change(_OldVsn, StateName, State, _Extra) -> peer_role(client) -> server; peer_role(server) -> client. +check_kex_strict(Msg, + #data{ssh_params = + #ssh{algorithms = + #alg{ + kex = Kex, + kex_strict_negotiated = KexStrictNegotiated}, + send_sequence = SendSeq, + recv_sequence = RecvSeq}}) -> + case check_msg_group(Msg, get_alg_group(Kex), KexStrictNegotiated) of + ok -> + ok; + error -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("KEX strict violation: send_sequence = ~p recv_sequence = ~p", + [SendSeq, RecvSeq])) + end. + +get_alg_group(Kex) when Kex == 'diffie-hellman-group16-sha512'; + Kex == 'diffie-hellman-group18-sha512'; + Kex == 'diffie-hellman-group14-sha256'; + Kex == 'diffie-hellman-group14-sha1'; + Kex == 'diffie-hellman-group1-sha1' -> + dh_alg; +get_alg_group(Kex) when Kex == 'diffie-hellman-group-exchange-sha256'; + Kex == 'diffie-hellman-group-exchange-sha1' -> + dh_gex_alg; +get_alg_group(Kex) when Kex == 'curve25519-sha256'; + Kex == 'curve25519-sha256@libssh.org'; + Kex == 'curve448-sha512'; + Kex == 'ecdh-sha2-nistp521'; + Kex == 'ecdh-sha2-nistp384'; + Kex == 'ecdh-sha2-nistp256' -> + ecdh_alg. + +check_msg_group(_Msg, _AlgGroup, false) -> ok; +check_msg_group(#ssh_msg_kexdh_init{}, dh_alg, true) -> ok; +check_msg_group(#ssh_msg_kexdh_reply{}, dh_alg, true) -> ok; +check_msg_group(#ssh_msg_kex_dh_gex_request_old{}, dh_gex_alg, true) -> ok; +check_msg_group(#ssh_msg_kex_dh_gex_request{}, dh_gex_alg, true) -> ok; +check_msg_group(#ssh_msg_kex_dh_gex_group{}, dh_gex_alg, true) -> ok; +check_msg_group(#ssh_msg_kex_dh_gex_init{}, dh_gex_alg, true) -> ok; +check_msg_group(#ssh_msg_kex_dh_gex_reply{}, dh_gex_alg, true) -> ok; +check_msg_group(#ssh_msg_kex_ecdh_init{}, ecdh_alg, true) -> ok; +check_msg_group(#ssh_msg_kex_ecdh_reply{}, ecdh_alg, true) -> ok; +check_msg_group(_Msg, _AlgGroup, _) -> error. + +%%%################################################################ +%%%# +%%%# Tracing +%%%# + +ssh_dbg_trace_points() -> [connection_events]. + +ssh_dbg_flags(connection_events) -> [c]. + +ssh_dbg_on(connection_events) -> dbg:tp(?MODULE, handle_event, 4, x). + +ssh_dbg_off(connection_events) -> dbg:ctpg(?MODULE, handle_event, 4). + +ssh_dbg_format(connection_events, {call, {?MODULE,handle_event, [EventType, EventContent, State, _Data]}}) -> + ["Connection event\n", + io_lib:format("[~w] EventType: ~p~nEventContent: ~p~nState: ~p~n", [?MODULE, EventType, EventContent, State]) + ]; +ssh_dbg_format(connection_events, {return_from, {?MODULE,handle_event,4}, Ret}) -> + ["Connection event result\n", + io_lib:format("[~w] ~p~n", [?MODULE, ssh_dbg:reduce_state(Ret, #data{})]) + ]. diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl index 3e96ca940200..e612ffd0fe30 100644 --- a/lib/ssh/src/ssh_transport.erl +++ b/lib/ssh/src/ssh_transport.erl @@ -26,12 +26,11 @@ -include_lib("public_key/include/public_key.hrl"). -include_lib("kernel/include/inet.hrl"). - -include("ssh_transport.hrl"). -include("ssh.hrl"). -export([versions/2, hello_version_msg/1]). --export([next_seqnum/1, +-export([next_seqnum/3, supported_algorithms/0, supported_algorithms/1, default_algorithms/0, default_algorithms/1, clear_default_algorithms_env/0, @@ -295,7 +294,12 @@ random_id(Nlo, Nup) -> hello_version_msg(Data) -> [Data,"\r\n"]. -next_seqnum(SeqNum) -> +next_seqnum({State, _Role, init}, 16#ffffffff, + #ssh{algorithms = #alg{kex_strict_negotiated = true}}) + when State == kexinit; State == key_exchange; State == new_keys -> + ?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + io_lib:format("KEX strict violation: recv_sequence = 16#ffffffff", [])); +next_seqnum(_State, SeqNum, _) -> (SeqNum + 1) band 16#ffffffff. is_valid_mac(_, _ , #ssh{recv_mac_size = 0}) -> @@ -1080,7 +1084,7 @@ known_host_key(#ssh{opts = Opts, peer = {PeerName,{IP,Port}}} = Ssh, %% algorithm. Each string MUST contain at least one algorithm name. select_algorithm(Role, Client, Server, #ssh{opts = Opts, - kex_strict_negotiated = KexStrictNegotiated0}, + kex_strict_negotiated = KexStrictNegotiated0}, ReNeg) -> KexStrictNegotiated = case ReNeg of @@ -1105,7 +1109,6 @@ select_algorithm(Role, Client, Server, _ -> KexStrictNegotiated0 end, - {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server), {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server), diff --git a/lib/ssh/test/ssh_protocol_SUITE.erl b/lib/ssh/test/ssh_protocol_SUITE.erl index 537642cff598..2e1c2a6c7685 100644 --- a/lib/ssh/test/ssh_protocol_SUITE.erl +++ b/lib/ssh/test/ssh_protocol_SUITE.erl @@ -55,7 +55,9 @@ ext_info_c/1, ext_info_s/1, kex_strict_negotiated/1, - kex_strict_msg_ignore/1, + kex_strict_violation_key_exchange/1, + kex_strict_violation_new_keys/1, + kex_strict_violation/1, kex_strict_msg_unknown/1, gex_client_init_option_groups/1, gex_client_init_option_groups_file/1, @@ -144,7 +146,9 @@ groups() -> gex_client_old_request_exact, gex_client_old_request_noexact, kex_strict_negotiated, - kex_strict_msg_ignore, + kex_strict_violation_key_exchange, + kex_strict_violation_new_keys, + kex_strict_violation, kex_strict_msg_unknown]}, {service_requests, [], [bad_service_name, bad_long_service_name, @@ -1007,22 +1011,145 @@ kex_strict_negotiated(Config0) -> ssh_test_lib:rm_log_handler(), ok. -%% Connect to an erlang server and inject unexpected SSH ignore -kex_strict_msg_ignore(Config) -> - ct:log("START: ~p~n=================================", [?FUNCTION_NAME]), - ExpectedReason = "strict KEX violation: unexpected SSH_MSG_IGNORE", - TestMessages = - [{send, ssh_msg_ignore}, - {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg}, - {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}], - kex_strict_helper(Config, TestMessages, ExpectedReason). +%% Connect to an erlang server and inject unexpected SSH message +%% ssh_fsm_kexinit in key_exchange state +kex_strict_violation_key_exchange(Config) -> + ExpectedReason = "KEX strict violation", + Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented], + TestProcedure = + fun(M) -> + ct:log( + "=================== START: ~p Message: ~p Expected Fail =================================", + [?FUNCTION_NAME, M]), + [receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + {send, M}, + {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}] + end, + [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) || + Msg <- Injections], + ct:log("========== END ========"), + ok. + +%% Connect to an erlang server and inject unexpected SSH message +%% ssh_fsm_kexinit in new_keys state +kex_strict_violation_new_keys(Config) -> + ExpectedReason = "KEX strict violation", + Injections = [ssh_msg_ignore, ssh_msg_debug, ssh_msg_unimplemented], + TestProcedure = + fun(M) -> + ct:log( + "=================== START: ~p Message: ~p Expected Fail =================================", + [?FUNCTION_NAME, M]), + [receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + {send, ssh_msg_kexdh_init}, + {send, M}, + {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg}, + {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}] + end, + [kex_strict_helper(Config, TestProcedure(Msg), ExpectedReason) || + Msg <- Injections], + ct:log("========== END ========"), + ok. + +%% Connect to an erlang server and inject unexpected SSH message +%% duplicated KEXINIT +kex_strict_violation(Config) -> + KexDhReply = + #ssh_msg_kexdh_reply{ + public_host_key = {{{'ECPoint',<<73,72,235,162,96,101,154,59,217,114,123,192,96,105,250,29,214,76,60,63,167,21,221,118,246,168,152,2,7,172,137,125>>}, + {namedCurve,{1,3,101,112}}}, + 'ssh-ed25519'}, + f = 18504393053016436370762156176197081926381112956345797067569792020930728564439992620494295053804030674742529174859108487694089045521619258420515443400605141150065440678508889060925968846155921972385560196703381004650914261218463420313738628465563288022895912907728767735629532940627575655703806353550720122093175255090704443612257683903495753071530605378193139909567971489952258218767352348904221407081210633467414579377014704081235998044497191940270966762124544755076128392259615566530695493013708460088312025006678879288856957348606386230195080105197251789635675011844976120745546472873505352732719507783227210178188, + h_sig = <<90,247,44,240,136,196,82,215,56,165,53,33,230,101,253, + 34,112,201,21,131,162,169,10,129,174,14,69,25,39,174, + 92,210,130,249,103,2,215,245,7,213,110,235,136,134,11, + 124,248,139,79,17,225,77,125,182,204,84,137,167,99,186, + 167,42,192,10>>}, + TestFlows = + [ + {kexinit, "KEX strict violation", + [receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + {send, ssh_msg_kexinit}, + {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]}, + {ssh_msg_kexdh_init, "KEX strict violation", + [receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + {send, ssh_msg_kexdh_init_dup}, + {match,# ssh_msg_kexdh_reply{_='_'}, receive_msg}, + {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]}, + {new_keys, "Message ssh_msg_newkeys in wrong state", + [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}, + {send, #ssh_msg_newkeys{}}, + {match, disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR), receive_msg}]}, + {ssh_msg_unexpected_dh_gex, "KEX strict violation", + [receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + %% dh_alg is expected but dh_gex_alg is provided + {send, #ssh_msg_kex_dh_gex_request{min = 1000, n = 3000, max = 4000}}, + {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]}, + {wrong_role, "KEX strict violation", + [receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + %% client should not send message below + {send, KexDhReply}, + {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]}, + {wrong_role2, "KEX strict violation", + [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}, + %% client should not send message below + {send, KexDhReply}, + {match, #ssh_msg_newkeys{_='_'}, receive_msg}, + {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}]} + ], + TestProcedure = + fun({Msg, _, P}) -> + ct:log( + "==== START: ~p (duplicated ~p) Expected Fail ====~n~p", + [?FUNCTION_NAME, Msg, P]), + P + end, + [kex_strict_helper(Config, TestProcedure(Procedure), Reason) || + Procedure = {_, Reason, _} <- TestFlows], + ct:log("==== END ====="), + ok. %% Connect to an erlang server and inject unexpected non-SSH binary kex_strict_msg_unknown(Config) -> ct:log("START: ~p~n=================================", [?FUNCTION_NAME]), ExpectedReason = "Bad packet: Size", TestMessages = - [{send, ssh_msg_unknown}, + [receive_hello, + {send, hello}, + {send, ssh_msg_kexinit}, + {match, #ssh_msg_kexinit{_='_'}, receive_msg}, + {send, ssh_msg_kexdh_init}, + {send, ssh_msg_unknown}, {match, #ssh_msg_kexdh_reply{_='_'}, receive_msg}, {match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}], kex_strict_helper(Config, TestMessages, ExpectedReason). @@ -1047,12 +1174,7 @@ kex_strict_helper(Config, TestMessages, ExpectedReason) -> {user_dir, user_dir(Config)}, {user_interaction, false} | proplists:get_value(extra_options,Config,[]) - ]}, - receive_hello, - {send, hello}, - {send, ssh_msg_kexinit}, - {match, #ssh_msg_kexinit{_='_'}, receive_msg}, - {send, ssh_msg_kexdh_init}] ++ + ]}] ++ TestMessages, InitialState), ct:sleep(100), diff --git a/lib/ssh/test/ssh_trpt_test_lib.erl b/lib/ssh/test/ssh_trpt_test_lib.erl index f03fee1662ed..e34db487e5a2 100644 --- a/lib/ssh/test/ssh_trpt_test_lib.erl +++ b/lib/ssh/test/ssh_trpt_test_lib.erl @@ -90,7 +90,8 @@ exec(Op, S0=#s{}) -> report_trace(throw, Term, S1), throw({Term,Op}); - error:Error -> + error:Error:St -> + ct:log("Stacktrace=~n~p", [St]), report_trace(error, Error, S1), error({Error,Op}); @@ -335,6 +336,17 @@ send(S0, ssh_msg_ignore) -> Msg = #ssh_msg_ignore{data = "unexpected_ignore_message"}, send(S0, Msg); +send(S0, ssh_msg_debug) -> + Msg = #ssh_msg_debug{ + always_display = true, + message = "some debug message", + language = "en"}, + send(S0, Msg); + +send(S0, ssh_msg_unimplemented) -> + Msg = #ssh_msg_unimplemented{sequence = 123}, + send(S0, Msg); + send(S0, ssh_msg_unknown) -> Msg = binary:encode_hex(<<"0000000C060900000000000000000000">>), send(S0, Msg); @@ -382,6 +394,26 @@ send(S0, ssh_msg_kexdh_init) when ?role(S0) == client -> end), send_bytes(NextKexMsgBin, S#s{ssh = C}); +send(S0, ssh_msg_kexdh_init_dup) when ?role(S0) == client -> + {OwnMsg, PeerMsg} = S0#s.alg_neg, + {ok, NextKexMsgBin, C} = + try ssh_transport:handle_kexinit_msg(PeerMsg, OwnMsg, S0#s.ssh, init) + catch + Class:Exc -> + fail("Algorithm negotiation failed!", + {"Algorithm negotiation failed at line ~p:~p~n~p:~s~nPeer: ~s~n Own: ~s", + [?MODULE,?LINE,Class,format_msg(Exc),format_msg(PeerMsg),format_msg(OwnMsg)]}, + S0) + end, + S = opt(print_messages, S0, + fun(X) when X==true;X==detail -> + #ssh{keyex_key = {{_Private, Public}, {_G, _P}}} = C, + Msg = #ssh_msg_kexdh_init{e = Public}, + {"Send (reconstructed)~n~s~n",[format_msg(Msg)]} + end), + send_bytes(NextKexMsgBin, S#s{ssh = C}), + send_bytes(NextKexMsgBin, S#s{ssh = C}); + send(S0, ssh_msg_kexdh_reply) -> Bytes = proplists:get_value(ssh_msg_kexdh_reply, S0#s.reply), S = opt(print_messages, S0, @@ -531,7 +563,10 @@ receive_binary_msg(S0=#s{}) -> S0#s.ssh) of {packet_decrypted, DecryptedBytes, EncryptedDataRest, Ssh1} -> - S1 = S0#s{ssh = Ssh1#ssh{recv_sequence = ssh_transport:next_seqnum(Ssh1#ssh.recv_sequence)}, + S1 = S0#s{ssh = Ssh1#ssh{recv_sequence = + ssh_transport:next_seqnum(undefined, + Ssh1#ssh.recv_sequence, + false)}, decrypted_data_buffer = <<>>, undecrypted_packet_length = undefined, aead_data = <<>>,