Skip to content

Commit 281a635

Browse files
garazdawiu3s
authored andcommitted
ssh: tighten DH bounds to 1<e<p-1 and add K check
Tighten e/f validation from inclusive [1, p-1] to strict (1 < e < p-1) on all plain-DH and GEX paths. Add missing 1 < K < p-1 check on the plain-DH client path (handle_kexdh_reply), matching what the GEX code already enforced. The RFCs (4253 §8, 4419 §3) only require inclusive bounds for e/f, but OpenSSH and Go both enforce strict bounds. The K check on plain DH is defense-in-depth — with safe primes and strict e/f, K ∈ {1, p-1} cannot occur, but the check guards against crypto backend bugs at zero cost. No interop risk: no legitimate peer sends e ∈ {0, 1, p-1, p}.
1 parent f835a5a commit 281a635

2 files changed

Lines changed: 84 additions & 36 deletions

File tree

lib/ssh/src/ssh_transport.erl

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -602,28 +602,34 @@ handle_kexdh_init(#ssh_msg_kexdh_init{e = E},
602602
%% server
603603
{G, P} = dh_group(Kex),
604604
if
605-
1=<E, E=<(P-1) ->
605+
1<E, E<(P-1) ->
606606
Sz = dh_bits(Algs),
607607
{Public, Private} = generate_key(dh, [P,G,2*Sz]),
608608
K = compute_key(dh, E, Private, [P,G]),
609-
MyPrivHostKey = get_host_key(SignAlg, Opts),
610-
MyPubHostKey = ssh_file:extract_public_key(MyPrivHostKey),
611-
H = kex_hash(Ssh0, MyPubHostKey, sha(Kex), {E,Public,K}),
612-
case sign(H, SignAlg, MyPrivHostKey, Ssh0) of
613-
{ok,H_SIG} ->
614-
{SshPacket, Ssh1} =
615-
ssh_packet(#ssh_msg_kexdh_reply{public_host_key = {MyPubHostKey,SignAlg},
616-
f = Public,
617-
h_sig = H_SIG
618-
}, Ssh0),
619-
{ok, SshPacket, Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}},
620-
shared_secret = ssh_bits:mpint(K),
621-
exchanged_hash = H,
622-
session_id = sid(Ssh1, H)}};
623-
{error,unsupported_sign_alg} ->
609+
if
610+
1<K, K<(P-1) ->
611+
MyPrivHostKey = get_host_key(SignAlg, Opts),
612+
MyPubHostKey = ssh_file:extract_public_key(MyPrivHostKey),
613+
H = kex_hash(Ssh0, MyPubHostKey, sha(Kex), {E,Public,K}),
614+
case sign(H, SignAlg, MyPrivHostKey, Ssh0) of
615+
{ok,H_SIG} ->
616+
{SshPacket, Ssh1} =
617+
ssh_packet(#ssh_msg_kexdh_reply{public_host_key = {MyPubHostKey,SignAlg},
618+
f = Public,
619+
h_sig = H_SIG
620+
}, Ssh0),
621+
{ok, SshPacket, Ssh1#ssh{keyex_key = {{Private, Public}, {G, P}},
622+
shared_secret = ssh_bits:mpint(K),
623+
exchanged_hash = H,
624+
session_id = sid(Ssh1, H)}};
625+
{error,unsupported_sign_alg} ->
626+
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
627+
io_lib:format("Unsupported algorithm ~p", [SignAlg],
628+
[{chars_limit, ssh_lib:max_log_len(Opts)}]))
629+
end;
630+
true ->
624631
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
625-
io_lib:format("Unsupported algorithm ~p", [SignAlg],
626-
[{chars_limit, ssh_lib:max_log_len(Opts)}]))
632+
"Key exchange failed, 'K' out of bounds")
627633
end;
628634
true ->
629635
MsgFun =
@@ -643,22 +649,28 @@ handle_kexdh_reply(#ssh_msg_kexdh_reply{public_host_key = PeerPubHostKey,
643649
#ssh{keyex_key = {{Private, Public}, {G, P}},
644650
algorithms = #alg{kex=Kex}} = Ssh0) ->
645651
%% client
646-
if
647-
1=<F, F=<(P-1)->
652+
if
653+
1<F, F<(P-1) ->
648654
K = compute_key(dh, F, Private, [P,G]),
649-
H = kex_hash(Ssh0, PeerPubHostKey, sha(Kex), {Public,F,K}),
650-
case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of
651-
ok ->
652-
{SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0),
653-
{ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K),
654-
exchanged_hash = H,
655-
session_id = sid(Ssh, H)})};
656-
Error ->
655+
if
656+
1<K, K<(P-1) ->
657+
H = kex_hash(Ssh0, PeerPubHostKey, sha(Kex), {Public,F,K}),
658+
case verify_host_key(Ssh0, PeerPubHostKey, H, H_SIG) of
659+
ok ->
660+
{SshPacket, Ssh} = ssh_packet(#ssh_msg_newkeys{}, Ssh0),
661+
{ok, SshPacket, install_alg(snd, Ssh#ssh{shared_secret = ssh_bits:mpint(K),
662+
exchanged_hash = H,
663+
session_id = sid(Ssh, H)})};
664+
Error ->
665+
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
666+
io_lib:format("Kexdh init failed. Verify host key: ~p",[Error],
667+
[{chars_limit, ssh_lib:max_log_len(Ssh0)}])
668+
)
669+
end;
670+
true ->
657671
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
658-
io_lib:format("Kexdh init failed. Verify host key: ~p",[Error],
659-
[{chars_limit, ssh_lib:max_log_len(Ssh0)}])
660-
)
661-
end;
672+
"Key exchange failed, 'K' out of bounds")
673+
end;
662674

