Add independent ciper and MAC algorithms negotiation for each direction#952
Add independent ciper and MAC algorithms negotiation for each direction#952yosuke-wolfssl wants to merge 1 commit intowolfSSL:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds RFC 4253 §7.1-compliant, per-direction cipher/MAC negotiation by tracking and applying independent peer vs local (C2S vs S2C) algorithm selections during KEXINIT/NEWKEYS processing, with a new regression test covering mixed-direction negotiation scenarios.
Changes:
- Extend
HandshakeInfoto store peer (opposite direction) cipher/MAC/AEAD and sizes independently. - Update
DoKexInit()to negotiate cipher/MAC independently per direction and populate the new handshake fields. - Update
DoNewKeys()to copy negotiated peer-direction algorithm selections from the handshake into the session, and add a regression test validating negotiation outcomes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
wolfssh/internal.h |
Adds per-direction (peer) negotiated cipher/MAC/AEAD fields to HandshakeInfo. |
src/internal.c |
Implements independent per-direction algorithm matching and propagates peer settings on NEWKEYS. |
tests/regress.c |
Adds a regression test ensuring negotiation differs per direction and handles AEAD vs non-AEAD correctly. |
Comments suppressed due to low confidence (1)
src/internal.c:4486
- The KEXINIT parsing now duplicates the same 'convert configured algo name-list into ID list' pattern multiple times (cipher S2C, MAC C2S, MAC S2C). Consider extracting a small helper to build
cannedList/cannedListSzfromssh->algoListCipher/ssh->algoListMacto reduce repetition and the chance of future divergence between the per-direction negotiation blocks.
if (ret == WS_SUCCESS && !ssh->handshake->peerAeadMode) {
cannedAlgoNamesSz = AlgoListSz(ssh->algoListMac);
cannedListSz = (word32)sizeof(cannedList);
ret = GetNameListRaw(cannedList, &cannedListSz,
(const byte*)ssh->algoListMac, cannedAlgoNamesSz);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
5cc90fa to
47c9e57
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #952
Scan targets checked: wolfssh-bugs, wolfssh-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
aidangarske
left a comment
There was a problem hiding this comment.
Skoll Code Review
Scan type: review-security
Overall recommendation: REQUEST_CHANGES
Findings: 3 total — 3 posted, 0 skipped
3 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [High] GenerateKeys skips both-direction MAC key derivation when only one direction is AEAD —
src/internal.c:2696-2709 - [Low] Test coverage asserts DoKexInit parse state only, not key derivation / data path for split-AEAD —
tests/regress.c:2142-2210 - [Info] HandshakeInfoNew skips peerMacSz/peerAeadMode initialization (safe due to WMEMSET but easy to miss) —
src/internal.c:572-590
Review generated by Skoll
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #952
Scan targets checked: wolfssh-bugs, wolfssh-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #952
Scan targets checked: wolfssh-bugs, wolfssh-src
No new issues found in the changed files. ✅
|
Hello @aidangarske , I fixed the issues you mentioned. |
aidangarske
left a comment
There was a problem hiding this comment.
Skoll Multi-Scan Review
Modes: review + review-security + audit
Overall recommendation: COMMENT
Findings: 4 total — 4 posted, 0 skipped
4 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [Medium] [audit] DoKexInit client-side branch (WOLFSSH_ENDPOINT_CLIENT alias mapping) lacks a direct asymmetric-algo test —
src/internal.c:4313-4326 - [Low] [review+review-security] Asymmetric AEAD reset between C2S and S2C branches in DoKexInit —
src/internal.c:4427-4478 - [Low] [review-security] Pointer aliases used after first ret==WS_SUCCESS block may trip strict-uninit warnings —
src/internal.c:4253-4326,4427-4527 - [Info] [review-security] Removed redundant ID_NONE/MSGID_NONE initializations in HandshakeInfoNew rely on enum values being 0 —
src/internal.c:572-576
Review generated by Skoll
…on, And add regress test
|
Hi @aidangarske , Sorry to take your time. |
aidangarske
left a comment
There was a problem hiding this comment.
Skoll Multi-Scan Review
Modes: audit + review + review-security
Overall recommendation: COMMENT
Findings: 7 total — 7 posted, 0 skipped
6 finding(s) posted as inline comments (see file-level comments below)
Posted findings
- [Medium] [audit] Asymmetric S2C-only encryption-algo mismatch error path is untested —
src/internal.c:4448-4466 - [Medium] [audit] Asymmetric S2C-only MAC-algo mismatch error path is untested —
src/internal.c:4510-4533 - [Medium] [review] TestIndependentAlgoNegotiation tests do not verify all ID/Sz fields are reset on each ssh instance —
tests/regress.c:2161-2332 - [Low] [audit] wolfSSH_TestGenerateKeys NULL-argument validation is untested —
src/internal.c:17962-17967 - [Low] [review] Missing blank line between TestGenerateKeysSplit and TestGenerateKeysSplitClient —
tests/regress.c:2446-2447 - [Low] [review] New tests silently ignore wolfSSH_TestDoKexInit return without explanation —
tests/regress.c:2191,2227,2278,2313,2366,2420,2479,2532 - [Low] [review] DoKexInit reads side from ssh->ctx->side a second time after caching it in
side—src/internal.c:4622,4642,4666,4673
Review generated by Skoll
| } | ||
| } | ||
|
|
||
| /* Enc Algorithms - Server to Client */ |
There was a problem hiding this comment.
🟠 [Medium] Asymmetric S2C-only encryption-algo mismatch error path is untested · path
Pre-PR, the S2C encryption algorithm was matched against the C2S-chosen algorithm only (MatchIdLists(side, list, listSz, &algoId, 1)). Post-PR, S2C is matched independently against ssh->algoListCipher via cannedList. This introduces a NEW failure mode: C2S successfully matches but S2C does not (or vice versa) — yielding WS_MATCH_ENC_ALGO_E from the S2C block. The new tests in TestIndependentAlgoNegotiation/...Client only cover the success paths for asymmetric cipher selection; no test exercises asymmetric mismatch where one direction agrees and the other fails. Grep confirms WS_MATCH_ENC_ALGO_E has zero references in tests/.
Fix: Add a regress.c test that builds a kex_init payload where the C2S cipher list contains an algorithm in the local algoListCipher but the S2C cipher list does not (e.g., local accepts aes128-cbc,aes256-ctr; payload offers C2S=aes128-cbc, S2C=3des-cbc). Verify wolfSSH_TestDoKexInit returns WS_MATCH_ENC_ALGO_E. Mirror the test for the inverse case (C2S fails, S2C succeeds).
| s2cKeys->macKeySz = KeySzForId(algoId); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🟠 [Medium] Asymmetric S2C-only MAC-algo mismatch error path is untested · path
The S2C MAC negotiation was rewritten from MatchIdLists(side, list, listSz, &algoId, 1) (which forced S2C to equal C2S) to an independent match against ssh->algoListMac. The new failure path WS_MATCH_MAC_ALGO_E can now trigger when C2S MAC matches but S2C MAC does not (and vice versa for the C2S branch at lines 4484-4508). Neither branch's WS_MATCH_MAC_ALGO_E return is exercised by any test. Grep across the repo confirms no test references WS_MATCH_MAC_ALGO_E.
Fix: Add a regress.c test that supplies asymmetric MAC lists where one direction shares no entries with ssh->algoListMac (e.g., local accepts hmac-sha1,hmac-sha2-256; payload offers C2S=hmac-sha1, S2C=hmac-md5). Assert wolfSSH_TestDoKexInit returns WS_MATCH_MAC_ALGO_E. Cover the inverse C2S-mismatch case as well.
| TestFirstPacketFollowsSkipped(); | ||
| } | ||
|
|
||
| #if !defined(WOLFSSH_NO_AES_CBC) && !defined(WOLFSSH_NO_AES_CTR) \ |
There was a problem hiding this comment.
🟠 [Medium] TestIndependentAlgoNegotiation tests do not verify all ID/Sz fields are reset on each ssh instance · test
The new tests only assert the split fields (peerEncryptId/peerMacId/peerAeadMode/peerBlockSz/peerMacSz) for the side they care about, but never assert peerBlockSz/peerMacSz directly. Given that the diff intentionally drops the explicit ID_NONE/MSGID_NONE initializers in HandshakeInfoNew (relying on the WMEMSET zero plus the assumption that ID_NONE == 0), an asymmetric subtle regression — e.g. peerBlockSz left at its MIN_BLOCK_SZ default in a code path that should overwrite it — would not be caught. A few additional AssertIntEQ lines for peerBlockSz, peerMacSz, blockSz, macSz after each negotiation would harden the tests against future refactors of HandshakeInfoNew or DoKexInit.
Fix: Add explicit assertions for the new peerBlockSz/peerMacSz fields (and for blockSz/macSz for symmetry) in both server and client variants.
| return DoKexInit(ssh, buf, len, idx); | ||
| } | ||
|
|
||
| int wolfSSH_TestGenerateKeys(WOLFSSH* ssh) |
There was a problem hiding this comment.
🔵 [Low] wolfSSH_TestGenerateKeys NULL-argument validation is untested · test
The newly added test helper validates ssh == NULL || ssh->handshake == NULL and returns WS_BAD_ARGUMENT, but no test in tests/regress.c (or anywhere else) calls it with NULL inputs to verify the guard. All four new test sites (TestGenerateKeysSplit, TestGenerateKeysSplitClient sub-tests A and B) invoke it only after AssertNotNull(ssh->handshake). While the function is test-only infrastructure, a sibling invariant check is cheap to add.
Fix: Add a one-line assertion in regress.c (e.g., inside or adjacent to TestGenerateKeysSplit): AssertIntEQ(wolfSSH_TestGenerateKeys(NULL), WS_BAD_ARGUMENT); and a second call with a freshly-created WOLFSSH* whose handshake has been freed/nulled to verify the second branch.
| #endif /* !WOLFSSH_NO_AES_GCM */ | ||
|
|
||
| wolfSSH_CTX_free(ctx); | ||
| } |
There was a problem hiding this comment.
🔵 [Low] Missing blank line between TestGenerateKeysSplit and TestGenerateKeysSplitClient · style
Every other top-level function in regress.c (including the other three new test functions added by this PR) is separated by a blank line. The closing brace of TestGenerateKeysSplit is followed immediately by the next function definition, which breaks consistency.
Fix: Add a blank line between the two functions to match the surrounding style.
| "hmac-sha1", /* C2S MAC */ | ||
| "hmac-sha2-256", /* S2C MAC */ | ||
| 0, payload, (word32)sizeof(payload)); | ||
| (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); |
There was a problem hiding this comment.
🔵 [Low] New tests silently ignore wolfSSH_TestDoKexInit return without explanation · test
All four new tests call (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); and then assert handshake state. The pre-existing RunFirstPacketFollowsCase (line 2088) has an explanatory comment about why the return is ignored (the tail of DoKexInit calls SendKexInit which fails for a stripped-down server with no keys). The four new test functions reuse this pattern but do not carry a similar explanation, so a future reader may mistakenly think the discarded return is a bug. For the client-side variants the rationale is even less obvious, since the privateKeyCount check does not apply to clients — the SendKexInit failure there comes from wolfSSH_SendPacket having no IO callback set up.
Fix: Add a brief comment in each new test (or factor a helper that wraps the call) explaining why the return value is intentionally discarded.
This PR adds independent ciper and MAC algorithms negotiation for each direction to comply with RFC 4253 section 7.1.
Also, new regress test for the case is added as well.