diff --git a/src/internal.c b/src/internal.c index 249462e31..5bdc1985f 100644 --- a/src/internal.c +++ b/src/internal.c @@ -570,13 +570,11 @@ static HandshakeInfo* HandshakeInfoNew(void* heap) heap, DYNTYPE_HS); if (newHs != NULL) { WMEMSET(newHs, 0, sizeof(HandshakeInfo)); - newHs->expectMsgId = MSGID_NONE; - newHs->kexId = ID_NONE; newHs->kexHashId = WC_HASH_TYPE_NONE; - newHs->pubKeyId = ID_NONE; - newHs->encryptId = ID_NONE; - newHs->macId = ID_NONE; newHs->blockSz = MIN_BLOCK_SZ; + newHs->peerBlockSz = MIN_BLOCK_SZ; + /* peerEncryptId, peerMacId, peerAeadMode, peerMacSz: left at 0 + * (== ID_NONE / no-MAC) by the WMEMSET above. */ newHs->eSz = (word32)sizeof(newHs->e); newHs->xSz = (word32)sizeof(newHs->x); #ifndef WOLFSSH_NO_DH_GEX_SHA256 @@ -2625,7 +2623,7 @@ static int GenerateKeys(WOLFSSH* ssh, byte hashId, byte doKeyPad) Keys* sK = NULL; int ret = WS_SUCCESS; - if (ssh == NULL) + if (ssh == NULL || ssh->handshake == NULL) ret = WS_BAD_ARGUMENT; else { if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { @@ -2658,19 +2656,17 @@ static int GenerateKeys(WOLFSSH* ssh, byte hashId, byte doKeyPad) sK->encKey, sK->encKeySz, ssh->k, ssh->kSz, ssh->h, ssh->hSz, ssh->sessionId, ssh->sessionIdSz, doKeyPad); - if (ret == WS_SUCCESS) { - if (!ssh->handshake->aeadMode) { - ret = GenerateKey(hashId, 'E', - cK->macKey, cK->macKeySz, - ssh->k, ssh->kSz, ssh->h, ssh->hSz, - ssh->sessionId, ssh->sessionIdSz, doKeyPad); - if (ret == WS_SUCCESS) { - ret = GenerateKey(hashId, 'F', - sK->macKey, sK->macKeySz, - ssh->k, ssh->kSz, ssh->h, ssh->hSz, - ssh->sessionId, ssh->sessionIdSz, doKeyPad); - } - } + if (ret == WS_SUCCESS && cK->macKeySz > 0) { + ret = GenerateKey(hashId, 'E', + cK->macKey, cK->macKeySz, + ssh->k, ssh->kSz, ssh->h, ssh->hSz, + ssh->sessionId, ssh->sessionIdSz, doKeyPad); + } + if (ret == WS_SUCCESS && sK->macKeySz > 0) { + ret = GenerateKey(hashId, 'F', + sK->macKey, sK->macKeySz, + ssh->k, ssh->kSz, ssh->h, ssh->hSz, + ssh->sessionId, ssh->sessionIdSz, doKeyPad); } #ifdef SHOW_SECRETS @@ -4258,6 +4254,18 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) word32 cannedAlgoNamesSz; word32 skipSz = 0; word32 begin; + /* handshake->keys/encryptId/... always represent the LOCAL endpoint's + * outgoing direction; peer* counterparts represent the peer's outgoing + * (= our incoming) direction. Server: local=S2C, peer=C2S. + * Client: local=C2S, peer=S2C. These aliases let the four enc/MAC + * parse sections store results without inline side checks, and keep the + * fields consistent with what GenerateKeys/SendNewKeys/DoNewKeys expect. */ + byte *c2sEncryptId = NULL, *c2sAeadMode = NULL, *c2sBlockSz = NULL, + *c2sMacId = NULL, *c2sMacSz = NULL; + Keys *c2sKeys = NULL; + byte *s2cEncryptId = NULL, *s2cAeadMode = NULL, *s2cBlockSz = NULL, + *s2cMacId = NULL, *s2cMacSz = NULL; + Keys *s2cKeys = NULL; WLOG(WS_LOG_DEBUG, "Entering DoKexInit()"); @@ -4300,6 +4308,35 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) begin = *idx; side = ssh->ctx->side; + if (side == WOLFSSH_ENDPOINT_SERVER) { + c2sEncryptId = &ssh->handshake->peerEncryptId; + c2sAeadMode = &ssh->handshake->peerAeadMode; + c2sBlockSz = &ssh->handshake->peerBlockSz; + c2sMacId = &ssh->handshake->peerMacId; + c2sMacSz = &ssh->handshake->peerMacSz; + c2sKeys = &ssh->handshake->peerKeys; + s2cEncryptId = &ssh->handshake->encryptId; + s2cAeadMode = &ssh->handshake->aeadMode; + s2cBlockSz = &ssh->handshake->blockSz; + s2cMacId = &ssh->handshake->macId; + s2cMacSz = &ssh->handshake->macSz; + s2cKeys = &ssh->handshake->keys; + } + else { + c2sEncryptId = &ssh->handshake->encryptId; + c2sAeadMode = &ssh->handshake->aeadMode; + c2sBlockSz = &ssh->handshake->blockSz; + c2sMacId = &ssh->handshake->macId; + c2sMacSz = &ssh->handshake->macSz; + c2sKeys = &ssh->handshake->keys; + s2cEncryptId = &ssh->handshake->peerEncryptId; + s2cAeadMode = &ssh->handshake->peerAeadMode; + s2cBlockSz = &ssh->handshake->peerBlockSz; + s2cMacId = &ssh->handshake->peerMacId; + s2cMacSz = &ssh->handshake->peerMacSz; + s2cKeys = &ssh->handshake->peerKeys; + } + /* Check that the cookie exists inside the message */ if (begin + COOKIE_SZ > len) { /* error, out of bounds */ @@ -4399,6 +4436,24 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ret = WS_MATCH_ENC_ALGO_E; } } + if (ret == WS_SUCCESS) { + *c2sEncryptId = algoId; + *c2sAeadMode = AeadModeForId(algoId); + *c2sBlockSz = BlockSzForId(algoId); + c2sKeys->encKeySz = KeySzForId(algoId); + if (!*c2sAeadMode) { + c2sKeys->ivSz = *c2sBlockSz; + } + else { + /* Reaching here requires an AEAD cipher ID, which requires + * WOLFSSH_NO_AES_GCM to be unset, hence WOLFSSH_NO_AEAD unset + * (see internal.h). */ + c2sKeys->ivSz = AEAD_NONCE_SZ; + *c2sMacSz = *c2sBlockSz; + *c2sMacId = ID_NONE; + c2sKeys->macKeySz = 0; + } + } /* Enc Algorithms - Server to Client */ if (ret == WS_SUCCESS) { @@ -4407,31 +4462,32 @@ 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; } } if (ret == WS_SUCCESS) { - ssh->handshake->encryptId = algoId; - ssh->handshake->aeadMode = AeadModeForId(algoId); - ssh->handshake->blockSz = BlockSzForId(algoId); - ssh->handshake->keys.encKeySz = - ssh->handshake->peerKeys.encKeySz = - KeySzForId(algoId); - if (!ssh->handshake->aeadMode) { - ssh->handshake->keys.ivSz = - ssh->handshake->peerKeys.ivSz = - ssh->handshake->blockSz; + *s2cEncryptId = algoId; + *s2cAeadMode = AeadModeForId(algoId); + *s2cBlockSz = BlockSzForId(algoId); + s2cKeys->encKeySz = KeySzForId(algoId); + if (!*s2cAeadMode) { + s2cKeys->ivSz = *s2cBlockSz; } else { -#ifndef WOLFSSH_NO_AEAD - ssh->handshake->keys.ivSz = - ssh->handshake->peerKeys.ivSz = - AEAD_NONCE_SZ; - ssh->handshake->macSz = ssh->handshake->blockSz; -#endif + /* Same invariant as C2S: AEAD cipher ID implies !WOLFSSH_NO_AEAD. */ + s2cKeys->ivSz = AEAD_NONCE_SZ; + *s2cMacSz = *s2cBlockSz; + *s2cMacId = ID_NONE; + s2cKeys->macKeySz = 0; } } @@ -4441,7 +4497,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 && !*c2sAeadMode) { cannedAlgoNamesSz = AlgoListSz(ssh->algoListMac); cannedListSz = (word32)sizeof(cannedList); ret = GetNameListRaw(cannedList, &cannedListSz, @@ -4453,6 +4509,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 { + *c2sMacId = algoId; + *c2sMacSz = MacSzForId(algoId); + c2sKeys->macKeySz = KeySzForId(algoId); + } } } @@ -4462,18 +4523,22 @@ 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) { - 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); + if (ret == WS_SUCCESS && !*s2cAeadMode) { + 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 { + *s2cMacId = algoId; + *s2cMacSz = MacSzForId(algoId); + s2cKeys->macKeySz = KeySzForId(algoId); + } } } @@ -6226,11 +6291,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) { @@ -17960,6 +18025,25 @@ int wolfSSH_TestDoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) return DoKexInit(ssh, buf, len, idx); } +int wolfSSH_TestDoNewKeys(WOLFSSH* ssh) +{ + /* DoNewKeys ignores buf/len/idx (marked WOLFSSH_UNUSED internally). */ + return DoNewKeys(ssh, NULL, 0, NULL); +} + +void wolfSSH_TestFreeHandshake(WOLFSSH* ssh) +{ + if (ssh != NULL) { + HandshakeInfoFree(ssh->handshake, ssh->ctx ? ssh->ctx->heap : NULL); + ssh->handshake = NULL; + } +} + +int wolfSSH_TestGenerateKeys(WOLFSSH* ssh, byte hashId) +{ + return GenerateKeys(ssh, (enum wc_HashType)hashId, 1); +} + int wolfSSH_TestDoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) { return DoKexDhInit(ssh, buf, len, idx); diff --git a/tests/regress.c b/tests/regress.c index 7d1dbb2be..18c10b080 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -2010,6 +2010,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; @@ -2137,6 +2167,868 @@ 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)); + /* DoKexInit's tail calls SendKexInit, which fails without a loaded host + * key. We only care about the negotiated algorithm IDs set during parse. */ + (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); + /* Key sizes — server: C2S→peerKeys, S2C→keys. Validates the + * side-aware DoKexInit fix: wrong mapping would swap these sizes. */ + AssertIntEQ(ssh->handshake->peerKeys.encKeySz, AES_128_KEY_SIZE); + AssertIntEQ(ssh->handshake->keys.encKeySz, AES_256_KEY_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.ivSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->keys.ivSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->keys.macKeySz, WC_SHA256_DIGEST_SIZE); + /* Block/mac sizes — server: C2S→peer*, S2C→local. */ + AssertIntEQ(ssh->handshake->peerBlockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->blockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->peerMacSz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->macSz, WC_SHA256_DIGEST_SIZE); + 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)); + /* DoKexInit's tail calls SendKexInit, which fails without a loaded host + * key. We only care about the negotiated algorithm IDs set during parse. */ + (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); + /* Key sizes for split-AEAD case. */ + AssertIntEQ(ssh->handshake->peerKeys.encKeySz, AES_128_KEY_SIZE); + AssertIntEQ(ssh->handshake->keys.encKeySz, AES_256_KEY_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.ivSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->keys.ivSz, AEAD_NONCE_SZ); + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->keys.macKeySz, 0); + /* Block/mac sizes: C2S non-AEAD peerMacSz=SHA1, S2C AEAD macSz=blockSz. */ + AssertIntEQ(ssh->handshake->peerBlockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->blockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->peerMacSz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->macSz, AES_BLOCK_SIZE); + wolfSSH_free(ssh); +#endif /* !WOLFSSH_NO_AES_GCM */ + + wolfSSH_CTX_free(ctx); +} + +static void TestIndependentAlgoNegotiationClient(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + byte payload[512]; + word32 payloadSz; + word32 idx; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + /* Sub-test A: different non-AEAD cipher and MAC per direction. + * Client mapping is the mirror of server: C2S→keys/encryptId, + * S2C→peerKeys/peerEncryptId. A swap bug would make these asserts fail. */ + 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)); + /* DoKexInit's tail calls wolfSSH_SendPacket, which fails because no IO + * callback is set up. We only care about the negotiated algorithm IDs. */ + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + /* Client: C2S is local outgoing → encryptId/keys */ + AssertIntEQ(ssh->handshake->encryptId, ID_AES128_CBC); + AssertIntEQ(ssh->handshake->peerEncryptId, ID_AES256_CTR); + AssertIntEQ(ssh->handshake->macId, ID_HMAC_SHA1); + AssertIntEQ(ssh->handshake->peerMacId, ID_HMAC_SHA2_256); + AssertIntEQ(ssh->handshake->aeadMode, 0); + AssertIntEQ(ssh->handshake->peerAeadMode, 0); + AssertIntEQ(ssh->handshake->keys.encKeySz, AES_128_KEY_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.encKeySz, AES_256_KEY_SIZE); + AssertIntEQ(ssh->handshake->keys.ivSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.ivSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->keys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, WC_SHA256_DIGEST_SIZE); + /* Block/mac sizes — client: C2S→local (block/macSz), S2C→peer (peerBlock/MacSz). */ + AssertIntEQ(ssh->handshake->blockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->peerBlockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->macSz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->peerMacSz, WC_SHA256_DIGEST_SIZE); + wolfSSH_free(ssh); + +#ifndef WOLFSSH_NO_AES_GCM + /* Sub-test B: AEAD S2C, non-AEAD C2S — client perspective. */ + 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)); + /* DoKexInit's tail calls wolfSSH_SendPacket, which fails because no IO + * callback is set up. We only care about the negotiated algorithm IDs. */ + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + /* Client: C2S→encryptId/keys, S2C→peerEncryptId/peerKeys */ + AssertIntEQ(ssh->handshake->encryptId, ID_AES128_CBC); + AssertIntEQ(ssh->handshake->peerEncryptId, ID_AES256_GCM); + AssertIntEQ(ssh->handshake->aeadMode, 0); + AssertIntEQ(ssh->handshake->peerAeadMode, 1); + AssertIntEQ(ssh->handshake->macId, ID_HMAC_SHA1); + AssertIntEQ(ssh->handshake->peerMacId, ID_NONE); + AssertIntEQ(ssh->handshake->keys.encKeySz, AES_128_KEY_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.encKeySz, AES_256_KEY_SIZE); + AssertIntEQ(ssh->handshake->keys.ivSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.ivSz, AEAD_NONCE_SZ); + AssertIntEQ(ssh->handshake->keys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, 0); + /* Block/mac sizes: C2S non-AEAD macSz=SHA1, S2C AEAD peerMacSz=peerBlockSz. */ + AssertIntEQ(ssh->handshake->blockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->peerBlockSz, AES_BLOCK_SIZE); + AssertIntEQ(ssh->handshake->macSz, WC_SHA_DIGEST_SIZE); + AssertIntEQ(ssh->handshake->peerMacSz, AES_BLOCK_SIZE); + wolfSSH_free(ssh); +#endif /* !WOLFSSH_NO_AES_GCM */ + + wolfSSH_CTX_free(ctx); +} + +/* Verify WS_MATCH_ENC_ALGO_E when exactly one direction's cipher list has no + * match in the local algoListCipher — the new per-direction S2C matching path + * introduced by the independent-algo-negotiation change. */ +static void TestEncMismatch(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: C2S matches, S2C does not. + * Local list accepts aes128-cbc and aes256-ctr. + * Peer offers C2S=aes128-cbc (in list) and S2C=3des-cbc (not in list). + * Expected: WS_MATCH_ENC_ALGO_E from the S2C block. */ + 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: in local list */ + "3des-cbc", /* S2C enc: not in local list */ + "hmac-sha1", /* C2S MAC */ + "hmac-sha1", /* S2C MAC */ + 0, payload, (word32)sizeof(payload)); + AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx), + WS_MATCH_ENC_ALGO_E); + wolfSSH_free(ssh); + + /* Sub-test B: S2C matches, C2S does not. + * Peer offers C2S=3des-cbc (not in list) and S2C=aes256-ctr (in list). + * Expected: WS_MATCH_ENC_ALGO_E from the C2S block. */ + 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, + "3des-cbc", /* C2S enc: not in local list */ + "aes256-ctr", /* S2C enc: in local list */ + "hmac-sha1", /* C2S MAC */ + "hmac-sha1", /* S2C MAC */ + 0, payload, (word32)sizeof(payload)); + AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx), + WS_MATCH_ENC_ALGO_E); + wolfSSH_free(ssh); + + wolfSSH_CTX_free(ctx); +} + +/* Verify WS_MATCH_MAC_ALGO_E when exactly one direction's MAC list has no + * match in the local algoListMac — the new per-direction S2C MAC matching path. + * Both cipher directions must succeed so that MAC negotiation is reached. */ +static void TestMacMismatch(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: C2S MAC matches, S2C MAC does not. + * Local MAC list accepts hmac-sha1 and hmac-sha2-256. + * Peer offers C2S=hmac-sha1 (in list) and S2C=hmac-md5 (not in list). + * Expected: WS_MATCH_MAC_ALGO_E from the S2C MAC block. */ + 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: in local list */ + "aes256-ctr", /* S2C enc: in local list */ + "hmac-sha1", /* C2S MAC: in local list */ + "hmac-md5", /* S2C MAC: not in local list */ + 0, payload, (word32)sizeof(payload)); + AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx), + WS_MATCH_MAC_ALGO_E); + wolfSSH_free(ssh); + + /* Sub-test B: S2C MAC matches, C2S MAC does not. + * Peer offers C2S=hmac-md5 (not in list) and S2C=hmac-sha2-256 (in list). + * Expected: WS_MATCH_MAC_ALGO_E from the C2S MAC block. */ + 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: in local list */ + "aes256-ctr", /* S2C enc: in local list */ + "hmac-md5", /* C2S MAC: not in local list */ + "hmac-sha2-256", /* S2C MAC: in local list */ + 0, payload, (word32)sizeof(payload)); + AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx), + WS_MATCH_MAC_ALGO_E); + wolfSSH_free(ssh); + + wolfSSH_CTX_free(ctx); +} + +static void TestGenerateKeysSplit(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + byte payload[512]; + word32 payloadSz; + word32 idx; + byte zeros[AES_256_KEY_SIZE]; + + WMEMSET(zeros, 0, sizeof(zeros)); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + /* Sub-test 0 (negative): GenerateKeys returns WS_BAD_ARGUMENT when + * ssh->handshake is NULL, exercising the guard added in GenerateKeys. */ + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + wolfSSH_TestFreeHandshake(ssh); /* properly frees before NULLing */ + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, 0), WS_BAD_ARGUMENT); + wolfSSH_free(ssh); + + /* Sub-test A: aes128-cbc C2S / aes256-ctr S2C, non-AEAD both dirs. + * Verifies GenerateKeys uses the correct key size for each 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)); + /* DoKexInit's tail calls SendKexInit, which fails without a loaded host + * key. We only care about the handshake state set during parse. */ + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + /* Synthetic K/H/sessionId — any non-zero values produce valid key material. */ + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S direction (server: peerKeys) — aes128-cbc + hmac-sha1. */ + AssertIntEQ(ssh->handshake->peerKeys.encKeySz, AES_128_KEY_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->peerKeys.encKey, zeros, + AES_128_KEY_SIZE) != 0); + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA_DIGEST_SIZE) != 0); + + /* S2C direction (server: keys) — aes256-ctr + hmac-sha2-256. */ + AssertIntEQ(ssh->handshake->keys.encKeySz, AES_256_KEY_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->keys.encKey, zeros, + AES_256_KEY_SIZE) != 0); + AssertIntEQ(ssh->handshake->keys.macKeySz, WC_SHA256_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA256_DIGEST_SIZE) != 0); + + /* C2S and S2C enc keys must be independent (different RFC labels C/D). */ + AssertTrue(WMEMCMP(ssh->handshake->peerKeys.encKey, + ssh->handshake->keys.encKey, AES_128_KEY_SIZE) != 0); + + wolfSSH_free(ssh); + +#ifndef WOLFSSH_NO_AES_GCM + /* Sub-test B: aes128-cbc C2S (non-AEAD) / aes256-gcm S2C (AEAD). + * Verifies that key 'F' is skipped for the AEAD direction (macKeySz==0). */ + 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 */ + "hmac-sha2-256", /* S2C MAC: skipped */ + 0, payload, (word32)sizeof(payload)); + /* DoKexInit's tail calls SendKexInit, which fails without a loaded host + * key. We only care about the handshake state set during parse. */ + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S hmac-sha1 MAC key must be generated. */ + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA_DIGEST_SIZE) != 0); + + /* S2C AEAD: macKeySz==0 so key 'F' was skipped; macKey stays all-zero. */ + AssertIntEQ(ssh->handshake->keys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + wolfSSH_free(ssh); +#endif /* !WOLFSSH_NO_AES_GCM */ + +#ifndef WOLFSSH_NO_AES_GCM + /* Sub-test C: aes256-gcm C2S (AEAD) / aes128-cbc S2C (non-AEAD) — mirror. + * Verifies that key 'E' is skipped (peerKeys.macKeySz==0) while key 'F' + * is generated for the non-AEAD S2C 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, + "aes256-gcm@openssh.com,aes128-cbc"), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, + "hmac-sha1,hmac-sha2-256"), WS_SUCCESS); + idx = 0; + payloadSz = BuildKexInitPayloadFull( + FPF_KEX_GOOD, FPF_KEY_GOOD, + "aes256-gcm@openssh.com", /* C2S enc: AEAD */ + "aes128-cbc", /* S2C enc: non-AEAD */ + "hmac-sha1", /* C2S MAC: skipped (AEAD) */ + "hmac-sha2-256", /* S2C MAC: negotiated */ + 0, payload, (word32)sizeof(payload)); + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S AEAD (server: peerKeys): macKeySz==0, key 'E' skipped. */ + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + /* S2C hmac-sha2-256 MAC key (server: keys) must be generated. */ + AssertIntEQ(ssh->handshake->keys.macKeySz, WC_SHA256_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA256_DIGEST_SIZE) != 0); + + wolfSSH_free(ssh); + + /* Sub-test D: aes256-gcm C2S (AEAD) / aes256-gcm S2C (AEAD) — symmetric. + * Both macKeySz==0; both key 'E' and key 'F' generation skipped. + * Directly validates the per-direction macKeySz>0 guards in GenerateKeys. */ + 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, + "aes256-gcm@openssh.com"), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, "hmac-sha1"), WS_SUCCESS); + idx = 0; + payloadSz = BuildKexInitPayloadFull( + FPF_KEX_GOOD, FPF_KEY_GOOD, + "aes256-gcm@openssh.com", /* C2S enc: AEAD */ + "aes256-gcm@openssh.com", /* S2C enc: AEAD */ + "hmac-sha1", /* C2S MAC: skipped (AEAD) */ + "hmac-sha1", /* S2C MAC: skipped (AEAD) */ + 0, payload, (word32)sizeof(payload)); + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S AEAD (server: peerKeys): macKeySz==0, key 'E' skipped. */ + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + /* S2C AEAD (server: keys): macKeySz==0, key 'F' skipped. */ + AssertIntEQ(ssh->handshake->keys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + wolfSSH_free(ssh); +#endif /* !WOLFSSH_NO_AES_GCM */ + + wolfSSH_CTX_free(ctx); +} + +static void TestGenerateKeysSplitClient(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + byte payload[512]; + word32 payloadSz; + word32 idx; + byte zeros[AES_256_KEY_SIZE]; + + WMEMSET(zeros, 0, sizeof(zeros)); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + /* Sub-test A: aes128-cbc C2S / aes256-ctr S2C — client mapping. + * Client: C2S→keys (local outgoing), S2C→peerKeys (peer outgoing). */ + 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)); + /* DoKexInit's tail calls wolfSSH_SendPacket, which fails because no IO + * callback is set up. We only care about the handshake state set during parse. */ + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S direction (client: keys) — aes128-cbc + hmac-sha1. */ + AssertIntEQ(ssh->handshake->keys.encKeySz, AES_128_KEY_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->keys.encKey, zeros, + AES_128_KEY_SIZE) != 0); + AssertIntEQ(ssh->handshake->keys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA_DIGEST_SIZE) != 0); + + /* S2C direction (client: peerKeys) — aes256-ctr + hmac-sha2-256. */ + AssertIntEQ(ssh->handshake->peerKeys.encKeySz, AES_256_KEY_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->peerKeys.encKey, zeros, + AES_256_KEY_SIZE) != 0); + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, WC_SHA256_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA256_DIGEST_SIZE) != 0); + + /* C2S and S2C enc keys must be independent. */ + AssertTrue(WMEMCMP(ssh->handshake->keys.encKey, + ssh->handshake->peerKeys.encKey, AES_128_KEY_SIZE) != 0); + + wolfSSH_free(ssh); + +#ifndef WOLFSSH_NO_AES_GCM + /* Sub-test B: aes128-cbc C2S (non-AEAD) / aes256-gcm S2C (AEAD) — client. + * keys.macKeySz must be set; peerKeys.macKeySz must be 0 (AEAD, no MAC). */ + 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 */ + "hmac-sha2-256", /* S2C MAC: skipped */ + 0, payload, (word32)sizeof(payload)); + /* DoKexInit's tail calls wolfSSH_SendPacket, which fails because no IO + * callback is set up. We only care about the handshake state set during parse. */ + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S hmac-sha1 MAC key (client: keys) must be generated. */ + AssertIntEQ(ssh->handshake->keys.macKeySz, WC_SHA_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA_DIGEST_SIZE) != 0); + + /* S2C AEAD (client: peerKeys): macKeySz==0, macKey stays all-zero. */ + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + wolfSSH_free(ssh); + + /* Sub-test C: aes256-gcm C2S (AEAD) / aes128-cbc S2C (non-AEAD) — mirror. + * Client: C2S→keys (local outgoing), S2C→peerKeys (peer outgoing). + * Verifies key 'E' skipped (keys.macKeySz==0) and key 'F' generated. */ + 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, + "aes256-gcm@openssh.com,aes128-cbc"), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, + "hmac-sha1,hmac-sha2-256"), WS_SUCCESS); + idx = 0; + payloadSz = BuildKexInitPayloadFull( + FPF_KEX_GOOD, FPF_KEY_GOOD, + "aes256-gcm@openssh.com", /* C2S enc: AEAD */ + "aes128-cbc", /* S2C enc: non-AEAD */ + "hmac-sha1", /* C2S MAC: skipped (AEAD) */ + "hmac-sha2-256", /* S2C MAC: negotiated */ + 0, payload, (word32)sizeof(payload)); + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S AEAD (client: keys): macKeySz==0, key 'E' skipped. */ + AssertIntEQ(ssh->handshake->keys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + /* S2C hmac-sha2-256 MAC key (client: peerKeys) must be generated. */ + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, WC_SHA256_DIGEST_SIZE); + AssertTrue(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA256_DIGEST_SIZE) != 0); + + wolfSSH_free(ssh); + + /* Sub-test D: aes256-gcm C2S (AEAD) / aes256-gcm S2C (AEAD) — symmetric. + * Both macKeySz==0; both key 'E' and key 'F' generation skipped. */ + 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, + "aes256-gcm@openssh.com"), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, "hmac-sha1"), WS_SUCCESS); + idx = 0; + payloadSz = BuildKexInitPayloadFull( + FPF_KEX_GOOD, FPF_KEY_GOOD, + "aes256-gcm@openssh.com", /* C2S enc: AEAD */ + "aes256-gcm@openssh.com", /* S2C enc: AEAD */ + "hmac-sha1", /* C2S MAC: skipped (AEAD) */ + "hmac-sha1", /* S2C MAC: skipped (AEAD) */ + 0, payload, (word32)sizeof(payload)); + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* C2S AEAD (client: keys): macKeySz==0, key 'E' skipped. */ + AssertIntEQ(ssh->handshake->keys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->keys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + /* S2C AEAD (client: peerKeys): macKeySz==0, key 'F' skipped. */ + AssertIntEQ(ssh->handshake->peerKeys.macKeySz, 0); + AssertIntEQ(WMEMCMP(ssh->handshake->peerKeys.macKey, zeros, + WC_SHA_DIGEST_SIZE), 0); + + wolfSSH_free(ssh); +#endif /* !WOLFSSH_NO_AES_GCM */ + + wolfSSH_CTX_free(ctx); +} +static void TestDoNewKeys(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + byte payload[512]; + word32 payloadSz; + word32 idx; + byte expectedPeerEncryptId; + byte expectedPeerMacId; + byte expectedPeerAeadMode; + + /* Sub-test A: aes128-cbc C2S / aes256-ctr S2C — non-AEAD both dirs. + * After DoNewKeys on the server, ssh->peer* must reflect the C2S (peer + * outgoing) direction, not the S2C (local outgoing) direction. */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + 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); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + /* Capture expected values before DoNewKeys frees handshake. */ + expectedPeerEncryptId = ssh->handshake->peerEncryptId; + expectedPeerMacId = ssh->handshake->peerMacId; + expectedPeerAeadMode = ssh->handshake->peerAeadMode; + AssertIntEQ(expectedPeerAeadMode, 0); /* non-AEAD C2S */ + + /* Peer has sent NewKeys; self has already sent its own (not keying). */ + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + AssertIntEQ(wolfSSH_TestDoNewKeys(ssh), WS_SUCCESS); + + /* handshake freed by DoNewKeys. */ + AssertTrue(ssh->handshake == NULL); + + /* ssh->peer* must reflect C2S direction, not S2C. */ + AssertIntEQ(ssh->peerEncryptId, expectedPeerEncryptId); + AssertIntEQ(ssh->peerMacId, expectedPeerMacId); + AssertIntEQ(ssh->peerAeadMode, 0); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + +#ifndef WOLFSSH_NO_AES_GCM + /* Sub-test B: aes256-gcm C2S (AEAD) / aes128-cbc S2C (non-AEAD). + * Verifies peerAeadMode==1 (C2S AEAD) rather than 0 (S2C non-AEAD), + * catching any regression back to handshake->aeadMode (S2C direction). */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + 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, + "aes256-gcm@openssh.com,aes128-cbc"), WS_SUCCESS); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, + "hmac-sha1,hmac-sha2-256"), WS_SUCCESS); + idx = 0; + payloadSz = BuildKexInitPayloadFull( + FPF_KEX_GOOD, FPF_KEY_GOOD, + "aes256-gcm@openssh.com", /* C2S enc: AEAD */ + "aes128-cbc", /* S2C enc: non-AEAD */ + "hmac-sha1", /* C2S MAC: skipped (AEAD) */ + "hmac-sha2-256", /* S2C MAC: negotiated */ + 0, payload, (word32)sizeof(payload)); + (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); + AssertNotNull(ssh->handshake); + + WMEMSET(ssh->k, 0xAA, WC_SHA256_DIGEST_SIZE); + ssh->kSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(ssh->h, 0xBB, WC_SHA256_DIGEST_SIZE); + ssh->hSz = WC_SHA256_DIGEST_SIZE; + WMEMCPY(ssh->sessionId, ssh->h, ssh->hSz); + ssh->sessionIdSz = ssh->hSz; + + AssertIntEQ(wolfSSH_TestGenerateKeys(ssh, ssh->handshake->kexHashId), WS_SUCCESS); + + expectedPeerEncryptId = ssh->handshake->peerEncryptId; + expectedPeerMacId = ssh->handshake->peerMacId; + expectedPeerAeadMode = ssh->handshake->peerAeadMode; + AssertIntEQ(expectedPeerAeadMode, 1); /* AEAD C2S */ + + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + AssertIntEQ(wolfSSH_TestDoNewKeys(ssh), WS_SUCCESS); + + AssertTrue(ssh->handshake == NULL); + + AssertIntEQ(ssh->peerEncryptId, expectedPeerEncryptId); + AssertIntEQ(ssh->peerMacId, expectedPeerMacId); + AssertIntEQ(ssh->peerAeadMode, 1); /* must be C2S AEAD, not S2C non-AEAD */ + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +#endif /* !WOLFSSH_NO_AES_GCM */ +} + +#endif /* AES_CBC + AES_CTR + HMAC guards */ + #endif /* first_packet_follows coverage guard */ @@ -2185,6 +3077,19 @@ 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(); + TestIndependentAlgoNegotiationClient(); + TestEncMismatch(); + TestMacMismatch(); + TestGenerateKeysSplit(); + TestGenerateKeysSplitClient(); + TestDoNewKeys(); #endif TestDisconnectSetsDisconnectError(); #if !(defined(WOLFSSH_NO_RSA) && defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 01ac5bd67..e45dba089 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -643,9 +643,14 @@ typedef struct HandshakeInfo { byte encryptId; byte macId; byte aeadMode; + byte peerEncryptId; + byte peerMacId; + byte peerAeadMode; byte blockSz; byte macSz; + byte peerBlockSz; + byte peerMacSz; word32 bannerLines; @@ -1345,6 +1350,9 @@ enum WS_MessageIdLimits { word32 len, word32* idx); WOLFSSH_API int wolfSSH_TestDoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); + WOLFSSH_API int wolfSSH_TestGenerateKeys(WOLFSSH* ssh, byte hashId); + WOLFSSH_API int wolfSSH_TestDoNewKeys(WOLFSSH* ssh); + WOLFSSH_API void wolfSSH_TestFreeHandshake(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_TestDoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); WOLFSSH_API int wolfSSH_TestDoKexDhReply(WOLFSSH* ssh, byte* buf,