663675
true ->
664676
?DISCONNECT(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED,
@@ -760,7 +772,7 @@ handle_kex_dh_gex_init(#ssh_msg_kex_dh_gex_init{e = E},
760772
opts = Opts} = Ssh0) ->
761773
%% server
762774
if
763-
1=<E, E=<(P-1) ->
775+
1<E, E<(P-1) ->
764776
K = compute_key(dh, E, Private, [P,G]),
765777
if
766778
1<K, K<(P-1) ->
@@ -805,8 +817,8 @@ handle_kex_dh_gex_reply(#ssh_msg_kex_dh_gex_reply{public_host_key = PeerPubHostK
805817
algorithms = #alg{kex=Kex}} =
806818
Ssh0) ->
807819
%% client
808-
if
809-
1=<F, F=<(P-1)->
820+
if
821+
1<F, F<(P-1) ->
810822
K = compute_key(dh, F, Private, [P,G]),
811823
if
812824
1<K, K<(P-1) ->

lib/ssh/test/ssh_protocol_SUITE.erl

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
kex_strict_violation/1,
6969
kex_strict_violation_2/1,
7070
kex_strict_msg_unknown/1,
71+
dh_kexdh_init_e_out_of_bounds/1,
7172
gex_client_init_option_groups/1,
7273
gex_client_init_option_groups_file/1,
7374
gex_client_init_option_groups_moduli_file/1,
@@ -185,7 +186,8 @@ groups() ->
185186
kex_strict_violation_new_keys,
186187
kex_strict_violation,
187188
kex_strict_violation_2,
188-
kex_strict_msg_unknown]},
189+
kex_strict_msg_unknown,
190+
dh_kexdh_init_e_out_of_bounds]},
189191
{service_requests, [], [bad_service_name,
190192
bad_long_service_name,
191193
bad_very_long_service_name,
@@ -1384,6 +1386,40 @@ kex_strict_msg_unknown(Config) ->
13841386
{match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
13851387
kex_strict_helper(Config, TestMessages, ExpectedReason).
13861388

1389+
%% RFC 4253 §8 / RFC 4419 §3: a peer's DH value must satisfy 1 < e < p-1.
1390+
%% A peer driving e (or, on the client side, f) to one of {0, 1, p-1, p}
1391+
%% must be rejected with SSH_DISCONNECT_KEY_EXCHANGE_FAILED. Verify this
1392+
%% for the server, which receives e from the client.
1393+
dh_kexdh_init_e_out_of_bounds(Config) ->
1394+
{_G, P} = ?dh_group14,
1395+
[verify_kexdh_init_rejected(Config, E) || E <- [0, 1, P-1, P]],
1396+
ok.
1397+
1398+
verify_kexdh_init_rejected(Config, E) ->
1399+
ct:log("Trying kexdh_init with e=~p", [E]),
1400+
{ok, InitialState} = ssh_trpt_test_lib:exec(
1401+
[{set_options, [print_ops, print_seqnums, print_messages]}]),
1402+
{ok, _} =
1403+
ssh_trpt_test_lib:exec(
1404+
[{connect,
1405+
ssh_test_lib:server_host(Config), ssh_test_lib:server_port(Config),
1406+
[{preferred_algorithms,
1407+
[{kex, ['diffie-hellman-group14-sha256']},
1408+
{cipher, ?DEFAULT_CIPHERS}]},
1409+
{silently_accept_hosts, true},
1410+
{recv_ext_info, false},
1411+
{user_dir, ssh_test_lib:user_dir(Config)},
1412+
{user_interaction, false}
1413+
| proplists:get_value(extra_options, Config, [])
1414+
]},
1415+
receive_hello,
1416+
{send, hello},
1417+
{send, ssh_msg_kexinit},
1418+
{match, #ssh_msg_kexinit{_='_'}, receive_msg},
1419+
{send, #ssh_msg_kexdh_init{e = E}},
1420+
{match, disconnect(?SSH_DISCONNECT_KEY_EXCHANGE_FAILED), receive_msg}],
1421+
InitialState).
1422+
13871423
kex_strict_helper(Config, TestMessages, ExpectedReason) ->
13881424
{ok, TestRef} = ssh_test_lib:add_log_handler(),
13891425
Level = ssh_test_lib:get_log_level(),

0 commit comments

Comments
 (0)