Skip to content

Commit f629eb6

Browse files
committed
Merge branch 'maint'
2 parents 23b8695 + 4b5e9c6 commit f629eb6

4 files changed

Lines changed: 119 additions & 18 deletions

File tree

lib/ssh/src/ssh_options.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ default(server) ->
558558
},
559559

560560
dh_gex_limits =>
561-
#{default => {0, infinity},
561+
#{default => {2047, infinity},
562562
chk => fun({I1,I2}) ->
563563
check_pos_integer(I1) andalso
564564
check_pos_integer(I2) andalso
@@ -705,7 +705,7 @@ default(client) ->
705705
},
706706

707707
dh_gex_limits =>
708-
#{default => {1024, 6144, 8192}, % FIXME: Is this true nowadays?
708+
#{default => {2047, 6144, 8192},
709709
chk => fun({Min,I,Max}) ->
710710
lists:all(fun check_pos_integer/1,
711711
[Min,I,Max]);

lib/ssh/src/ssh_transport.erl

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -777,14 +777,22 @@ adjust_gex_min_max(Min0, Max0, Opts) ->
777777
end.
778778

779779

780-
handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g = G}, Ssh0) ->
781-
%% client
782-
Sz = dh_bits(Ssh0#ssh.algorithms),
783-
{Public, Private} = generate_key(dh, [P,G,2*Sz]),
784-
{SshPacket, Ssh1} =
785-
ssh_packet(#ssh_msg_kex_dh_gex_init{e = Public}, Ssh0), % Pub = G^Priv mod P (def)
786-
{ok, SshPacket,
787-
Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}}.
780+
handle_kex_dh_gex_group(#ssh_msg_kex_dh_gex_group{p = P, g = G},
781+
#ssh{keyex_info = {Min, _Max, _NBits}} = Ssh0) ->
782+
%% client — validate group parameters from server (RFC 4419 §3)
783+
PBits = nbits(P),
784+
MinBits = max(Min, ?DH_GEX_MIN_BITS),
785+
case validate_dh_gex_group(P, G, PBits, MinBits) of
786+
ok ->
787+
Sz = dh_bits(Ssh0#ssh.algorithms),
788+
{Public, Private} = generate_key(dh, [P, G, 2*Sz]),
789+
{SshPacket, Ssh1} =
790+
ssh_packet(#ssh_msg_kex_dh_gex_init{e = Public}, Ssh0),
791+
{ok, SshPacket,
792+
Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}}}};
793+
{error, Reason} ->
794+
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED, Reason)
795+
end.
788796

789797
handle_kex_dh_gex_init(#ssh_msg_kex_dh_gex_init{e = E},
790798
#ssh{keyex_key = {{Private, Public}, {G, P}},
@@ -2411,6 +2419,24 @@ dh_bits(#alg{encrypt = Encrypt,
24112419
mac_key_bytes(SendMac)
24122420
]).
24132421

2422+
%% Validate DH GEX group parameters received from the server.
2423+
%% Checks generator bounds and prime size against requested limits.
2424+
validate_dh_gex_group(P, G, _PBits, _MinBits)
2425+
when G =< 1;
2426+
G >= P - 1 ->
2427+
{error, "DH GEX invalid generator"};
2428+
validate_dh_gex_group(_P, _G, PBits, MinBits) when PBits < MinBits ->
2429+
{error, io_lib:format("DH GEX group too small: ~p bits (minimum ~p)",
2430+
[PBits, MinBits])};
2431+
validate_dh_gex_group(_P, _G, _PBits, _MinBits) ->
2432+
ok.
2433+
2434+
%% Number of significant bits in a positive integer.
2435+
nbits(N) when is_integer(N), N > 0 ->
2436+
bit_size(binary:encode_unsigned(N));
2437+
nbits(_) ->
2438+
0.
2439+
24142440
ecdh_curve('ecdh-sha2-nistp256') -> secp256r1;
24152441
ecdh_curve('ecdh-sha2-nistp384') -> secp384r1;
24162442
ecdh_curve('ecdh-sha2-nistp521') -> secp521r1;

lib/ssh/src/ssh_transport.hrl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@
3434

