@@ -993,6 +993,28 @@ static void TestKexDhReplyRejectsWhenCallbackRejects(void)
993993
994994#endif /* KEXDH_REPLY_REGRESS_KEX_ALGO */
995995
996+ static word32 ParseChannelOpenFailRecipient (const byte * pkt , word32 sz )
997+ {
998+ word32 chan ;
999+ /* SSH binary-packet layout: 4 (len) + 1 (pad_len) + 1 (msg_id) = 6;
1000+ * + 4 for the recipient_channel field itself gives the 10-byte minimum. */
1001+ AssertTrue (sz >= 10 );
1002+ AssertIntEQ (pkt [5 ], MSGID_CHANNEL_OPEN_FAIL );
1003+ WMEMCPY (& chan , pkt + 6 , sizeof (chan ));
1004+ return ntohl (chan );
1005+ }
1006+
1007+ static word32 ParseChannelOpenFailReason (const byte * pkt , word32 sz )
1008+ {
1009+ word32 reason ;
1010+ /* SSH binary-packet layout: 4 (len) + 1 (pad_len) + 1 (msg_id) + 4 (chan) = 10;
1011+ * + 4 for the reason field itself gives the 14-byte minimum. */
1012+ AssertTrue (sz >= 14 );
1013+ AssertIntEQ (pkt [5 ], MSGID_CHANNEL_OPEN_FAIL );
1014+ WMEMCPY (& reason , pkt + 10 , sizeof (reason ));
1015+ return ntohl (reason );
1016+ }
1017+
9961018static void AssertChannelOpenFailResponse (const ChannelOpenHarness * harness ,
9971019 int ret )
9981020{
@@ -1347,6 +1369,54 @@ static void TestChannelOpenCallbackRejectSendsOpenFail(void)
13471369 FreeChannelOpenHarness (& harness );
13481370}
13491371
1372+ static void TestSecondSessionChannelRejected (void )
1373+ {
1374+ ChannelOpenHarness harness ;
1375+ byte in1 [128 ];
1376+ byte in2 [128 ];
1377+ word32 in1Sz ;
1378+ word32 in2Sz ;
1379+ int ret ;
1380+
1381+ in1Sz = BuildChannelOpenPacket ("session" , 7 , 0x4000 , 0x8000 ,
1382+ NULL , 0 , in1 , sizeof (in1 ));
1383+ in2Sz = BuildChannelOpenPacket ("session" , 8 , 0x4000 , 0x8000 ,
1384+ NULL , 0 , in2 , sizeof (in2 ));
1385+
1386+ InitChannelOpenHarness (& harness , in1 , in1Sz );
1387+
1388+ /* First channel open must succeed */
1389+ ret = DoReceive (harness .ssh );
1390+ AssertIntEQ (ret , WS_SUCCESS );
1391+ AssertIntEQ (harness .io .inOff , harness .io .inSz );
1392+ AssertTrue (harness .io .outSz > 0 );
1393+ AssertIntEQ (ParseMsgId (harness .io .out , harness .io .outSz ),
1394+ MSGID_CHANNEL_OPEN_CONF );
1395+ AssertIntEQ (harness .ssh -> channelListSz , 1 );
1396+
1397+ /* Repoint input and rewind outSz so the second response writes from offset 0.
1398+ * io.out and outCap need no change - both still refer to harness.out[256]. */
1399+ harness .io .in = in2 ;
1400+ harness .io .inSz = in2Sz ;
1401+ harness .io .inOff = 0 ;
1402+ harness .io .outSz = 0 ;
1403+
1404+ /* Second session channel open must be rejected */
1405+ ret = DoReceive (harness .ssh );
1406+ AssertIntEQ (ret , WS_SUCCESS );
1407+ AssertIntEQ (harness .io .inOff , harness .io .inSz );
1408+ AssertTrue (harness .io .outSz > 0 );
1409+ AssertIntEQ (ParseMsgId (harness .io .out , harness .io .outSz ),
1410+ MSGID_CHANNEL_OPEN_FAIL );
1411+ AssertIntEQ (ParseChannelOpenFailRecipient (harness .io .out , harness .io .outSz ),
1412+ 8 ); /* RFC 4254 5.1: server must echo the peer's channel ID */
1413+ AssertIntEQ (ParseChannelOpenFailReason (harness .io .out , harness .io .outSz ),
1414+ OPEN_ADMINISTRATIVELY_PROHIBITED );
1415+ AssertIntEQ (harness .ssh -> channelListSz , 1 ); /* original channel intact */
1416+
1417+ FreeChannelOpenHarness (& harness );
1418+ }
1419+
13501420#ifdef WOLFSSH_FWD
13511421static void TestDirectTcpipRejectSendsOpenFail (void )
13521422{
@@ -3179,10 +3249,82 @@ static void TestDoNewKeys(void)
31793249 wolfSSH_free (ssh );
31803250 wolfSSH_CTX_free (ctx );
31813251#endif /* !WOLFSSH_NO_AES_GCM */
3252+
3253+ /* Sub-test E: SELF_IS_KEYING guard - DoNewKeys must return
3254+ * WS_INVALID_STATE_E when the local side has not yet sent its own
3255+ * NEWKEYS (WOLFSSH_SELF_IS_KEYING still set). */
3256+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_SERVER , NULL );
3257+ AssertNotNull (ctx );
3258+ ssh = wolfSSH_new (ctx );
3259+ AssertNotNull (ssh );
3260+ AssertIntEQ (wolfSSH_SetAlgoListKex (ssh , FPF_KEX_GOOD ), WS_SUCCESS );
3261+ AssertIntEQ (wolfSSH_SetAlgoListKey (ssh , FPF_KEY_GOOD ), WS_SUCCESS );
3262+ AssertIntEQ (wolfSSH_SetAlgoListCipher (ssh ,
3263+ "aes128-cbc,aes256-ctr" ), WS_SUCCESS );
3264+ AssertIntEQ (wolfSSH_SetAlgoListMac (ssh ,
3265+ "hmac-sha1,hmac-sha2-256" ), WS_SUCCESS );
3266+ idx = 0 ;
3267+ payloadSz = BuildKexInitPayloadFull (
3268+ FPF_KEX_GOOD , FPF_KEY_GOOD ,
3269+ "aes128-cbc" , /* C2S enc */
3270+ "aes256-ctr" , /* S2C enc */
3271+ "hmac-sha1" , /* C2S MAC */
3272+ "hmac-sha2-256" , /* S2C MAC */
3273+ 0 , payload , (word32 )sizeof (payload ));
3274+ (void )wolfSSH_TestDoKexInit (ssh , payload , payloadSz , & idx );
3275+ AssertNotNull (ssh -> handshake );
3276+
3277+ /* peer has sent NEWKEYS but local NEWKEYS not yet sent.
3278+ * Key-material setup (k, h, sessionId, GenerateKeys) is intentionally
3279+ * absent - SELF_IS_KEYING must fire before key derivation reads those fields. */
3280+ ssh -> isKeying = WOLFSSH_SELF_IS_KEYING | WOLFSSH_PEER_IS_KEYING ;
3281+ AssertIntEQ (wolfSSH_TestDoNewKeys (ssh , NULL , 0 , NULL ), WS_INVALID_STATE_E );
3282+
3283+ /* DoNewKeys bailed before cleanup - handshake must still be allocated. */
3284+ AssertNotNull (ssh -> handshake );
3285+
3286+ wolfSSH_free (ssh );
3287+ wolfSSH_CTX_free (ctx );
31823288}
31833289
31843290#endif /* AES_CBC + AES_CTR + HMAC guards */
31853291
3292+ /* DoKexInit's PEER_IS_KEYING guard must return WS_INVALID_STATE_E when a
3293+ * second SSH_MSG_KEXINIT arrives while a key exchange is already in progress,
3294+ * preventing HandshakeInfo corruption if the outer IsMessageAllowed filter
3295+ * were ever bypassed. */
3296+ static void TestDoKexInitRejectsWhenPeerIsKeying (void )
3297+ {
3298+ WOLFSSH_CTX * ctx ;
3299+ WOLFSSH * ssh ;
3300+ byte payload [512 ];
3301+ word32 payloadSz ;
3302+ word32 idx = 0 ;
3303+
3304+ ctx = wolfSSH_CTX_new (WOLFSSH_ENDPOINT_SERVER , NULL );
3305+ AssertNotNull (ctx );
3306+
3307+ ssh = wolfSSH_new (ctx );
3308+ AssertNotNull (ssh );
3309+
3310+ AssertIntEQ (wolfSSH_SetAlgoListKex (ssh , FPF_KEX_GOOD ), WS_SUCCESS );
3311+ AssertIntEQ (wolfSSH_SetAlgoListKey (ssh , FPF_KEY_GOOD ), WS_SUCCESS );
3312+
3313+ payloadSz = BuildKexInitPayload (ssh , FPF_KEX_GOOD , FPF_KEY_GOOD , 0 ,
3314+ payload , (word32 )sizeof (payload ));
3315+
3316+ ssh -> isKeying |= WOLFSSH_PEER_IS_KEYING ;
3317+
3318+ AssertIntEQ (wolfSSH_TestDoKexInit (ssh , payload , payloadSz , & idx ),
3319+ WS_INVALID_STATE_E );
3320+ /* wolfSSH_new pre-allocates handshake; DoKexInit must not free it on
3321+ * early return, so the ongoing key-exchange state is preserved. */
3322+ AssertNotNull (ssh -> handshake );
3323+
3324+ wolfSSH_free (ssh );
3325+ wolfSSH_CTX_free (ctx );
3326+ }
3327+
31863328#endif /* first_packet_follows coverage guard */
31873329
31883330
@@ -3213,6 +3355,7 @@ int main(int argc, char** argv)
32133355 TestChannelBlockedBeforeAuth (ssh );
32143356 TestChannelAllowedAfterAuth (ssh );
32153357 TestChannelOpenCallbackRejectSendsOpenFail ();
3358+ TestSecondSessionChannelRejected ();
32163359#ifdef WOLFSSH_FWD
32173360 TestDirectTcpipRejectSendsOpenFail ();
32183361 TestDirectTcpipNoFwdCbSendsOpenFail ();
@@ -3232,6 +3375,7 @@ int main(int argc, char** argv)
32323375 && !defined(WOLFSSH_NO_RSA_SHA2_256 )
32333376 TestFirstPacketFollows ();
32343377 TestKexInitReservedNonZeroRejected ();
3378+ TestDoKexInitRejectsWhenPeerIsKeying ();
32353379#endif
32363380#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256 ) && !defined(WOLFSSH_NO_RSA ) \
32373381 && !defined(WOLFSSH_NO_CURVE25519_SHA256 ) \
0 commit comments