Skip to content

Commit dd6812f

Browse files
committed
Merge branch 'fix/ssh-mlkem-hybrid-x25519-secret-encoding' into maint
* fix/ssh-mlkem-hybrid-x25519-secret-encoding: ssh: use static key in test suite ssh: Skip mlkem768x25519 test on unsupported platforms ssh: fix mlkem768x25519 hybrid shared secret encoding
2 parents b0b5735 + 20ce349 commit dd6812f

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

lib/ssh/src/ssh_transport.erl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
-define(MIN_DH_KEY_SIZE, 400).
6868

6969
%%% For test suites
70-
-export([pack/3, adjust_algs_for_peer_version/2]).
70+
-export([pack/3, adjust_algs_for_peer_version/2, hybrid_common/4]).
7171

7272
%%%----------------------------------------------------------------------------
7373
%%%
@@ -2395,10 +2395,8 @@ compute_key(Algorithm, PeerPublic, MyPrivate, Args) ->
23952395

23962396
hybrid_common(K_pq_secret, Curve, PeerPublic, MyPrivate) ->
23972397
K_cl_secret = compute_key(ecdh, PeerPublic, MyPrivate, Curve),
2398-
K_cl_secret_mpint = <<?Empint(K_cl_secret)>>,
2399-
K_cl_secret_mpint_trim =
2400-
binary:part(K_cl_secret_mpint, byte_size(K_cl_secret_mpint), -?X25519_PUBLICKEY_SIZE),
2401-
crypto:hash(sha(Curve), <<K_pq_secret/binary, K_cl_secret_mpint_trim/binary>>).
2398+
K_cl_secret_fixed = <<K_cl_secret:(?X25519_PUBLICKEY_SIZE*8)/big-unsigned-integer>>,
2399+
crypto:hash(sha(Curve), <<K_pq_secret/binary, K_cl_secret_fixed/binary>>).
24022400

24032401
dh_bits(#alg{encrypt = Encrypt,
24042402
send_mac = SendMac}) ->

lib/ssh/test/ssh_algorithms_SUITE.erl

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
-export([
4444
interpolate/1,
45+
mlkem768x25519_hybrid_secret_encoding/1,
4546
simple_connect/1,
4647
simple_exec/1,
4748
simple_exec_groups/0,
@@ -61,9 +62,9 @@ suite() ->
6162
[{ct_hooks,[ts_install_cth]},
6263
{timetrap,{seconds,120}}].
6364

64-
all() ->
65+
all() ->
6566
%% [{group,kex},{group,cipher}... etc
66-
[{group,C} || C <- tags()].
67+
[mlkem768x25519_hybrid_secret_encoding | [{group,C} || C <- tags()]].
6768

6869

6970
groups() ->
@@ -290,6 +291,14 @@ init_per_testcase(sshc_simple_exec_os_cmd, _, Config) ->
290291
start_pubkey_daemon([proplists:get_value(pref_algs,Config)],
291292
[{extra_daemon,true}|Config]);
292293

294+
init_per_testcase(mlkem768x25519_hybrid_secret_encoding, _, Config) ->
295+
case lists:member(x25519, crypto:supports(curves))
296+
andalso lists:member(mlkem768, crypto:supports(kems))
297+
of
298+
false -> {skip, "X25519 or ML-KEM768 not supported"};
299+
true -> Config
300+
end;
301+
293302
init_per_testcase(_, _, Config) ->
294303
Config.
295304

@@ -311,6 +320,35 @@ end_per_testcase(_TC, Config) ->
311320

312321
%%--------------------------------------------------------------------
313322
%% Test Cases --------------------------------------------------------
323+
%%--------------------------------------------------------------------
324+
%% Regression test for the mlkem768x25519-sha256 hybrid key exchange: the
325+
%% classical (X25519) shared secret must be hashed as a fixed-width 32-byte
326+
%% octet string. Encoding it as a trimmed mpint dropped a genuine leading 0x00
327+
%% byte ~1/512 of the time (when the secret's most significant byte is 0x00 and
328+
%% the next byte is < 0x80), so the two peers derived different exchange hashes
329+
%% and the handshake failed with "incorrect signature".
330+
%%
331+
%% The keys below produce a shared secret starting with <<0x00, 0x42, ...>>
332+
%% which triggers the edge case (0x42 < 0x80).
333+
mlkem768x25519_hybrid_secret_encoding(_Config) ->
334+
PeerPublic = <<16#94,16#5E,16#6F,16#5A,16#CA,16#B1,16#AD,16#A6,
335+
16#31,16#CF,16#12,16#F5,16#47,16#A4,16#25,16#6B,
336+
16#6A,16#E5,16#3A,16#A2,16#48,16#6A,16#0D,16#08,
337+
16#C6,16#D6,16#73,16#2C,16#B3,16#E2,16#0E,16#5B>>,
338+
MyPrivate = <<16#58,16#EF,16#AB,16#DA,16#4C,16#C5,16#6B,16#8E,
339+
16#B2,16#43,16#8A,16#86,16#92,16#2C,16#5D,16#73,
340+
16#83,16#98,16#B4,16#38,16#0E,16#A3,16#91,16#37,
341+
16#F9,16#38,16#5B,16#E5,16#BA,16#B5,16#94,16#6D>>,
342+
%% Verify the shared secret triggers the edge case
343+
<<0, B1, _/binary>> = crypto:compute_key(ecdh, PeerPublic, MyPrivate, x25519),
344+
true = B1 < 16#80,
345+
%% A spec-conformant peer (e.g. OpenSSH) hashes K_pq concatenated with the
346+
%% X25519 secret as a fixed-width 32-byte string, preserving the leading 0x00.
347+
K_pq = crypto:strong_rand_bytes(32),
348+
Raw = crypto:compute_key(ecdh, PeerPublic, MyPrivate, x25519),
349+
Expected = crypto:hash(sha256, <<K_pq/binary, Raw/binary>>),
350+
Expected = ssh_transport:hybrid_common(K_pq, x25519, PeerPublic, MyPrivate).
351+
314352
%%--------------------------------------------------------------------
315353
%% A simple sftp transfer
316354
simple_sftp(Config) ->

0 commit comments

Comments
 (0)