Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -19901,6 +19901,12 @@ int wolfSSH_TestDoReceive(WOLFSSH* ssh)
return DoReceive(ssh);
}

int wolfSSH_TestBuildNameList(char* buf, word32 bufSz,
const byte* src, word32 srcSz)
{
return BuildNameList(buf, bufSz, src, srcSz);
}

int wolfSSH_TestDoUserAuthBanner(WOLFSSH* ssh, byte* buf, word32 len,
word32* idx)
{
Expand Down
43 changes: 43 additions & 0 deletions tests/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -2724,6 +2724,47 @@ static void test_wolfSSH_SFTP_SetDefaultPath(void) { ; }
#endif /* WOLFSSH_SFTP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED */


#ifdef WOLFSSH_TEST_INTERNAL
static void test_wolfSSH_BuildNameList(void)
{
/* Fixed, valid IDs; oracle = comma-joined IdToName() per RFC 4253 7.1 */
const byte ids[] = { ID_ECDH_SHA2_NISTP256, ID_AES128_CTR,
ID_HMAC_SHA2_256 };
const word32 n = (word32)(sizeof(ids) / sizeof(ids[0]));
char expected[256];
char buf[256];
word32 i;
int sizeMode, writeMode;
size_t expLen;

expected[0] = '\0';
for (i = 0; i < n; i++) {
if (i > 0)
strcat(expected, ",");
strcat(expected, IdToName(ids[i])); /* independent oracle */
}
expLen = strlen(expected);

sizeMode = wolfSSH_TestBuildNameList(NULL, 0, ids, n);
writeMode = wolfSSH_TestBuildNameList(buf, sizeof(buf), ids, n);

AssertIntEQ(sizeMode, (int)expLen); /* size mode == oracle */
AssertIntEQ(writeMode, (int)expLen); /* write mode == oracle */
AssertIntEQ(strcmp(buf, expected), 0); /* content == oracle */
AssertIntNE(buf[writeMode - 1], ','); /* no trailing comma */
AssertIntNE(buf[writeMode - 1], '\0'); /* last char is a name */

/* Undersized buffer must be rejected with a negative error, not
* overflow. BuildNameList sets idx = WS_BUFFER_E then returns idx - 1,
* so the exact code is implementation detail; the safety property is
* that the call fails rather than writing past the buffer. */
AssertIntLT(wolfSSH_TestBuildNameList(buf, 1, ids, n), 0);
}
#else
static void test_wolfSSH_BuildNameList(void) { ; }
#endif


#if defined(WOLFSSH_SCP) && !defined(NO_WOLFSSH_CLIENT) && \
!defined(SINGLE_THREADED) && !defined(NO_FILESYSTEM) && \
!defined(WOLFSSH_SCP_USER_CALLBACKS) && !defined(WOLFSSH_ZEPHYR)
Expand Down Expand Up @@ -3867,6 +3908,8 @@ int wolfSSH_ApiTest(int argc, char** argv)
test_wolfSSH_SCP_ReKey_ToServer();
test_wolfSSH_SCP_ReKey_ToServer_NonBlock();

test_wolfSSH_BuildNameList();

/* SFTP tests */
test_wolfSSH_SFTP_SendReadPacket();
test_wolfSSH_SFTP_PartialSend();
Expand Down
129 changes: 129 additions & 0 deletions tests/unit.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include <wolfssl/wolfcrypt/random.h>
#include <wolfssl/wolfcrypt/integer.h>
#include <wolfssl/wolfcrypt/hmac.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#ifndef WOLFSSH_NO_RSA
#include <wolfssl/wolfcrypt/rsa.h>
#include <wolfssl/wolfcrypt/asn.h>
Expand Down Expand Up @@ -1016,6 +1017,127 @@ static int test_DoReceive_VerifyMacFailure(void)
#endif /* WOLFSSH_TEST_INTERNAL && any HMAC SHA variant enabled */


#if defined(WOLFSSH_TEST_INTERNAL) && !defined(WOLFSSH_NO_AES_GCM)
/* Verify DoReceive rejects a tampered AES-256-GCM packet, returning
* WS_FATAL_ERROR with ssh->error == AES_GCM_AUTH_E. The valid ciphertext and
* tag are produced by a SEPARATE Aes context (independent oracle), never
* ssh->decryptCipher, so the test is not self-referential. One ciphertext byte
* is flipped before delivery; the AEAD auth check must reject it. */
static int test_DoReceive_AeadTamperFailure(void)
{
WOLFSSH_CTX* ctx = NULL;
WOLFSSH* ssh = NULL;
int ret;
int result = 0;
int encReady = 0;
Aes enc;
byte key[AES_256_KEY_SIZE];
byte iv[AEAD_NONCE_SZ];
byte pt[16];
byte ct[16];
byte tag[AES_BLOCK_SIZE];
byte lenField[UINT32_SZ];
byte pkt[UINT32_SZ + 16 + AES_BLOCK_SIZE];
word32 curSz = 16;
word32 totalLen = UINT32_SZ + 16 + AES_BLOCK_SIZE;

ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
if (ctx == NULL)
return -300;
ssh = wolfSSH_new(ctx);
if (ssh == NULL) {
wolfSSH_CTX_free(ctx);
return -301;
}

WMEMSET(key, 0xA5, sizeof(key));
WMEMSET(iv, 0x5A, sizeof(iv));

/* Plaintext = MSG_IGNORE carrying an empty string. Contents past what
* triggers decrypt are irrelevant: the auth reject fires before DoPacket. */
WMEMSET(pt, 0, sizeof(pt));
pt[0] = 4; /* padding_length */
pt[1] = MSGID_IGNORE; /* pt[2..5] = uint32 string len 0; rest padding */

/* AAD = 4-byte big-endian packet_length field (== curSz). */
lenField[0] = (byte)(curSz >> 24);
lenField[1] = (byte)(curSz >> 16);
lenField[2] = (byte)(curSz >> 8);
lenField[3] = (byte)(curSz);

/* Independent oracle: encrypt with a SEPARATE Aes context. */
ret = wc_AesInit(&enc, ssh->ctx->heap, INVALID_DEVID);
if (ret != 0) {
result = -302;
goto done;
}
encReady = 1;
ret = wc_AesGcmSetKey(&enc, key, sizeof(key));
if (ret == 0)
ret = wc_AesGcmEncrypt(&enc, ct, pt, curSz, iv, sizeof(iv),
tag, sizeof(tag), lenField, UINT32_SZ);
if (ret != 0) {
result = -303;
goto done;
}

/* Set up ssh receive state to match DecryptAead's expectations. */
ssh->peerEncryptId = ID_AES256_GCM;
ssh->peerAeadMode = 1;
ssh->peerBlockSz = AES_BLOCK_SIZE;
ssh->peerMacSz = AES_BLOCK_SIZE; /* GCM tag size, per SetPeerAlgoIds */
WMEMCPY(ssh->peerKeys.iv, iv, sizeof(iv));
ssh->peerKeys.ivSz = AEAD_NONCE_SZ;
ret = wc_AesInit(&ssh->decryptCipher.aes, ssh->ctx->heap, INVALID_DEVID);
if (ret == 0)
ret = wc_AesGcmSetKey(&ssh->decryptCipher.aes, key, sizeof(key));
if (ret != 0) {
result = -304;
goto done;
}
ssh->decryptCipher.cipherType = ID_AES256_GCM;
ssh->decryptCipher.isInit = 1; /* let wolfSSH_free clean it up */
ssh->peerSeq = 0;
ssh->curSz = 0;
ssh->processReplyState = PROCESS_INIT;
ssh->error = 0;

/* Assemble wire packet [lenField][ct][tag], then tamper one byte. */
WMEMCPY(pkt, lenField, UINT32_SZ);
WMEMCPY(pkt + UINT32_SZ, ct, curSz);
WMEMCPY(pkt + UINT32_SZ + curSz, tag, sizeof(tag));
pkt[UINT32_SZ] ^= 0x01; /* flip a ciphertext byte */

ShrinkBuffer(&ssh->inputBuffer, 1);
ret = GrowBuffer(&ssh->inputBuffer, totalLen);
if (ret != WS_SUCCESS) {
result = -305;
goto done;
}
WMEMCPY(ssh->inputBuffer.buffer, pkt, totalLen);
ssh->inputBuffer.length = totalLen;
ssh->inputBuffer.idx = 0;

ret = wolfSSH_TestDoReceive(ssh);
if (ret != WS_FATAL_ERROR) {
result = -306;
goto done;
}
if (ssh->error != AES_GCM_AUTH_E) {
result = -307;
goto done;
}

done:
if (encReady)
wc_AesFree(&enc);
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
return result;
}
#endif /* WOLFSSH_TEST_INTERNAL && !WOLFSSH_NO_AES_GCM */


#ifdef WOLFSSH_TEST_INTERNAL
/* Verify DoReceive rejects a binary packet whose padding_length is below the
* RFC 4253 section 6 minimum of four bytes, returning WS_BUFFER_E. The packet
Expand Down Expand Up @@ -7639,6 +7761,13 @@ int wolfSSH_UnitTest(int argc, char** argv)
testResult = testResult || unitResult;
#endif

#if defined(WOLFSSH_TEST_INTERNAL) && !defined(WOLFSSH_NO_AES_GCM)
unitResult = test_DoReceive_AeadTamperFailure();
printf("DoReceiveAeadTamper: %s\n",
(unitResult == 0 ? "SUCCESS" : "FAILED"));
testResult = testResult || unitResult;
#endif

#ifdef WOLFSSH_TEST_INTERNAL
unitResult = test_DoReceive_RejectsShortPadding();
printf("DoReceiveRejectsShortPadding: %s\n",
Expand Down
2 changes: 2 additions & 0 deletions wolfssh/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,8 @@ enum WS_MessageIdLimits {
WOLFSSH_API int wolfSSH_TestScpExtractFileName(const char* filePath,
char* fileName, word32 fileNameSz);
#endif /* WOLFSSH_SCP && !WOLFSSH_SCP_USER_CALLBACKS */
WOLFSSH_API int wolfSSH_TestBuildNameList(char* buf, word32 bufSz,
const byte* src, word32 srcSz);
#endif /* WOLFSSH_TEST_INTERNAL */

/* dynamic memory types */
Expand Down
Loading