public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/erlang] f44: Backported fixes for CVE-2026-65634, CVE-2026-68956, CVE-2026-89422
@ 2026-09-23 20:27 Peter Lemenkov
  0 siblings, 0 replies; only message in thread
From: Peter Lemenkov @ 2026-09-23 20:27 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/erlang
            Branch : f44
            Commit : a131da91b10624199ad49adee66c82cd27a06ede
            Author : Peter Lemenkov <lemenkov@gmail.com>
            Date   : 2026-09-23T22:26:58+02:00
            Stats  : +1403/-1 in 4 file(s)
            URL    : https://src.fedoraproject.org/rpms/erlang/c/a131da91b10624199ad49adee66c82cd27a06ede?branch=f44

            Log:
            Backported fixes for CVE-2026-65634, CVE-2026-68956, CVE-2026-89422

Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>

---
diff --git a/erlang.spec b/erlang.spec
index 5481d2b..ccf6c1e 100644
--- a/erlang.spec
+++ b/erlang.spec
@@ -70,7 +70,7 @@
 
 Name:		erlang
 Version:	26.2.5.21
-Release:	7%{?dist}
+Release:	8%{?dist}
 Summary:	General-purpose programming language and runtime environment
 
 License:	Apache-2.0
@@ -119,6 +119,9 @@ Patch17: otp-0017-ssl-Add-pre-TLS-1.3-client-cipher-suite-check.patch
 Patch18: otp-0018-inets-Fix-rejection-of-invalid-chunk-sizes.patch
 Patch19: otp-0019-ssl-Use-digraph-to-ensure-robust-cert-chain-building.patch
 Patch20: otp-0020-public_key-Cap-policy-tree-growth-to-prevent-DoS.patch
+Patch21: otp-0021-asn1-Mitigate-a-DoS-vector.patch
+Patch22: otp-0022-ssh-Limit-size-of-channel-cache-entries.patch
+Patch23: otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch
 # end of autogenerated patch tag list
 
 BuildRequires:	gcc
@@ -1967,6 +1970,9 @@ ERL_TOP=${ERL_TOP} make TARGET=${TARGET} release_tests
 
 
 %changelog
+* Wed Sep 23 2026 Peter Lemenkov <lemenkov@gmail.com> - 26.2.5.21-8
+- Backported fixes for CVE-2026-65634, CVE-2026-68956, CVE-2026-89422
+
 * Mon Sep  7 2026 Peter Lemenkov <lemenkov@gmail.com> - 26.2.5.21-7
 - Backported fixes for CVE-2026-58227 and CVE-2026-59251
 