3535
-define(MAX_NUM_ALGORITHMS, 200).
3636

37-
-define(DEFAULT_DH_GROUP_MIN, 1024).
37+
-define(DEFAULT_DH_GROUP_MIN, 2047).
3838
-define(DEFAULT_DH_GROUP_NBITS, 2048).
3939
-define(DEFAULT_DH_GROUP_MAX, 8192).
4040

41+
%% Absolute minimum DH group size the client will accept from a server
42+
%% during GEX, regardless of dh_gex_limits configuration.
43+
%% Matches the smallest built-in group in pubkey_moduli.hrl.
44+
%% OpenSSH uses 2048 (DH_GRP_MIN); our moduli are labeled 2047.
45+
-define(DH_GEX_MIN_BITS, 2047).
46+
4147
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4248
%%
4349
%% BASIC transport messages

lib/ssh/test/ssh_protocol_SUITE.erl

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
-include("ssh_auth.hrl").
3333
-include("ssh_test_lib.hrl").
3434

35+
%% RFC 3526 Group 14 — 2048-bit MODP prime (generator = 2).
36+
-define(RFC3526_GROUP14_PRIME,
37+
16#FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF).
38+
3539
-export([
3640
suite/0,
3741
all/0,
@@ -82,6 +86,8 @@
8286
gex_client_init_option_groups_moduli_file/1,
8387
gex_client_old_request_exact/1,
8488
gex_client_old_request_noexact/1,
89+
gex_client_rejects_small_group/1,
90+
gex_client_rejects_bad_generator/1,
8591
gex_server_gex_limit/1,
8692
lib_match/1,
8793
lib_no_match/1,
@@ -209,6 +215,8 @@ groups() ->
209215
gex_client_init_option_groups_file,
210216
gex_client_old_request_exact,
211217
gex_client_old_request_noexact,
218+
gex_client_rejects_small_group,
219+
gex_client_rejects_bad_generator,
212220
kex_strict_negotiated,
213221
kex_strict_violation_key_exchange,
214222
kex_strict_violation_new_keys,
@@ -301,6 +309,9 @@ init_per_testcase(TC, Config) when TC == kex_strict_negotiated;
301309
Level = ssh_test_lib:get_log_level(),
302310
ssh_test_lib:set_log_level(debug),
303311
[{saved_log_level, Level} | Config];
312+
init_per_testcase(TC, Config) when TC == gex_client_rejects_small_group;
313+
TC == gex_client_rejects_bad_generator ->
314+
Config;
304315
init_per_testcase(TC, Config) when TC == gex_client_init_option_groups ;
305316
TC == gex_client_init_option_groups_moduli_file ;
306317
TC == gex_client_init_option_groups_file ;
@@ -309,10 +320,8 @@ init_per_testcase(TC, Config) when TC == gex_client_init_option_groups ;
309320
TC == gex_client_old_request_noexact ->
310321
Opts = case TC of
311322
gex_client_init_option_groups ->
312-
[{dh_gex_groups,
313-
[{1023, 5,
314-
16#D9277DAA27DB131C03B108D41A76B4DA8ACEECCCAE73D2E48CEDAAA70B09EF9F04FB020DCF36C51B8E485B26FABE0337E24232BE4F4E693548310244937433FB1A5758195DC73B84ADEF8237472C46747D79DC0A2CF8A57CE8DBD8F466A20F8551E7B1B824B2E4987A8816D9BC0741C2798F3EBAD3ADEBCC78FCE6A770E2EC9F
315-
}]}];
323+
[{dh_gex_groups,
324+
[{2048, 2, ?RFC3526_GROUP14_PRIME}]}];
316325
gex_client_init_option_groups_file ->
317326
DataDir = proplists:get_value(data_dir, Config),
318327
F = filename:join(DataDir, "dh_group_test"),
@@ -663,9 +672,8 @@ no_common_alg_client_disconnects(Config) ->
663672

664673
%%%--------------------------------------------------------------------
665674
gex_client_init_option_groups(Config) ->
666-
do_gex_client_init(Config, {512, 2048, 4000},
667-
{5,16#D9277DAA27DB131C03B108D41A76B4DA8ACEECCCAE73D2E48CEDAAA70B09EF9F04FB020DCF36C51B8E485B26FABE0337E24232BE4F4E693548310244937433FB1A5758195DC73B84ADEF8237472C46747D79DC0A2CF8A57CE8DBD8F466A20F8551E7B1B824B2E4987A8816D9BC0741C2798F3EBAD3ADEBCC78FCE6A770E2EC9F}
668-
).
675+
do_gex_client_init(Config, {2048, 2048, 4000},
676+
{2, ?RFC3526_GROUP14_PRIME}).
669677

670678
gex_client_init_option_groups_file(Config) ->
671679
do_gex_client_init(Config, {2000, 2048, 4000},
@@ -742,6 +750,67 @@ do_gex_client_init_old(Config, N, {G,P}) ->
742750
]
743751
).
744752

753+
%%%--------------------------------------------------------------------
754+
%%% Client rejects a DH GEX group with too-small prime (512 bits).
755+
%%% The test lib acts as server and sends a bad GEX_GROUP; the real
756+
%%% OTP client must disconnect with KEY_EXCHANGE_FAILED.
757+
gex_client_rejects_small_group(Config) ->
758+
%% 512-bit number (not even prime — doesn't matter, size check rejects first)
759+
SmallP = 16#D4BCD52406F2C926B7E8BE5FF5D2B2E3B956F79441CE5B2E35,
760+
gex_client_rejects_group(Config, SmallP, 2).
761+
762+
%%% Client rejects a DH GEX group with invalid generator (G=1).
763+
gex_client_rejects_bad_generator(Config) ->
764+
%% Valid 2048-bit prime (RFC 3526 group 14), but generator = 1
765+
gex_client_rejects_group(Config, ?RFC3526_GROUP14_PRIME, 1).
766+
767+
gex_client_rejects_group(Config, P, G) ->
768+
{ok, InitialState} = ssh_trpt_test_lib:exec(listen),
769+
HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
770+
Parent = self(),
771+
772+
%% Server side: accept, negotiate DH-GEX, send bad group
773+
Pid =
774+
spawn_link(
775+
fun() ->
776+
Parent !
777+
{result, self(),
778+
ssh_trpt_test_lib:exec(
779+
[{set_options, [print_ops, print_seqnums, print_messages]},
780+
{accept, [{system_dir, ssh_test_lib:system_dir(Config)},
781+
{user_dir, ssh_test_lib:user_dir(Config)}]},
782+
receive_hello,
783+
{send, hello},
784+
{send, ssh_msg_kexinit},
785+
{match, #ssh_msg_kexinit{_='_'}, receive_msg},
786+
{match, #ssh_msg_kex_dh_gex_request{_='_'}, receive_msg},
787+
{send, #ssh_msg_kex_dh_gex_group{p = P, g = G}},
788+
{match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED),
789+
receive_msg}
790+
],
791+
InitialState)}
792+
end),
793+
794+
%% Client side: connect forcing DH-GEX
795+
Result = std_connect(HostPort, Config,
796+
[{preferred_algorithms,
797+
[{kex, ['diffie-hellman-group-exchange-sha256']},
798+
{cipher, ?DEFAULT_CIPHERS}]}]),
799+
ct:log("Client connect result: ~p", [Result]),
800+
801+
receive
802+
{result, Pid, {ok, _}} ->
803+
ok;
804+
{result, Pid, {error, {Op, ExecResult, S}}} ->
805+
ct:log("ERROR!~nOp = ~p~nExecResult = ~p~nState =~n~s",
806+
[Op, ExecResult, ssh_trpt_test_lib:format_msg(S)]),
807+
{fail, ExecResult};
808+
{result, Pid, X} ->
809+
ct:fail(X)
810+
after
811+
30000 -> ct:fail("timeout ~p:~p", [?MODULE, ?LINE])
812+
end.
813+
745814
%%%--------------------------------------------------------------------
746815
bad_service_name(Config) ->
747816
bad_service_name(Config, "kfglkjf").

0 commit comments

Comments
 (0)