diff options
author | Hans Nilsson <[email protected]> | 2017-04-10 16:25:06 +0200 |
---|---|---|
committer | Hans Nilsson <[email protected]> | 2017-04-26 12:13:01 +0200 |
commit | 29d7533c715f972ee996382c2c45cc0c055e10d2 (patch) | |
tree | b370920e82c28335cb601d997920d41ea6cd76cd /lib/ssh/src | |
parent | c0d2e134f90ddd3fd2f5b0f9a94a5b0d55c93416 (diff) | |
download | otp-29d7533c715f972ee996382c2c45cc0c055e10d2.tar.gz otp-29d7533c715f972ee996382c2c45cc0c055e10d2.tar.bz2 otp-29d7533c715f972ee996382c2c45cc0c055e10d2.zip |
ssh: Implement ext-info extension. draft-ietf-curdle-ssh-ext-info
This is only a draft extension, but it is quite stable and already supported
by some implementations. OpenSSH has had it for some year now.
Diffstat (limited to 'lib/ssh/src')
-rw-r--r-- | lib/ssh/src/ssh.hrl | 7 | ||||
-rw-r--r-- | lib/ssh/src/ssh_connection_handler.erl | 142 | ||||
-rw-r--r-- | lib/ssh/src/ssh_dbg.erl | 5 | ||||
-rw-r--r-- | lib/ssh/src/ssh_message.erl | 33 | ||||
-rw-r--r-- | lib/ssh/src/ssh_options.erl | 22 | ||||
-rw-r--r-- | lib/ssh/src/ssh_transport.erl | 221 | ||||
-rw-r--r-- | lib/ssh/src/ssh_transport.hrl | 15 |
7 files changed, 334 insertions, 111 deletions
diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl index c7ed11895c..1a95bb27e7 100644 --- a/lib/ssh/src/ssh.hrl +++ b/lib/ssh/src/ssh.hrl @@ -145,6 +145,9 @@ c_keyinit, %% binary payload of kexinit packet s_keyinit, %% binary payload of kexinit packet + send_ext_info, %% May send ext-info to peer + recv_ext_info, %% Expect ext-info from peer + algorithms, %% #alg{} kex, %% key exchange algorithm @@ -216,7 +219,9 @@ compress, decompress, c_lng, - s_lng + s_lng, + send_ext_info, + recv_ext_info }). -record(ssh_key, diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index 84bb7dc23f..0ff7c9b3a1 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -453,7 +453,9 @@ init_ssh_record(Role, _Socket, PeerAddr, Opts) -> PeerName = case ?GET_INTERNAL_OPT(host, Opts) of PeerIP when is_tuple(PeerIP) -> inet_parse:ntoa(PeerIP); - PeerName0 -> + PeerName0 when is_atom(PeerName0) -> + atom_to_list(PeerName0); + PeerName0 when is_list(PeerName0) -> PeerName0 end, S0#ssh{c_vsn = Vsn, @@ -493,6 +495,7 @@ init_ssh_record(Role, _Socket, PeerAddr, Opts) -> | {key_exchange_dh_gex_init, server, renegotiate_flag()} | {key_exchange_dh_gex_reply, client, renegotiate_flag()} | {new_keys, role()} + | {ext_info, role(), renegotiate_flag()} | {service_request, role()} | {userauth, role()} | {userauth_keyboard_interactive, role()} @@ -589,13 +592,17 @@ handle_event(_, {#ssh_msg_kexinit{}=Kex, Payload}, {kexinit,Role,ReNeg}, handle_event(_, #ssh_msg_kexdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, KexdhReply, Ssh1} = ssh_transport:handle_kexdh_init(Msg, D#data.ssh_params), send_bytes(KexdhReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kexdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> - {ok, NewKeys, Ssh} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params), + {ok, NewKeys, Ssh1} = ssh_transport:handle_kexdh_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%%---- diffie-hellman group exchange @@ -620,13 +627,17 @@ handle_event(_, #ssh_msg_kex_dh_gex_group{} = Msg, {key_exchange,client,ReNeg}, handle_event(_, #ssh_msg_kex_ecdh_init{} = Msg, {key_exchange,server,ReNeg}, D) -> {ok, KexEcdhReply, Ssh1} = ssh_transport:handle_kex_ecdh_init(Msg, D#data.ssh_params), send_bytes(KexEcdhReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; handle_event(_, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) -> - {ok, NewKeys, Ssh} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params), + {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_ecdh_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; @@ -635,8 +646,10 @@ handle_event(_, #ssh_msg_kex_ecdh_reply{} = Msg, {key_exchange,client,ReNeg}, D) handle_event(_, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,server,ReNeg}, D) -> {ok, KexGexReply, Ssh1} = ssh_transport:handle_kex_dh_gex_init(Msg, D#data.ssh_params), send_bytes(KexGexReply, D), - {ok, NewKeys, Ssh} = ssh_transport:new_keys_message(Ssh1), + {ok, NewKeys, Ssh2} = ssh_transport:new_keys_message(Ssh1), send_bytes(NewKeys, D), + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh2), + send_bytes(ExtInfo, D), {next_state, {new_keys,server,ReNeg}, D#data{ssh_params=Ssh}}; @@ -645,30 +658,60 @@ handle_event(_, #ssh_msg_kex_dh_gex_init{} = Msg, {key_exchange_dh_gex_init,serv handle_event(_, #ssh_msg_kex_dh_gex_reply{} = Msg, {key_exchange_dh_gex_reply,client,ReNeg}, D) -> {ok, NewKeys, Ssh1} = ssh_transport:handle_kex_dh_gex_reply(Msg, D#data.ssh_params), send_bytes(NewKeys, D), - {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh1}}; + {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + send_bytes(ExtInfo, D), + {next_state, {new_keys,client,ReNeg}, D#data{ssh_params=Ssh}}; %%% ######## {new_keys, client|server} #### %% First key exchange round: -handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,Role,init}, D) -> +handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,client,init}, D) -> {ok, Ssh1} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), - Ssh = case Role of - client -> - {MsgReq, Ssh2} = ssh_auth:service_request_msg(Ssh1), - send_bytes(MsgReq, D), - Ssh2; - server -> - Ssh1 - end, - {next_state, {service_request,Role}, D#data{ssh_params=Ssh}}; + %% {ok, ExtInfo, Ssh2} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {MsgReq, Ssh} = ssh_auth:service_request_msg(Ssh1), + send_bytes(MsgReq, D), + {next_state, {ext_info,client,init}, D#data{ssh_params=Ssh}}; + +handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,server,init}, D) -> + {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), + %% {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {next_state, {ext_info,server,init}, D#data{ssh_params=Ssh}}; %% Subsequent key exchange rounds (renegotiation): handle_event(_, #ssh_msg_newkeys{} = Msg, {new_keys,Role,renegotiate}, D) -> {ok, Ssh} = ssh_transport:handle_new_keys(Msg, D#data.ssh_params), - {next_state, {connected,Role}, D#data{ssh_params=Ssh}}; + %% {ok, ExtInfo, Ssh} = ssh_transport:ext_info_message(Ssh1), + %% send_bytes(ExtInfo, D), + {next_state, {ext_info,Role,renegotiate}, D#data{ssh_params=Ssh}}; + + +%%% ######## {ext_info, client|server, init|renegotiate} #### + +handle_event(_, #ssh_msg_ext_info{}=Msg, {ext_info,Role,init}, D0) -> + D = handle_ssh_msg_ext_info(Msg, D0), + {next_state, {service_request,Role}, D}; + +handle_event(_, #ssh_msg_ext_info{}=Msg, {ext_info,Role,renegotiate}, D0) -> + D = handle_ssh_msg_ext_info(Msg, D0), + {next_state, {connected,Role}, D}; + +handle_event(_, #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 + {next_state, {service_request,Role}, D, [postpone]}; -%%% ######## {service_request, client|server} +handle_event(internal, Msg, {ext_info,Role,renegotiate}, D) when is_tuple(Msg) -> + %% If something else arrives, goto next state and handle the event in that one + {next_state, {connected,Role}, D, [postpone]}; + +%%% ######## {service_request, client|server} #### handle_event(_, Msg = #ssh_msg_service_request{name=ServiceName}, StateName = {service_request,server}, D) -> case ServiceName of @@ -747,6 +790,11 @@ handle_event(_, end; %%---- userauth success to client +handle_event(_, #ssh_msg_ext_info{}=Msg, {userauth,client}, D0) -> + %% FIXME: need new state to receive this msg! + D = handle_ssh_msg_ext_info(Msg, D0), + {keep_state, D}; + handle_event(_, #ssh_msg_userauth_success{}, {userauth,client}, D=#data{ssh_params = Ssh}) -> D#data.starter ! ssh_connected, {next_state, {connected,client}, D#data{ssh_params=Ssh#ssh{authenticated = true}}}; @@ -849,6 +897,11 @@ handle_event(_, #ssh_msg_userauth_failure{}, {userauth_keyboard_interactive_info end, {next_state, {userauth,client}, D, [postpone]}; +handle_event(_, #ssh_msg_ext_info{}=Msg, {userauth_keyboard_interactive_info_response, client}, D0) -> + %% FIXME: need new state to receive this msg! + D = handle_ssh_msg_ext_info(Msg, D0), + {keep_state, D}; + handle_event(_, #ssh_msg_userauth_success{}, {userauth_keyboard_interactive_info_response, client}, D) -> {next_state, {userauth,client}, D, [postpone]}; @@ -1080,26 +1133,34 @@ handle_event({call,_}, _, StateName, _) when StateName /= {connected,server}, StateName /= {connected,client} -> {keep_state_and_data, [postpone]}; -handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {request, ChannelPid, ChannelId, Type, Data, Timeout}, StateName, D0) + when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> D = handle_request(ChannelPid, ChannelId, Type, Data, true, From, D0), %% Note reply to channel will happen later when reply is recived from peer on the socket start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_request_idle_timer_check(D)}; -handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {request, ChannelId, Type, Data, Timeout}, StateName, D0) + when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> D = handle_request(ChannelId, Type, Data, true, From, D0), %% Note reply to channel will happen later when reply is recived from peer on the socket start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_request_idle_timer_check(D)}; -handle_event({call,From}, {data, ChannelId, Type, Data, Timeout}, {connected,_}, D0) -> +handle_event({call,From}, {data, ChannelId, Type, Data, Timeout}, StateName, D0) + when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> {{replies, Replies}, Connection} = ssh_connection:channel_data(ChannelId, Type, Data, D0#data.connection_state, From), {Repls,D} = send_replies(Replies, D0#data{connection_state = Connection}), start_channel_request_timer(ChannelId, From, Timeout), % FIXME: No message exchange so why? {keep_state, D, Repls}; -handle_event({call,From}, {eof, ChannelId}, {connected,_}, D0) -> +handle_event({call,From}, {eof, ChannelId}, StateName, D0) + when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> case ssh_channel:cache_lookup(cache(D0), ChannelId) of #channel{remote_id = Id, sent_close = false} -> D = send_msg(ssh_connection:channel_eof_msg(Id), D0), @@ -1110,8 +1171,9 @@ handle_event({call,From}, {eof, ChannelId}, {connected,_}, D0) -> handle_event({call,From}, {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout}, - {connected,_}, - D0) -> + StateName, + D0) when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> erlang:monitor(process, ChannelPid), {ChannelId, D1} = new_channel_id(D0), D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, @@ -1131,7 +1193,9 @@ handle_event({call,From}, start_channel_request_timer(ChannelId, From, Timeout), {keep_state, cache_cancel_idle_timer(D)}; -handle_event({call,From}, {send_window, ChannelId}, {connected,_}, D) -> +handle_event({call,From}, {send_window, ChannelId}, StateName, D) + when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{send_window_size = WinSize, send_packet_size = Packsize} -> @@ -1141,7 +1205,9 @@ handle_event({call,From}, {send_window, ChannelId}, {connected,_}, D) -> end, {keep_state_and_data, [{reply,From,Reply}]}; -handle_event({call,From}, {recv_window, ChannelId}, {connected,_}, D) -> +handle_event({call,From}, {recv_window, ChannelId}, StateName, D) + when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> Reply = case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{recv_window_size = WinSize, recv_packet_size = Packsize} -> @@ -1151,7 +1217,9 @@ handle_event({call,From}, {recv_window, ChannelId}, {connected,_}, D) -> end, {keep_state_and_data, [{reply,From,Reply}]}; -handle_event({call,From}, {close, ChannelId}, {connected,_}, D0) -> +handle_event({call,From}, {close, ChannelId}, StateName, D0) + when element(1,StateName) == connected ; + element(1,StateName) == ext_info -> case ssh_channel:cache_lookup(cache(D0), ChannelId) of #channel{remote_id = Id} = Channel -> D1 = send_msg(ssh_connection:channel_close_msg(Id), D0), @@ -1323,7 +1391,8 @@ handle_event(Type, Ev, StateName, D) -> Descr = case catch atom_to_list(element(1,Ev)) of "ssh_msg_" ++_ when Type==internal -> - "Message in wrong state"; +%% "Message in wrong state"; +lists:flatten(io_lib:format("Message ~p in wrong state (~p)", [element(1,Ev), StateName])); _ -> "Internal error" end, @@ -1516,6 +1585,8 @@ send_msg(Msg, State=#data{ssh_params=Ssh0}) when is_tuple(Msg) -> send_bytes(Bytes, State), State#data{ssh_params=Ssh}. +send_bytes("", _D) -> + ok; send_bytes(Bytes, #data{socket = Socket, transport_cb = Transport}) -> _ = Transport:send(Socket, Bytes), ok. @@ -1622,6 +1693,19 @@ cache(#data{connection_state=C}) -> C#connection.channel_cache. %%%---------------------------------------------------------------- +handle_ssh_msg_ext_info(#ssh_msg_ext_info{}, D=#data{ssh_params = #ssh{recv_ext_info=false}} ) -> + % The peer sent this although we didn't allow it! + D; +handle_ssh_msg_ext_info(#ssh_msg_ext_info{data=Data}, D0) -> + lists:foldl(fun ext_info/2, D0, Data). + +%% ext_info({ExtName,ExtValue}, D0) -> +%% D0; +ext_info(_, D0) -> + %% Not implemented + D0. + +%%%---------------------------------------------------------------- handle_request(ChannelPid, ChannelId, Type, Data, WantReply, From, D) -> case ssh_channel:cache_lookup(cache(D), ChannelId) of #channel{remote_id = Id} = Channel -> diff --git a/lib/ssh/src/ssh_dbg.erl b/lib/ssh/src/ssh_dbg.erl index 0345bbdea7..9431bf1817 100644 --- a/lib/ssh/src/ssh_dbg.erl +++ b/lib/ssh/src/ssh_dbg.erl @@ -56,7 +56,7 @@ messages(Write, MangleArg) when is_function(Write,2), dbg_ssh_messages() -> dbg:tp(ssh_message,encode,1, x), dbg:tp(ssh_message,decode,1, x), - dbg:tpl(ssh_transport,select_algorithm,3, x), + dbg:tpl(ssh_transport,select_algorithm,4, x), dbg:tp(ssh_transport,hello_version_msg,1, x), dbg:tp(ssh_transport,handle_hello_version,1, x). @@ -77,7 +77,7 @@ msg_formater({trace_ts,Pid,return_from,{ssh_message,decode,1},Msg,TS}, D) -> msg_formater({trace_ts,_Pid,call,{ssh_transport,select_algorithm,_},_TS}, D) -> D; -msg_formater({trace_ts,Pid,return_from,{ssh_transport,select_algorithm,3},{ok,Alg},TS}, D) -> +msg_formater({trace_ts,Pid,return_from,{ssh_transport,select_algorithm,_},{ok,Alg},TS}, D) -> fmt("~n~s ~p ALGORITHMS~n~s~n", [ts(TS),Pid, wr_record(Alg)], D); msg_formater({trace_ts,_Pid,call,{ssh_transport,hello_version_msg,_},_TS}, D) -> @@ -160,6 +160,7 @@ shrink_bin(X) -> X. ?wr_record(ssh_msg_kexdh_init); ?wr_record(ssh_msg_kexdh_reply); ?wr_record(ssh_msg_newkeys); +?wr_record(ssh_msg_ext_info); ?wr_record(ssh_msg_kex_dh_gex_request); ?wr_record(ssh_msg_kex_dh_gex_request_old); ?wr_record(ssh_msg_kex_dh_gex_group); diff --git a/lib/ssh/src/ssh_message.erl b/lib/ssh/src/ssh_message.erl index 562f040477..56f678876c 100644 --- a/lib/ssh/src/ssh_message.erl +++ b/lib/ssh/src/ssh_message.erl @@ -215,6 +215,16 @@ encode(#ssh_msg_service_accept{ }) -> <<?Ebyte(?SSH_MSG_SERVICE_ACCEPT), ?Estring_utf8(Service)>>; +encode(#ssh_msg_ext_info{ + nr_extensions = N, + data = Data + }) -> + lists:foldl(fun({ExtName,ExtVal}, Acc) -> + <<Acc/binary, ?Estring(ExtName), ?Estring(ExtVal)>> + end, + <<?Ebyte(?SSH_MSG_EXT_INFO), ?Euint32(N)>>, + Data); + encode(#ssh_msg_newkeys{}) -> <<?Ebyte(?SSH_MSG_NEWKEYS)>>; @@ -435,6 +445,18 @@ decode(<<?BYTE(?SSH_MSG_USERAUTH_INFO_RESPONSE), ?UINT32(Num), Data/binary>>) -> num_responses = Num, data = Data}; +decode(<<?BYTE(?SSH_MSG_EXT_INFO), ?UINT32(N), BinData/binary>>) -> + Data = bin_foldr( + fun(Bin,Acc) when length(Acc) == N -> + {Bin,Acc}; + (<<?DEC_BIN(V0,__0), ?DEC_BIN(V1,__1), Rest/binary>>, Acc) -> + {Rest,[{binary_to_list(V0),binary_to_list(V1)}|Acc]} + end, [], BinData), + #ssh_msg_ext_info{ + nr_extensions = N, + data = Data + }; + %%% Keyexchange messages decode(<<?BYTE(?SSH_MSG_KEXINIT), Cookie:128, Data/binary>>) -> decode_kex_init(Data, [Cookie, ssh_msg_kexinit], 10); @@ -537,17 +559,28 @@ decode(<<?BYTE(?SSH_MSG_DEBUG), ?BYTE(Bool), ?DEC_BIN(Msg,__0), ?DEC_BIN(Lang,__ %%% Helper functions %%% +bin_foldr(Fun, Acc, Bin) -> + lists:reverse(bin_foldl(Fun, Acc, Bin)). + +bin_foldl(_, Acc, <<>>) -> Acc; +bin_foldl(Fun, Acc0, Bin0) -> + {Bin,Acc} = Fun(Bin0,Acc0), + bin_foldl(Fun, Acc, Bin). + +%%%---------------------------------------------------------------- decode_keyboard_interactive_prompts(<<>>, Acc) -> lists:reverse(Acc); decode_keyboard_interactive_prompts(<<?DEC_BIN(Prompt,__0), ?BYTE(Bool), Bin/binary>>, Acc) -> decode_keyboard_interactive_prompts(Bin, [{Prompt, erl_boolean(Bool)} | Acc]). +%%%---------------------------------------------------------------- erl_boolean(0) -> false; erl_boolean(1) -> true. +%%%---------------------------------------------------------------- decode_kex_init(<<?BYTE(Bool), ?UINT32(X)>>, Acc, 0) -> list_to_tuple(lists:reverse([X, erl_boolean(Bool) | Acc])); decode_kex_init(<<?BYTE(Bool)>>, Acc, 0) -> diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl index ee3cdbb8a0..6e898b4fde 100644 --- a/lib/ssh/src/ssh_options.erl +++ b/lib/ssh/src/ssh_options.erl @@ -614,11 +614,23 @@ default(common) -> }, {max_random_length_padding, def} => - #{default => ?MAX_RND_PADDING_LEN, - chk => fun check_non_neg_integer/1, - class => user_options - } - }. + #{default => ?MAX_RND_PADDING_LEN, + chk => fun check_non_neg_integer/1, + class => user_options + }, + + {send_ext_info, def} => + #{default => true, + chk => fun erlang:is_boolean/1, + class => user_options + }, + + {recv_ext_info, def} => + #{default => true, + chk => fun erlang:is_boolean/1, + class => user_options + } + }. %%%================================================================ diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl index 6b47868d5c..d623d24529 100644 --- a/lib/ssh/src/ssh_transport.erl +++ b/lib/ssh/src/ssh_transport.erl @@ -38,6 +38,7 @@ handle_hello_version/1, key_exchange_init_msg/1, key_init/3, new_keys_message/1, + ext_info_message/1, handle_kexinit_msg/3, handle_kexdh_init/2, handle_kex_dh_gex_group/2, handle_kex_dh_gex_init/2, handle_kex_dh_gex_reply/2, handle_new_keys/2, handle_kex_dh_gex_request/2, @@ -230,7 +231,7 @@ key_exchange_init_msg(Ssh0) -> kex_init(#ssh{role = Role, opts = Opts, available_host_keys = HostKeyAlgs}) -> Random = ssh_bits:random(16), PrefAlgs = ?GET_OPT(preferred_algorithms, Opts), - kexinit_message(Role, Random, PrefAlgs, HostKeyAlgs). + kexinit_message(Role, Random, PrefAlgs, HostKeyAlgs, Opts). key_init(client, Ssh, Value) -> Ssh#ssh{c_keyinit = Value}; @@ -238,10 +239,11 @@ key_init(server, Ssh, Value) -> Ssh#ssh{s_keyinit = Value}. -kexinit_message(_Role, Random, Algs, HostKeyAlgs) -> +kexinit_message(Role, Random, Algs, HostKeyAlgs, Opts) -> #ssh_msg_kexinit{ cookie = Random, - kex_algorithms = to_strings( get_algs(kex,Algs) ), + kex_algorithms = to_strings( get_algs(kex,Algs) ) + ++ kex_ext_info(Role,Opts), server_host_key_algorithms = HostKeyAlgs, encryption_algorithms_client_to_server = c2s(cipher,Algs), encryption_algorithms_server_to_client = s2c(cipher,Algs), @@ -263,39 +265,42 @@ get_algs(Key, Algs) -> proplists:get_value(Key, Algs, default_algorithms(Key)). to_strings(L) -> lists:map(fun erlang:atom_to_list/1, L). new_keys_message(Ssh0) -> - {SshPacket, Ssh} = - ssh_packet(#ssh_msg_newkeys{}, Ssh0), + {SshPacket, Ssh1} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), + Ssh = install_alg(snd, Ssh1), {ok, SshPacket, Ssh}. handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, - #ssh{role = client} = Ssh0) -> - {ok, Algoritms} = select_algorithm(client, Own, CounterPart), - case verify_algorithm(Algoritms) of - true -> - key_exchange_first_msg(Algoritms#alg.kex, - Ssh0#ssh{algorithms = Algoritms}); - {false,Alg} -> - %% TODO: Correct code? - ssh_connection_handler:disconnect( - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - description = "Selection of key exchange algorithm failed: " - ++ Alg - }) + #ssh{role = client} = Ssh) -> + try + {ok, Algorithms} = select_algorithm(client, Own, CounterPart, Ssh#ssh.opts), + true = verify_algorithm(Algorithms), + Algorithms + of + Algos -> + key_exchange_first_msg(Algos#alg.kex, + Ssh#ssh{algorithms = Algos}) + catch + _:_ -> + ssh_connection_handler:disconnect( + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Selection of key exchange algorithm failed"}) end; handle_kexinit_msg(#ssh_msg_kexinit{} = CounterPart, #ssh_msg_kexinit{} = Own, - #ssh{role = server} = Ssh) -> - {ok, Algoritms} = select_algorithm(server, CounterPart, Own), - case verify_algorithm(Algoritms) of - true -> - {ok, Ssh#ssh{algorithms = Algoritms}}; - {false,Alg} -> - ssh_connection_handler:disconnect( - #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, - description = "Selection of key exchange algorithm failed: " - ++ Alg - }) + #ssh{role = server} = Ssh) -> + try + {ok, Algorithms} = select_algorithm(server, CounterPart, Own, Ssh#ssh.opts), + true = verify_algorithm(Algorithms), + Algorithms + of + Algos -> + {ok, Ssh#ssh{algorithms = Algos}} + catch + _:_ -> + ssh_connection_handler:disconnect( + #ssh_msg_disconnect{code = ?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, + description = "Selection of key exchange algorithm failed"}) end. @@ -308,6 +313,8 @@ verify_algorithm(#alg{decrypt = undefined}) -> {false, "decrypt"}; verify_algorithm(#alg{compress = undefined}) -> {false, "compress"}; verify_algorithm(#alg{decompress = undefined}) -> {false, "decompress"}; verify_algorithm(#alg{kex = Kex}) -> + %% This also catches the error if 'ext-info-s' or 'ext-info-c' is selected. + %% (draft-ietf-curdle-ssh-ext-info-04 2.2) case lists:member(Kex, supported_algorithms(kex)) of true -> true; false -> {false, "kex"} @@ -414,9 +421,9 @@ handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey, case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -584,9 +591,9 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; _Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -660,9 +667,9 @@ handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of ok -> {SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0), - {ok, SshPacket, Ssh#ssh{shared_secret = ssh_bits:mpint(K), - exchanged_hash = H, - session_id = sid(Ssh, H)}}; + {ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K), + exchanged_hash = H, + session_id = sid(Ssh, H)})}; Error -> ssh_connection_handler:disconnect( #ssh_msg_disconnect{ @@ -682,7 +689,7 @@ handle_kex_ecdh_reply(#ssh_msg_kex_ecdh_reply{public_host_key = PeerPubHostKey, %%%---------------------------------------------------------------- handle_new_keys(#ssh_msg_newkeys{}, Ssh0) -> - try install_alg(Ssh0) of + try install_alg(rcv, Ssh0) of #ssh{} = Ssh -> {ok, Ssh} catch @@ -693,6 +700,34 @@ handle_new_keys(#ssh_msg_newkeys{}, Ssh0) -> }) end. + +%%%---------------------------------------------------------------- +kex_ext_info(Role, Opts) -> + case ?GET_OPT(recv_ext_info,Opts) of + true when Role==client -> ["ext-info-c"]; + true when Role==server -> ["ext-info-s"]; + false -> [] + end. + +ext_info_message(#ssh{role=client, + algorithms=#alg{send_ext_info=true}} = Ssh0) -> + %% FIXME: no extensions implemented for clients + {ok, "", Ssh0}; + +ext_info_message(#ssh{role=server, + algorithms=#alg{send_ext_info=true}} = Ssh0) -> + AlgsList = lists:map(fun erlang:atom_to_list/1, + ssh_transport:default_algorithms(public_key)), + Msg = #ssh_msg_ext_info{nr_extensions = 1, + data = [{"server-sig-algs", string:join(AlgsList,",")}] + }, + {SshPacket, Ssh} = ssh_packet(Msg, Ssh0), + {ok, SshPacket, Ssh}; + +ext_info_message(Ssh0) -> + {ok, "", Ssh0}. % "" means: 'do not send' + +%%%---------------------------------------------------------------- %% select session id sid(#ssh{session_id = undefined}, H) -> H; @@ -812,7 +847,7 @@ known_host_key(#ssh{opts = Opts, key_cb = {KeyCb,KeyCbOpts}, peer = {PeerName,_} %% %% The first algorithm in each list MUST be the preferred (guessed) %% algorithm. Each string MUST contain at least one algorithm name. -select_algorithm(Role, Client, Server) -> +select_algorithm(Role, Client, Server, Opts) -> {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server), {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server), @@ -837,17 +872,34 @@ select_algorithm(Role, Client, Server) -> Kex = select(Client#ssh_msg_kexinit.kex_algorithms, Server#ssh_msg_kexinit.kex_algorithms), - Alg = #alg{kex = Kex, - hkey = HK, - encrypt = Encrypt, - decrypt = Decrypt, - send_mac = SendMac, - recv_mac = RecvMac, - compress = Compression, - decompress = Decompression, - c_lng = C_Lng, - s_lng = S_Lng}, - {ok, Alg}. + SendExtInfo = + %% To send we must have that option enabled and ... + ?GET_OPT(send_ext_info,Opts) andalso + %% ... the peer must have told us to send: + case Role of + server -> lists:member("ext-info-c", Client#ssh_msg_kexinit.kex_algorithms); + client -> lists:member("ext-info-s", Server#ssh_msg_kexinit.kex_algorithms) + end, + + RecvExtInfo = + %% The peer should not send unless told so by us (which is + %% guided by an option). + %% (However a malicious peer could send anyway, so we must be prepared) + ?GET_OPT(recv_ext_info,Opts), + + {ok, #alg{kex = Kex, + hkey = HK, + encrypt = Encrypt, + decrypt = Decrypt, + send_mac = SendMac, + recv_mac = RecvMac, + compress = Compression, + decompress = Decompression, + c_lng = C_Lng, + s_lng = S_Lng, + send_ext_info = SendExtInfo, + recv_ext_info = RecvExtInfo + }}. %%% It is an agreed problem with RFC 5674 that if the selection is @@ -928,45 +980,66 @@ select_compression_decompression(server, Client, Server) -> Server#ssh_msg_kexinit.compression_algorithms_server_to_client), {Compression, Decompression}. -install_alg(SSH) -> - SSH1 = alg_final(SSH), - SSH2 = alg_setup(SSH1), - alg_init(SSH2). +%% DIr = rcv | snd +install_alg(Dir, SSH) -> + SSH1 = alg_final(Dir, SSH), + SSH2 = alg_setup(Dir, SSH1), + alg_init(Dir, SSH2). -alg_setup(SSH) -> +alg_setup(snd, SSH) -> ALG = SSH#ssh.algorithms, SSH#ssh{kex = ALG#alg.kex, hkey = ALG#alg.hkey, encrypt = ALG#alg.encrypt, - decrypt = ALG#alg.decrypt, send_mac = ALG#alg.send_mac, send_mac_size = mac_digest_size(ALG#alg.send_mac), + compress = ALG#alg.compress, + c_lng = ALG#alg.c_lng, + s_lng = ALG#alg.s_lng, + send_ext_info = ALG#alg.send_ext_info, + recv_ext_info = ALG#alg.recv_ext_info + }; + +alg_setup(rcv, SSH) -> + ALG = SSH#ssh.algorithms, + SSH#ssh{kex = ALG#alg.kex, + hkey = ALG#alg.hkey, + decrypt = ALG#alg.decrypt, recv_mac = ALG#alg.recv_mac, recv_mac_size = mac_digest_size(ALG#alg.recv_mac), - compress = ALG#alg.compress, decompress = ALG#alg.decompress, c_lng = ALG#alg.c_lng, s_lng = ALG#alg.s_lng, - algorithms = undefined + send_ext_info = ALG#alg.send_ext_info, + recv_ext_info = ALG#alg.recv_ext_info }. -alg_init(SSH0) -> + +alg_init(snd, SSH0) -> {ok,SSH1} = send_mac_init(SSH0), - {ok,SSH2} = recv_mac_init(SSH1), - {ok,SSH3} = encrypt_init(SSH2), - {ok,SSH4} = decrypt_init(SSH3), - {ok,SSH5} = compress_init(SSH4), - {ok,SSH6} = decompress_init(SSH5), - SSH6. - -alg_final(SSH0) -> + {ok,SSH2} = encrypt_init(SSH1), + {ok,SSH3} = compress_init(SSH2), + SSH3; + +alg_init(rcv, SSH0) -> + {ok,SSH1} = recv_mac_init(SSH0), + {ok,SSH2} = decrypt_init(SSH1), + {ok,SSH3} = decompress_init(SSH2), + SSH3. + + +alg_final(snd, SSH0) -> {ok,SSH1} = send_mac_final(SSH0), - {ok,SSH2} = recv_mac_final(SSH1), - {ok,SSH3} = encrypt_final(SSH2), - {ok,SSH4} = decrypt_final(SSH3), - {ok,SSH5} = compress_final(SSH4), - {ok,SSH6} = decompress_final(SSH5), - SSH6. + {ok,SSH2} = encrypt_final(SSH1), + {ok,SSH3} = compress_final(SSH2), + SSH3; + +alg_final(rcv, SSH0) -> + {ok,SSH1} = recv_mac_final(SSH0), + {ok,SSH2} = decrypt_final(SSH1), + {ok,SSH3} = decompress_final(SSH2), + SSH3. + select_all(CL, SL) when length(CL) + length(SL) < ?MAX_NUM_ALGORITHMS -> A = CL -- SL, %% algortihms only used by client diff --git a/lib/ssh/src/ssh_transport.hrl b/lib/ssh/src/ssh_transport.hrl index 19b3f5c437..faae6008f2 100644 --- a/lib/ssh/src/ssh_transport.hrl +++ b/lib/ssh/src/ssh_transport.hrl @@ -48,6 +48,7 @@ -define(SSH_MSG_DEBUG, 4). -define(SSH_MSG_SERVICE_REQUEST, 5). -define(SSH_MSG_SERVICE_ACCEPT, 6). +-define(SSH_MSG_EXT_INFO, 7). -define(SSH_MSG_KEXINIT, 20). -define(SSH_MSG_NEWKEYS, 21). @@ -88,6 +89,20 @@ name %% string }). +-record(ssh_msg_ext_info, + { + nr_extensions, %% uint32 + + %% repeat the following 2 fields "nr-extensions" times: + %% string extension-name + %% string extension-value + + data %% [{extension-name, %% string + %% extension-value}, %% string + %% ... + %% ] + }). + -record(ssh_msg_kexinit, { cookie, %% random(16) |