-
Notifications
You must be signed in to change notification settings - Fork 114
Harden KEXINIT parsing #1055
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ejohnstown
wants to merge
2
commits into
wolfSSL:master
Choose a base branch
from
ejohnstown:kexinit-parse
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+97
−12
Open
Harden KEXINIT parsing #1055
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,6 +141,12 @@ static word32 AppendUint32(byte* buf, word32 bufSz, word32 idx, word32 value) | |
| return idx; | ||
| } | ||
|
|
||
| static word32 ReadUint32(const byte* buf) | ||
| { | ||
| return ((word32)buf[0] << 24) | ((word32)buf[1] << 16) | | ||
| ((word32)buf[2] << 8) | (word32)buf[3]; | ||
| } | ||
|
|
||
| static word32 AppendData(byte* buf, word32 bufSz, word32 idx, | ||
| const byte* data, word32 dataSz) | ||
| { | ||
|
|
@@ -431,12 +437,6 @@ typedef struct { | |
| word32 steps; | ||
| } KexReplyRunResult; | ||
|
|
||
| static word32 ReadUint32(const byte* buf) | ||
| { | ||
| return ((word32)buf[0] << 24) | ((word32)buf[1] << 16) | | ||
| ((word32)buf[2] << 8) | (word32)buf[3]; | ||
| } | ||
|
|
||
| static int ReadStringRef(word32* strSz, const byte** str, | ||
| const byte* buf, word32 len, word32* idx) | ||
| { | ||
|
|
@@ -2980,6 +2980,88 @@ static void TestKexInitNameListCaps(void) | |
| WFREE(list, NULL, DYNTYPE_STRING); | ||
| } | ||
|
|
||
| /* A peer name list may legally end with a trailing comma. GetNameListRaw | ||
| * used to fold that comma into the final name, yielding ID_UNKNOWN and a | ||
| * failed negotiation. Build a KEXINIT whose KEX list ends in a comma and | ||
| * assert the algorithm still negotiates. */ | ||
| static void TestKexInitTrailingComma(void) | ||
| { | ||
| WOLFSSH_CTX* ctx; | ||
| WOLFSSH* ssh; | ||
| byte payload[512]; | ||
| word32 payloadSz; | ||
| word32 idx = 0; | ||
|
|
||
| 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); | ||
|
|
||
| /* KEX list carries a trailing comma. */ | ||
| payloadSz = BuildKexInitPayload(ssh, FPF_KEX_GOOD ",", FPF_KEY_GOOD, | ||
| 0, payload, (word32)sizeof(payload)); | ||
|
|
||
| /* The tail (host key/send) errors on this bare ssh, but negotiation | ||
| * runs first and records the result in handshake->kexId. */ | ||
| (void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx); | ||
|
|
||
| AssertNotNull(ssh->handshake); | ||
| AssertIntEQ(ssh->handshake->kexId, ID_ECDH_SHA2_NISTP256); | ||
|
|
||
| wolfSSH_free(ssh); | ||
| wolfSSH_CTX_free(ctx); | ||
| } | ||
|
|
||
| /* The two KEXINIT language name-lists were skipped with an unchecked | ||
| * begin += skipSz, so a bogus length could wrap begin back into the payload | ||
| * and let a malformed packet parse as valid. GetSkip now bounds the declared | ||
| * length against the remaining payload; an overlong language length must be | ||
| * rejected. */ | ||
| static void TestKexInitLanguageLengthOverflow(void) | ||
| { | ||
| WOLFSSH_CTX* ctx; | ||
| WOLFSSH* ssh; | ||
| byte payload[512]; | ||
| word32 payloadSz; | ||
| word32 idx = 0; | ||
| word32 off; | ||
| int i; | ||
|
|
||
| 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); | ||
|
|
||
| payloadSz = BuildKexInitPayload(ssh, FPF_KEX_GOOD, FPF_KEY_GOOD, | ||
| 0, payload, (word32)sizeof(payload)); | ||
|
|
||
| /* Walk past the cookie and the eight algorithm name-lists to reach the | ||
| * first (client-to-server) language length field. */ | ||
| off = COOKIE_SZ; | ||
| for (i = 0; i < 8; i++) | ||
| off += UINT32_SZ + ReadUint32(payload + off); | ||
|
|
||
| /* 0xFFFFFFFC = 2^32 - 4. After reading this 4-byte length the parser is | ||
| * 4 bytes past 'off', so the old unchecked begin += skipSz wraps begin | ||
| * back to 'off' itself -- in bounds, not past the buffer. The buggy | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove emdash? comment seems a little too long maybe reduce the size |
||
| * parser then keeps going from the wrong offset and reads a non-zero | ||
| * reserved field, returning WS_PARSE_E. The checked GetSkip instead | ||
| * rejects the overlong length outright with WS_BUFFER_E, which is what | ||
| * locks the fix: the pre-fix code returns a different (non-buffer) | ||
| * error here. */ | ||
| (void)AppendUint32(payload, (word32)sizeof(payload), off, 0xFFFFFFFCu); | ||
|
|
||
| AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx), | ||
| WS_BUFFER_E); | ||
|
|
||
| wolfSSH_free(ssh); | ||
| wolfSSH_CTX_free(ctx); | ||
| } | ||
|
|
||
| #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) | ||
|
|
@@ -4391,6 +4473,8 @@ int main(int argc, char** argv) | |
| TestFirstPacketFollows(); | ||
| TestKexInitReservedNonZeroRejected(); | ||
| TestKexInitNameListCaps(); | ||
| TestKexInitTrailingComma(); | ||
| TestKexInitLanguageLengthOverflow(); | ||
| TestDoKexInitRejectsWhenPeerIsKeying(); | ||
| #endif | ||
| #if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) && !defined(WOLFSSH_NO_RSA) \ | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 [Low] Second KEXINIT language skip (server-to-client) has no independent regression coverage · test
The PR hardens both language name-list skips in DoKexInit: the client-to-server list at src/internal.c:4730 and the server-to-client list at src/internal.c:4736, both now bounded via GetSkip. The new regression test only corrupts the first (client-to-server) language length: it walks past the cookie and the eight algorithm name-lists to reach 'off', overwrites that C2S length with 0xFFFFFFFC, and asserts WS_BUFFER_E. Because parsing fails at the C2S list, execution never reaches the S2C GetSkip at internal.c:4736, so that changed branch is never exercised. If a future refactor reverted only the S2C skip back to the unchecked begin += skipSz, this test would still pass. The two call sites are identical, so this is a coverage gap rather than a current correctness bug.
Fix: Add a mirrored case (or parameterize the test over the two language fields) that advances past the first language name-list and corrupts the second (server-to-client) language length, confirming it is also rejected with WS_BUFFER_E so both changed branches at internal.c:4730 and internal.c:4736 are independently locked.