From 47c9e573f79a4a848cb9baa2166dc507ae501b05 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Fri, 24 Apr 2026 09:04:06 +0900 Subject: [PATCH] Add independent ciper and MAC algorithms negotiation for each direction, And add regress test --- src/internal.c | 85 +++++++++++++++++++++++------------ tests/regress.c | 110 +++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 5 +++ 3 files changed, 171 insertions(+), 29 deletions(-) diff --git a/src/internal.c b/src/internal.c index d4ddb71be..7d6a64f71 100644 --- a/src/internal.c +++ b/src/internal.c @@ -578,6 +578,9 @@ static HandshakeInfo* HandshakeInfoNew(void* heap) newHs->encryptId = ID_NONE; newHs->macId = ID_NONE; newHs->blockSz = MIN_BLOCK_SZ; + newHs->peerEncryptId = ID_NONE; + newHs->peerMacId = ID_NONE; + newHs->peerBlockSz = MIN_BLOCK_SZ; newHs->eSz = (word32)sizeof(newHs->e); newHs->xSz = (word32)sizeof(newHs->x); #ifndef WOLFSSH_NO_DH_GEX_SHA256 @@ -4422,6 +4425,22 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ret = WS_MATCH_ENC_ALGO_E; } } + if (ret == WS_SUCCESS) { + ssh->handshake->peerEncryptId = algoId; + ssh->handshake->peerAeadMode = AeadModeForId(algoId); + ssh->handshake->peerBlockSz = BlockSzForId(algoId); + ssh->handshake->peerKeys.encKeySz = KeySzForId(algoId); + if (!ssh->handshake->peerAeadMode) { + ssh->handshake->peerKeys.ivSz = ssh->handshake->peerBlockSz; + } + else { + /* Reaching here requires peerAeadMode==1, which requires an AEAD + * cipher ID, which requires WOLFSSH_NO_AES_GCM to be unset, which + * means WOLFSSH_NO_AEAD is also unset (see internal.h). */ + ssh->handshake->peerKeys.ivSz = AEAD_NONCE_SZ; + ssh->handshake->peerMacSz = ssh->handshake->peerBlockSz; + } + } /* Enc Algorithms - Server to Client */ if (ret == WS_SUCCESS) { @@ -4430,7 +4449,13 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ret = GetNameList(list, &listSz, buf, len, &begin); } if (ret == WS_SUCCESS) { - algoId = MatchIdLists(side, list, listSz, &algoId, 1); + cannedAlgoNamesSz = AlgoListSz(ssh->algoListCipher); + cannedListSz = (word32)sizeof(cannedList); + ret = GetNameListRaw(cannedList, &cannedListSz, + (const byte*)ssh->algoListCipher, cannedAlgoNamesSz); + } + if (ret == WS_SUCCESS) { + algoId = MatchIdLists(side, list, listSz, cannedList, cannedListSz); if (algoId == ID_UNKNOWN) { WLOG(WS_LOG_DEBUG, "Unable to negotiate Encryption Algo S2C"); ret = WS_MATCH_ENC_ALGO_E; @@ -4440,21 +4465,14 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ssh->handshake->encryptId = algoId; ssh->handshake->aeadMode = AeadModeForId(algoId); ssh->handshake->blockSz = BlockSzForId(algoId); - ssh->handshake->keys.encKeySz = - ssh->handshake->peerKeys.encKeySz = - KeySzForId(algoId); + ssh->handshake->keys.encKeySz = KeySzForId(algoId); if (!ssh->handshake->aeadMode) { - ssh->handshake->keys.ivSz = - ssh->handshake->peerKeys.ivSz = - ssh->handshake->blockSz; + ssh->handshake->keys.ivSz = ssh->handshake->blockSz; } else { -#ifndef WOLFSSH_NO_AEAD - ssh->handshake->keys.ivSz = - ssh->handshake->peerKeys.ivSz = - AEAD_NONCE_SZ; + /* Same invariant: aeadMode==1 implies !WOLFSSH_NO_AEAD. */ + ssh->handshake->keys.ivSz = AEAD_NONCE_SZ; ssh->handshake->macSz = ssh->handshake->blockSz; -#endif } } @@ -4464,7 +4482,7 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) listSz = (word32)sizeof(list); ret = GetNameList(list, &listSz, buf, len, &begin); } - if (ret == WS_SUCCESS && !ssh->handshake->aeadMode) { + if (ret == WS_SUCCESS && !ssh->handshake->peerAeadMode) { cannedAlgoNamesSz = AlgoListSz(ssh->algoListMac); cannedListSz = (word32)sizeof(cannedList); ret = GetNameListRaw(cannedList, &cannedListSz, @@ -4476,6 +4494,11 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) WLOG(WS_LOG_DEBUG, "Unable to negotiate MAC Algo C2S"); ret = WS_MATCH_MAC_ALGO_E; } + else { + ssh->handshake->peerMacId = algoId; + ssh->handshake->peerMacSz = MacSzForId(algoId); + ssh->handshake->peerKeys.macKeySz = KeySzForId(algoId); + } } } @@ -4486,17 +4509,21 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ret = GetNameList(list, &listSz, buf, len, &begin); } if (ret == WS_SUCCESS && !ssh->handshake->aeadMode) { - algoId = MatchIdLists(side, list, listSz, &algoId, 1); - if (algoId == ID_UNKNOWN) { - WLOG(WS_LOG_DEBUG, "Unable to negotiate MAC Algo S2C"); - ret = WS_MATCH_MAC_ALGO_E; - } - else { - ssh->handshake->macId = algoId; - ssh->handshake->macSz = MacSzForId(algoId); - ssh->handshake->keys.macKeySz = - ssh->handshake->peerKeys.macKeySz = - KeySzForId(algoId); + cannedAlgoNamesSz = AlgoListSz(ssh->algoListMac); + cannedListSz = (word32)sizeof(cannedList); + ret = GetNameListRaw(cannedList, &cannedListSz, + (const byte*)ssh->algoListMac, cannedAlgoNamesSz); + if (ret == WS_SUCCESS) { + algoId = MatchIdLists(side, list, listSz, cannedList, cannedListSz); + if (algoId == ID_UNKNOWN) { + WLOG(WS_LOG_DEBUG, "Unable to negotiate MAC Algo S2C"); + ret = WS_MATCH_MAC_ALGO_E; + } + else { + ssh->handshake->macId = algoId; + ssh->handshake->macSz = MacSzForId(algoId); + ssh->handshake->keys.macKeySz = KeySzForId(algoId); + } } } @@ -6238,11 +6265,11 @@ static int DoNewKeys(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) } if (ret == WS_SUCCESS) { - ssh->peerEncryptId = ssh->handshake->encryptId; - ssh->peerMacId = ssh->handshake->macId; - ssh->peerBlockSz = ssh->handshake->blockSz; - ssh->peerMacSz = ssh->handshake->macSz; - ssh->peerAeadMode = ssh->handshake->aeadMode; + ssh->peerEncryptId = ssh->handshake->peerEncryptId; + ssh->peerMacId = ssh->handshake->peerMacId; + ssh->peerBlockSz = ssh->handshake->peerBlockSz; + ssh->peerMacSz = ssh->handshake->peerMacSz; + ssh->peerAeadMode = ssh->handshake->peerAeadMode; WMEMCPY(&ssh->peerKeys, &ssh->handshake->peerKeys, sizeof(Keys)); switch (ssh->peerEncryptId) { diff --git a/tests/regress.c b/tests/regress.c index 82ed68114..9371fa247 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -1989,6 +1989,36 @@ static word32 BuildKexInitPayload(WOLFSSH* ssh, const char* kexList, return idx; } +#if !defined(WOLFSSH_NO_AES_CBC) && !defined(WOLFSSH_NO_AES_CTR) \ + && !defined(WOLFSSH_NO_HMAC_SHA1) && !defined(WOLFSSH_NO_HMAC_SHA2_256) +/* Like BuildKexInitPayload but with explicit per-direction cipher/MAC lists. */ +static word32 BuildKexInitPayloadFull(const char* kexList, + const char* keyList, const char* encC2S, const char* encS2C, + const char* macC2S, const char* macS2C, + byte firstPacketFollows, byte* out, word32 outSz) +{ + word32 idx = 0; + + AssertTrue(idx + COOKIE_SZ <= outSz); + WMEMSET(out + idx, 0, COOKIE_SZ); + idx += COOKIE_SZ; + idx = AppendString(out, outSz, idx, kexList); + idx = AppendString(out, outSz, idx, keyList); + idx = AppendString(out, outSz, idx, encC2S); + idx = AppendString(out, outSz, idx, encS2C); + idx = AppendString(out, outSz, idx, macC2S); + idx = AppendString(out, outSz, idx, macS2C); + idx = AppendString(out, outSz, idx, "none"); + idx = AppendString(out, outSz, idx, "none"); + idx = AppendString(out, outSz, idx, ""); + idx = AppendString(out, outSz, idx, ""); + idx = AppendByte(out, outSz, idx, firstPacketFollows); + idx = AppendUint32(out, outSz, idx, 0); /* reserved */ + + return idx; +} +#endif /* AES_CBC + AES_CTR + HMAC guards (BuildKexInitPayloadFull) */ + typedef struct { const char* description; const char* kexList; @@ -2107,6 +2137,79 @@ static void TestFirstPacketFollows(void) TestFirstPacketFollowsSkipped(); } +#if !defined(WOLFSSH_NO_AES_CBC) && !defined(WOLFSSH_NO_AES_CTR) \ + && !defined(WOLFSSH_NO_HMAC_SHA1) && !defined(WOLFSSH_NO_HMAC_SHA2_256) +static void TestIndependentAlgoNegotiation(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + byte payload[512]; + word32 payloadSz; + word32 idx; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + /* Sub-test A: different non-AEAD cipher and MAC per direction */ + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_SetAlgoListKex(ssh, FPF_KEX_GOOD), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, FPF_KEY_GOOD), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListCipher(ssh, + "aes128-cbc,aes256-ctr"), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, + "hmac-sha1,hmac-sha2-256"), WS_SUCCESS); + idx = 0; + payloadSz = BuildKexInitPayloadFull( + FPF_KEX_GOOD, FPF_KEY_GOOD, + "aes128-cbc", /* C2S enc */ + "aes256-ctr", /* S2C enc */ + "hmac-sha1", /* C2S MAC */ + "hmac-sha2-256", /* S2C MAC */ + 0, payload, (word32)sizeof(payload)); + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + AssertIntEQ(ssh->handshake->peerEncryptId, ID_AES128_CBC); + AssertIntEQ(ssh->handshake->encryptId, ID_AES256_CTR); + AssertIntEQ(ssh->handshake->peerMacId, ID_HMAC_SHA1); + AssertIntEQ(ssh->handshake->macId, ID_HMAC_SHA2_256); + AssertIntEQ(ssh->handshake->peerAeadMode, 0); + AssertIntEQ(ssh->handshake->aeadMode, 0); + wolfSSH_free(ssh); + +#ifndef WOLFSSH_NO_AES_GCM + /* Sub-test B: AEAD S2C, non-AEAD C2S — MAC only negotiated for C2S */ + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_SetAlgoListKex(ssh, FPF_KEX_GOOD), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, FPF_KEY_GOOD), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListCipher(ssh, + "aes128-cbc,aes256-gcm@openssh.com"), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, + "hmac-sha1,hmac-sha2-256"), WS_SUCCESS); + idx = 0; + payloadSz = BuildKexInitPayloadFull( + FPF_KEX_GOOD, FPF_KEY_GOOD, + "aes128-cbc", /* C2S enc: non-AEAD */ + "aes256-gcm@openssh.com", /* S2C enc: AEAD */ + "hmac-sha1", /* C2S MAC: negotiated */ + "hmac-sha2-256", /* S2C MAC: skipped (aeadMode) */ + 0, payload, (word32)sizeof(payload)); + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + AssertIntEQ(ssh->handshake->peerEncryptId, ID_AES128_CBC); + AssertIntEQ(ssh->handshake->encryptId, ID_AES256_GCM); + AssertIntEQ(ssh->handshake->peerAeadMode, 0); + AssertIntEQ(ssh->handshake->aeadMode, 1); + AssertIntEQ(ssh->handshake->peerMacId, ID_HMAC_SHA1); + AssertIntEQ(ssh->handshake->macId, ID_NONE); + wolfSSH_free(ssh); +#endif /* !WOLFSSH_NO_AES_GCM */ + + wolfSSH_CTX_free(ctx); +} +#endif /* AES_CBC + AES_CTR + HMAC guards */ + #endif /* first_packet_follows coverage guard */ @@ -2155,6 +2258,13 @@ int main(int argc, char** argv) && !defined(WOLFSSH_NO_CURVE25519_SHA256) \ && !defined(WOLFSSH_NO_RSA_SHA2_256) TestFirstPacketFollows(); +#endif +#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) && !defined(WOLFSSH_NO_RSA) \ + && !defined(WOLFSSH_NO_CURVE25519_SHA256) \ + && !defined(WOLFSSH_NO_RSA_SHA2_256) \ + && !defined(WOLFSSH_NO_AES_CBC) && !defined(WOLFSSH_NO_AES_CTR) \ + && !defined(WOLFSSH_NO_HMAC_SHA1) && !defined(WOLFSSH_NO_HMAC_SHA2_256) + TestIndependentAlgoNegotiation(); #endif TestDisconnectSetsDisconnectError(); TestClientBuffersIdempotent(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 5616b5556..949dbfeba 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -636,9 +636,14 @@ typedef struct HandshakeInfo { byte encryptId; byte macId; byte aeadMode; + byte peerEncryptId; + byte peerMacId; + byte peerAeadMode; byte blockSz; byte macSz; + byte peerBlockSz; + byte peerMacSz; Keys keys; Keys peerKeys;