diff --git a/otp-0021-asn1-Mitigate-a-DoS-vector.patch b/otp-0021-asn1-Mitigate-a-DoS-vector.patch
new file mode 100644
index 0000000..d1b1985
--- /dev/null
+++ b/otp-0021-asn1-Mitigate-a-DoS-vector.patch
@@ -0,0 +1,217 @@
+From: =?UTF-8?q?John=20H=C3=B6gberg?= <john@erlang.org>
+Date: Tue, 28 Jul 2026 11:56:41 +0200
+Subject: [PATCH] asn1: Mitigate a DoS vector
+
+
+diff --git a/lib/asn1/src/asn1rtt_ber.erl b/lib/asn1/src/asn1rtt_ber.erl
+index f2458d25c5..cce748002e 100644
+--- a/lib/asn1/src/asn1rtt_ber.erl
++++ b/lib/asn1/src/asn1rtt_ber.erl
+@@ -536,21 +536,20 @@ decode_tag_and_length(<<Class:2, Form:1, 31:5, 1:1, TagPart1:7, 0:1, TagPartLast
+     << V:Length/binary, RestBuffer2/binary>> = RestBuffer,
+     {Form, (Class bsl 16) bor TagNo, V, RestBuffer2};
+ decode_tag_and_length(<<Class:2, Form:1, 31:5, Buffer/binary>>) ->
+-    {TagNo, Buffer1} = decode_tag(Buffer, 0),
++    {TagNo, Buffer1} = decode_tag(Buffer),
+     {Length, RestBuffer} = decode_length(Buffer1),
+-    << V:Length/binary, RestBuffer2/binary>> = RestBuffer,
++    <<V:Length/binary, RestBuffer2/binary>> = RestBuffer,
+     {Form, (Class bsl 16) bor TagNo, V, RestBuffer2}.
+ 
++decode_tag(Buffer) ->
++    decode_tag(Buffer, 0, 0).
+ 
+-
+-%% last partial tag
+-decode_tag(<<0:1,PartialTag:7, Buffer/binary>>, TagAck) ->
++decode_tag(<<0:1, PartialTag:7, Buffer/binary>>, N, TagAck) when N < 32 ->
+     TagNo = (TagAck bsl 7) bor PartialTag,
+     {TagNo, Buffer};
+-%% more tags
+-decode_tag(<<_:1,PartialTag:7, Buffer/binary>>, TagAck) ->
++decode_tag(<<_:1, PartialTag:7, Buffer/binary>>, N, TagAck) when N < 32 ->
+     TagAck1 = (TagAck bsl 7) bor PartialTag,
+-    decode_tag(Buffer, TagAck1).
++    decode_tag(Buffer, N + 1, TagAck1).
+ 
+ %%=======================================================================
+ %%
+@@ -1172,7 +1171,7 @@ mk_object_val(Val, Ack, Len) ->
+ 
+ decode_object_identifier(Tlv, Tags) ->
+     Val = match_tags(Tlv, Tags),
+-    [AddedObjVal|ObjVals] = dec_subidentifiers(Val,0,[]),
++    [AddedObjVal|ObjVals] = dec_subidentifiers(Val),
+     {Val1, Val2} = if
+ 		       AddedObjVal < 40 ->
+ 			   {0, AddedObjVal};
+@@ -1183,12 +1182,20 @@ decode_object_identifier(Tlv, Tags) ->
+ 		   end,
+     list_to_tuple([Val1, Val2 | ObjVals]).
+ 
+-dec_subidentifiers(<<>>,_Av,Al) ->
+-    lists:reverse(Al);
+-dec_subidentifiers(<<1:1,H:7,T/binary>>,Av,Al) ->
+-    dec_subidentifiers(T,(Av bsl 7) + H,Al);
+-dec_subidentifiers(<<H,T/binary>>,Av,Al) ->
+-    dec_subidentifiers(T,0,[((Av bsl 7) + H)|Al]).
++dec_subidentifiers(<<Octets/binary>>) ->
++    dec_subidentifiers_1(Octets, 0, 0).
++
++%% Reject overlong OID components to mitigate a DoS vector. This should be
++%% enough bits for all legitimate uses until someone can prove otherwise. For
++%% comparison, golang limits to 30 bits per component.
++dec_subidentifiers_1(<<1:1, H:7, T/binary>>, N, Av0) when N < 16 ->
++    Av = (Av0 bsl 7) bor H,
++    dec_subidentifiers_1(T, N + 1, Av);
++dec_subidentifiers_1(<<H, T/binary>>, N, Av0) when N < 16 ->
++    Av = (Av0 bsl 7) bor H,
++    [Av | dec_subidentifiers_1(T, 0, 0)];
++dec_subidentifiers_1(<<>>, _N, _Av) ->
++    [].
+ 
+ %%============================================================================
+ %% RELATIVE-OID, ITU_T X.690 Chapter 8.20
+@@ -1215,7 +1222,7 @@ enc_relative_oid(Val) ->
+ %%============================================================================
+ decode_relative_oid(Tlv, Tags) ->
+     Val = match_tags(Tlv, Tags),
+-    ObjVals = dec_subidentifiers(Val,0,[]),
++    ObjVals = dec_subidentifiers(Val),
+     list_to_tuple(ObjVals).
+ 
+ %%============================================================================
+@@ -1411,19 +1418,21 @@ dynamicsort_SETOF(ListOfEncVal) ->
+ 
+ %% multiple octet tag
+ dynsort_decode_tag(<<Class:2,_Form:1,31:5,Buffer/binary>>) ->
+-    TagNum = dynsort_decode_tag(Buffer, 0),
++    TagNum = dynsort_decode_tag_1(Buffer, 0, 0),
+     {Class,TagNum};
+ 
+ %% single tag (< 31 tags)
+ dynsort_decode_tag(<<Class:2,_Form:1,TagNum:5,_/binary>>) ->
+     {Class,TagNum}.
+ 
+-dynsort_decode_tag(<<0:1,PartialTag:7,_/binary>>, TagAcc) ->
++%% Reject overlong tags to mitigate a DoS vector. This should be enough bits
++%% for all legitimate uses until someone can prove otherwise.
++dynsort_decode_tag_1(<<0:1,PartialTag:7,_/binary>>, N, TagAcc) when N < 16 ->
+     (TagAcc bsl 7) bor PartialTag;
+-dynsort_decode_tag(<<_:1,PartialTag:7,Buffer/binary>>, TagAcc0) ->
++dynsort_decode_tag_1(<<_:1,PartialTag:7,Buffer/binary>>, N, TagAcc0)
++  when N < 16 ->
+     TagAcc = (TagAcc0 bsl 7) bor PartialTag,
+-    dynsort_decode_tag(Buffer, TagAcc).
+-
++    dynsort_decode_tag_1(Buffer, N + 1, TagAcc).
+ 
+ %%-------------------------------------------------------------------------
+ %% INTERNAL HELPER FUNCTIONS (not exported)
+diff --git a/lib/asn1/src/asn1rtt_jer.erl b/lib/asn1/src/asn1rtt_jer.erl
+index a6d6363eb6..665191b1ff 100644
+--- a/lib/asn1/src/asn1rtt_jer.erl
++++ b/lib/asn1/src/asn1rtt_jer.erl
+@@ -393,9 +393,16 @@ oid2json([],Acc) ->
+     list_to_binary(lists:reverse(Acc)).
+ 
+ json2oid(OidStr) when is_binary(OidStr) ->
+-    OidList = binary:split(OidStr,[<<".">>],[global]),
+-    OidNumList = [binary_to_integer(Num)||Num <- OidList],
+-    list_to_tuple(OidNumList).
++    SubIds = binary:split(OidStr,[<<".">>],[global]),
++    list_to_tuple(json2oid_1(SubIds)).
++
++%% Reject overlong OID components to mitigate a DoS vector. This should be
++%% enough bits for all legitimate uses until someone can prove otherwise. For
++%% comparison, golang limits to 30 bits per component.
++json2oid_1([SubId | SubIds]) when byte_size(SubId) < 32 ->
++    [binary_to_integer(SubId) | json2oid_1(SubIds)];
++json2oid_1([]) ->
++    [].
+ 
+ jer_bit_str2bitstr(Compact = {_Unused,_Binary}, _NamedBitList) ->
+     jer_compact2bitstr(Compact);
+diff --git a/lib/asn1/src/asn1rtt_per_common.erl b/lib/asn1/src/asn1rtt_per_common.erl
+index 5b5f47dfee..80022a90c9 100644
+--- a/lib/asn1/src/asn1rtt_per_common.erl
++++ b/lib/asn1/src/asn1rtt_per_common.erl
+@@ -90,7 +90,7 @@ decode_big_chars(Val, N) ->
+     decode_big_chars_1(decode_chars(Val, N)).
+ 
+ decode_oid(Octets) ->
+-    [First|Rest] = dec_subidentifiers(Octets, 0, []),
++    [First|Rest] = dec_subidentifiers(Octets),
+     Idlist = if
+ 		 First < 40 ->
+ 		     [0,First|Rest];
+@@ -102,7 +102,7 @@ decode_oid(Octets) ->
+     list_to_tuple(Idlist).
+ 
+ decode_relative_oid(Octets) ->
+-    list_to_tuple(dec_subidentifiers(Octets, 0, [])).
++    list_to_tuple(dec_subidentifiers(Octets)).
+ 
+ encode_chars(Val, NumBits) ->
+     << <<C:NumBits>> || C <- Val >>.
+@@ -372,12 +372,20 @@ decode_big_chars_1([H|T]) ->
+     [list_to_tuple(binary_to_list(<<H:32>>))|decode_big_chars_1(T)];
+ decode_big_chars_1([]) -> [].
+ 
+-dec_subidentifiers([H|T], Av, Al) when H >=16#80 ->
+-    dec_subidentifiers(T, (Av bsl 7) bor (H band 16#7F), Al);
+-dec_subidentifiers([H|T], Av, Al) ->
+-    dec_subidentifiers(T, 0, [(Av bsl 7) bor H|Al]);
+-dec_subidentifiers([], _Av, Al) ->
+-    lists:reverse(Al).
++dec_subidentifiers(Octets) ->
++    dec_subidentifiers_1(Octets, 0, 0).
++
++%% Reject overlong OID components to mitigate a DoS vector. This should be
++%% enough bits for all legitimate uses until someone can prove otherwise. For
++%% comparison, golang limits to 30 bits per component.
++dec_subidentifiers_1([H | T], N, Av0) when H >= 16#80, N < 16 ->
++    Av = (Av0 bsl 7) bor (H band 16#7F),
++    dec_subidentifiers_1(T, N + 1, Av);
++dec_subidentifiers_1([H | T], N, Av0) when N < 16 ->
++    Av = (Av0 bsl 7) bor H,
++    [Av | dec_subidentifiers_1(T, 0, 0)];
++dec_subidentifiers_1([], _N, _Av) ->
++    [].
+ 
+ enc_char(C0, Lb, Tab) ->
+     try	element(C0-Lb, Tab) of
+diff --git a/lib/asn1/test/testPrim.erl b/lib/asn1/test/testPrim.erl
+index ce7a7f28fa..236d704135 100644
+--- a/lib/asn1/test/testPrim.erl
++++ b/lib/asn1/test/testPrim.erl
+@@ -163,7 +163,28 @@ obj_id(_) ->
+     %%==========================================================
+ 
+     [roundtrip('ObjId', V) ||
+-	V <- [{0,22,3},{1,39,3},{2,100,3},{2,16303,3},{2,16304,3}]],
++        V <- [{0,22,3},
++              {1,39,3},
++              {2,100,3},
++              {2,16303,3},
++              {2,16304,3},
++              {2,1 bsl (14 * 7),3}]],
++
++    try
++        [roundtrip('ObjId', V) ||
++         V <- [{0,22,3},
++              {1,39,3},
++              {2,100,3},
++              {2,16303,3},
++              {2,16304,3},
++              {2,1 bsl (20 * 7),3}]]
++    of
++        [_|_]=Res -> ct:fail("Failed to reject overlong oid ~p", [Res])
++    catch
++        _:_ ->
++            ok
++    end,
++
+     ok.
+ 
+ rel_oid(_Rules) ->

diff --git a/otp-0022-ssh-Limit-size-of-channel-cache-entries.patch b/otp-0022-ssh-Limit-size-of-channel-cache-entries.patch
new file mode 100644
index 0000000..293634d
--- /dev/null
+++ b/otp-0022-ssh-Limit-size-of-channel-cache-entries.patch
@@ -0,0 +1,765 @@
+From: =?UTF-8?q?Micha=C5=82=20W=C4=85sowski?= <michal@erlang.org>
+Date: Mon, 31 Aug 2026 19:01:07 +0200
+Subject: [PATCH] ssh: Limit size of channel cache entries
+
+(cherry picked from commit e2bbad0107bba41491a4d549686fd023db8740da)
+
+[Backport to OTP-26: dropped -doc attributes (docs not updated), kept
+ cache_find/2 in ssh_client_channel, and kept the simple
+ end_per_testcase/2 in ssh_connection_SUITE since the logger-event
+ verification framework does not exist on this branch.]
+
+Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
+Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>
+
+diff --git a/lib/ssh/src/ssh_channel_sup.erl b/lib/ssh/src/ssh_channel_sup.erl
+index 44f8df753b..450b04cf03 100644
+--- a/lib/ssh/src/ssh_channel_sup.erl
++++ b/lib/ssh/src/ssh_channel_sup.erl
+@@ -25,9 +25,8 @@
+ -module(ssh_channel_sup).
+ 
+ -behaviour(supervisor).
+--include("ssh.hrl").
+ 
+--export([start_link/1, start_child/8]).
++-export([start_link/1, start_child/7]).
+ 
+ %% Supervisor callback
+ -export([init/1]).
+@@ -39,16 +38,11 @@ start_link(Args) ->
+     supervisor:start_link(?MODULE, [Args]).
+ 
+ 
+-start_child(client, ChannelSup, ConnRef, Callback, Id, Args, Exec, _Opts) when is_pid(ConnRef) ->
++start_child(client, ChannelSup, ConnRef, Callback, Id, Args, Exec) when is_pid(ConnRef) ->
+     start_the_channel(ssh_client_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec);
+ 
+-start_child(server, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts) when is_pid(ConnRef) ->
+-     case max_num_channels_not_exceeded(ChannelSup, Opts) of
+-         true ->
+-             start_the_channel(ssh_server_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec);
+-         false ->
+-             {error, max_num_channels_exceeded}
+-    end.
++start_child(server, ChannelSup, ConnRef, Callback, Id, Args, Exec) when is_pid(ConnRef) ->
++    start_the_channel(ssh_server_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec).
+ 
+ 
+ %%%=========================================================================
+@@ -64,14 +58,6 @@ init(_Args) ->
+ %%%=========================================================================
+ %%%  Internal functions
+ %%%=========================================================================
+-max_num_channels_not_exceeded(ChannelSup, Opts) ->
+-    MaxNumChannels = ?GET_OPT(max_channels, Opts),
+-    NumChannels = length([x || {_,_,worker,[ssh_server_channel]} <- 
+-				   supervisor:which_children(ChannelSup)]),
+-    %% Note that NumChannels is BEFORE starting a new one
+-    NumChannels < MaxNumChannels.
+-
+-
+ start_the_channel(ChanMod, ChannelSup, ConnRef, Callback, Id, Args, Exec) ->
+     ChildSpec =
+         #{id       => make_ref(),
+diff --git a/lib/ssh/src/ssh_client_channel.erl b/lib/ssh/src/ssh_client_channel.erl
+index 6b6623945f..b907553ce6 100644
+--- a/lib/ssh/src/ssh_client_channel.erl
++++ b/lib/ssh/src/ssh_client_channel.erl
+@@ -67,7 +67,7 @@
+ 	 terminate/2, code_change/3]).
+ 
+ %% Internal application API
+--export([cache_create/0, cache_lookup/2, cache_update/2, 
++-export([cache_create/0, cache_lookup/2, cache_insert/3, cache_update/2,
+ 	 cache_delete/1, cache_delete/2,  cache_foldl/3,
+ 	 cache_info/2,  cache_find/2,
+ 	 get_print_info/1, get_print_info/2
+@@ -343,6 +343,20 @@ cache_lookup(Cache, Key) ->
+ 	    undefined
+     end.
+ 
++cache_insert(Cache, #channel{local_id = Id} = Entry, infinity) when Id =/= undefined ->
++    true = ets:insert_new(Cache, Entry),
++    ok;
++cache_insert(Cache, #channel{local_id = Id} = Entry, Limit) when Id =/= undefined,
++                                                                 is_integer(Limit),
++                                                                 Limit > 0 ->
++    case cache_info(num_entries, Cache) of
++        Num when Num >= Limit ->
++            {error, max_num_channels_exceeded};
++        _Num ->
++            true = ets:insert_new(Cache, Entry),
++            ok
++    end.
++
+ cache_update(Cache, #channel{local_id = Id} = Entry) when Id =/= undefined ->
+     ets:insert(Cache, Entry).
+ 
+@@ -356,7 +370,7 @@ cache_foldl(Fun, Acc, Cache) ->
+     ets:foldl(Fun, Acc, Cache).
+     
+ cache_info(num_entries, Cache) ->
+-    proplists:get_value(size, ets:info(Cache)).
++    ets:info(Cache, size).
+ 
+ cache_find(ChannelPid, Cache) ->
+    case ets:match_object(Cache, #channel{user = ChannelPid}) of
+diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl
+index cf2c434e08..fb0bc07f74 100644
+--- a/lib/ssh/src/ssh_connection.erl
++++ b/lib/ssh/src/ssh_connection.erl
+@@ -609,8 +609,9 @@ handle_msg(#ssh_msg_channel_open{channel_type = "session" = Type,
+     
+     if 
+ 	MinAcceptedPackSz =< PacketSz ->
+-	    try setup_session(Connection0, RemoteId,
+-			      Type, WindowSz, PacketSz) of
++            Limit = ?GET_OPT(max_channels, SSHopts),
++            try setup_session(Connection0, RemoteId,
++                              Type, WindowSz, PacketSz, Limit) of
+ 		Result ->
+ 		    Result
+ 	    catch _:_ ->
+@@ -640,7 +641,6 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip",
+                        channel_id_seed = ChId,
+                        suggest_window_size = WinSz,
+                        suggest_packet_size = PktSz,
+-                       options = Options,
+                        connection_supervisor = ConnectionSup
+                       } = C,
+ 	   client, _SSH) ->
+@@ -650,20 +650,20 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip",
+                 case gen_tcp:connect(ConnectToHost, ConnectToPort, [{active,false}, binary]) of
+                     {ok,Sock} ->
+                         {ok,Pid} = ssh_connection_sup:start_channel(client, ConnectionSup, self(),
+-                                                                   ssh_tcpip_forward_client, ChId,
+-                                                                   [Sock], undefined, Options),
+-                        ssh_client_channel:cache_update(Cache,
+-                                                        #channel{type = "forwarded-tcpip",
+-                                                                 sys = "none",
+-                                                                 local_id = ChId,
+-                                                                 remote_id = RemoteId,
+-                                                                 user = Pid,
+-                                                                 recv_window_size = WinSz,
+-                                                                 recv_packet_size = PktSz,
+-                                                                 send_window_size = WindowSize,
+-                                                                 send_packet_size = PacketSize,
+-                                                                 send_buf = queue:new()
+-                                                                }),
++                                                                    ssh_tcpip_forward_client, ChId,
++                                                                    [Sock], undefined),
++                        Channel = #channel{type = "forwarded-tcpip",
++                                           sys = "none",
++                                           local_id = ChId,
++                                           remote_id = RemoteId,
++                                           user = Pid,
++                                           recv_window_size = WinSz,
++                                           recv_packet_size = PktSz,
++                                           send_window_size = WindowSize,
++                                           send_packet_size = PacketSize,
++                                           send_buf = queue:new()
++                                          },
++                        ok = ssh_client_channel:cache_insert(Cache, Channel, infinity),
+                         gen_tcp:controlling_process(Sock, Pid),
+                         inet:setopts(Sock, [{active,once}]),
+                         {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz),
+@@ -687,26 +687,26 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip",
+     {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}};
+ 
+ handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip",
+-				 sender_channel = RemoteId,
++                                 sender_channel = RemoteId,
+                                  initial_window_size = WindowSize,
+                                  maximum_packet_size = PacketSize,
+                                  data = <<?DEC_BIN(HostToConnect,_L1),        ?UINT32(PortToConnect),
+                                           ?DEC_BIN(_OriginatorIPaddress,_L2), ?UINT32(_OrignatorPort)
+                                         >>
+-                                }, 
+-	   #connection{channel_cache = Cache,
++                                } = Msg,
++           #connection{channel_cache = Cache,
+                        channel_id_seed = ChId,
+                        suggest_window_size = WinSz,
+                        suggest_packet_size = PktSz,
+                        options = Options,
+                        connection_supervisor = ConnectionSup
+                       } = C,
+-	   server, _SSH) ->
+-    {ReplyMsg, NextChId} =
++           server, SSH) ->
++    Result =
+         case ?GET_OPT(tcpip_tunnel_in, Options) of
+             %% May add more to the option, like allowed ip/port pairs to connect to
+             false ->
+-                {channel_open_failure_msg(RemoteId, 
++                {channel_open_failure_msg(RemoteId,
+                                           ?SSH_OPEN_CONNECT_FAILED,
+                                           "Forwarding disabled", "en"),
+                  ChId};
+@@ -715,36 +715,54 @@ handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip",
+                 case gen_tcp:connect(binary_to_list(HostToConnect), PortToConnect,
+                                      [{active,false}, binary]) of
+                     {ok,Sock} ->
+-                        {ok,Pid} = ssh_connection_sup:start_channel(server, ConnectionSup, self(),
+-                                                                   ssh_tcpip_forward_srv, ChId,
+-                                                                   [Sock], undefined, Options),
+-                        ssh_client_channel:cache_update(Cache,
+-                                                        #channel{type = "direct-tcpip",
+-                                                                 sys = "none",
+-                                                                 local_id = ChId,
+-                                                                 remote_id = RemoteId,
+-                                                                 user = Pid,
+-                                                                 recv_window_size = WinSz,
+-                                                                 recv_packet_size = PktSz,
+-                                                                 send_window_size = WindowSize,
+-                                                                 send_packet_size = PacketSize,
+-                                                                 send_buf = queue:new()
+-                                                                }),
+-                        gen_tcp:controlling_process(Sock, Pid),
+-                        inet:setopts(Sock, [{active,once}]),
+-
+-                        {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz),
+-                         ChId + 1};
++                        Limit = ?GET_OPT(max_channels, Options),
++                        Channel = #channel{type = "direct-tcpip",
++                                           sys = "none",
++                                           local_id = ChId,
++                                           remote_id = RemoteId,
++                                           recv_window_size = WinSz,
++                                           recv_packet_size = PktSz,
++                                           send_window_size = WindowSize,
++                                           send_packet_size = PacketSize,
++                                           send_buf = queue:new()
++                                          },
++                        case ssh_client_channel:cache_insert(Cache, Channel, Limit) of
++                            ok ->
++                                {ok,Pid} = ssh_connection_sup:start_channel(server, ConnectionSup, self(),
++                                                                            ssh_tcpip_forward_srv, ChId,
++                                                                            [Sock], undefined),
++                                ssh_client_channel:cache_update(Cache, Channel#channel{user = Pid}),
++                                gen_tcp:controlling_process(Sock, Pid),
++                                inet:setopts(Sock, [{active,once}]),
++
++                                {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz),
++                                 ChId + 1};
++                            {error, max_num_channels_exceeded} ->
++                                gen_tcp:close(Sock),
++                                MsgFun = fun(M, L) ->
++                                                 io_lib:format("Connection terminated. Message: ~w"
++                                                               " reached a limit of: ~p", [M, L],
++                                                               [{chars_limit, ssh_lib:max_log_len(SSH)}])
++                                         end,
++                                ?LOG_DEBUG(MsgFun, [Msg, Limit]),
++                                {send_disconnect, {?SSH_DISCONNECT_BY_APPLICATION, "Connection terminated. "
++                                                   "Channel limit reached."}}
++                        end;
+ 
+                     {error,Error} ->
+-                        {channel_open_failure_msg(RemoteId, 
++                        {channel_open_failure_msg(RemoteId,
+                                                   ?SSH_OPEN_CONNECT_FAILED,
+                                                   io_lib:format("Forwarded connection refused: ~p",[Error]),
+                                                   "en"),
+                          ChId}
+                 end
+         end,
+-    {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}};
++    case Result of
++        {send_disconnect, Reason} ->
++            {send_disconnect, Reason, handle_stop(C)};
++        {ReplyMsg, NextChId} ->
++            {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}}
++    end;
+ 
+ handle_msg(#ssh_msg_channel_open{channel_type = "session",
+ 				 sender_channel = RemoteId}, 
+@@ -1136,15 +1154,15 @@ encode_ip(Addr) when is_list(Addr) ->
+ 
+ %%%----------------------------------------------------------------
+ %%% Create the channel data when an ssh_msg_open_channel message
+-%%% of "session" typ is handled
++%%% of "session" type is handled
+ %%%
+ setup_session(#connection{channel_cache = Cache,
+                           channel_id_seed = NewChannelID,
+                           suggest_window_size = WinSz,
+                           suggest_packet_size = PktSz
+-			 } = C,
+-	      RemoteId, Type, WindowSize, PacketSize) when is_integer(WinSz),
+-                                                           is_integer(PktSz) ->
++                         } = C,
++              RemoteId, Type, WindowSize, PacketSize, ChannelLimit) when is_integer(WinSz),
++                                                                         is_integer(PktSz) ->
+     NextChannelID = NewChannelID + 1,
+     Channel =
+         #channel{type = Type,
+@@ -1157,7 +1175,7 @@ setup_session(#connection{channel_cache = Cache,
+                  send_buf = queue:new(),
+                  remote_id = RemoteId
+                 },
+-    ssh_client_channel:cache_update(Cache, Channel),
++    ok = ssh_client_channel:cache_insert(Cache, Channel, ChannelLimit),
+     OpenConfMsg = channel_open_confirmation_msg(RemoteId, NewChannelID,
+ 						WinSz, 
+ 						PktSz),
+@@ -1168,15 +1186,15 @@ setup_session(#connection{channel_cache = Cache,
+ %%%----------------------------------------------------------------
+ %%% Start a cli or subsystem
+ %%%
+-start_cli(#connection{options = Options, 
+-		      cli_spec = CliSpec,
++start_cli(#connection{cli_spec = CliSpec,
+ 		      exec = Exec,
+ 		      connection_supervisor = ConnectionSup}, ChannelId) ->
+     case CliSpec of
+         no_cli ->
+             {error, cli_disabled};
+         {CbModule, Args} ->
+-            ssh_connection_sup:start_channel(server, ConnectionSup, self(), CbModule, ChannelId, Args, Exec, Options)
++            ssh_connection_sup:start_channel(server, ConnectionSup, self(),
++                                             CbModule, ChannelId, Args, Exec)
+     end.
+ 
+ 
+@@ -1186,7 +1204,8 @@ start_subsystem(BinName, #connection{options = Options,
+     Name = binary_to_list(BinName),
+     case check_subsystem(Name, Options) of
+ 	{Callback, Opts} when is_atom(Callback), Callback =/= none ->
+-            ssh_connection_sup:start_channel(server, ConnectionSup, self(), Callback, ChannelId, Opts, undefined, Options);
++            ssh_connection_sup:start_channel(server, ConnectionSup, self(),
++                                             Callback, ChannelId, Opts, undefined);
+         {none, _} ->
+             {error, bad_subsystem};
+ 	{_, _} ->
+diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl
+index e2311b119a..362633c991 100644
+--- a/lib/ssh/src/ssh_connection_handler.erl
++++ b/lib/ssh/src/ssh_connection_handler.erl
+@@ -190,16 +190,16 @@ open_channel(ConnectionHandler,
+ 	  Timeout}).
+ 
+ %%--------------------------------------------------------------------
+-%%% Start a channel handling process in the superviser tree
++%%% Start a channel handling process in the supervisor tree
+ -spec start_channel(connection_ref(), atom(), channel_id(), list(), term()) ->
+                            {ok, pid()} | {error, term()}.
+ 
+ %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+ start_channel(ConnectionHandler, CallbackModule, ChannelId, Args, Exec) ->
+-    {ok, {ConnectionSup,Role,Opts}} = call(ConnectionHandler, get_misc),
++    {ok, {ConnectionSup,Role}} = call(ConnectionHandler, get_misc),
+     ssh_connection_sup:start_channel(Role, ConnectionSup,
+                                     ConnectionHandler, CallbackModule, ChannelId,
+-                                    Args, Exec, Opts).
++                                    Args, Exec).
+ 
+ %%--------------------------------------------------------------------
+ handle_direct_tcpip(ConnectionHandler, ListenHost, ListenPort, ConnectToHost, ConnectToPort, Timeout) ->
+@@ -762,6 +762,13 @@ handle_event(internal, {conn_msg,Msg}, StateName, #data{connection_state = Conne
+             end,
+             {stop_and_reply, {shutdown,normal}, Repls, D};
+ 
++        {send_disconnect, {Code, Description}, RepliesConn} ->
++            {Replies, D1} = send_replies(RepliesConn, D0),
++            D = send_msg(#ssh_msg_disconnect{code = Code,
++                                             description = Description},
++                         D1),
++            {stop_and_reply, {shutdown, Description}, Replies, D};
++
+ 	{Replies, Connection} when is_list(Replies) ->
+ 	    {Repls, D} = 
+ 		case StateName of
+@@ -1034,37 +1041,48 @@ handle_event({call,From}, {eof, ChannelId}, StateName, D0)
+ handle_event({call,From}, get_misc, StateName,
+              #data{connection_state = #connection{options = Opts}} = D) when ?CONNECTED(StateName) ->
+     ConnectionSup = ?GET_INTERNAL_OPT(connection_sup, Opts),
+-    Reply = {ok, {ConnectionSup, ?role(StateName), Opts}},
++    Reply = {ok, {ConnectionSup, ?role(StateName)}},
+     {keep_state, D, [{reply,From,Reply}]};
+ 
+ handle_event({call,From},
+-	     {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout},
+-	     StateName,
+-	     D0 = #data{connection_state = C}) when ?CONNECTED(StateName) ->
+-    erlang:monitor(process, ChannelPid),
+-    {ChannelId, D1} = new_channel_id(D0),
++             {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout},
++             StateName,
++             D0 = #data{connection_state = C = #connection{channel_id_seed = ChannelId,
++                                                           suggest_window_size = SuggestWindowSize,
++                                                           suggest_packet_size = SuggestPacketSize}})
++  when ?CONNECTED(StateName) ->
+     WinSz = case InitialWindowSize of
+-                undefined -> C#connection.suggest_window_size;
++                undefined -> SuggestWindowSize;
+                 _ -> InitialWindowSize
+             end,
+     PktSz = case MaxPacketSize of
+-                undefined -> C#connection.suggest_packet_size;
++                undefined -> SuggestPacketSize;
+                 _ -> MaxPacketSize
+             end,
+-    D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, WinSz, PktSz, Data),
+-		  D1),
+-    ssh_client_channel:cache_update(cache(D2),
+-			     #channel{type = Type,
+-				      sys = "none",
+-				      user = ChannelPid,
+-				      local_id = ChannelId,
+-				      recv_window_size = WinSz,
+-				      recv_packet_size = PktSz,
+-				      send_buf = queue:new()
+-				     }),
+-    D = add_request(true, ChannelId, From, D2),
+-    start_channel_request_timer(ChannelId, From, Timeout),
+-    {keep_state, D, cond_set_idle_timer(D)};
++    Limit = case ?role(StateName) of
++                server -> ?GET_OPT(max_channels, C#connection.options);
++                client -> infinity
++            end,
++    Channel = #channel{type = Type,
++                       sys = "none",
++                       user = ChannelPid,
++                       local_id = ChannelId,
++                       recv_window_size = WinSz,
++                       recv_packet_size = PktSz,
++                       send_buf = queue:new()
++                      },
++    case ssh_client_channel:cache_insert(cache(D0), Channel, Limit) of
++        ok ->
++            erlang:monitor(process, ChannelPid),
++            D1 = D0#data{connection_state = C#connection{channel_id_seed = ChannelId + 1}},
++            D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, WinSz,PktSz, Data),
++                          D1),
++            D = add_request(true, ChannelId, From, D2),
++            start_channel_request_timer(ChannelId, From, Timeout),
++            {keep_state, D, cond_set_idle_timer(D)};
++        Other ->
++            {keep_state, D0, [{reply,From,Other}]}
++    end;
+ 
+ handle_event({call,From}, {send_window, ChannelId}, StateName, D)
+   when ?CONNECTED(StateName) ->
+@@ -1360,16 +1378,39 @@ handle_event(info, check_cache, _, D) ->
+     {keep_state, D, cond_set_idle_timer(D)};
+ 
+ handle_event(info, {fwd_connect_received, Sock, ChId, ChanCB}, StateName, #data{connection_state = Connection}) ->
+-    #connection{options = Options,
+-                channel_cache = Cache,
++    #connection{channel_cache = Cache,
+                 connection_supervisor = ConnectionSup} = Connection,
+     Channel = ssh_client_channel:cache_lookup(Cache, ChId),
+-    {ok,Pid} = ssh_connection_sup:start_channel(?role(StateName), ConnectionSup, self(), ChanCB, ChId, [Sock], undefined, Options),
++    {ok,Pid} = ssh_connection_sup:start_channel(?role(StateName), ConnectionSup, self(), ChanCB, ChId, [Sock], undefined),
+     ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}),
+     gen_tcp:controlling_process(Sock, Pid),
+     inet:setopts(Sock, [{active,once}]),
+     keep_state_and_data;
+ 
++handle_event(info, {fwd_connect_failed, {error, max_num_channels_exceeded}}, StateName,
++             #data{connection_state = #connection{options = Opts}} = D0) ->
++    %% Keep same behavior as before, if channel limit is reached for port forwarding channels
++    %% we close the connection. Previously this was a crash in {fwd_connect_received, ...} on
++    %% {ok, Pid} = ssh_connection_sup:start_channel(...).
++    MsgFun =
++        fun(debug) ->
++                Limit = ?GET_OPT(max_channels, Opts),
++                io_lib:format("Connection terminated. Port forward request reached a "
++                              "channel limit of: ~p",
++                              [Limit]);
++           (_) ->
++                "Connection terminated. Channel limit reached."
++        end,
++    {Shutdown, D} =
++        ?send_disconnect(?SSH_DISCONNECT_BY_APPLICATION,
++                         "Connection terminated. Channel limit reached.",
++                         ?SELECT_MSG(MsgFun),
++                         StateName, D0),
++    {stop, Shutdown, D};
++handle_event(info, {fwd_connect_failed, _Other}, _StateName, _D) ->
++    %% Ignore other reasons
++    keep_state_and_data;
++
+ handle_event({call,From},
+              {handle_direct_tcpip, ListenHost, ListenPort, ConnectToHost, ConnectToPort, _Timeout},
+              _StateName,
+@@ -1792,12 +1833,6 @@ add_request(Fun, ChannelId, From, #data{connection_state =
+     Requests = [{ChannelId, From, Fun} | Requests0],
+     State#data{connection_state = Connection#connection{requests = Requests}}.
+ 
+-new_channel_id(#data{connection_state = #connection{channel_id_seed = Id} =
+-			 Connection}
+-	       = State) ->
+-    {Id, State#data{connection_state =
+-			Connection#connection{channel_id_seed = Id + 1}}}.
+-
+ 
+ %%%----------------------------------------------------------------
+ start_rekeying(Role, D0) ->
+diff --git a/lib/ssh/src/ssh_connection_sup.erl b/lib/ssh/src/ssh_connection_sup.erl
+index 3d8ac4171e..a0ddfc6680 100644
+--- a/lib/ssh/src/ssh_connection_sup.erl
++++ b/lib/ssh/src/ssh_connection_sup.erl
+@@ -29,7 +29,7 @@
+ -include("ssh.hrl").
+ 
+ -export([start_link/4,
+-         start_channel/8,
++         start_channel/7,
+          tcpip_fwd_supervisor/1
+ 	]).
+ 
+@@ -47,9 +47,9 @@ start_link(Role, Id, Socket, Options) ->
+             Other
+     end.
+ 
+-start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec, Opts) ->
++start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec) ->
+     ChannelSup = channel_supervisor(SupPid),
+-    ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts).
++    ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec).
+ 
+ tcpip_fwd_supervisor(ConnectionSup) ->
+     find_child(tcpip_forward_acceptor_sup, ConnectionSup).
+diff --git a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl
+index c3ebb902e3..20fade1297 100644
+--- a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl
++++ b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl
+@@ -87,8 +87,9 @@ acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB,
+                 {ok,ChId} ->
+                     gen_tcp:controlling_process(Sock, ConnPid),
+                     ConnPid ! {fwd_connect_received, Sock, ChId, ChanCB};
+-                _ ->
+-                    gen_tcp:close(Sock)
++                Other ->
++                    gen_tcp:close(Sock),
++                    ConnPid ! {fwd_connect_failed, Other}
+             end,
+             acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB, ConnPid);
+ 
+diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl
+index b9f5e6355e..40c141602e 100644
+--- a/lib/ssh/test/ssh_connection_SUITE.erl
++++ b/lib/ssh/test/ssh_connection_SUITE.erl
+@@ -73,6 +73,8 @@
+          kex_error/1,
+          interrupted_send/1,
+          max_channels_option/1,
++         max_channels_to_server/1,
++         max_channels_from_server/1,
+          no_sensitive_leak/1,
+          ptty_alloc/1,
+          ptty_alloc_default/1,
+@@ -183,6 +185,8 @@ all() ->
+      no_sensitive_leak,
+      start_subsystem_on_closed_channel,
+      max_channels_option,
++     max_channels_to_server,
++     max_channels_from_server,
+      handler_down_before_open
+     ].
+ groups() ->
+@@ -1768,19 +1772,22 @@ max_channels_option(Config) when is_list(Config) ->
+ 						      {user_interaction, true},
+ 						      {user_dir, UserDir}]),
+ 
+-    %% Allocate a number of ChannelId:s to play with. (This operation is not
+-    %% counted by the max_channel option).
++    %% Allocate a number of ChannelId:s to play with.
+     {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
+     {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity),
+     {ok, ChannelId2} = ssh_connection:session_channel(ConnectionRef, infinity),
+-    {ok, ChannelId3} = ssh_connection:session_channel(ConnectionRef, infinity),
+-    {ok, ChannelId4} = ssh_connection:session_channel(ConnectionRef, infinity),
+-    {ok, ChannelId5} = ssh_connection:session_channel(ConnectionRef, infinity),
+-    {ok, ChannelId6} = ssh_connection:session_channel(ConnectionRef, infinity),
+-    {ok, _ChannelId7} = ssh_connection:session_channel(ConnectionRef, infinity),
+-
+-    %% Now start to open the channels (this is counted my max_channels) to check that
+-    %% it gives a failure at right place
++    {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++        ssh_connection:session_channel(ConnectionRef, infinity),
++    {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++        ssh_connection:session_channel(ConnectionRef, infinity),
++    {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++        ssh_connection:session_channel(ConnectionRef, infinity),
++    {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++        ssh_connection:session_channel(ConnectionRef, infinity),
++    {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++        ssh_connection:session_channel(ConnectionRef, infinity),
++
++    %% Now start subsystems in open channels.
+ 
+     %%%---- Channel 1(3): shell
+     ok = ssh_connection:shell(ConnectionRef,ChannelId0),
+@@ -1802,16 +1809,29 @@ max_channels_option(Config) when is_list(Config) ->
+     after 5000 ->
+ 	    ct:fail("Exec #1 Timeout")
+     end,
++    %%%---- wait for exec to terminate
++    receive
++        {ssh_cm,ConnectionRef,{closed,ChannelId2}} -> ok
++    after 5000 ->
++            ct:log("Timeout waiting for '{ssh_cm,~p,{closed,~p}}'~n"
++                   "Message queue:~n~p",
++                   [ConnectionRef,ChannelId2,erlang:process_info(self(),messages)]),
++            ct:fail("exit Timeout",[])
++    end,
++
++    %% Now ChannelId2 should be closed now, we can open ChannelId3
++    {ok, ChannelId3} = ssh_connection:session_channel(ConnectionRef, infinity),
+ 
+     %%%---- Channel 3(3): subsystem "echo_n" (Note that ChannelId2 should be closed now)
+     ?wait_match(success, ssh_connection:subsystem(ConnectionRef, ChannelId3, "echo_n", infinity)),
+ 
+-    %%%---- Channel 4(3) !: exec  This should fail
+-    failure = ssh_connection:exec(ConnectionRef, ChannelId4, "testing2.\n", infinity),
++    %%%---- Channel 4(3) !: creating channel should fail
++    {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++        ssh_connection:session_channel(ConnectionRef, infinity),
+ 
+     %%%---- close the shell (Frees one channel)
+     ok = ssh_connection:send(ConnectionRef, ChannelId0, "exit().\n", 5000),
+-    
++
+     %%%---- wait for the subsystem to terminate
+     receive
+ 	{ssh_cm,ConnectionRef,{closed,ChannelId0}} -> ok
+@@ -1822,15 +1842,114 @@ max_channels_option(Config) when is_list(Config) ->
+ 	    ct:fail("exit Timeout",[])
+     end,
+ 
++    %%%---- Channel 3(3) !: exec  This should succeed
++    {ok, ChannelId4} = ssh_connection:session_channel(ConnectionRef, infinity),
++    success = ssh_connection:exec(ConnectionRef, ChannelId4, "testing2.\n", infinity),
++    %%%---- wait for exec to terminate
++    receive
++        {ssh_cm,ConnectionRef,{closed,ChannelId4}} -> ok
++    after 5000 ->
++            ct:log("Timeout waiting for '{ssh_cm,~p,{closed,~p}}'~n"
++                   "Message queue:~n~p",
++                   [ConnectionRef,ChannelId4,erlang:process_info(self(),messages)]),
++            ct:fail("exit Timeout",[])
++    end,
++
+     %%---- Try that we can open one channel instead of the closed one
++    {ok, ChannelId5} = ssh_connection:session_channel(ConnectionRef, infinity),
+     ?wait_match(success, ssh_connection:subsystem(ConnectionRef, ChannelId5, "echo_n", infinity)),
+ 
+     %%---- But not a fourth one...
+-    failure = ssh_connection:subsystem(ConnectionRef, ChannelId6, "echo_n", infinity),
++    {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} =
++        ssh_connection:session_channel(ConnectionRef, infinity),
+ 
+     ssh:close(ConnectionRef),
+     ssh:stop_daemon(Pid).
+ 
++%%--------------------------------------------------------------------
++max_channels_to_server(Config) when is_list(Config) ->
++    max_channels_forwarding_helper(Config, true).
++
++%%--------------------------------------------------------------------
++max_channels_from_server(Config) when is_list(Config) ->
++    max_channels_forwarding_helper(Config, false).
++
++max_channels_forwarding_helper(Config, ToServer) ->
++    PrivDir = proplists:get_value(priv_dir, Config),
++    UserDir = filename:join(PrivDir, nopubkey),
++    file:make_dir(UserDir),
++    SysDir = proplists:get_value(data_dir, Config),
++
++    {ok, TargetSock} = gen_tcp:listen(0, [{active, false}, binary, {reuseaddr, true}]),
++    {ok, {TargetHost, TargetPort}} = inet:sockname(TargetSock),
++
++    Options =
++        case ToServer of
++            true ->
++                [{tcpip_tunnel_in, true}];
++            false ->
++                [{tcpip_tunnel_out, true}]
++        end,
++
++    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
++                                             {user_dir, UserDir},
++                                             {password, "morot"},
++                                             {max_channels, 2}] ++
++                                                Options),
++
++    Ref = make_ref(),
++    Parent = self(),
++    ConnectionRef = ssh_test_lib:connect(Host, Port,
++                                         [{silently_accept_hosts, true},
++                                          {user, "foo"},
++                                          {password, "morot"},
++                                          {user_interaction, false},
++                                          {user_dir, UserDir},
++                                          {disconnectfun, fun(Reason) -> Parent ! {Ref, Reason} end}]),
++
++    {ok, ListenPort} =
++        case ToServer of
++            true ->
++                ssh:tcpip_tunnel_to_server(ConnectionRef,
++                                           {127,0,0,1},
++                                           0,
++                                           TargetHost,
++                                           TargetPort,
++                                           timer:seconds(5));
++            false ->
++                ssh:tcpip_tunnel_from_server(ConnectionRef,
++                                             {127,0,0,1},
++                                             0,
++                                             TargetHost,
++                                             TargetPort,
++                                             timer:seconds(5))
++        end,
++
++    {ok, Sock1} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]),
++    {ok, Sock2} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]),
++    {ok, _} = gen_tcp:accept(TargetSock),
++    {ok, _} = gen_tcp:accept(TargetSock),
++
++    %% Sockets are connected
++    {error, timeout} = gen_tcp:recv(Sock1, 0, 0),
++    {error, timeout} = gen_tcp:recv(Sock2, 0, 0),
++
++    %% Upon hitting the channel limit, server will close all sockets
++    {ok, Sock3} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]),
++    receive
++        {Ref, "Received disconnect: Connection terminated. Channel limit reached."} ->
++            ok
++    after 2000 ->
++            ct:fail("Connection should be closed!")
++    end,
++    {error, closed} = gen_tcp:recv(Sock1, 0, timer:seconds(5)),
++    {error, closed} = gen_tcp:recv(Sock2, 0, timer:seconds(5)),
++    {error, closed} = gen_tcp:recv(Sock3, 0, timer:seconds(5)),
++
++    gen_tcp:close(TargetSock),
++    ssh:stop_daemon(Pid).
++
++%%--------------------------------------------------------------------
+ handler_down_before_open(Config) ->
+     %% Start echo subsystem with a delay in init() - until a signal is received
+     %% One client opens a channel on the connection
+diff --git a/lib/ssh/test/ssh_options_SUITE.erl b/lib/ssh/test/ssh_options_SUITE.erl
+index 4d590072e3..57c9f1c908 100644
+--- a/lib/ssh/test/ssh_options_SUITE.erl
++++ b/lib/ssh/test/ssh_options_SUITE.erl
+@@ -28,6 +28,7 @@
+ -include_lib("common_test/include/ct.hrl").
+ -include_lib("kernel/include/file.hrl").
+ -include("ssh_test_lib.hrl").
++-include("ssh_connect.hrl").
+ 
+ %%% Test cases
+ -export([
+@@ -1197,7 +1198,7 @@ ssh_daemon_minimal_remote_max_packet_size_option(Config) ->
+ 
+     %% Try the limits of the minimal_remote_max_packet_size:
+     {ok, _ChannelId} = ssh_connection:session_channel(Conn, 100, 14, infinity),
+-    {open_error,_,"Maximum packet size below 14 not supported",_} = 
++    {open_error, ?SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "Maximum packet size below 14 not supported", <<"en">>} =
+ 	ssh_connection:session_channel(Conn, 100, 13, infinity),
+ 
+     ssh:close(Conn),

diff --git a/otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch b/otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch
new file mode 100644
index 0000000..e9fbfac
--- /dev/null
+++ b/otp-0023-ssl-Reject-unsolicited-TLS-1.3-pre_shared_key-in-cli.patch
@@ -0,0 +1,414 @@
+From: Ingela Anderton Andin <ingela@erlang.org>
+Date: Wed, 16 Sep 2026 14:55:28 +0200
+Subject: [PATCH] ssl: Reject unsolicited TLS-1.3 pre_shared_key in client
+
+A TLS-1.3 client that offered no PSK accepted a ServerHello carrying a
+pre_shared_key extension: get_pre_shared_key/4 fell back to the all-zero
+"no PSK" value and handle_resumption/2 set resumption=true on the mere
+presence of the extension, routing the client past the certificate
+states (wait_cert_cr/wait_cert/wait_cv) straight to wait_finished. Since
+the handshake is keyed with the ordinary non-PSK schedule, any peer that
+completes ECDHE could impersonate any server to a verify_peer client
+without presenting a certificate (server-authentication bypass).
+
+Fix (RFC 8446 4.2.11): in get_pre_shared_key/4 every clause reachable
+only when the server DID send pre_shared_key now aborts with a fatal
+illegal_parameter alert (reason {unsolicited_pre_shared_key, _}) instead
+of returning the zero PSK - the client offered no identities, so
+selected_identity cannot be in range. The auto clause still unlocks
+tickets first. This alone closes the hole: handle_server_hello/2 fails
+in wait_sh before any keys are derived.
+
+Defence in depth: move the handle_resumption/2 call in
+handle_server_hello/2 to after get_pre_shared_key/4 has accepted the
+server's PSK selection, so resumption mode can never be entered for a
+PSK the client did not offer.
+
+Add regression test tls13_reject_unsolicited_psk (rogue_server_tests
+group in tls_api_SUITE): a certificate-less raw-TCP TLS-1.3 server sends
+an otherwise-valid flight with an unsolicited pre_shared_key. Verified
+the test fails (client connects) without the fix and passes
+(illegal_parameter) with it. Covers session_tickets=disabled and =auto.
+
+(cherry picked from commit fd1d9d07fc92ec0d59f96dfb66182195882bb7dd)
+
+[Backport to OTP-26: the rogue_server_tests group and its generic
+ helpers (rogue_observe/1, rogue_handshake/2, rogue_record/2,
+ rogue_read_record/1) come from earlier upstream commits not present
+ on this branch, so they are added here with only the
+ tls13_reject_unsolicited_psk test case.]
+
+Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
+Signed-off-by: Peter Lemenkov <lemenkov@gmail.com>
+
+diff --git a/lib/ssl/src/tls_client_connection_1_3.erl b/lib/ssl/src/tls_client_connection_1_3.erl
+index 928360374a..c4461c5adb 100644
+--- a/lib/ssl/src/tls_client_connection_1_3.erl
++++ b/lib/ssl/src/tls_client_connection_1_3.erl
+@@ -666,10 +666,6 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite,
+         %% Go to state 'start' if server replies with 'HelloRetryRequest'.
+         Maybe(tls_handshake_1_3:maybe_hello_retry_request(ServerHello, State0)),
+ 
+-        %% Resumption and PSK
+-        State1 = tls_gen_connection_1_3:handle_resumption(State0,
+-                                                          ServerPreSharedKey),
+-
+         Maybe(validate_cipher_suite(SelectedCipherSuite, ClientCiphers)),
+         Maybe(validate_server_key_share(ClientGroups, ServerKeyShare)),
+ 
+@@ -681,7 +677,7 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite,
+             client_private_key(SelectedGroup,
+                                ClientKeyShare#key_share_client_hello.client_shares),
+         %% Update state
+-        State2 = tls_handshake_1_3:update_start_state(State1,
++        State2 = tls_handshake_1_3:update_start_state(State0,
+                                     #{cipher => SelectedCipherSuite,
+                                      key_share => ClientKeyShare,
+                                      session_id => SessionId,
+@@ -698,13 +694,22 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite,
+                                                          UseTicket,
+                                                          HKDFAlgo,
+                                                          ServerPreSharedKey)),
+-        State3 =
++
++        %% Resumption and PSK. Only enter resumption mode once the server's PSK
++        %% selection has been accepted by get_pre_shared_key/4 above; an
++        %% unsolicited pre_shared_key aborts before this point, so the
++        %% resumption flag (which skips the certificate states) can never be
++        %% set for a PSK the client did not offer. Defence in depth on top of
++        %% the get_pre_shared_key/4 check.
++        State3 = tls_gen_connection_1_3:handle_resumption(State2,
++                                                          ServerPreSharedKey),
++        State4 =
+             tls_handshake_1_3:calculate_handshake_secrets(ServerPublicKey,
+                                                           ClientPrivateKey,
+                                                           SelectedGroup,
+-                                                          PSK, State2),
+-        State4 = ssl_record:step_encryption_state_read(State3),
+-        {State4, wait_ee}
++                                                          PSK, State3),
++        State5 = ssl_record:step_encryption_state_read(State4),
++        {State5, wait_ee}
+     catch
+         {Ref, {State, StateName, ServerHello}} ->
+             {State, StateName, ServerHello};
+diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl
+index 0a0c34d8fb..58adef4161 100644
+--- a/lib/ssl/src/tls_handshake_1_3.erl
++++ b/lib/ssl/src/tls_handshake_1_3.erl
+@@ -1106,28 +1106,32 @@ get_pre_shared_key({_, PSK}, _) ->
+ %% Server initiates a full handshake
+ get_pre_shared_key(_, _, HKDFAlgo, undefined) ->
+     {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
+-%% Session resumption not configured
+-get_pre_shared_key(undefined, _, HKDFAlgo, _) ->
+-    {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
+-get_pre_shared_key(_, undefined, HKDFAlgo, _) ->
+-    {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
++%% Session resumption not configured, i.e. we did not offer any PSK, so the
++%% server's selected_identity cannot be within the range we supplied.
++%% RFC 8446 Section 4.2.11: abort with an "illegal_parameter" alert instead of
++%% silently falling back to the (zero) "no PSK" value, which would skip server
++%% authentication.
++get_pre_shared_key(undefined, _, _, ServerPSK) ->
++    {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
++get_pre_shared_key(_, undefined, _, ServerPSK) ->
++    {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
+ %% Session resumption
+-get_pre_shared_key(manual = SessionTickets, UseTicket, HKDFAlgo, ServerPSK) ->
++get_pre_shared_key(manual = SessionTickets, UseTicket, _HKDFAlgo, ServerPSK) ->
+     TicketData = get_ticket_data(self(), SessionTickets, UseTicket),
+     case choose_psk(TicketData, ServerPSK) of
+-        undefined -> %% full handshake, default PSK
+-            {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
++        undefined -> %% No PSK was offered that matches the server selection
++            {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
+         illegal_parameter ->
+             {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)};
+         {_, PSK, _, _, _} ->
+             {ok, PSK}
+     end;
+-get_pre_shared_key(auto = SessionTickets, UseTicket, HKDFAlgo, ServerPSK) ->
++get_pre_shared_key(auto = SessionTickets, UseTicket, _HKDFAlgo, ServerPSK) ->
+     TicketData = get_ticket_data(self(), SessionTickets, UseTicket),
+     case choose_psk(TicketData, ServerPSK) of
+-        undefined -> %% full handshake, default PSK
++        undefined -> %% No PSK was offered that matches the server selection
+             tls_client_ticket_store:unlock_tickets(self(), UseTicket),
+-            {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))};
++            {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})};
+         illegal_parameter ->
+             tls_client_ticket_store:unlock_tickets(self(), UseTicket),
+             {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)};
+diff --git a/lib/ssl/test/tls_api_SUITE.erl b/lib/ssl/test/tls_api_SUITE.erl
+index 04a1151f04..79ffef785f 100644
+--- a/lib/ssl/test/tls_api_SUITE.erl
++++ b/lib/ssl/test/tls_api_SUITE.erl
+@@ -30,6 +30,7 @@
+ -include_lib("ssl/src/tls_handshake.hrl").
+ -include_lib("ssl/src/ssl_alert.hrl").
+ -include_lib("ssl/src/ssl_cipher.hrl").
++-include_lib("ssl/src/tls_handshake_1_3.hrl").
+ 
+ %% Common test
+ -export([all/0,
+@@ -114,7 +115,9 @@
+          reuseaddr/0,
+          reuseaddr/1,
+          signature_algs/0,
+-         signature_algs/1
++         signature_algs/1,
++         tls13_reject_unsolicited_psk/0,
++         tls13_reject_unsolicited_psk/1
+         ]).
+ 
+ %% Apply export
+@@ -130,6 +133,12 @@
+ 
+ -define(SLEEP, 500).
+ -define(CORRECT_PASSWORD, "hello test").
++
++%% TLS 1.3 constants for the unsolicited-pre_shared_key rogue-server test.
++-define(TLS13_KEY_SHARE_EXT, 51).
++-define(TLS13_PRE_SHARED_KEY_EXT, 41).
++-define(TLS13_SUPPORTED_VERSIONS_EXT, 43).
++-define(TLS13_GROUP_X25519, 16#001d).
+ -define(INCORRECT_PASSWORD, "hello").
+ -define(BADARG_PASSWORD, hello).
+ 
+@@ -142,7 +151,8 @@ all() ->
+      {group, 'tlsv1.3'},
+      {group, 'tlsv1.2'},
+      {group, 'tlsv1.1'},
+-     {group, 'tlsv1'}
++     {group, 'tlsv1'},
++     {group, rogue_server_tests}
+     ].
+ 
+ groups() ->
+@@ -151,7 +161,8 @@ groups() ->
+                                         tls_13_middlebox_reject_change_cipher_spec_as_first_msg]) -- [sockname]},
+      {'tlsv1.2', [],  api_tests()},
+      {'tlsv1.1', [],  api_tests()},
+-     {'tlsv1', [],  api_tests()}
++     {'tlsv1', [],  api_tests()},
++     {rogue_server_tests, [parallel], rogue_server_tests()}
+     ].
+ 
+ api_tests() ->
+@@ -191,6 +202,9 @@ api_tests() ->
+      reuseaddr
+     ].
+ 
++rogue_server_tests() ->
++    [tls13_reject_unsolicited_psk].
++
+ init_per_suite(Config0) ->
+     catch crypto:stop(),
+     try crypto:start() of
+@@ -754,6 +768,205 @@ tls_dont_crash_on_handshake_garbage(Config) ->
+             ssl_test_lib:check_server_alert(Server, handshake_failure)
+     end.
+ 
++%%--------------------------------------------------------------------
++tls13_reject_unsolicited_psk() ->
++    [{doc, "A malicious/on-path server sends a complete, otherwise-valid "
++      "TLS-1.3 flight (ServerHello, ChangeCipherSpec, encrypted "
++      "{EncryptedExtensions, Finished}) that carries a pre_shared_key "
++      "extension the client never offered, and NO Certificate/CertificateVerify. "
++      "RFC 8446 4.2.11 requires the client to abort with illegal_parameter. On "
++      "vulnerable code the verify_peer client instead accepts the unsolicited "
++      "PSK, keys the handshake with the zero PSK, skips the certificate states "
++      "and connects -- a complete server-authentication bypass "
++      "(ANT-2026-HEF8F3FY / OTP-20388). Checked for session_tickets=disabled "
++      "(default) and =auto with an empty ticket store."}].
++tls13_reject_unsolicited_psk(Config) when is_list(Config) ->
++    ClientOpts = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
++    ok = rogue_tls13_psk(ClientOpts, []),
++    ok = rogue_tls13_psk(ClientOpts, [{session_tickets, auto}]),
++    ok.
++
++%% Drive a real TLS-1.3 verify_peer client against a raw-TCP attacker that
++%% forges the full flight, and assert the mandated illegal_parameter alert.
++rogue_tls13_psk(ClientOpts0, ExtraOpts) ->
++    {ok, LSock} = gen_tcp:listen(0, [binary, {active, false},
++                                     {reuseaddr, true}, {packet, 0}]),
++    {ok, Port} = inet:port(LSock),
++    Parent = self(),
++    Attacker = spawn_link(fun() -> Parent ! {attacker, rogue_tls13_server(LSock)} end),
++    ClientOpts = ExtraOpts ++
++        [{versions, ['tlsv1.3']},
++         {ciphers, ["TLS_AES_128_GCM_SHA256"]},
++         {supported_groups, [x25519]},
++         {server_name_indication, disable},
++         {active, false} | ClientOpts0],
++    Result = ssl:connect("localhost", Port, ClientOpts, 5000),
++    gen_tcp:close(LSock),
++    Observed = receive {attacker, O} -> O after 5000 -> unlink(Attacker), timeout end,
++    ct:log("extra=~p~n ssl:connect -> ~p~n attacker -> ~p",
++           [ExtraOpts, Result, Observed]),
++    %% With a well-formed flight, a PATCHED client aborts with
++    %% illegal_parameter in wait_sh (the unsolicited-PSK check). A
++    %% VULNERABLE client instead accepts the unsolicited PSK, skips the
++    %% certificate states and returns {ok, Socket} (verified: without the
++    %% fix this case fails here with {ok,_}). The reason atom
++    %% {unsolicited_pre_shared_key,_} is not rendered into the tls_alert
++    %% description string, so we assert on the description + the fact that
++    %% a vulnerable client connects.
++    case Result of
++        {error, {tls_alert, {illegal_parameter, _}}} ->
++            ok;
++        {ok, Sock} ->
++            ssl:close(Sock),
++            ct:fail("SERVER-AUTHENTICATION BYPASS: verify_peer client connected "
++                    "to a certificate-less server that sent an unsolicited "
++                    "pre_shared_key (extra ~p). Attacker observed ~p.",
++                    [ExtraOpts, Observed]);
++        Other ->
++            ct:fail("Expected illegal_parameter for unsolicited pre_shared_key "
++                    "(extra ~p); got ~p (attacker ~p)",
++                    [ExtraOpts, Other, Observed])
++    end.
++
++%% Raw-TCP rogue TLS-1.3 server. No certificate. Sends a valid ServerHello +
++%% CCS + encrypted {EncryptedExtensions, Finished} using the RFC 8446 7.1 key
++%% schedule so that an UNPATCHED client would complete the handshake.
++rogue_tls13_server(LSock) ->
++    {ok, Sock} = gen_tcp:accept(LSock, 5000),
++    {?HANDSHAKE, CHHandshake} = rogue_read_record(Sock),
++    ClientShare = rogue_tls13_client_x25519(CHHandshake),
++    SessionId = rogue_tls13_client_session_id(CHHandshake),
++
++    {ServerPub, ServerPriv} = crypto:generate_key(ecdh, x25519),
++    SHBody = rogue_tls13_server_hello_body(ServerPub, SessionId),
++    SHHandshake = rogue_handshake(?SERVER_HELLO, SHBody),
++
++    %% RFC 8446 7.1 handshake secrets (transcript = ClientHello ++ ServerHello).
++    Alg = sha256,
++    HashLen = 32,
++    Shared = crypto:compute_key(ecdh, ClientShare, ServerPriv, x25519),
++    Zero = binary:copy(<<0>>, HashLen),
++    EarlySecret = tls_v1:hkdf_extract(Alg, Zero, Zero),
++    DerivedEarly = rogue_derive_secret(Alg, HashLen, EarlySecret, <<"derived">>, <<>>),
++    HSSecret = tls_v1:hkdf_extract(Alg, DerivedEarly, Shared),
++    Transcript0 = <<CHHandshake/binary, SHHandshake/binary>>,
++    ServerHS = rogue_derive_secret(Alg, HashLen, HSSecret, <<"s hs traffic">>, Transcript0),
++    Key = tls_v1:hkdf_expand_label(ServerHS, <<"key">>, <<>>, 16, Alg),
++    IV  = tls_v1:hkdf_expand_label(ServerHS, <<"iv">>,  <<>>, 12, Alg),
++
++    EE = <<?ENCRYPTED_EXTENSIONS, 2:24, 0:16>>,
++    Transcript1 = <<Transcript0/binary, EE/binary>>,
++    FinKey = tls_v1:hkdf_expand_label(ServerHS, <<"finished">>, <<>>, HashLen, Alg),
++    THash = crypto:hash(Alg, Transcript1),
++    VerifyData = tls_v1:hmac_hash(Alg, FinKey, THash),
++    Finished = <<?FINISHED, (byte_size(VerifyData)):24, VerifyData/binary>>,
++
++    Inner = <<EE/binary, Finished/binary, ?HANDSHAKE>>,
++    EncRecord = rogue_tls13_encrypt(Key, IV, 0, Inner),
++
++    ok = gen_tcp:send(Sock, rogue_record(?HANDSHAKE, SHHandshake)),
++    ok = gen_tcp:send(Sock, <<20, 3, 3, 1:16, 1>>),   %% ChangeCipherSpec
++    ok = gen_tcp:send(Sock, EncRecord),
++
++    Obs = rogue_observe(Sock),
++    gen_tcp:close(Sock),
++    Obs.
++
++rogue_tls13_server_hello_body(ServerPub, SessionId) ->
++    Random = crypto:strong_rand_bytes(32),
++    SVExt  = <<?TLS13_SUPPORTED_VERSIONS_EXT:16, 2:16, 16#03, 16#04>>,
++    KSData = <<?TLS13_GROUP_X25519:16, (byte_size(ServerPub)):16, ServerPub/binary>>,
++    KSExt  = <<?TLS13_KEY_SHARE_EXT:16, (byte_size(KSData)):16, KSData/binary>>,
++    PSKExt = <<?TLS13_PRE_SHARED_KEY_EXT:16, 2:16, 0:16>>,  %% selected_identity = 0
++    Exts   = <<SVExt/binary, KSExt/binary, PSKExt/binary>>,
++    %% RFC 8446 4.1.3: the ServerHello MUST echo the client's
++    %% legacy_session_id, otherwise the client aborts with
++    %% session_id_echo_mismatch before the pre_shared_key is examined.
++    <<16#03, 16#03, Random/binary,
++      (byte_size(SessionId)), SessionId/binary,
++      16#13, 16#01, 0,                                %% TLS_AES_128_GCM_SHA256, compression=null
++      (byte_size(Exts)):16, Exts/binary>>.
++
++rogue_derive_secret(Alg, HashLen, Secret, Label, Messages) ->
++    Hash = crypto:hash(Alg, Messages),
++    tls_v1:hkdf_expand_label(Secret, Label, Hash, HashLen, Alg).
++
++%% Encrypt one TLS-1.3 record (outer type application_data), RFC 8446 5.2.
++rogue_tls13_encrypt(Key, IV, SeqNo, Inner) ->
++    TagLen = 16,
++    Len = byte_size(Inner) + TagLen,
++    AAD = <<?APPLICATION_DATA, 3, 3, Len:16>>,
++    Nonce = rogue_tls13_nonce(SeqNo, IV),
++    {Enc, Tag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, Nonce, Inner, AAD, TagLen, true),
++    Payload = <<Enc/binary, Tag/binary>>,
++    <<?APPLICATION_DATA, 3, 3, (byte_size(Payload)):16, Payload/binary>>.
++
++rogue_tls13_nonce(SeqNo, IV) ->
++    Padded = <<0:((byte_size(IV) - 8) * 8), SeqNo:64>>,
++    crypto:exor(Padded, IV).
++
++%% Parse client's legacy_session_id from the ClientHello handshake message.
++rogue_tls13_client_session_id(CHHandshake) ->
++    <<_HsType, _HsLen:24, Body/binary>> = CHHandshake,
++    <<_LegacyVsn:16, _Random:32/binary, SidLen, Sid:SidLen/binary, _/binary>> = Body,
++    Sid.
++
++%% Parse client's x25519 key_share from the ClientHello handshake message.
++rogue_tls13_client_x25519(CHHandshake) ->
++    <<_HsType, _HsLen:24, Body/binary>> = CHHandshake,
++    <<_LegacyVsn:16, _Random:32/binary, R0/binary>> = Body,
++    <<SidLen, R1/binary>> = R0,
++    <<_Sid:SidLen/binary, R2/binary>> = R1,
++    <<CsLen:16, R3/binary>> = R2,
++    <<_Cs:CsLen/binary, R4/binary>> = R3,
++    <<CompLen, R5/binary>> = R4,
++    <<_Comp:CompLen/binary, R6/binary>> = R5,
++    <<_ExtsLen:16, Exts/binary>> = R6,
++    rogue_tls13_find_key_share(Exts).
++
++rogue_tls13_find_key_share(<<?TLS13_KEY_SHARE_EXT:16, ExtLen:16, ExtData:ExtLen/binary, _/binary>>) ->
++    <<_SharesLen:16, Shares/binary>> = ExtData,
++    rogue_tls13_find_x25519(Shares);
++rogue_tls13_find_key_share(<<_Type:16, ExtLen:16, _ExtData:ExtLen/binary, Rest/binary>>) ->
++    rogue_tls13_find_key_share(Rest);
++rogue_tls13_find_key_share(<<>>) ->
++    ct:fail(client_offered_no_key_share).
++
++rogue_tls13_find_x25519(<<?TLS13_GROUP_X25519:16, KeLen:16, Ke:KeLen/binary, _/binary>>) ->
++    Ke;
++rogue_tls13_find_x25519(<<_Group:16, KeLen:16, _Ke:KeLen/binary, Rest/binary>>) ->
++    rogue_tls13_find_x25519(Rest);
++rogue_tls13_find_x25519(<<>>) ->
++    ct:fail(client_offered_no_x25519_share).
++
++rogue_observe(Sock) ->
++    case rogue_read_record(Sock) of
++        {?HANDSHAKE, <<MsgType, _/binary>>} ->
++            {client_handshake, MsgType};
++        {?ALERT, <<Level, Desc>>} ->
++            {client_alert, Level, Desc};
++        {error, closed} ->
++            client_closed;
++        Other ->
++            {other, Other}
++    end.
++
++rogue_handshake(Type, Body) ->
++    <<Type, (byte_size(Body)):24, Body/binary>>.
++
++rogue_record(ContentType, Payload) ->
++    <<ContentType, 3, 3, (byte_size(Payload)):16, Payload/binary>>.
++
++rogue_read_record(Sock) ->
++    case gen_tcp:recv(Sock, 5, 5000) of
++        {ok, <<CT, _Maj, _Min, Len:16>>} ->
++            case gen_tcp:recv(Sock, Len, 5000) of
++                {ok, Payload} -> {CT, Payload};
++                Err -> Err
++            end;
++        {error, _} = E -> E
++    end.
++
+ %%--------------------------------------------------------------------
+ tls_tcp_error_propagation_in_active_mode() ->
+     [{doc,"Test that process receives {ssl_error, Socket, closed} when tcp error ocurres"}].

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-09-23 20:27 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-23 20:27 [rpms/erlang] f44: Backported fixes for CVE-2026-65634, CVE-2026-68956, CVE-2026-89422 Peter Lemenkov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox