[Pkg-erlang-devel] Bug#1105009: bookworm-pu: package erlang/1:25.2.3+dfsg-1+deb12u2

Sergei Golovan sgolovan at debian.org
Fri May 9 22:12:58 BST 2025


Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: erlang at packages.debian.org
Control: affects -1 + src:erlang
User: release.debian.org at packages.debian.org
Usertags: pu

[ Reason ]
Recently, another vulnerability was published for the ssh
application in Erlang (CVE-2025-46712, see [1], [2] for detail).

Debian security team contacted me (see [3]) and suggested
not to do DSA but to upload the fixed package into
proposed-updates.

[ Impact ]
Without the fix, in theory, everyone that runs SSH daemon
from the Debian Erlang distribution have a weakness in
its key exchange mechanism. I'm not aware if an exploit
of this weakness exists as for now.

[ Tests ]
Upstream added a few tests for the new code. I don't run them
automatically on build (or by autopkgtests), but I've successfully
run them manually on amd64 architecture machine. There wasn't
any breakage of old tests as well.

[ Risks ]
I don't expect any breakage by introducing the new code.

[ Checklist ]
  [+] *all* changes are documented in the d/changelog
  [+] I reviewed all changes and I approve them
  [+] attach debdiff against the package in (old)stable
  [+] the issue is verified as fixed in unstable

[ Changes ]
The main patch is to lib/ssh/src/ssh_fsm_kexinit.erl, it implements
the strict KEX algorithm and adds checks for strict KEX essentially
to all steps of key exchange process.

The important additional patch is to
lib/ssh/test/ssh_protocol_SUITE.erl, where three new tests have
been added for this new code. Also, the auxiliary test library
lib/ssh/test/ssh_trpt_test_lib.erl is also augmented.

[1] https://security-tracker.debian.org/tracker/CVE-2025-46712
[2] https://www.cve.org/CVERecord?id=CVE-2025-46712
[3] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1104963

-- 
Sergei Golovan
-------------- next part --------------
diff -Nru erlang-25.2.3+dfsg/debian/changelog erlang-25.2.3+dfsg/debian/changelog
--- erlang-25.2.3+dfsg/debian/changelog	2025-04-20 08:09:59.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/changelog	2025-05-09 09:29:41.000000000 +0300
@@ -1,3 +1,9 @@
+erlang (1:25.2.3+dfsg-1+deb12u2) bookworm-proposed-updates; urgency=medium
+
+  * ssh: fix strict KEX hardening (CVE-2025-46712) (closes: #1104963).
+
+ -- Sergei Golovan <sgolovan at debian.org>  Fri, 09 May 2025 09:29:41 +0300
+
 erlang (1:25.2.3+dfsg-1+deb12u1) bookworm-security; urgency=high
 
   [ Salvatore Bonaccorso ]
diff -Nru erlang-25.2.3+dfsg/debian/patches/series erlang-25.2.3+dfsg/debian/patches/series
--- erlang-25.2.3+dfsg/debian/patches/series	2025-04-20 08:09:59.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/series	2025-05-09 09:29:41.000000000 +0300
@@ -13,3 +13,4 @@
 ssh-use-chars_limit-for-bad-packets-error-messages.patch
 ssh-custom_kexinit-test-added.patch
 ssh-early-RCE-fix.patch
+ssh-strict-KEX-exchange-hardening.patch
diff -Nru erlang-25.2.3+dfsg/debian/patches/ssh-strict-KEX-exchange-hardening.patch erlang-25.2.3+dfsg/debian/patches/ssh-strict-KEX-exchange-hardening.patch
--- erlang-25.2.3+dfsg/debian/patches/ssh-strict-KEX-exchange-hardening.patch	1970-01-01 03:00:00.000000000 +0300
+++ erlang-25.2.3+dfsg/debian/patches/ssh-strict-KEX-exchange-hardening.patch	2025-05-09 09:29:41.000000000 +0300
@@ -0,0 +1,607 @@
+From: Jakub Witczak <kuba at erlang.org>
+Date: Tue, 6 May 2025 17:01:29 +0200
+Subject: 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
+Origin: https://github.com/erlang/otp/commit/e4b56a9f4a511aa9990dd86c16c61439c828df83
+Bug-Debian: https://bugs.debian.org/1104963
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-46712
+
+--- 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").
+ 
+ %%====================================================================
+@@ -705,16 +704,6 @@
+     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;
+ 
+@@ -1118,11 +1107,14 @@
+     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
+--- 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 @@
+ 
+ %%--------------------------------------------------------------------
+ 
+-%%% ######## {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 @@
+ 	  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 @@
+     {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 @@
+ 
+ %%%---- 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 @@
+     {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 @@
+     {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 @@
+     %% 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_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 @@
+ 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 at 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{})])
++    ].
+--- 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,
+@@ -297,7 +296,12 @@
+ 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}) ->
+@@ -1082,7 +1086,7 @@
+ %%   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
+@@ -1108,7 +1112,6 @@
+             _ ->
+                 KexStrictNegotiated0
+         end,
+-
+     {Encrypt0, Decrypt0} = select_encrypt_decrypt(Role, Client, Server),
+     {SendMac0, RecvMac0} = select_send_recv_mac(Role, Client, Server),
+ 
+--- a/lib/ssh/test/ssh_protocol_SUITE.erl
++++ b/lib/ssh/test/ssh_protocol_SUITE.erl
+@@ -54,7 +54,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,
+@@ -143,7 +145,9 @@
+ 		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,
+@@ -930,22 +934,145 @@
+     logger:set_primary_config(Level),
+     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).
+@@ -970,12 +1097,7 @@
+              {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),
+--- a/lib/ssh/test/ssh_trpt_test_lib.erl
++++ b/lib/ssh/test/ssh_trpt_test_lib.erl
+@@ -90,7 +90,8 @@
+ 	    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 @@
+     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 @@
+ 	    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 @@
+            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 = <<>>,


More information about the Pkg-erlang-devel mailing list