mirror of https://github.com/wolfSSL/wolfssh.git
4368 lines
152 KiB
C
4368 lines
152 KiB
C
/* regress.c
|
|
*
|
|
* Regression coverage for message ordering / keying state handling.
|
|
*
|
|
* Copyright (C) 2014-2026 wolfSSL Inc.
|
|
*
|
|
* This file is part of wolfSSH.
|
|
*
|
|
* wolfSSH is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* wolfSSH is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with wolfSSH. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#ifdef WOLFSSL_USER_SETTINGS
|
|
#include <wolfssl/wolfcrypt/settings.h>
|
|
#else
|
|
#include <wolfssl/options.h>
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <arpa/inet.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <wolfssh/port.h>
|
|
#include <wolfssh/ssh.h>
|
|
#include <wolfssh/internal.h>
|
|
#ifdef WOLFSSH_SFTP
|
|
#include <wolfssh/wolfsftp.h>
|
|
#endif
|
|
#include "apps/wolfssh/common.h"
|
|
|
|
#ifndef WOLFSSH_NO_ABORT
|
|
#define WABORT() abort()
|
|
#else
|
|
#define WABORT()
|
|
#endif
|
|
|
|
#define PrintError(description, result) do { \
|
|
printf("\nERROR - %s line %d failed with:", __FILE__, __LINE__); \
|
|
printf("\n expected: "); printf description; \
|
|
printf("\n result: "); printf result; printf("\n\n"); \
|
|
} while(0)
|
|
|
|
#define Fail(description, result) do { \
|
|
PrintError(description, result); \
|
|
WABORT(); \
|
|
} while(0)
|
|
|
|
#define Assert(test, description, result) if (!(test)) Fail(description, result)
|
|
|
|
#define AssertTrue(x) Assert((x), ("%s is true", #x), (#x " => FALSE"))
|
|
#define AssertFalse(x) Assert(!(x), ("%s is false", #x), (#x " => TRUE"))
|
|
#define AssertNotNull(x) Assert((x), ("%s is not null", #x), (#x " => NULL"))
|
|
#define AssertIntEQ(x, y) do { int _x = (int)(x); int _y = (int)(y); \
|
|
Assert(_x == _y, ("%s == %s", #x, #y), ("%d != %d", _x, _y)); } while (0)
|
|
|
|
|
|
static void ResetSession(WOLFSSH* ssh)
|
|
{
|
|
if (ssh->handshake != NULL) {
|
|
WFREE(ssh->handshake, ssh->ctx->heap, DYNTYPE_HS);
|
|
ssh->handshake = NULL;
|
|
}
|
|
ssh->isKeying = 0;
|
|
ssh->connectState = CONNECT_BEGIN;
|
|
ssh->error = 0;
|
|
}
|
|
|
|
|
|
static HandshakeInfo* AllocHandshake(WOLFSSH* ssh)
|
|
{
|
|
HandshakeInfo* hs;
|
|
|
|
hs = (HandshakeInfo*)WMALLOC(sizeof(HandshakeInfo), ssh->ctx->heap,
|
|
DYNTYPE_HS);
|
|
AssertNotNull(hs);
|
|
WMEMSET(hs, 0, sizeof(HandshakeInfo));
|
|
hs->blockSz = MIN_BLOCK_SZ;
|
|
hs->eSz = (word32)sizeof(hs->e);
|
|
hs->xSz = (word32)sizeof(hs->x);
|
|
|
|
return hs;
|
|
}
|
|
|
|
/* Build a minimal SSH binary packet carrying only a message ID.
|
|
* Layout: uint32 packetLen, byte padLen, payload[msgId], pad[padLen].
|
|
* Choose padLen so total is 8-byte aligned for the clear transport case. */
|
|
static word32 BuildPacket(byte msgId, byte* out, word32 outSz)
|
|
{
|
|
byte padLen = 6; /* 1 (msgId) +1 (padLen) +6 = 8 */
|
|
word32 packetLen = 1 + 1 + padLen; /* payload + padLen field + pad */
|
|
word32 need = 4 + packetLen;
|
|
|
|
AssertTrue(outSz >= need);
|
|
out[0] = (byte)(packetLen >> 24);
|
|
out[1] = (byte)(packetLen >> 16);
|
|
out[2] = (byte)(packetLen >> 8);
|
|
out[3] = (byte)(packetLen);
|
|
out[4] = padLen;
|
|
out[5] = msgId;
|
|
WMEMSET(out + 6, 0, padLen);
|
|
return need;
|
|
}
|
|
|
|
static byte ParseMsgId(const byte* pkt, word32 sz)
|
|
{
|
|
AssertTrue(sz >= 6);
|
|
return pkt[5];
|
|
}
|
|
|
|
static word32 AppendByte(byte* buf, word32 bufSz, word32 idx, byte value)
|
|
{
|
|
AssertTrue(idx < bufSz);
|
|
buf[idx++] = value;
|
|
return idx;
|
|
}
|
|
|
|
static word32 AppendUint32(byte* buf, word32 bufSz, word32 idx, word32 value)
|
|
{
|
|
word32 netValue = htonl(value);
|
|
|
|
AssertTrue(idx + UINT32_SZ <= bufSz);
|
|
WMEMCPY(buf + idx, &netValue, UINT32_SZ);
|
|
idx += UINT32_SZ;
|
|
return idx;
|
|
}
|
|
|
|
static word32 AppendData(byte* buf, word32 bufSz, word32 idx,
|
|
const byte* data, word32 dataSz)
|
|
{
|
|
AssertTrue(idx + dataSz <= bufSz);
|
|
if (dataSz > 0) {
|
|
WMEMCPY(buf + idx, data, dataSz);
|
|
idx += dataSz;
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
static word32 AppendString(byte* buf, word32 bufSz, word32 idx,
|
|
const char* value)
|
|
{
|
|
word32 valueSz = (word32)WSTRLEN(value);
|
|
|
|
idx = AppendUint32(buf, bufSz, idx, valueSz);
|
|
return AppendData(buf, bufSz, idx, (const byte*)value, valueSz);
|
|
}
|
|
|
|
static word32 WrapPacket(byte msgId, const byte* payload, word32 payloadSz,
|
|
byte* out, word32 outSz)
|
|
{
|
|
word32 idx = 0;
|
|
word32 packetLen;
|
|
word32 need;
|
|
byte padLen = MIN_PAD_LENGTH;
|
|
|
|
while (((UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ + payloadSz + padLen) %
|
|
MIN_BLOCK_SZ) != 0) {
|
|
padLen++;
|
|
}
|
|
|
|
packetLen = PAD_LENGTH_SZ + MSG_ID_SZ + payloadSz + padLen;
|
|
need = UINT32_SZ + packetLen;
|
|
|
|
AssertTrue(outSz >= need);
|
|
|
|
idx = AppendUint32(out, outSz, idx, packetLen);
|
|
idx = AppendByte(out, outSz, idx, padLen);
|
|
idx = AppendByte(out, outSz, idx, msgId);
|
|
idx = AppendData(out, outSz, idx, payload, payloadSz);
|
|
AssertTrue(idx + padLen <= outSz);
|
|
WMEMSET(out + idx, 0, padLen);
|
|
idx += padLen;
|
|
|
|
return idx;
|
|
}
|
|
|
|
static word32 BuildChannelOpenPacket(const char* type, word32 peerChannelId,
|
|
word32 peerInitialWindowSz, word32 peerMaxPacketSz,
|
|
const byte* extra, word32 extraSz, byte* out, word32 outSz)
|
|
{
|
|
byte payload[256];
|
|
word32 idx = 0;
|
|
|
|
idx = AppendString(payload, sizeof(payload), idx, type);
|
|
idx = AppendUint32(payload, sizeof(payload), idx, peerChannelId);
|
|
idx = AppendUint32(payload, sizeof(payload), idx, peerInitialWindowSz);
|
|
idx = AppendUint32(payload, sizeof(payload), idx, peerMaxPacketSz);
|
|
idx = AppendData(payload, sizeof(payload), idx, extra, extraSz);
|
|
|
|
return WrapPacket(MSGID_CHANNEL_OPEN, payload, idx, out, outSz);
|
|
}
|
|
|
|
static word32 BuildDisconnectPacket(word32 reason, byte* out, word32 outSz)
|
|
{
|
|
byte payload[64];
|
|
word32 idx = 0;
|
|
|
|
idx = AppendUint32(payload, sizeof(payload), idx, reason);
|
|
idx = AppendUint32(payload, sizeof(payload), idx, 0);
|
|
idx = AppendUint32(payload, sizeof(payload), idx, 0);
|
|
|
|
return WrapPacket(MSGID_DISCONNECT, payload, idx, out, outSz);
|
|
}
|
|
|
|
#ifdef WOLFSSH_FWD
|
|
static word32 BuildDirectTcpipExtra(const char* host, word32 hostPort,
|
|
const char* origin, word32 originPort, byte* out, word32 outSz)
|
|
{
|
|
word32 idx = 0;
|
|
|
|
idx = AppendString(out, outSz, idx, host);
|
|
idx = AppendUint32(out, outSz, idx, hostPort);
|
|
idx = AppendString(out, outSz, idx, origin);
|
|
idx = AppendUint32(out, outSz, idx, originPort);
|
|
|
|
return idx;
|
|
}
|
|
|
|
static word32 BuildGlobalRequestFwdPacket(const char* bindAddr, word32 bindPort,
|
|
int isCancel, byte wantReply, byte* out, word32 outSz)
|
|
{
|
|
byte payload[256];
|
|
word32 idx = 0;
|
|
const char* reqName = isCancel ? "cancel-tcpip-forward" : "tcpip-forward";
|
|
|
|
idx = AppendString(payload, sizeof(payload), idx, reqName);
|
|
idx = AppendByte (payload, sizeof(payload), idx, wantReply);
|
|
idx = AppendString(payload, sizeof(payload), idx, bindAddr);
|
|
idx = AppendUint32(payload, sizeof(payload), idx, bindPort);
|
|
|
|
return WrapPacket(MSGID_GLOBAL_REQUEST, payload, idx, out, outSz);
|
|
}
|
|
#endif
|
|
|
|
/* Simple in-memory transport harness */
|
|
typedef struct {
|
|
byte* in; /* data to feed into client */
|
|
word32 inSz;
|
|
word32 inOff;
|
|
byte* out; /* data written by client */
|
|
word32 outSz;
|
|
word32 outCap;
|
|
} MemIo;
|
|
|
|
static int MemRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
(void)ssh;
|
|
MemIo* io = (MemIo*)ctx;
|
|
word32 remain = io->inSz - io->inOff;
|
|
if (remain == 0)
|
|
return WS_CBIO_ERR_WANT_READ;
|
|
if (sz > remain)
|
|
sz = remain;
|
|
WMEMCPY(buf, io->in + io->inOff, sz);
|
|
io->inOff += sz;
|
|
return (int)sz;
|
|
}
|
|
|
|
static int MemSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
(void)ssh;
|
|
MemIo* io = (MemIo*)ctx;
|
|
if (io->outSz + sz > io->outCap) {
|
|
return WS_CBIO_ERR_GENERAL;
|
|
}
|
|
WMEMCPY(io->out + io->outSz, buf, sz);
|
|
io->outSz += sz;
|
|
return (int)sz;
|
|
}
|
|
|
|
static void MemIoInit(MemIo* io, byte* in, word32 inSz, byte* out, word32 outCap)
|
|
{
|
|
io->in = in;
|
|
io->inSz = inSz;
|
|
io->inOff = 0;
|
|
io->out = out;
|
|
io->outSz = 0;
|
|
io->outCap = outCap;
|
|
}
|
|
|
|
typedef struct {
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
MemIo io;
|
|
byte out[256];
|
|
} ChannelOpenHarness;
|
|
|
|
static void InitChannelOpenHarness(ChannelOpenHarness* harness,
|
|
byte* in, word32 inSz)
|
|
{
|
|
WMEMSET(harness, 0, sizeof(*harness));
|
|
|
|
harness->ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
|
|
AssertNotNull(harness->ctx);
|
|
|
|
wolfSSH_SetIORecv(harness->ctx, MemRecv);
|
|
wolfSSH_SetIOSend(harness->ctx, MemSend);
|
|
|
|
harness->ssh = wolfSSH_new(harness->ctx);
|
|
AssertNotNull(harness->ssh);
|
|
|
|
MemIoInit(&harness->io, in, inSz, harness->out, sizeof(harness->out));
|
|
wolfSSH_SetIOReadCtx(harness->ssh, &harness->io);
|
|
wolfSSH_SetIOWriteCtx(harness->ssh, &harness->io);
|
|
harness->ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
|
|
}
|
|
|
|
static void FreeChannelOpenHarness(ChannelOpenHarness* harness)
|
|
{
|
|
if (harness->ssh != NULL)
|
|
wolfSSH_free(harness->ssh);
|
|
if (harness->ctx != NULL)
|
|
wolfSSH_CTX_free(harness->ctx);
|
|
}
|
|
|
|
#if !defined(NO_WOLFSSH_SERVER) && !defined(NO_WOLFSSH_CLIENT) && \
|
|
!defined(WOLFSSH_NO_RSA) && !defined(NO_FILESYSTEM)
|
|
#if !defined(WOLFSSH_NO_DH_GROUP14_SHA256)
|
|
#define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group14-sha256"
|
|
#elif !defined(WOLFSSH_NO_DH_GROUP16_SHA512)
|
|
#define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group16-sha512"
|
|
#elif !defined(WOLFSSH_NO_DH_GROUP14_SHA1)
|
|
#define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group14-sha1"
|
|
#elif !defined(WOLFSSH_NO_DH_GROUP1_SHA1)
|
|
#define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group1-sha1"
|
|
#endif
|
|
#endif
|
|
|
|
/* Read a whole file into buf, returning the byte count (0 on any failure).
|
|
* Used by the DH KEX regression below and by TestAppendKeyToFile, so it is
|
|
* available whenever either of those is compiled. */
|
|
#if defined(KEXDH_REPLY_REGRESS_KEX_ALGO) || defined(WOLFSSH_TEST_INTERNAL)
|
|
static word32 LoadFileBuffer(const char* path, byte* buf, word32 bufSz)
|
|
{
|
|
WFILE* file;
|
|
long fileSz;
|
|
word32 readSz;
|
|
|
|
if (path == NULL || buf == NULL || bufSz == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (WFOPEN(NULL, &file, path, "rb") != 0 || file == WBADFILE) {
|
|
return 0;
|
|
}
|
|
WFSEEK(NULL, file, 0, WSEEK_END);
|
|
fileSz = WFTELL(NULL, file);
|
|
WREWIND(NULL, file);
|
|
|
|
if (fileSz <= 0 || (word32)fileSz > bufSz) {
|
|
WFCLOSE(NULL, file);
|
|
return 0;
|
|
}
|
|
|
|
readSz = (word32)WFREAD(NULL, buf, 1, fileSz, file);
|
|
WFCLOSE(NULL, file);
|
|
|
|
if (readSz != (word32)fileSz) {
|
|
return 0;
|
|
}
|
|
|
|
return readSz;
|
|
}
|
|
#endif /* KEXDH_REPLY_REGRESS_KEX_ALGO || WOLFSSH_TEST_INTERNAL */
|
|
|
|
#ifdef KEXDH_REPLY_REGRESS_KEX_ALGO
|
|
|
|
#define REGRESS_DUPLEX_QUEUE_SZ 32768U
|
|
#define REGRESS_MUTATION_SCRATCH_SZ 4096U
|
|
#define REGRESS_SERVER_KEY_PATH "keys/server-key-rsa.der"
|
|
#define REGRESS_USERNAME "jill"
|
|
#define REGRESS_PASSWORD "upthehill"
|
|
#define REGRESS_MAX_HANDSHAKE_STEPS 2048
|
|
#define REGRESS_SSH_PROTO_PREFIX "SSH-"
|
|
#define REGRESS_SSH_PROTO_PREFIX_SZ 4U
|
|
|
|
typedef struct {
|
|
byte data[REGRESS_DUPLEX_QUEUE_SZ];
|
|
word32 len;
|
|
} DuplexQueue;
|
|
|
|
typedef struct {
|
|
byte enabled;
|
|
int parseError;
|
|
word32 matchedPackets;
|
|
word32 mutatedPackets;
|
|
byte scratch[REGRESS_MUTATION_SCRATCH_SZ];
|
|
word32 scratchSz;
|
|
} KexReplyMutator;
|
|
|
|
typedef struct DuplexEndpoint {
|
|
DuplexQueue inbound;
|
|
struct DuplexEndpoint* peer;
|
|
KexReplyMutator* mutator;
|
|
byte isServer;
|
|
} DuplexEndpoint;
|
|
|
|
typedef struct {
|
|
WOLFSSH_CTX* clientCtx;
|
|
WOLFSSH_CTX* serverCtx;
|
|
WOLFSSH* client;
|
|
WOLFSSH* server;
|
|
DuplexEndpoint clientIo;
|
|
DuplexEndpoint serverIo;
|
|
KexReplyMutator mutator;
|
|
} KexReplyHarness;
|
|
|
|
typedef struct {
|
|
int clientRet;
|
|
int clientErr;
|
|
int serverRet;
|
|
int serverErr;
|
|
int clientSuccess;
|
|
int serverSuccess;
|
|
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)
|
|
{
|
|
if (strSz == NULL || str == NULL || buf == NULL || idx == NULL) {
|
|
return WS_BAD_ARGUMENT;
|
|
}
|
|
|
|
if (*idx > len || len - *idx < LENGTH_SZ) {
|
|
return WS_PARSE_E;
|
|
}
|
|
|
|
*strSz = ReadUint32(buf + *idx);
|
|
*idx += LENGTH_SZ;
|
|
if (*strSz > len - *idx) {
|
|
return WS_PARSE_E;
|
|
}
|
|
|
|
*str = buf + *idx;
|
|
*idx += *strSz;
|
|
|
|
return WS_SUCCESS;
|
|
}
|
|
|
|
static word32 AppendBlob(byte* buf, word32 bufSz, word32 idx,
|
|
const byte* data, word32 dataSz)
|
|
{
|
|
idx = AppendUint32(buf, bufSz, idx, dataSz);
|
|
return AppendData(buf, bufSz, idx, data, dataSz);
|
|
}
|
|
|
|
static int RegressionClientUserAuth(byte authType,
|
|
WS_UserAuthData* authData, void* ctx)
|
|
{
|
|
static const char password[] = REGRESS_PASSWORD;
|
|
|
|
(void)ctx;
|
|
|
|
if (authType != WOLFSSH_USERAUTH_PASSWORD || authData == NULL) {
|
|
return WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
|
|
}
|
|
|
|
authData->sf.password.password = (byte*)password;
|
|
authData->sf.password.passwordSz = (word32)WSTRLEN(password);
|
|
|
|
return WOLFSSH_USERAUTH_SUCCESS;
|
|
}
|
|
|
|
static int RegressionServerUserAuth(byte authType,
|
|
WS_UserAuthData* authData, void* ctx)
|
|
{
|
|
static const char password[] = REGRESS_PASSWORD;
|
|
word32 passwordSz = (word32)WSTRLEN(password);
|
|
|
|
(void)ctx;
|
|
|
|
if (authType != WOLFSSH_USERAUTH_PASSWORD || authData == NULL) {
|
|
return WOLFSSH_USERAUTH_FAILURE;
|
|
}
|
|
|
|
if (authData->sf.password.password == NULL ||
|
|
authData->sf.password.passwordSz != passwordSz) {
|
|
return WOLFSSH_USERAUTH_FAILURE;
|
|
}
|
|
|
|
if (WMEMCMP(authData->sf.password.password, password, passwordSz) != 0) {
|
|
return WOLFSSH_USERAUTH_FAILURE;
|
|
}
|
|
|
|
return WOLFSSH_USERAUTH_SUCCESS;
|
|
}
|
|
|
|
static int AcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz,
|
|
void* ctx)
|
|
{
|
|
(void)pubKey;
|
|
(void)pubKeySz;
|
|
(void)ctx;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int RejectAnyServerHostKey(const byte* pubKey, word32 pubKeySz,
|
|
void* ctx)
|
|
{
|
|
(void)pubKey;
|
|
(void)pubKeySz;
|
|
(void)ctx;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int QueueAppend(DuplexQueue* queue, const byte* data, word32 dataSz)
|
|
{
|
|
if (queue == NULL || data == NULL) {
|
|
return WS_BAD_ARGUMENT;
|
|
}
|
|
|
|
if (dataSz > sizeof(queue->data) - queue->len) {
|
|
return WS_BUFFER_E;
|
|
}
|
|
|
|
WMEMCPY(queue->data + queue->len, data, dataSz);
|
|
queue->len += dataSz;
|
|
|
|
return WS_SUCCESS;
|
|
}
|
|
|
|
static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz,
|
|
const char* replacement, byte* out, word32 outSz, word32* outLen)
|
|
{
|
|
const byte* payload;
|
|
const byte* pubKey;
|
|
const byte* f;
|
|
const byte* sigBlob;
|
|
const byte* sigName;
|
|
const byte* sigData;
|
|
word32 packetLen, padLen, payloadSz;
|
|
word32 pubKeySz, fSz, sigBlobSz;
|
|
word32 sigNameSz, sigDataSz;
|
|
word32 idx = 0;
|
|
word32 innerIdx = 0;
|
|
word32 outerIdx = 0;
|
|
word32 innerSigSz;
|
|
byte payloadBuf[REGRESS_MUTATION_SCRATCH_SZ];
|
|
byte innerSig[REGRESS_MUTATION_SCRATCH_SZ];
|
|
|
|
if (replacement == NULL || out == NULL || outLen == NULL) {
|
|
return WS_BAD_ARGUMENT;
|
|
}
|
|
|
|
if (packetSz < UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ) {
|
|
return 0;
|
|
}
|
|
|
|
packetLen = ReadUint32(packet);
|
|
if (packetLen + UINT32_SZ != packetSz) {
|
|
return 0;
|
|
}
|
|
|
|
padLen = packet[UINT32_SZ];
|
|
if (packetLen < PAD_LENGTH_SZ + MSG_ID_SZ + padLen) {
|
|
return WS_PARSE_E;
|
|
}
|
|
|
|
if (packet[UINT32_SZ + PAD_LENGTH_SZ] != MSGID_KEXDH_REPLY) {
|
|
return 0;
|
|
}
|
|
|
|
payload = packet + UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ;
|
|
payloadSz = packetSz - UINT32_SZ - PAD_LENGTH_SZ - MSG_ID_SZ - padLen;
|
|
|
|
if (ReadStringRef(&pubKeySz, &pubKey, payload, payloadSz, &idx) !=
|
|
WS_SUCCESS) {
|
|
return WS_PARSE_E;
|
|
}
|
|
if (ReadStringRef(&fSz, &f, payload, payloadSz, &idx) != WS_SUCCESS) {
|
|
return WS_PARSE_E;
|
|
}
|
|
if (ReadStringRef(&sigBlobSz, &sigBlob, payload, payloadSz, &idx) !=
|
|
WS_SUCCESS) {
|
|
return WS_PARSE_E;
|
|
}
|
|
|
|
if (ReadStringRef(&sigNameSz, &sigName, sigBlob, sigBlobSz, &innerIdx) !=
|
|
WS_SUCCESS) {
|
|
return WS_PARSE_E;
|
|
}
|
|
if (ReadStringRef(&sigDataSz, &sigData, sigBlob, sigBlobSz, &innerIdx) !=
|
|
WS_SUCCESS) {
|
|
return WS_PARSE_E;
|
|
}
|
|
|
|
if (innerIdx != sigBlobSz) {
|
|
return WS_PARSE_E;
|
|
}
|
|
|
|
(void)sigName;
|
|
(void)sigNameSz;
|
|
|
|
innerSigSz = 0;
|
|
innerSigSz = AppendString(innerSig, sizeof(innerSig), innerSigSz,
|
|
replacement);
|
|
innerSigSz = AppendBlob(innerSig, sizeof(innerSig), innerSigSz,
|
|
sigData, sigDataSz);
|
|
|
|
outerIdx = 0;
|
|
outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx,
|
|
pubKey, pubKeySz);
|
|
outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx, f, fSz);
|
|
outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx,
|
|
innerSig, innerSigSz);
|
|
*outLen = WrapPacket(MSGID_KEXDH_REPLY, payloadBuf, outerIdx, out, outSz);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int RewriteKexDhReplySignatureName(const byte* packet, word32 packetSz,
|
|
const char* replacement, byte* out, word32 outSz, word32* outLen)
|
|
{
|
|
word32 offset = 0;
|
|
|
|
if (packet == NULL || replacement == NULL || out == NULL || outLen == NULL) {
|
|
return WS_BAD_ARGUMENT;
|
|
}
|
|
|
|
while (packetSz - offset >= UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ) {
|
|
word32 curPacketSz = ReadUint32(packet + offset) + UINT32_SZ;
|
|
int rewriteRet;
|
|
|
|
if (curPacketSz > packetSz - offset) {
|
|
return 0;
|
|
}
|
|
|
|
if (packet[offset + UINT32_SZ + PAD_LENGTH_SZ] == MSGID_KEXDH_REPLY) {
|
|
rewriteRet = RewriteSingleKexDhReplyPacket(packet + offset,
|
|
curPacketSz, replacement, out, outSz, outLen);
|
|
if (rewriteRet <= 0) {
|
|
return rewriteRet;
|
|
}
|
|
|
|
if (packetSz - offset - curPacketSz > outSz - *outLen) {
|
|
return WS_BUFFER_E;
|
|
}
|
|
|
|
WMEMCPY(out + *outLen, packet + offset + curPacketSz,
|
|
packetSz - offset - curPacketSz);
|
|
*outLen += packetSz - offset - curPacketSz;
|
|
|
|
return 1;
|
|
}
|
|
|
|
offset += curPacketSz;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int DuplexRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
DuplexEndpoint* endpoint = (DuplexEndpoint*)ctx;
|
|
word32 readSz;
|
|
|
|
(void)ssh;
|
|
|
|
if (endpoint == NULL || buf == NULL) {
|
|
return WS_CBIO_ERR_GENERAL;
|
|
}
|
|
|
|
if (endpoint->inbound.len == 0) {
|
|
return WS_CBIO_ERR_WANT_READ;
|
|
}
|
|
|
|
readSz = sz;
|
|
if (readSz > endpoint->inbound.len) {
|
|
readSz = endpoint->inbound.len;
|
|
}
|
|
|
|
WMEMCPY(buf, endpoint->inbound.data, readSz);
|
|
endpoint->inbound.len -= readSz;
|
|
if (endpoint->inbound.len > 0) {
|
|
WMEMMOVE(endpoint->inbound.data, endpoint->inbound.data + readSz,
|
|
endpoint->inbound.len);
|
|
}
|
|
|
|
return (int)readSz;
|
|
}
|
|
|
|
static int DuplexSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
DuplexEndpoint* endpoint = (DuplexEndpoint*)ctx;
|
|
const byte* output = (const byte*)buf;
|
|
word32 outputSz = sz;
|
|
int ret;
|
|
|
|
(void)ssh;
|
|
|
|
if (endpoint == NULL || endpoint->peer == NULL || buf == NULL) {
|
|
return WS_CBIO_ERR_GENERAL;
|
|
}
|
|
|
|
if (endpoint->isServer && endpoint->mutator != NULL &&
|
|
endpoint->mutator->enabled &&
|
|
endpoint->mutator->mutatedPackets == 0 &&
|
|
outputSz >= UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ &&
|
|
!(outputSz >= REGRESS_SSH_PROTO_PREFIX_SZ &&
|
|
WMEMCMP(output, REGRESS_SSH_PROTO_PREFIX,
|
|
REGRESS_SSH_PROTO_PREFIX_SZ) == 0)) {
|
|
word32 mutatedSz = 0;
|
|
int mutateRet;
|
|
|
|
mutateRet = RewriteKexDhReplySignatureName(output, outputSz, "ssh-rsa",
|
|
endpoint->mutator->scratch,
|
|
(word32)sizeof(endpoint->mutator->scratch), &mutatedSz);
|
|
if (mutateRet < 0) {
|
|
endpoint->mutator->parseError = mutateRet;
|
|
return WS_CBIO_ERR_GENERAL;
|
|
}
|
|
if (mutateRet > 0) {
|
|
endpoint->mutator->matchedPackets++;
|
|
endpoint->mutator->mutatedPackets++;
|
|
endpoint->mutator->scratchSz = mutatedSz;
|
|
output = endpoint->mutator->scratch;
|
|
outputSz = mutatedSz;
|
|
}
|
|
}
|
|
|
|
ret = QueueAppend(&endpoint->peer->inbound, output, outputSz);
|
|
if (ret != WS_SUCCESS) {
|
|
return WS_CBIO_ERR_GENERAL;
|
|
}
|
|
|
|
return (int)sz;
|
|
}
|
|
|
|
static void InitDuplexPair(DuplexEndpoint* client, DuplexEndpoint* server,
|
|
KexReplyMutator* mutator)
|
|
{
|
|
WMEMSET(client, 0, sizeof(*client));
|
|
WMEMSET(server, 0, sizeof(*server));
|
|
|
|
client->peer = server;
|
|
server->peer = client;
|
|
server->mutator = mutator;
|
|
server->isServer = 1;
|
|
}
|
|
|
|
static void FreeKexReplyHarness(KexReplyHarness* harness)
|
|
{
|
|
if (harness->client != NULL) {
|
|
wolfSSH_free(harness->client);
|
|
}
|
|
if (harness->server != NULL) {
|
|
wolfSSH_free(harness->server);
|
|
}
|
|
if (harness->clientCtx != NULL) {
|
|
wolfSSH_CTX_free(harness->clientCtx);
|
|
}
|
|
if (harness->serverCtx != NULL) {
|
|
wolfSSH_CTX_free(harness->serverCtx);
|
|
}
|
|
}
|
|
|
|
static void InitKexReplyHarnessEx(KexReplyHarness* harness,
|
|
const char* keyAlgo, byte mutateReply, byte skipPublicKeyCheck)
|
|
{
|
|
byte keyBuf[2048];
|
|
word32 keySz;
|
|
|
|
WMEMSET(harness, 0, sizeof(*harness));
|
|
|
|
InitDuplexPair(&harness->clientIo, &harness->serverIo, &harness->mutator);
|
|
harness->mutator.enabled = mutateReply;
|
|
|
|
harness->clientCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
|
|
AssertNotNull(harness->clientCtx);
|
|
harness->serverCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
|
|
AssertNotNull(harness->serverCtx);
|
|
|
|
AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(harness->clientCtx,
|
|
KEXDH_REPLY_REGRESS_KEX_ALGO), WS_SUCCESS);
|
|
AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(harness->serverCtx,
|
|
KEXDH_REPLY_REGRESS_KEX_ALGO), WS_SUCCESS);
|
|
AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(harness->clientCtx, keyAlgo),
|
|
WS_SUCCESS);
|
|
AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(harness->serverCtx, keyAlgo),
|
|
WS_SUCCESS);
|
|
|
|
wolfSSH_SetIORecv(harness->clientCtx, DuplexRecv);
|
|
wolfSSH_SetIOSend(harness->clientCtx, DuplexSend);
|
|
wolfSSH_SetIORecv(harness->serverCtx, DuplexRecv);
|
|
wolfSSH_SetIOSend(harness->serverCtx, DuplexSend);
|
|
|
|
wolfSSH_SetUserAuth(harness->clientCtx, RegressionClientUserAuth);
|
|
wolfSSH_SetUserAuth(harness->serverCtx, RegressionServerUserAuth);
|
|
if (!skipPublicKeyCheck) {
|
|
wolfSSH_CTX_SetPublicKeyCheck(harness->clientCtx, AcceptAnyServerHostKey);
|
|
}
|
|
|
|
keySz = LoadFileBuffer(REGRESS_SERVER_KEY_PATH, keyBuf, sizeof(keyBuf));
|
|
AssertTrue(keySz > 0);
|
|
AssertIntEQ(wolfSSH_CTX_UsePrivateKey_buffer(harness->serverCtx, keyBuf,
|
|
keySz, WOLFSSH_FORMAT_ASN1), WS_SUCCESS);
|
|
|
|
harness->client = wolfSSH_new(harness->clientCtx);
|
|
AssertNotNull(harness->client);
|
|
harness->server = wolfSSH_new(harness->serverCtx);
|
|
AssertNotNull(harness->server);
|
|
|
|
wolfSSH_SetIOReadCtx(harness->client, &harness->clientIo);
|
|
wolfSSH_SetIOWriteCtx(harness->client, &harness->clientIo);
|
|
wolfSSH_SetIOReadCtx(harness->server, &harness->serverIo);
|
|
wolfSSH_SetIOWriteCtx(harness->server, &harness->serverIo);
|
|
|
|
AssertIntEQ(wolfSSH_SetUsername(harness->client, REGRESS_USERNAME),
|
|
WS_SUCCESS);
|
|
}
|
|
|
|
static void InitKexReplyHarness(KexReplyHarness* harness,
|
|
const char* keyAlgo, byte mutateReply)
|
|
{
|
|
InitKexReplyHarnessEx(harness, keyAlgo, mutateReply, 0);
|
|
}
|
|
|
|
static int IsHandshakeRetryable(int err)
|
|
{
|
|
return err == WS_WANT_READ || err == WS_WANT_WRITE ||
|
|
err == WS_AUTH_PENDING;
|
|
}
|
|
|
|
static void RunKexReplyHandshake(KexReplyHarness* harness,
|
|
KexReplyRunResult* result)
|
|
{
|
|
word32 step;
|
|
|
|
WMEMSET(result, 0, sizeof(*result));
|
|
result->clientRet = WS_FATAL_ERROR;
|
|
result->serverRet = WS_FATAL_ERROR;
|
|
|
|
for (step = 0; step < REGRESS_MAX_HANDSHAKE_STEPS; step++) {
|
|
if (!result->clientSuccess) {
|
|
result->clientRet = wolfSSH_connect(harness->client);
|
|
result->clientErr = wolfSSH_get_error(harness->client);
|
|
if (result->clientRet == WS_SUCCESS) {
|
|
result->clientSuccess = 1;
|
|
}
|
|
else if (!IsHandshakeRetryable(result->clientErr)) {
|
|
result->steps = step + 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!result->serverSuccess) {
|
|
result->serverRet = wolfSSH_accept(harness->server);
|
|
result->serverErr = wolfSSH_get_error(harness->server);
|
|
if (result->serverRet == WS_SUCCESS) {
|
|
result->serverSuccess = 1;
|
|
}
|
|
else if (!IsHandshakeRetryable(result->serverErr)) {
|
|
result->steps = step + 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (result->clientSuccess && result->serverSuccess) {
|
|
result->steps = step + 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
result->steps = REGRESS_MAX_HANDSHAKE_STEPS;
|
|
}
|
|
|
|
static void AssertHandshakeSucceeds(const char* keyAlgo)
|
|
{
|
|
KexReplyHarness harness;
|
|
KexReplyRunResult result;
|
|
|
|
InitKexReplyHarness(&harness, keyAlgo, 0);
|
|
RunKexReplyHandshake(&harness, &result);
|
|
|
|
AssertTrue(result.clientSuccess);
|
|
AssertTrue(result.serverSuccess);
|
|
AssertIntEQ(harness.mutator.mutatedPackets, 0);
|
|
AssertIntEQ(harness.client->connectState, CONNECT_SERVER_CHANNEL_REQUEST_DONE);
|
|
AssertIntEQ(harness.server->acceptState, ACCEPT_CLIENT_SESSION_ESTABLISHED);
|
|
|
|
FreeKexReplyHarness(&harness);
|
|
}
|
|
|
|
static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo)
|
|
{
|
|
KexReplyHarness harness;
|
|
KexReplyRunResult result;
|
|
|
|
InitKexReplyHarness(&harness, keyAlgo, 1);
|
|
RunKexReplyHandshake(&harness, &result);
|
|
|
|
AssertIntEQ(harness.mutator.parseError, 0);
|
|
AssertIntEQ(harness.mutator.matchedPackets, 1);
|
|
AssertIntEQ(harness.mutator.mutatedPackets, 1);
|
|
AssertFalse(result.clientSuccess);
|
|
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
|
|
AssertTrue(result.clientRet == WS_FATAL_ERROR);
|
|
AssertTrue(result.clientErr != WS_WANT_READ && result.clientErr != WS_WANT_WRITE);
|
|
|
|
FreeKexReplyHarness(&harness);
|
|
}
|
|
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_256
|
|
static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void)
|
|
{
|
|
AssertHandshakeSucceeds("rsa-sha2-256");
|
|
AssertHandshakeRejectsMutatedReply("rsa-sha2-256");
|
|
}
|
|
#endif
|
|
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_512
|
|
static void TestKexDhReplyRejectsRsaSha2_512SigNameDowngrade(void)
|
|
{
|
|
AssertHandshakeSucceeds("rsa-sha2-512");
|
|
AssertHandshakeRejectsMutatedReply("rsa-sha2-512");
|
|
}
|
|
#endif
|
|
|
|
static void AssertHandshakeRejectsWithNoPublicKeyCheck(const char* keyAlgo)
|
|
{
|
|
KexReplyHarness harness;
|
|
KexReplyRunResult result;
|
|
|
|
InitKexReplyHarnessEx(&harness, keyAlgo, 0, 1 /* skipPublicKeyCheck */);
|
|
RunKexReplyHandshake(&harness, &result);
|
|
|
|
AssertFalse(result.clientSuccess);
|
|
AssertTrue(result.clientRet == WS_FATAL_ERROR);
|
|
AssertTrue(result.clientErr != WS_WANT_READ && result.clientErr != WS_WANT_WRITE);
|
|
AssertIntEQ(result.clientErr, WS_PUBKEY_REJECTED_E);
|
|
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
|
|
|
|
FreeKexReplyHarness(&harness);
|
|
}
|
|
|
|
static void TestKexDhReplyRejectsNoPublicKeyCheck(void)
|
|
{
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_256
|
|
AssertHandshakeRejectsWithNoPublicKeyCheck("rsa-sha2-256");
|
|
#endif
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_512
|
|
AssertHandshakeRejectsWithNoPublicKeyCheck("rsa-sha2-512");
|
|
#endif
|
|
}
|
|
|
|
static void AssertHandshakeRejectsWhenCallbackRejects(const char* keyAlgo)
|
|
{
|
|
KexReplyHarness harness;
|
|
KexReplyRunResult result;
|
|
|
|
InitKexReplyHarness(&harness, keyAlgo, 0);
|
|
wolfSSH_CTX_SetPublicKeyCheck(harness.clientCtx, RejectAnyServerHostKey);
|
|
RunKexReplyHandshake(&harness, &result);
|
|
|
|
AssertFalse(result.clientSuccess);
|
|
AssertTrue(result.clientRet == WS_FATAL_ERROR);
|
|
AssertTrue(result.clientErr != WS_WANT_READ && result.clientErr != WS_WANT_WRITE);
|
|
AssertIntEQ(result.clientErr, WS_PUBKEY_REJECTED_E);
|
|
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
|
|
|
|
FreeKexReplyHarness(&harness);
|
|
}
|
|
|
|
static void TestKexDhReplyRejectsWhenCallbackRejects(void)
|
|
{
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_256
|
|
AssertHandshakeRejectsWhenCallbackRejects("rsa-sha2-256");
|
|
#endif
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_512
|
|
AssertHandshakeRejectsWhenCallbackRejects("rsa-sha2-512");
|
|
#endif
|
|
}
|
|
|
|
#endif /* KEXDH_REPLY_REGRESS_KEX_ALGO */
|
|
|
|
static word32 ParseChannelOpenFailRecipient(const byte* pkt, word32 sz)
|
|
{
|
|
word32 chan;
|
|
/* SSH binary-packet layout: 4 (len) + 1 (pad_len) + 1 (msg_id) = 6;
|
|
* + 4 for the recipient_channel field itself gives the 10-byte minimum. */
|
|
AssertTrue(sz >= 10);
|
|
AssertIntEQ(pkt[5], MSGID_CHANNEL_OPEN_FAIL);
|
|
WMEMCPY(&chan, pkt + 6, sizeof(chan));
|
|
return ntohl(chan);
|
|
}
|
|
|
|
static word32 ParseChannelOpenFailReason(const byte* pkt, word32 sz)
|
|
{
|
|
word32 reason;
|
|
/* SSH binary-packet layout: 4 (len) + 1 (pad_len) + 1 (msg_id) + 4 (chan) = 10;
|
|
* + 4 for the reason field itself gives the 14-byte minimum. */
|
|
AssertTrue(sz >= 14);
|
|
AssertIntEQ(pkt[5], MSGID_CHANNEL_OPEN_FAIL);
|
|
WMEMCPY(&reason, pkt + 10, sizeof(reason));
|
|
return ntohl(reason);
|
|
}
|
|
|
|
static void AssertChannelOpenFailResponse(const ChannelOpenHarness* harness,
|
|
int ret)
|
|
{
|
|
byte msgId;
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertIntEQ(harness->io.inOff, harness->io.inSz);
|
|
AssertTrue(harness->io.outSz > 0);
|
|
AssertTrue(harness->io.outSz <= harness->io.outCap);
|
|
|
|
msgId = ParseMsgId(harness->io.out, harness->io.outSz);
|
|
AssertIntEQ(msgId, MSGID_CHANNEL_OPEN_FAIL);
|
|
AssertFalse(msgId == MSGID_REQUEST_FAILURE);
|
|
AssertIntEQ(harness->ssh->channelListSz, 0);
|
|
AssertTrue(harness->ssh->channelList == NULL);
|
|
}
|
|
|
|
#ifdef WOLFSSH_FWD
|
|
static word32 ParsePayloadLen(const byte* packet, word32 packetSz)
|
|
{
|
|
word32 packetLen;
|
|
byte padLen;
|
|
|
|
AssertNotNull(packet);
|
|
AssertTrue(packetSz >= 6);
|
|
|
|
WMEMCPY(&packetLen, packet, sizeof(packetLen));
|
|
packetLen = ntohl(packetLen);
|
|
padLen = packet[4];
|
|
|
|
AssertTrue(packetLen >= (word32)padLen + 1);
|
|
AssertTrue(packetSz >= packetLen + 4);
|
|
|
|
return packetLen - padLen - 1;
|
|
}
|
|
|
|
static const byte* ParseGlobalRequestName(const byte* packet, word32 packetSz,
|
|
word32* nameSz)
|
|
{
|
|
word32 packetLen;
|
|
word32 payloadLen;
|
|
word32 strSz;
|
|
const byte* payload;
|
|
|
|
AssertNotNull(packet);
|
|
AssertNotNull(nameSz);
|
|
AssertTrue(packetSz >= 10);
|
|
|
|
WMEMCPY(&packetLen, packet, sizeof(packetLen));
|
|
packetLen = ntohl(packetLen);
|
|
AssertTrue(packetSz >= packetLen + 4);
|
|
|
|
payloadLen = ParsePayloadLen(packet, packetSz);
|
|
payload = packet + 5;
|
|
|
|
AssertTrue(payloadLen >= 1 + sizeof(word32));
|
|
AssertIntEQ(payload[0], MSGID_GLOBAL_REQUEST);
|
|
|
|
WMEMCPY(&strSz, payload + 1, sizeof(strSz));
|
|
strSz = ntohl(strSz);
|
|
AssertTrue(payloadLen >= 1 + sizeof(word32) + strSz);
|
|
|
|
*nameSz = strSz;
|
|
return payload + 1 + sizeof(word32);
|
|
}
|
|
|
|
static void AssertGlobalRequestReply(const ChannelOpenHarness* harness,
|
|
byte expectedMsgId)
|
|
{
|
|
byte msgId;
|
|
word32 payloadLen;
|
|
|
|
AssertTrue(harness->io.outSz > 0);
|
|
msgId = ParseMsgId(harness->io.out, harness->io.outSz);
|
|
AssertIntEQ(msgId, expectedMsgId);
|
|
|
|
payloadLen = ParsePayloadLen(harness->io.out, harness->io.outSz);
|
|
if (expectedMsgId == MSGID_REQUEST_FAILURE) {
|
|
AssertIntEQ(payloadLen, 1);
|
|
}
|
|
else if (expectedMsgId == MSGID_REQUEST_SUCCESS) {
|
|
const byte* reqName;
|
|
word32 reqNameSz;
|
|
|
|
reqName = ParseGlobalRequestName(harness->io.in, harness->io.inSz,
|
|
&reqNameSz);
|
|
|
|
if (reqNameSz == sizeof("tcpip-forward") - 1 &&
|
|
WMEMCMP(reqName, "tcpip-forward",
|
|
sizeof("tcpip-forward") - 1) == 0) {
|
|
AssertIntEQ(payloadLen, 5);
|
|
}
|
|
else if (reqNameSz == sizeof("cancel-tcpip-forward") - 1 &&
|
|
WMEMCMP(reqName, "cancel-tcpip-forward",
|
|
sizeof("cancel-tcpip-forward") - 1) == 0) {
|
|
AssertIntEQ(payloadLen, 1);
|
|
}
|
|
else {
|
|
Fail(("unexpected global request name"),
|
|
("%.*s", (int)reqNameSz, reqName));
|
|
}
|
|
}
|
|
}
|
|
|
|
static word32 ParseGlobalRequestSuccessPort(const byte* packet, word32 packetSz)
|
|
{
|
|
word32 port;
|
|
|
|
AssertNotNull(packet);
|
|
AssertTrue(packetSz >= 10);
|
|
AssertIntEQ(packet[5], MSGID_REQUEST_SUCCESS);
|
|
WMEMCPY(&port, packet + 6, sizeof(port));
|
|
|
|
return ntohl(port);
|
|
}
|
|
#endif
|
|
|
|
static int RejectChannelOpenCb(WOLFSSH_CHANNEL* channel, void* ctx)
|
|
{
|
|
(void)channel;
|
|
(void)ctx;
|
|
|
|
return WS_BAD_ARGUMENT;
|
|
}
|
|
|
|
#ifdef WOLFSSH_FWD
|
|
static int RejectDirectTcpipSetup(WS_FwdCbAction action, void* ctx,
|
|
const char* host, word32 port)
|
|
{
|
|
(void)ctx;
|
|
(void)host;
|
|
(void)port;
|
|
|
|
if (action == WOLFSSH_FWD_LOCAL_SETUP)
|
|
return WS_FWD_SETUP_E;
|
|
|
|
return WS_SUCCESS;
|
|
}
|
|
|
|
static int AcceptFwdCb(WS_FwdCbAction action, void* ctx,
|
|
const char* host, word32 port)
|
|
{
|
|
(void)action;
|
|
(void)ctx;
|
|
(void)host;
|
|
(void)port;
|
|
|
|
return WS_SUCCESS;
|
|
}
|
|
|
|
#define REGRESS_FWD_ALLOC_PORT 49152
|
|
|
|
static int AllocatePortFwdCb(WS_FwdCbAction action, void* ctx,
|
|
const char* host, word32 port)
|
|
{
|
|
(void)ctx;
|
|
(void)host;
|
|
|
|
/* A return at or above WS_FWD_PORT_CHECK reports the allocated port for a
|
|
* port-0 request; WS_FWD_SUCCESS (0) otherwise. */
|
|
if (action == WOLFSSH_FWD_REMOTE_SETUP && port == 0)
|
|
return REGRESS_FWD_ALLOC_PORT;
|
|
|
|
return WS_SUCCESS;
|
|
}
|
|
|
|
/* Accepts the remote setup but never reports an allocated port. Records
|
|
* whether the server asks it to clean the setup back up. */
|
|
static int NoPortFwdCb(WS_FwdCbAction action, void* ctx,
|
|
const char* host, word32 port)
|
|
{
|
|
int* cleanupCalled = (int*)ctx;
|
|
(void)host;
|
|
(void)port;
|
|
|
|
if (action == WOLFSSH_FWD_REMOTE_CLEANUP && cleanupCalled != NULL)
|
|
*cleanupCalled = 1;
|
|
|
|
return WS_SUCCESS;
|
|
}
|
|
|
|
/* Rejects the remote setup with a WS_FwdCbError status. The server must send a
|
|
* failure and must NOT ask for cleanup, since the setup never succeeded. */
|
|
static int RejectRemoteSetupFwdCb(WS_FwdCbAction action, void* ctx,
|
|
const char* host, word32 port)
|
|
{
|
|
int* cleanupCalled = (int*)ctx;
|
|
(void)host;
|
|
(void)port;
|
|
|
|
if (action == WOLFSSH_FWD_REMOTE_SETUP)
|
|
return WS_FWD_SETUP_E;
|
|
if (action == WOLFSSH_FWD_REMOTE_CLEANUP && cleanupCalled != NULL)
|
|
*cleanupCalled = 1;
|
|
|
|
return WS_SUCCESS;
|
|
}
|
|
#endif
|
|
|
|
|
|
/* Reject auth messages while the peer is still keying and the client
|
|
* expects the KEX reply. */
|
|
static void TestAuthMessageBlockedDuringKeying(WOLFSSH* ssh)
|
|
{
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT;
|
|
ssh->handshake = AllocHandshake(ssh);
|
|
ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY;
|
|
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_FAILURE,
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
|
|
/* The expected message must be allowed and clear the expectation. */
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXDH_REPLY,
|
|
WS_MSG_RECV);
|
|
AssertTrue(allowed);
|
|
AssertIntEQ(ssh->handshake->expectMsgId, MSGID_NONE);
|
|
}
|
|
|
|
/* Reject USERAUTH_FAILURE with password list during keying (password-leak PoC). */
|
|
static void TestUserauthFailureDuringKeying(WOLFSSH* ssh)
|
|
{
|
|
byte buf[32];
|
|
word32 sz;
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT;
|
|
ssh->handshake = AllocHandshake(ssh);
|
|
ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY;
|
|
|
|
sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
}
|
|
|
|
|
|
/* Expect an abort/error to be set when password-leak sequence hits during keying. */
|
|
static void TestPasswordLeakAborts(WOLFSSH* ssh)
|
|
{
|
|
byte buf[32];
|
|
word32 sz;
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT;
|
|
ssh->handshake = AllocHandshake(ssh);
|
|
ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY;
|
|
|
|
sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
AssertTrue(ssh->error != 0); /* should set an error / abort path */
|
|
}
|
|
|
|
|
|
/* Reject USERAUTH_SUCCESS before the client has even sent a userauth request. */
|
|
static void TestPrematureUserauthSuccess(WOLFSSH* ssh)
|
|
{
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->connectState = CONNECT_KEYED;
|
|
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_SUCCESS,
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
}
|
|
|
|
|
|
/* Reject a spoofed sequence: bogus USERAUTH_SUCCESS followed by channel msgs. */
|
|
static void TestChannelSpoofSequence(WOLFSSH* ssh)
|
|
{
|
|
byte buf[32];
|
|
word32 sz;
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->connectState = CONNECT_KEYED;
|
|
|
|
sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
|
|
sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
|
|
sz = BuildPacket(MSGID_CHANNEL_SUCCESS, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
|
|
sz = BuildPacket(MSGID_CHANNEL_DATA, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
}
|
|
|
|
/* Expect abort/error on spoofed auth+channel sequence. */
|
|
static void TestChannelSpoofAborts(WOLFSSH* ssh)
|
|
{
|
|
byte buf[32];
|
|
word32 sz;
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->connectState = CONNECT_KEYED;
|
|
|
|
sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
|
|
sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
|
|
AssertTrue(ssh->error != 0);
|
|
}
|
|
|
|
|
|
/* Reject USERAUTH_FAILURE(publickey) before any auth request (static-signature PoC). */
|
|
static void TestPublicKeyFailureBeforeRequest(WOLFSSH* ssh)
|
|
{
|
|
byte buf[32];
|
|
word32 sz;
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->connectState = CONNECT_KEYED;
|
|
|
|
sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
}
|
|
|
|
/* Expect abort/error when publickey failure arrives before any request. */
|
|
static void TestPublicKeyFailureAborts(WOLFSSH* ssh)
|
|
{
|
|
byte buf[32];
|
|
word32 sz;
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->connectState = CONNECT_KEYED;
|
|
|
|
sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf));
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz),
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
AssertTrue(ssh->error != 0);
|
|
}
|
|
|
|
|
|
/* Reject channel messages before user authentication completes. */
|
|
static void TestChannelBlockedBeforeAuth(WOLFSSH* ssh)
|
|
{
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->connectState = CONNECT_KEYED;
|
|
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN,
|
|
WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
}
|
|
|
|
|
|
/* Allow channel messages after user authentication completes. */
|
|
static void TestChannelAllowedAfterAuth(WOLFSSH* ssh)
|
|
{
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE;
|
|
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN,
|
|
WS_MSG_RECV);
|
|
AssertTrue(allowed);
|
|
}
|
|
|
|
static void TestChannelOpenCallbackRejectSendsOpenFail(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[128];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
inSz = BuildChannelOpenPacket("session", 7, 0x4000, 0x8000,
|
|
NULL, 0, in, sizeof(in));
|
|
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetChannelOpenCb(harness.ctx, RejectChannelOpenCb),
|
|
WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
AssertChannelOpenFailResponse(&harness, ret);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestSecondSessionChannelRejected(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in1[128];
|
|
byte in2[128];
|
|
word32 in1Sz;
|
|
word32 in2Sz;
|
|
int ret;
|
|
|
|
in1Sz = BuildChannelOpenPacket("session", 7, 0x4000, 0x8000,
|
|
NULL, 0, in1, sizeof(in1));
|
|
in2Sz = BuildChannelOpenPacket("session", 8, 0x4000, 0x8000,
|
|
NULL, 0, in2, sizeof(in2));
|
|
|
|
InitChannelOpenHarness(&harness, in1, in1Sz);
|
|
|
|
/* First channel open must succeed */
|
|
ret = DoReceive(harness.ssh);
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertIntEQ(harness.io.inOff, harness.io.inSz);
|
|
AssertTrue(harness.io.outSz > 0);
|
|
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
|
|
MSGID_CHANNEL_OPEN_CONF);
|
|
AssertIntEQ(harness.ssh->channelListSz, 1);
|
|
|
|
/* Repoint input and rewind outSz so the second response writes from offset 0.
|
|
* io.out and outCap need no change - both still refer to harness.out[256]. */
|
|
harness.io.in = in2;
|
|
harness.io.inSz = in2Sz;
|
|
harness.io.inOff = 0;
|
|
harness.io.outSz = 0;
|
|
|
|
/* Second session channel open must be rejected */
|
|
ret = DoReceive(harness.ssh);
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertIntEQ(harness.io.inOff, harness.io.inSz);
|
|
AssertTrue(harness.io.outSz > 0);
|
|
AssertIntEQ(ParseMsgId(harness.io.out, harness.io.outSz),
|
|
MSGID_CHANNEL_OPEN_FAIL);
|
|
AssertIntEQ(ParseChannelOpenFailRecipient(harness.io.out, harness.io.outSz),
|
|
8); /* RFC 4254 5.1: server must echo the peer's channel ID */
|
|
AssertIntEQ(ParseChannelOpenFailReason(harness.io.out, harness.io.outSz),
|
|
OPEN_ADMINISTRATIVELY_PROHIBITED);
|
|
AssertIntEQ(harness.ssh->channelListSz, 1); /* original channel intact */
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
#ifdef WOLFSSH_FWD
|
|
static void TestDirectTcpipRejectSendsOpenFail(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte extra[128];
|
|
byte in[192];
|
|
word32 extraSz;
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222,
|
|
extra, sizeof(extra));
|
|
inSz = BuildChannelOpenPacket("direct-tcpip", 9, 0x4000, 0x8000,
|
|
extra, extraSz, in, sizeof(in));
|
|
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, RejectDirectTcpipSetup, NULL),
|
|
WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
AssertChannelOpenFailResponse(&harness, ret);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestDirectTcpipNoFwdCbSendsOpenFail(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte extra[128];
|
|
byte in[192];
|
|
word32 extraSz;
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222,
|
|
extra, sizeof(extra));
|
|
inSz = BuildChannelOpenPacket("direct-tcpip", 9, 0x4000, 0x8000,
|
|
extra, extraSz, in, sizeof(in));
|
|
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
/* Intentionally do NOT register fwdCb */
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
AssertChannelOpenFailResponse(&harness, ret);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestForwardedTcpipOnServerSendsOpenFail(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte extra[128];
|
|
byte in[192];
|
|
word32 extraSz;
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
/* forwarded-tcpip is only ever sent server-to-client. A server receiving
|
|
* one is the wrong direction and must be rejected even with a fwdCb set,
|
|
* before the forwarding policy hook runs. */
|
|
extraSz = BuildDirectTcpipExtra("127.0.0.1", 8080, "127.0.0.1", 2222,
|
|
extra, sizeof(extra));
|
|
inSz = BuildChannelOpenPacket("forwarded-tcpip", 9, 0x4000, 0x8000,
|
|
extra, extraSz, in, sizeof(in));
|
|
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL),
|
|
WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
AssertChannelOpenFailResponse(&harness, ret);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdNoCbSendsFailure(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
/* no fwdCb registered */
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdNoCbNoReplyKeepsConnection(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
/* wantReply=0: no reply sent, connection must stay alive */
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 0, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
/* no fwdCb registered */
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertIntEQ(harness.io.outSz, 0); /* no reply sent */
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdWithCbSendsSuccess(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdPort0ReturnsAllocatedPort(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
/* A bind port of 0 asks the server to allocate a port. The success reply
|
|
* must carry the port the callback allocated, not the requested 0. */
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 0, 0, 1, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AllocatePortFwdCb, NULL),
|
|
WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS);
|
|
AssertIntEQ(ParseGlobalRequestSuccessPort(harness.io.out, harness.io.outSz),
|
|
REGRESS_FWD_ALLOC_PORT);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdPort0NoAllocSendsFailure(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
int cleanupCalled = 0;
|
|
|
|
/* The peer asked the server to choose a port (0), but the callback
|
|
* accepts without reporting one. The server must reject and tear the
|
|
* setup back down rather than reply with a non-compliant port 0. */
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 0, 0, 1, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, NoPortFwdCb, NULL),
|
|
WS_SUCCESS);
|
|
AssertIntEQ(wolfSSH_SetFwdCbCtx(harness.ssh, &cleanupCalled), WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
|
|
AssertIntEQ(cleanupCalled, 1);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdRemoteSetupErrorSendsFailure(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
int cleanupCalled = 0;
|
|
|
|
/* The callback rejects the remote setup with a WS_FwdCbError status (below
|
|
* WS_FWD_PORT_CHECK). The server must reply with failure and must not run
|
|
* cleanup, since the setup never succeeded. */
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 0, 1, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, RejectRemoteSetupFwdCb, NULL),
|
|
WS_SUCCESS);
|
|
AssertIntEQ(wolfSSH_SetFwdCbCtx(harness.ssh, &cleanupCalled), WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
|
|
AssertIntEQ(cleanupCalled, 0);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdPort0NoAllocNoReplyKeepsConnection(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
int cleanupCalled = 0;
|
|
|
|
/* Same port-0 rejection as above, but wantReply=0. The server must still
|
|
* tear the setup back down, send no reply, and keep the connection alive
|
|
* rather than treating the rejection as a fatal error. */
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 0, 0, 0, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, NoPortFwdCb, NULL),
|
|
WS_SUCCESS);
|
|
AssertIntEQ(wolfSSH_SetFwdCbCtx(harness.ssh, &cleanupCalled), WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertIntEQ(harness.io.outSz, 0); /* no reply sent */
|
|
AssertIntEQ(cleanupCalled, 1);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdCancelNoCbSendsFailure(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertGlobalRequestReply(&harness, MSGID_REQUEST_FAILURE);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
static void TestGlobalRequestFwdCancelWithCbSendsSuccess(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[256];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
inSz = BuildGlobalRequestFwdPacket("0.0.0.0", 2222, 1, 1, in, sizeof(in));
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertIntEQ(wolfSSH_CTX_SetFwdCb(harness.ctx, AcceptFwdCb, NULL), WS_SUCCESS);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
AssertGlobalRequestReply(&harness, MSGID_REQUEST_SUCCESS);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
|
|
/* Verify DoRequestSuccess correctly consumes a uint32 port payload (RFC 4254
|
|
* sec 4) without treating it as a length prefix, which would overrun the
|
|
* buffer and produce WS_BUFFER_E. */
|
|
static void TestRequestSuccessWithPortParsesCorrectly(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte payload[UINT32_SZ];
|
|
byte in[64];
|
|
word32 inSz;
|
|
word32 idx = 0;
|
|
int ret;
|
|
|
|
idx = AppendUint32(payload, sizeof(payload), idx, 2222);
|
|
inSz = WrapPacket(MSGID_REQUEST_SUCCESS, payload, idx, in, sizeof(in));
|
|
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
|
|
AssertIntEQ(ret, WS_SUCCESS);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
#endif
|
|
|
|
#ifdef WOLFSSH_AGENT
|
|
static void TestAgentChannelNullAgentSendsOpenFail(void)
|
|
{
|
|
ChannelOpenHarness harness;
|
|
byte in[128];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
inSz = BuildChannelOpenPacket("auth-agent@openssh.com", 11, 0x4000,
|
|
0x8000, NULL, 0, in, sizeof(in));
|
|
|
|
InitChannelOpenHarness(&harness, in, inSz);
|
|
AssertTrue(harness.ssh->agent == NULL);
|
|
|
|
ret = DoReceive(harness.ssh);
|
|
AssertChannelOpenFailResponse(&harness, ret);
|
|
|
|
FreeChannelOpenHarness(&harness);
|
|
}
|
|
#endif
|
|
|
|
|
|
/* Reject a peer KEXINIT once keying is in progress. */
|
|
static void TestKexInitRejectedWhenKeying(WOLFSSH* ssh)
|
|
{
|
|
int allowed;
|
|
|
|
ResetSession(ssh);
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
ssh->connectState = CONNECT_SERVER_KEXINIT_DONE;
|
|
|
|
allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXINIT, WS_MSG_RECV);
|
|
AssertFalse(allowed);
|
|
}
|
|
|
|
static void TestDisconnectSetsDisconnectError(void)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
MemIo io;
|
|
byte in[128];
|
|
byte out[32];
|
|
word32 inSz;
|
|
int ret;
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
|
|
AssertNotNull(ctx);
|
|
|
|
wolfSSH_SetIORecv(ctx, MemRecv);
|
|
wolfSSH_SetIOSend(ctx, MemSend);
|
|
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
|
|
inSz = BuildDisconnectPacket(WOLFSSH_DISCONNECT_BY_APPLICATION,
|
|
in, sizeof(in));
|
|
MemIoInit(&io, in, inSz, out, sizeof(out));
|
|
wolfSSH_SetIOReadCtx(ssh, &io);
|
|
wolfSSH_SetIOWriteCtx(ssh, &io);
|
|
|
|
ret = DoReceive(ssh);
|
|
AssertIntEQ(ret, WS_FATAL_ERROR);
|
|
AssertIntEQ(wolfSSH_get_error(ssh), WS_DISCONNECT);
|
|
AssertIntEQ(io.inOff, io.inSz);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
#ifdef WOLFSSH_SFTP
|
|
static void TestOct2DecRejectsInvalidNonLeadingDigit(void)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
byte invalidOct[] = "0718";
|
|
int ret;
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
|
|
AssertNotNull(ctx);
|
|
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
|
|
ret = wolfSSH_oct2dec(ssh, invalidOct, (word32)WSTRLEN((char*)invalidOct));
|
|
AssertIntEQ(ret, WS_BAD_ARGUMENT);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
#endif /* WOLFSSH_SFTP */
|
|
|
|
#if !(defined(WOLFSSH_NO_RSA) && defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256))
|
|
/* Ensure client buffer cleanup tolerates multiple invocations after allocs. */
|
|
static void TestClientBuffersIdempotent(void)
|
|
{
|
|
#ifndef WOLFSSH_NO_RSA
|
|
{
|
|
int ret;
|
|
|
|
ret = ClientUsePubKey("keys/gretel-key-rsa.pub");
|
|
AssertIntEQ(ret, 0);
|
|
ret = ClientSetPrivateKey("keys/gretel-key-rsa.pem");
|
|
AssertIntEQ(ret, 0);
|
|
|
|
ClientFreeBuffers();
|
|
/* Should be safe to call again without double free. */
|
|
ClientFreeBuffers();
|
|
}
|
|
#endif
|
|
|
|
#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256
|
|
{
|
|
int ret;
|
|
|
|
ret = ClientUsePubKey("keys/gretel-key-ecc.pub");
|
|
AssertIntEQ(ret, 0);
|
|
ret = ClientSetPrivateKey("keys/gretel-key-ecc.pem");
|
|
AssertIntEQ(ret, 0);
|
|
|
|
ClientFreeBuffers();
|
|
/* Should be safe to call again without double free. */
|
|
ClientFreeBuffers();
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
/* Simulate Ctrl+D (stdin EOF) during password prompt; expect failure but no crash. */
|
|
static void TestPasswordEofNoCrash(void)
|
|
{
|
|
WS_UserAuthData auth;
|
|
int savedStdin, devNull, ret;
|
|
|
|
if (!isatty(STDIN_FILENO)) {
|
|
return; /* headless/CI: skip tty-dependent check */
|
|
}
|
|
|
|
WMEMSET(&auth, 0, sizeof(auth));
|
|
|
|
savedStdin = dup(STDIN_FILENO);
|
|
AssertTrue(savedStdin >= 0);
|
|
devNull = open("/dev/null", O_RDONLY);
|
|
AssertTrue(devNull >= 0);
|
|
AssertTrue(dup2(devNull, STDIN_FILENO) >= 0);
|
|
|
|
ret = ClientUserAuth(WOLFSSH_USERAUTH_PASSWORD, &auth, NULL);
|
|
printf("TestPasswordEofNoCrash ret=%d\n", ret);
|
|
AssertIntEQ(ret, WOLFSSH_USERAUTH_FAILURE);
|
|
|
|
close(devNull);
|
|
dup2(savedStdin, STDIN_FILENO);
|
|
close(savedStdin);
|
|
|
|
ClientFreeBuffers();
|
|
}
|
|
|
|
/* When the send path is back-pressured (WANT_WRITE), wolfSSH_worker()
|
|
* still needs to service Receive() so window-adjusts can arrive and
|
|
* unblock the flow control. Verify the receive callback is invoked even
|
|
* when the first send attempt would block. */
|
|
#ifndef WOLFSSH_TEST_BLOCK
|
|
static int recvCallCount;
|
|
|
|
static int WantWriteSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
(void)ssh; (void)buf; (void)sz; (void)ctx;
|
|
return WS_CBIO_ERR_WANT_WRITE;
|
|
}
|
|
|
|
static int WantReadRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
(void)ssh; (void)buf; (void)sz; (void)ctx;
|
|
recvCallCount++;
|
|
return WS_CBIO_ERR_WANT_READ;
|
|
}
|
|
|
|
#ifndef WOLFSSH_TEST_BLOCK
|
|
static void TestWorkerReadsWhenSendWouldBlock(void)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
int ret;
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
|
|
AssertNotNull(ctx);
|
|
|
|
wolfSSH_SetIOSend(ctx, WantWriteSend);
|
|
wolfSSH_SetIORecv(ctx, WantReadRecv);
|
|
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
|
|
/* prime with pending outbound data so wolfSSH_SendPacket() is hit */
|
|
ssh->outputBuffer.length = 1;
|
|
ssh->outputBuffer.idx = 0;
|
|
ssh->outputBuffer.buffer[0] = 0;
|
|
|
|
recvCallCount = 0;
|
|
|
|
/* call worker; expect it to attempt send, notice back-pressure, and have
|
|
* invoked recv once. Depending on how DoReceive handles WANT_READ, the
|
|
* return may be WANT_WRITE or a fatal error; the important part is that
|
|
* recv was exercised. */
|
|
ret = wolfSSH_worker(ssh, NULL);
|
|
|
|
AssertTrue(ret == WS_WANT_WRITE || ret == WS_FATAL_ERROR);
|
|
AssertIntEQ(recvCallCount, 1);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
#endif /* !WOLFSSH_TEST_BLOCK */
|
|
#endif
|
|
|
|
|
|
#ifdef WOLFSSH_SFTP
|
|
/* Test that wolfSSH_SFTP_buffer_send() properly handles WS_WANT_WRITE when
|
|
* SSH output buffer has pending data. This is a regression test for
|
|
* the SFTP hang issue with non-blocking sockets.
|
|
*
|
|
* The fix checks for pending data in ssh->outputBuffer at the start of
|
|
* wolfSSH_SFTP_buffer_send() and returns WS_WANT_WRITE if the flush fails. */
|
|
static int sftpWantWriteCallCount = 0;
|
|
|
|
static int SftpWantWriteSendCb(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
(void)ssh; (void)buf; (void)ctx;
|
|
sftpWantWriteCallCount++;
|
|
/* First call returns WANT_WRITE, subsequent calls succeed */
|
|
if (sftpWantWriteCallCount == 1) {
|
|
return WS_CBIO_ERR_WANT_WRITE;
|
|
}
|
|
return (int)sz;
|
|
}
|
|
|
|
static int SftpDummyRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
|
|
{
|
|
(void)ssh; (void)buf; (void)sz; (void)ctx;
|
|
return WS_CBIO_ERR_WANT_READ;
|
|
}
|
|
|
|
static void TestSftpBufferSendPendingOutput(void)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
byte testData[16];
|
|
int ret;
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
|
|
AssertNotNull(ctx);
|
|
|
|
wolfSSH_SetIOSend(ctx, SftpWantWriteSendCb);
|
|
wolfSSH_SetIORecv(ctx, SftpDummyRecv);
|
|
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
|
|
WMEMSET(testData, 0x42, sizeof(testData));
|
|
|
|
/* Simulate pending data in SSH output buffer (as if previous send
|
|
* returned WS_WANT_WRITE and data was buffered).
|
|
* Note: outputBuffer is initialized by BufferInit() with bufferSz set
|
|
* to at least STATIC_BUFFER_LEN (16 bytes), so we use a smaller value. */
|
|
ssh->outputBuffer.length = 8; /* 8 bytes pending */
|
|
ssh->outputBuffer.idx = 0; /* none sent yet */
|
|
|
|
sftpWantWriteCallCount = 0;
|
|
|
|
/* Call wolfSSH_TestSftpBufferSend - should return WS_WANT_WRITE because
|
|
* the fix detects pending data in outputBuffer and tries to flush it,
|
|
* which fails with WS_WANT_WRITE from our callback.
|
|
*
|
|
* Before the fix, the function would ignore the pending SSH output buffer
|
|
* data and proceed to send new SFTP data, leading to a hang because the
|
|
* pending data was never flushed. */
|
|
ret = wolfSSH_TestSftpBufferSend(ssh, testData, sizeof(testData), 0);
|
|
AssertIntEQ(ret, WS_WANT_WRITE);
|
|
|
|
/* Verify the SSH output buffer still has pending data */
|
|
AssertTrue(ssh->outputBuffer.length > ssh->outputBuffer.idx);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
#if !defined(NO_WOLFSSH_SERVER) && !defined(USE_WINDOWS_API) && \
|
|
!defined(NO_FILESYSTEM)
|
|
/* Write a big-endian uint32 (the SFTP wire encoding). */
|
|
static void SftpPutU32(word32 val, byte* out)
|
|
{
|
|
out[0] = (byte)(val >> 24);
|
|
out[1] = (byte)(val >> 16);
|
|
out[2] = (byte)(val >> 8);
|
|
out[3] = (byte)(val);
|
|
}
|
|
|
|
/* Read a big-endian uint32 (the SFTP wire encoding). */
|
|
static word32 SftpGetU32(const byte* in)
|
|
{
|
|
return ((word32)in[0] << 24) | ((word32)in[1] << 16) |
|
|
((word32)in[2] << 8) | (word32)in[3];
|
|
}
|
|
|
|
/* Return 1 if needle occurs in haystack, 0 otherwise. */
|
|
static int SftpBufContains(const byte* hay, word32 haySz,
|
|
const byte* needle, word32 needleSz)
|
|
{
|
|
word32 i;
|
|
|
|
if (hay == NULL || needle == NULL || needleSz == 0 || haySz < needleSz) {
|
|
return 0;
|
|
}
|
|
for (i = 0; i + needleSz <= haySz; i++) {
|
|
if (WMEMCMP(hay + i, needle, needleSz) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Adversarial regression test for the SFTP file-handle IDOR class affecting
|
|
* RecvWrite -> pwrite, RecvRead -> pread, RecvFSetSTAT -> fchmod, and
|
|
* RecvClose -> close.
|
|
*
|
|
* Before the per-session opaque-handle rework these handlers accepted a raw
|
|
* file-descriptor integer from the peer after only a byte-length check and
|
|
* passed it straight to the matching syscall. This test opens a descriptor the
|
|
* server holds but never handed out over SFTP, then forges SFTP handles that
|
|
* encode that descriptor and confirms every handler rejects them and leaves the
|
|
* victim file untouched. A legitimately opened handle is exercised first as a
|
|
* positive control so a blanket-reject regression cannot pass silently.
|
|
*
|
|
* Two forged handle encodings are tried against every handler: the original
|
|
* exploit form (raw sizeof(WFD) bytes), which must now fail the 8-byte size
|
|
* check, and a correctly sized opaque ID whose first word is the victim fd,
|
|
* which must fail the per-session ownership lookup. POSIX only. */
|
|
static void TestSftpForgedHandleRejected(void)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
int f;
|
|
int rid = 100;
|
|
int rc;
|
|
WFD victimFd = -1;
|
|
WFD ownedRead;
|
|
WSTAT_T st;
|
|
word32 victimMode;
|
|
word32 idx;
|
|
word32 replySz;
|
|
word32 ofst[2] = {0, 0};
|
|
const byte* reply;
|
|
|
|
char ownedPath[64];
|
|
char victimPath[64];
|
|
const char sentinel[] = "DAEMON-PRIVATE-SECRET";
|
|
const char positive[] = "POSITIVE-CONTROL-DATA";
|
|
const char attack[] = "HANDLE-IDOR-OVERWRITE";
|
|
|
|
byte legitHandle[WOLFSSH_HANDLE_ID_SZ]; /* first opened file -> id {0,0} */
|
|
byte forgedId[WOLFSSH_HANDLE_ID_SZ]; /* 8-byte id whose word[0]==fd */
|
|
byte forgedRaw[sizeof(WFD)]; /* original raw-fd exploit bytes */
|
|
const byte* fHandle[2];
|
|
word32 fHandleSz[2];
|
|
byte pkt[256];
|
|
byte rbuf[64];
|
|
char cwd[WOLFSSH_MAX_FILENAME];
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
|
|
AssertNotNull(ctx);
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
|
|
/* unique per-process fixture names so parallel runs don't collide and we
|
|
* never clobber a same-named file in the working directory */
|
|
WSNPRINTF(ownedPath, sizeof(ownedPath), "wolfssh_poc_owned_%d.tmp",
|
|
(int)getpid());
|
|
WSNPRINTF(victimPath, sizeof(victimPath), "wolfssh_poc_victim_%d.tmp",
|
|
(int)getpid());
|
|
|
|
/* capture the handlers' buffered replies instead of leaking them */
|
|
AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS);
|
|
|
|
/* confine the server to an absolute working directory */
|
|
WMEMSET(cwd, 0, sizeof(cwd));
|
|
AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1));
|
|
AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS);
|
|
|
|
/* ---- positive control: legitimately open a file over SFTP ----
|
|
* RecvOpen assigns the first handle the per-session id {0,0}. */
|
|
WMEMSET(legitHandle, 0, sizeof(legitHandle));
|
|
|
|
idx = 0;
|
|
SftpPutU32((word32)WSTRLEN(ownedPath), pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, ownedPath, WSTRLEN(ownedPath));
|
|
idx += (word32)WSTRLEN(ownedPath);
|
|
SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT |
|
|
WOLFSSH_FXF_TRUNC, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* no attributes */
|
|
AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
|
|
/* write through the legitimate handle: must succeed */
|
|
idx = 0;
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, legitHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset hi */
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ; /* offset lo */
|
|
SftpPutU32((word32)(sizeof(positive) - 1), pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, positive, sizeof(positive) - 1);
|
|
idx += (word32)(sizeof(positive) - 1);
|
|
AssertIntEQ(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
|
|
/* confirm the bytes really landed in the owned file */
|
|
ownedRead = WOPEN(ssh->fs, ownedPath, WOLFSSH_O_RDONLY, 0);
|
|
AssertTrue(ownedRead >= 0);
|
|
WMEMSET(rbuf, 0, sizeof(rbuf));
|
|
rc = WPREAD(ssh->fs, ownedRead, rbuf, (word32)(sizeof(positive) - 1), ofst);
|
|
AssertIntEQ(rc, (int)(sizeof(positive) - 1));
|
|
AssertIntEQ(WMEMCMP(rbuf, positive, sizeof(positive) - 1), 0);
|
|
WCLOSE(ssh->fs, ownedRead);
|
|
|
|
/* positive control: FSTAT on the legitimate handle must report the owned
|
|
* file's real size (the bytes just written). The handle is the opaque id
|
|
* {0,0}; a regression that fstat()s the handle bytes as a raw descriptor
|
|
* would stat fd 0 (stdin) and return the wrong size, which this catches. */
|
|
idx = 0;
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, legitHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertIntEQ(wolfSSH_SFTP_RecvFSTAT(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz);
|
|
AssertNotNull(reply);
|
|
/* header(9) = len(4) type(1) reqId(4); ATTRS payload = flags(4) szHi(4) szLo(4) */
|
|
AssertTrue(replySz >= WOLFSSH_SFTP_HEADER + (UINT32_SZ * 3));
|
|
AssertIntEQ(reply[LENGTH_SZ], WOLFSSH_FTP_ATTRS);
|
|
AssertTrue((SftpGetU32(reply + WOLFSSH_SFTP_HEADER) & WOLFSSH_FILEATRB_SIZE)
|
|
!= 0);
|
|
AssertIntEQ((int)SftpGetU32(reply + WOLFSSH_SFTP_HEADER + (UINT32_SZ * 2)),
|
|
(int)(sizeof(positive) - 1));
|
|
|
|
/* ---- victim: a descriptor the server holds, never opened over SFTP ---- */
|
|
victimFd = WOPEN(ssh->fs, victimPath,
|
|
WOLFSSH_O_RDWR | WOLFSSH_O_CREAT | WOLFSSH_O_TRUNC, 0600);
|
|
AssertTrue(victimFd >= 0);
|
|
AssertIntEQ(WPWRITE(ssh->fs, victimFd, (byte*)sentinel,
|
|
(word32)(sizeof(sentinel) - 1), ofst),
|
|
(int)(sizeof(sentinel) - 1));
|
|
AssertIntEQ(WFCHMOD(ssh->fs, victimFd, 0600), 0);
|
|
AssertIntEQ(WFSTAT(ssh->fs, victimFd, &st), 0);
|
|
victimMode = (word32)(st.st_mode & 0777);
|
|
|
|
/* the two forged handle encodings every handler must reject */
|
|
WMEMCPY(forgedRaw, &victimFd, sizeof(WFD));
|
|
SftpPutU32((word32)victimFd, forgedId);
|
|
SftpPutU32(0, forgedId + UINT32_SZ);
|
|
fHandle[0] = forgedRaw; fHandleSz[0] = (word32)sizeof(WFD);
|
|
fHandle[1] = forgedId; fHandleSz[1] = WOLFSSH_HANDLE_ID_SZ;
|
|
|
|
for (f = 0; f < 2; f++) {
|
|
/* --- 4232: forged WRITE must not pwrite() the victim fd --- */
|
|
idx = 0;
|
|
SftpPutU32(fHandleSz[f], pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, fHandle[f], fHandleSz[f]); idx += fHandleSz[f];
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32((word32)(sizeof(attack) - 1), pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, attack, sizeof(attack) - 1);
|
|
idx += (word32)(sizeof(attack) - 1);
|
|
AssertTrue(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
/* --- 4346: forged READ must not pread() the victim fd --- */
|
|
idx = 0;
|
|
SftpPutU32(fHandleSz[f], pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, fHandle[f], fHandleSz[f]); idx += fHandleSz[f];
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32((word32)(sizeof(sentinel) - 1), pkt + idx); idx += UINT32_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvRead(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
/* the secret must never appear in the buffered reply */
|
|
reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz);
|
|
AssertIntEQ(SftpBufContains(reply, replySz, (const byte*)sentinel,
|
|
(word32)(sizeof(sentinel) - 1)), 0);
|
|
|
|
/* --- 4343: forged FSETSTAT must not fchmod() the victim fd --- */
|
|
idx = 0;
|
|
SftpPutU32(fHandleSz[f], pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, fHandle[f], fHandleSz[f]); idx += fHandleSz[f];
|
|
SftpPutU32(WOLFSSH_FILEATRB_PERM, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0777, pkt + idx); idx += UINT32_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvFSetSTAT(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
/* --- forged FSTAT must not fstat() the victim fd --- */
|
|
idx = 0;
|
|
SftpPutU32(fHandleSz[f], pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, fHandle[f], fHandleSz[f]); idx += fHandleSz[f];
|
|
AssertTrue(wolfSSH_SFTP_RecvFSTAT(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
/* --- 4349: forged CLOSE must not close() the victim fd --- */
|
|
idx = 0;
|
|
SftpPutU32(fHandleSz[f], pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, fHandle[f], fHandleSz[f]); idx += fHandleSz[f];
|
|
AssertTrue(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
}
|
|
|
|
/* ---- verify the victim was left completely untouched ---- */
|
|
WMEMSET(rbuf, 0, sizeof(rbuf));
|
|
rc = WPREAD(ssh->fs, victimFd, rbuf, (word32)(sizeof(sentinel) - 1), ofst);
|
|
AssertTrue(rc >= 0); /* 4349: not closed */
|
|
AssertIntEQ(rc, (int)(sizeof(sentinel) - 1));
|
|
AssertIntEQ(WMEMCMP(rbuf, sentinel, sizeof(sentinel) - 1), 0); /* 4232 */
|
|
AssertIntEQ(WFSTAT(ssh->fs, victimFd, &st), 0);
|
|
AssertIntEQ((int)(st.st_mode & 0777), (int)victimMode); /* 4343 */
|
|
|
|
/* the legitimate handle must still be open and owned: a real CLOSE on it
|
|
* succeeds, proving the forged closes did not disturb the session list */
|
|
idx = 0;
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, legitHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
|
|
if (victimFd >= 0) {
|
|
WCLOSE(ssh->fs, victimFd);
|
|
}
|
|
(void)WREMOVE(ssh->fs, ownedPath);
|
|
(void)WREMOVE(ssh->fs, victimPath);
|
|
wolfSSH_SFTP_TestRecvStateFree(ssh);
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
#ifndef NO_WOLFSSH_DIR
|
|
/* File and directory handle IDs are drawn from a single shared counter
|
|
* (ssh->handleIdCount), so a file handle and a directory handle can never
|
|
* collide. This test confirms that property and that each SFTP operation
|
|
* routes only to its own resource type:
|
|
* - a directory handle is rejected by the file handlers (READ/WRITE/FSETSTAT)
|
|
* - a file handle is rejected by the directory handler (READDIR)
|
|
* - closing the file leaves the directory handle valid (no cross-type close)
|
|
* With per-type counters both handles could share id {0,0} and a CLOSE could
|
|
* match the wrong resource. */
|
|
static void TestSftpHandleNamespaceIsolation(void)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
int rid = 200;
|
|
word32 idx;
|
|
word32 replySz;
|
|
const byte* reply;
|
|
const word32 hOff = WOLFSSH_SFTP_HEADER + UINT32_SZ; /* handle in reply */
|
|
|
|
char ownedPath[64];
|
|
const char attack[] = "WRONG-TYPE";
|
|
byte dirHandle[WOLFSSH_HANDLE_ID_SZ];
|
|
byte fileHandle[WOLFSSH_HANDLE_ID_SZ];
|
|
byte pkt[256];
|
|
char cwd[WOLFSSH_MAX_FILENAME];
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
|
|
AssertNotNull(ctx);
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS);
|
|
|
|
/* unique per-process fixture name (see TestSftpForgedHandleRejected) */
|
|
WSNPRINTF(ownedPath, sizeof(ownedPath), "wolfssh_poc_ns_%d.tmp",
|
|
(int)getpid());
|
|
|
|
WMEMSET(cwd, 0, sizeof(cwd));
|
|
AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1));
|
|
AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS);
|
|
|
|
/* open a directory -> first id from the shared counter */
|
|
idx = 0;
|
|
SftpPutU32(1, pkt + idx); idx += UINT32_SZ; /* path "." */
|
|
pkt[idx++] = '.';
|
|
AssertIntEQ(wolfSSH_SFTP_RecvOpenDir(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz);
|
|
AssertNotNull(reply);
|
|
AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ);
|
|
WMEMCPY(dirHandle, reply + hOff, WOLFSSH_HANDLE_ID_SZ);
|
|
|
|
/* open a file -> next id from the same counter */
|
|
idx = 0;
|
|
SftpPutU32((word32)WSTRLEN(ownedPath), pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, ownedPath, WSTRLEN(ownedPath));
|
|
idx += (word32)WSTRLEN(ownedPath);
|
|
SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT |
|
|
WOLFSSH_FXF_TRUNC, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz);
|
|
AssertNotNull(reply);
|
|
AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ);
|
|
WMEMCPY(fileHandle, reply + hOff, WOLFSSH_HANDLE_ID_SZ);
|
|
|
|
/* shared namespace: the file and directory handles must not collide */
|
|
AssertTrue(WMEMCMP(dirHandle, fileHandle, WOLFSSH_HANDLE_ID_SZ) != 0);
|
|
|
|
/* --- file handlers must reject the directory handle --- */
|
|
idx = 0; /* READ */
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, dirHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(16, pkt + idx); idx += UINT32_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvRead(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
idx = 0; /* WRITE */
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, dirHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32((word32)(sizeof(attack) - 1), pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, attack, sizeof(attack) - 1);
|
|
idx += (word32)(sizeof(attack) - 1);
|
|
AssertTrue(wolfSSH_SFTP_RecvWrite(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
idx = 0; /* FSETSTAT */
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, dirHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
SftpPutU32(WOLFSSH_FILEATRB_PERM, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0777, pkt + idx); idx += UINT32_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvFSetSTAT(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
idx = 0; /* FSTAT */
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, dirHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvFSTAT(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
/* --- directory handler must reject the file handle --- */
|
|
idx = 0; /* READDIR */
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, fileHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvReadDir(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
/* closing the file must not disturb the directory handle: close the file,
|
|
* then the directory close still succeeds (it would fail if the ids had
|
|
* collided and the file close had matched the directory). */
|
|
idx = 0;
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, fileHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
|
|
idx = 0;
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, dirHandle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertIntEQ(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
|
|
(void)WREMOVE(ssh->fs, ownedPath);
|
|
wolfSSH_SFTP_TestRecvStateFree(ssh);
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
#endif /* NO_WOLFSSH_DIR */
|
|
|
|
/* A failed close() must still drop the handle from the session tracking list;
|
|
* otherwise the stale descriptor lingers and is closed a second time when the
|
|
* session is torn down. Open a file, invalidate its descriptor out of band so
|
|
* RecvClose's close() fails, then confirm RecvClose reports the failure yet
|
|
* the handle is gone from the list. */
|
|
static void TestSftpCloseFailureRemovesHandle(void)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
int rid = 400;
|
|
word32 idx;
|
|
word32 replySz;
|
|
const byte* reply;
|
|
const word32 hOff = WOLFSSH_SFTP_HEADER + UINT32_SZ; /* handle in reply */
|
|
byte handle[WOLFSSH_HANDLE_ID_SZ];
|
|
byte pkt[256];
|
|
char cwd[WOLFSSH_MAX_FILENAME];
|
|
char path[64];
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
|
|
AssertNotNull(ctx);
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
AssertIntEQ(wolfSSH_SFTP_TestRecvStateInit(ssh), WS_SUCCESS);
|
|
|
|
/* unique per-process fixture name (see TestSftpForgedHandleRejected) */
|
|
WSNPRINTF(path, sizeof(path), "wolfssh_closefail_%d.tmp", (int)getpid());
|
|
|
|
WMEMSET(cwd, 0, sizeof(cwd));
|
|
AssertNotNull(WGETCWD(ssh->fs, cwd, sizeof(cwd) - 1));
|
|
AssertIntEQ(wolfSSH_SFTP_SetDefaultPath(ssh, cwd), WS_SUCCESS);
|
|
|
|
idx = 0;
|
|
SftpPutU32((word32)WSTRLEN(path), pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, path, WSTRLEN(path));
|
|
idx += (word32)WSTRLEN(path);
|
|
SftpPutU32(WOLFSSH_FXF_READ | WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT |
|
|
WOLFSSH_FXF_TRUNC, pkt + idx); idx += UINT32_SZ;
|
|
SftpPutU32(0, pkt + idx); idx += UINT32_SZ;
|
|
AssertIntEQ(wolfSSH_SFTP_RecvOpen(ssh, rid++, pkt, idx), WS_SUCCESS);
|
|
reply = wolfSSH_SFTP_TestRecvReply(ssh, &replySz);
|
|
AssertNotNull(reply);
|
|
AssertTrue(replySz >= hOff + WOLFSSH_HANDLE_ID_SZ);
|
|
WMEMCPY(handle, reply + hOff, WOLFSSH_HANDLE_ID_SZ);
|
|
AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), 1);
|
|
|
|
/* close the underlying descriptor behind the server's back */
|
|
AssertIntEQ(wolfSSH_SFTP_TestInvalidateHeadFd(ssh), WS_SUCCESS);
|
|
|
|
/* RecvClose now sees close() fail, but must still remove the handle */
|
|
idx = 0;
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
AssertIntEQ(wolfSSH_SFTP_TestFileHandleCount(ssh), 0);
|
|
|
|
/* a second close of the same handle finds nothing and still fails */
|
|
idx = 0;
|
|
SftpPutU32(WOLFSSH_HANDLE_ID_SZ, pkt + idx); idx += UINT32_SZ;
|
|
WMEMCPY(pkt + idx, handle, WOLFSSH_HANDLE_ID_SZ);
|
|
idx += WOLFSSH_HANDLE_ID_SZ;
|
|
AssertTrue(wolfSSH_SFTP_RecvClose(ssh, rid++, pkt, idx) != WS_SUCCESS);
|
|
|
|
(void)WREMOVE(ssh->fs, path);
|
|
wolfSSH_SFTP_TestRecvStateFree(ssh);
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
#endif /* !NO_WOLFSSH_SERVER && !USE_WINDOWS_API && !NO_FILESYSTEM */
|
|
|
|
#if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME)
|
|
static void TestNucleusMonthConversion(void)
|
|
{
|
|
AssertIntEQ(wolfSSH_TestNucleusMonthFromDate((word16)(1U << 5)), 0);
|
|
AssertIntEQ(wolfSSH_TestNucleusMonthFromDate((word16)(12U << 5)), 11);
|
|
}
|
|
#endif
|
|
#endif /* WOLFSSH_SFTP */
|
|
|
|
|
|
#ifdef WOLFSSH_KEYBOARD_INTERACTIVE
|
|
static int KbPreparePacketFailUserAuth(byte authType, WS_UserAuthData* authData,
|
|
void* ctx)
|
|
{
|
|
static byte* responses[1];
|
|
static word32 responseLens[1];
|
|
static byte response[] = "regress";
|
|
|
|
(void)ctx;
|
|
|
|
if (authType != WOLFSSH_USERAUTH_KEYBOARD || authData == NULL) {
|
|
return WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
|
|
}
|
|
|
|
if (authData->sf.keyboard.promptCount != 1 ||
|
|
authData->sf.keyboard.prompts == NULL) {
|
|
return WOLFSSH_USERAUTH_INVALID_PASSWORD;
|
|
}
|
|
|
|
responses[0] = response;
|
|
responseLens[0] = (word32)sizeof(response) - 1;
|
|
authData->sf.keyboard.responseCount = 1;
|
|
authData->sf.keyboard.responseLengths = responseLens;
|
|
authData->sf.keyboard.responses = responses;
|
|
|
|
return WOLFSSH_USERAUTH_SUCCESS;
|
|
}
|
|
|
|
static void TestKeyboardResponsePreparePacketFailure(WOLFSSH* ssh,
|
|
WOLFSSH_CTX* ctx)
|
|
{
|
|
byte* prompt;
|
|
byte** prompts;
|
|
byte* promptEcho;
|
|
int ret;
|
|
|
|
AssertNotNull(ssh);
|
|
AssertNotNull(ctx);
|
|
|
|
ResetSession(ssh);
|
|
wolfSSH_SetUserAuth(ctx, KbPreparePacketFailUserAuth);
|
|
|
|
prompt = (byte*)WMALLOC(9, ctx->heap, DYNTYPE_STRING); /* "Password" */
|
|
prompts = (byte**)WMALLOC(sizeof(byte*), ctx->heap, DYNTYPE_STRING);
|
|
promptEcho = (byte*)WMALLOC(1, ctx->heap, DYNTYPE_STRING);
|
|
AssertNotNull(prompt);
|
|
AssertNotNull(prompts);
|
|
AssertNotNull(promptEcho);
|
|
|
|
WMEMCPY(prompt, "Password", 8);
|
|
prompt[8] = '\0';
|
|
prompts[0] = prompt;
|
|
promptEcho[0] = 0;
|
|
|
|
ssh->kbAuth.promptCount = 1;
|
|
ssh->kbAuth.prompts = prompts;
|
|
ssh->kbAuth.promptEcho = promptEcho;
|
|
ssh->kbAuth.promptName = NULL;
|
|
ssh->kbAuth.promptInstruction = NULL;
|
|
ssh->kbAuth.promptLanguage = NULL;
|
|
|
|
/* Force PreparePacket() to fail with WS_OVERFLOW_E. */
|
|
ssh->outputBuffer.length = 0;
|
|
ssh->outputBuffer.idx = 1;
|
|
|
|
ret = SendUserAuthKeyboardResponse(ssh);
|
|
AssertIntEQ(ret, WS_OVERFLOW_E);
|
|
|
|
/* Ensure packet purge/reset happened cleanly. */
|
|
AssertIntEQ(ssh->outputBuffer.idx, 0);
|
|
AssertIntEQ(ssh->outputBuffer.length, 0);
|
|
|
|
/* Verify SendUserAuthKeyboardResponse() cleaned up kbAuth state. */
|
|
AssertIntEQ(ssh->kbAuth.promptCount, 0);
|
|
AssertTrue(ssh->kbAuth.prompts == NULL);
|
|
AssertTrue(ssh->kbAuth.promptEcho == NULL);
|
|
}
|
|
|
|
static void TestKeyboardResponseNoUserAuthCallback(WOLFSSH* ssh,
|
|
WOLFSSH_CTX* ctx)
|
|
{
|
|
int ret;
|
|
|
|
AssertNotNull(ssh);
|
|
AssertNotNull(ctx);
|
|
|
|
ResetSession(ssh);
|
|
wolfSSH_SetUserAuth(ctx, NULL);
|
|
|
|
ret = SendUserAuthKeyboardResponse(ssh);
|
|
AssertIntEQ(ret, WS_INVALID_STATE_E);
|
|
|
|
/* No packet should have been started. */
|
|
AssertIntEQ(ssh->outputBuffer.length, 0);
|
|
AssertIntEQ(ssh->outputBuffer.idx, 0);
|
|
}
|
|
|
|
static void TestKeyboardResponseNullSsh(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = SendUserAuthKeyboardResponse(NULL);
|
|
AssertIntEQ(ret, WS_BAD_ARGUMENT);
|
|
}
|
|
|
|
static void TestKeyboardResponseNullCtx(WOLFSSH* ssh)
|
|
{
|
|
WOLFSSH_CTX* savedCtx;
|
|
int ret;
|
|
|
|
AssertNotNull(ssh);
|
|
|
|
savedCtx = ssh->ctx;
|
|
ssh->ctx = NULL;
|
|
|
|
ret = SendUserAuthKeyboardResponse(ssh);
|
|
AssertIntEQ(ret, WS_BAD_ARGUMENT);
|
|
|
|
ssh->ctx = savedCtx;
|
|
}
|
|
#endif /* WOLFSSH_KEYBOARD_INTERACTIVE */
|
|
|
|
|
|
#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) \
|
|
&& !defined(WOLFSSH_NO_RSA) \
|
|
&& !defined(WOLFSSH_NO_CURVE25519_SHA256) \
|
|
&& !defined(WOLFSSH_NO_RSA_SHA2_256)
|
|
|
|
#define FPF_KEX_GOOD "ecdh-sha2-nistp256"
|
|
#define FPF_KEX_BAD "curve25519-sha256"
|
|
#define FPF_KEY_GOOD "ssh-rsa"
|
|
#define FPF_KEY_BAD "rsa-sha2-256"
|
|
|
|
/* Build a KEXINIT payload using the server ssh's own canned cipher/MAC lists
|
|
* so negotiation succeeds whichever AES/HMAC modes are compiled in. */
|
|
static word32 BuildKexInitPayload(WOLFSSH* ssh, const char* kexList,
|
|
const char* keyList, byte firstPacketFollows,
|
|
byte* out, word32 outSz)
|
|
{
|
|
word32 idx = 0;
|
|
|
|
/* cookie */
|
|
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, ssh->algoListCipher);
|
|
idx = AppendString(out, outSz, idx, ssh->algoListCipher);
|
|
idx = AppendString(out, outSz, idx, ssh->algoListMac);
|
|
idx = AppendString(out, outSz, idx, ssh->algoListMac);
|
|
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;
|
|
}
|
|
|
|
#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;
|
|
const char* keyList;
|
|
byte firstPacketFollows;
|
|
byte expectIgnore;
|
|
} FirstPacketFollowsCase;
|
|
|
|
static const FirstPacketFollowsCase firstPacketFollowsCases[] = {
|
|
{ "follows=0, guesses irrelevant: flag stays off",
|
|
FPF_KEX_BAD "," FPF_KEX_GOOD, FPF_KEY_BAD "," FPF_KEY_GOOD, 0, 0 },
|
|
{ "follows=1, both guesses match: do not skip",
|
|
FPF_KEX_GOOD, FPF_KEY_GOOD, 1, 0 },
|
|
{ "follows=1, KEX guess wrong: skip",
|
|
FPF_KEX_BAD "," FPF_KEX_GOOD, FPF_KEY_GOOD, 1, 1 },
|
|
{ "follows=1, host-key guess wrong: skip", /* regression case */
|
|
FPF_KEX_GOOD, FPF_KEY_BAD "," FPF_KEY_GOOD, 1, 1 },
|
|
{ "follows=1, both guesses wrong: skip",
|
|
FPF_KEX_BAD "," FPF_KEX_GOOD, FPF_KEY_BAD "," FPF_KEY_GOOD, 1, 1 },
|
|
};
|
|
|
|
static void RunFirstPacketFollowsCase(const FirstPacketFollowsCase* tc)
|
|
{
|
|
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);
|
|
|
|
payloadSz = BuildKexInitPayload(ssh, tc->kexList, tc->keyList,
|
|
tc->firstPacketFollows, payload, sizeof(payload));
|
|
|
|
/* DoKexInit's tail hashes and sends a response; on a stripped-down
|
|
* WOLFSSH without a loaded host key or a primed peer proto id, that
|
|
* tail errors. We only care about the parse path up through
|
|
* first_packet_follows, where ignoreNextKexMsg is set. */
|
|
(void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx);
|
|
|
|
AssertNotNull(ssh->handshake);
|
|
if (ssh->handshake->ignoreNextKexMsg != tc->expectIgnore) {
|
|
Fail(("ignoreNextKexMsg == %u (%s)",
|
|
tc->expectIgnore, tc->description),
|
|
("%u", ssh->handshake->ignoreNextKexMsg));
|
|
}
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
typedef int (*FirstPacketFollowsSkipFn)(WOLFSSH* ssh, byte* buf, word32 len,
|
|
word32* idx);
|
|
|
|
/* With ignoreNextKexMsg set, the target Do* handler must consume the packet,
|
|
* clear the flag, and not advance the peer's state past KEXINIT_DONE. */
|
|
static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn,
|
|
const char* label, byte endpointType, byte initState)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
byte payload[8];
|
|
word32 idx = 0;
|
|
int ret;
|
|
|
|
ctx = wolfSSH_CTX_new(endpointType, NULL);
|
|
AssertNotNull(ctx);
|
|
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
AssertNotNull(ssh->handshake);
|
|
|
|
ssh->handshake->ignoreNextKexMsg = 1;
|
|
if (endpointType == WOLFSSH_ENDPOINT_SERVER)
|
|
ssh->clientState = initState;
|
|
else
|
|
ssh->serverState = initState;
|
|
|
|
/* Garbage payload that must never be parsed when skipped. */
|
|
WMEMSET(payload, 0xAB, sizeof(payload));
|
|
|
|
ret = fn(ssh, payload, sizeof(payload), &idx);
|
|
if (ret != WS_SUCCESS) {
|
|
Fail(("%s returns WS_SUCCESS when skipping", label), ("%d", ret));
|
|
}
|
|
AssertIntEQ(idx, sizeof(payload));
|
|
AssertIntEQ(ssh->handshake->ignoreNextKexMsg, 0);
|
|
if (endpointType == WOLFSSH_ENDPOINT_SERVER)
|
|
AssertIntEQ(ssh->clientState, initState);
|
|
else
|
|
AssertIntEQ(ssh->serverState, initState);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
static void TestFirstPacketFollowsSkipped(void)
|
|
{
|
|
RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhInit,
|
|
"DoKexDhInit", WOLFSSH_ENDPOINT_SERVER, CLIENT_KEXINIT_DONE);
|
|
#ifndef WOLFSSH_NO_DH_GEX_SHA256
|
|
RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhGexRequest,
|
|
"DoKexDhGexRequest", WOLFSSH_ENDPOINT_SERVER, CLIENT_KEXINIT_DONE);
|
|
#endif
|
|
RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhReply,
|
|
"DoKexDhReply", WOLFSSH_ENDPOINT_CLIENT, SERVER_KEXINIT_DONE);
|
|
}
|
|
|
|
static void TestFirstPacketFollows(void)
|
|
{
|
|
size_t i;
|
|
size_t n = sizeof(firstPacketFollowsCases)
|
|
/ sizeof(firstPacketFollowsCases[0]);
|
|
|
|
for (i = 0; i < n; i++) {
|
|
RunFirstPacketFollowsCase(&firstPacketFollowsCases[i]);
|
|
}
|
|
TestFirstPacketFollowsSkipped();
|
|
}
|
|
|
|
/* RFC 4253 7.1: the trailing uint32 in KEXINIT is reserved and must be zero.
|
|
* DoKexInit used to advance begin by that value (treating it as a length);
|
|
* the current code rejects any non-zero value with WS_PARSE_E. Lock the
|
|
* strict-rejection branch in so a regression that re-relaxes the check or
|
|
* reverts to skipping skipSz bytes would fail this test. */
|
|
static void TestKexInitReservedNonZeroRejected(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);
|
|
|
|
payloadSz = BuildKexInitPayload(ssh, FPF_KEX_GOOD, FPF_KEY_GOOD,
|
|
0, payload, (word32)sizeof(payload));
|
|
|
|
/* BuildKexInitPayload puts the reserved uint32 in the final 4 bytes.
|
|
* Overwrite them with a non-zero value to exercise the strict branch. */
|
|
AssertTrue(payloadSz >= UINT32_SZ);
|
|
(void)AppendUint32(payload, (word32)sizeof(payload),
|
|
payloadSz - UINT32_SZ, 0xDEADBEEFu);
|
|
|
|
AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx),
|
|
WS_PARSE_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)
|
|
{
|
|
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;
|
|
Keys savedPeerKeys;
|
|
|
|
/* 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 */
|
|
WMEMCPY(&savedPeerKeys, &ssh->handshake->peerKeys, sizeof(Keys));
|
|
|
|
/* Peer has sent NewKeys; self has already sent its own (not keying). */
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
AssertIntEQ(wolfSSH_TestDoNewKeys(ssh, NULL, 0, NULL), 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);
|
|
AssertTrue(WMEMCMP(&ssh->peerKeys, &savedPeerKeys, sizeof(Keys)) == 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 */
|
|
WMEMCPY(&savedPeerKeys, &ssh->handshake->peerKeys, sizeof(Keys));
|
|
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
AssertIntEQ(wolfSSH_TestDoNewKeys(ssh, NULL, 0, NULL), 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 */
|
|
AssertTrue(WMEMCMP(&ssh->peerKeys, &savedPeerKeys, sizeof(Keys)) == 0);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
#endif /* !WOLFSSH_NO_AES_GCM */
|
|
|
|
/* Sub-test C: client mirror of A -- aes128-cbc C2S / aes256-ctr S2C.
|
|
* Client: C2S->keys (local), S2C->peerKeys (peer). After DoNewKeys,
|
|
* ssh->peer* must reflect the S2C (server outgoing) direction. */
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, 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; /* S2C on client */
|
|
expectedPeerMacId = ssh->handshake->peerMacId;
|
|
expectedPeerAeadMode = ssh->handshake->peerAeadMode;
|
|
AssertIntEQ(expectedPeerAeadMode, 0); /* non-AEAD S2C */
|
|
WMEMCPY(&savedPeerKeys, &ssh->handshake->peerKeys, sizeof(Keys));
|
|
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
AssertIntEQ(wolfSSH_TestDoNewKeys(ssh, NULL, 0, NULL), WS_SUCCESS);
|
|
|
|
AssertTrue(ssh->handshake == NULL);
|
|
|
|
/* ssh->peer* must reflect S2C direction, not C2S. */
|
|
AssertIntEQ(ssh->peerEncryptId, expectedPeerEncryptId);
|
|
AssertIntEQ(ssh->peerMacId, expectedPeerMacId);
|
|
AssertIntEQ(ssh->peerAeadMode, 0);
|
|
AssertTrue(WMEMCMP(&ssh->peerKeys, &savedPeerKeys, sizeof(Keys)) == 0);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
|
|
#ifndef WOLFSSH_NO_AES_GCM
|
|
/* Sub-test D: client mirror of B -- aes128-cbc C2S (non-AEAD) /
|
|
* aes256-gcm S2C (AEAD). Verifies peerAeadMode==1 (S2C AEAD) rather
|
|
* than 0 (C2S non-AEAD), catching regression to handshake->aeadMode. */
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, 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-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 (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);
|
|
|
|
expectedPeerEncryptId = ssh->handshake->peerEncryptId;
|
|
expectedPeerMacId = ssh->handshake->peerMacId;
|
|
expectedPeerAeadMode = ssh->handshake->peerAeadMode;
|
|
AssertIntEQ(expectedPeerAeadMode, 1); /* AEAD S2C */
|
|
WMEMCPY(&savedPeerKeys, &ssh->handshake->peerKeys, sizeof(Keys));
|
|
|
|
ssh->isKeying = WOLFSSH_PEER_IS_KEYING;
|
|
/* Exercise the len != 0 rejection while handshake is still allocated,
|
|
* so the guard is reached and not short-circuited by handshake == NULL. */
|
|
AssertIntEQ(wolfSSH_TestDoNewKeys(ssh, NULL, 1, NULL), WS_BAD_ARGUMENT);
|
|
AssertNotNull(ssh->handshake);
|
|
AssertIntEQ(wolfSSH_TestDoNewKeys(ssh, NULL, 0, NULL), WS_SUCCESS);
|
|
|
|
AssertTrue(ssh->handshake == NULL);
|
|
|
|
AssertIntEQ(ssh->peerEncryptId, expectedPeerEncryptId);
|
|
AssertIntEQ(ssh->peerMacId, expectedPeerMacId);
|
|
AssertIntEQ(ssh->peerAeadMode, 1); /* must be S2C AEAD, not C2S non-AEAD */
|
|
AssertTrue(WMEMCMP(&ssh->peerKeys, &savedPeerKeys, sizeof(Keys)) == 0);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
#endif /* !WOLFSSH_NO_AES_GCM */
|
|
|
|
/* Sub-test E: SELF_IS_KEYING guard - DoNewKeys must return
|
|
* WS_INVALID_STATE_E when the local side has not yet sent its own
|
|
* NEWKEYS (WOLFSSH_SELF_IS_KEYING still set). */
|
|
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);
|
|
|
|
/* peer has sent NEWKEYS but local NEWKEYS not yet sent.
|
|
* Key-material setup (k, h, sessionId, GenerateKeys) is intentionally
|
|
* absent - SELF_IS_KEYING must fire before key derivation reads those fields. */
|
|
ssh->isKeying = WOLFSSH_SELF_IS_KEYING | WOLFSSH_PEER_IS_KEYING;
|
|
AssertIntEQ(wolfSSH_TestDoNewKeys(ssh, NULL, 0, NULL), WS_INVALID_STATE_E);
|
|
|
|
/* DoNewKeys bailed before cleanup - handshake must still be allocated. */
|
|
AssertNotNull(ssh->handshake);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
#endif /* AES_CBC + AES_CTR + HMAC guards */
|
|
|
|
/* DoKexInit's PEER_IS_KEYING guard must return WS_INVALID_STATE_E when a
|
|
* second SSH_MSG_KEXINIT arrives while a key exchange is already in progress,
|
|
* preventing HandshakeInfo corruption if the outer IsMessageAllowed filter
|
|
* were ever bypassed. */
|
|
static void TestDoKexInitRejectsWhenPeerIsKeying(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);
|
|
|
|
payloadSz = BuildKexInitPayload(ssh, FPF_KEX_GOOD, FPF_KEY_GOOD, 0,
|
|
payload, (word32)sizeof(payload));
|
|
|
|
ssh->isKeying |= WOLFSSH_PEER_IS_KEYING;
|
|
|
|
AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx),
|
|
WS_INVALID_STATE_E);
|
|
/* wolfSSH_new pre-allocates handshake; DoKexInit must not free it on
|
|
* early return, so the ongoing key-exchange state is preserved. */
|
|
AssertNotNull(ssh->handshake);
|
|
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
}
|
|
|
|
#endif /* first_packet_follows coverage guard */
|
|
|
|
|
|
/* Regression coverage for issue 5575: the documented ssh://hostname form must
|
|
* set the hostname even without an explicit port, and a malformed destination
|
|
* with no host text must leave the hostname unset so the client can reject it.
|
|
*/
|
|
static void TestClientParseDestination(void)
|
|
{
|
|
char* user;
|
|
char* hostname;
|
|
word16 port;
|
|
|
|
/* ssh:// without an explicit port: hostname set, default port kept. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
AssertIntEQ(port, 22);
|
|
AssertTrue(user == NULL);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* ssh://user@host without a port: user and hostname set, default port. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://tester@127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, "tester"), 0);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
AssertIntEQ(port, 22);
|
|
WFREE(user, NULL, 0);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* ssh://host:port: explicit port parsed. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://127.0.0.1:2222",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
AssertIntEQ(port, 2222);
|
|
AssertTrue(user == NULL);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* ssh://user@host:port: all parts parsed. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://tester@127.0.0.1:2222",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, "tester"), 0);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
AssertIntEQ(port, 2222);
|
|
WFREE(user, NULL, 0);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* Plain (non-URI) hostname. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
AssertIntEQ(port, 22);
|
|
AssertTrue(user == NULL);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* Plain (non-URI) user@hostname. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("tester@127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, "tester"), 0);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
AssertIntEQ(port, 22);
|
|
WFREE(user, NULL, 0);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* Malformed URI with no host text: hostname stays unset. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertTrue(hostname == NULL);
|
|
AssertTrue(user == NULL);
|
|
|
|
/* Malformed URI with a user but no host text: user set, hostname unset. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://tester@",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, "tester"), 0);
|
|
AssertTrue(hostname == NULL);
|
|
WFREE(user, NULL, 0);
|
|
|
|
/* A pre-seeded user (as config_init_default does from $USER) is freed and
|
|
* replaced when the destination carries its own user. */
|
|
hostname = NULL; port = 22;
|
|
user = (char*)WMALLOC(WSTRLEN("seeded") + 1, NULL, 0);
|
|
AssertNotNull(user);
|
|
WMEMCPY(user, "seeded", WSTRLEN("seeded") + 1);
|
|
AssertIntEQ(ClientParseDestination("tester@127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, "tester"), 0);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
WFREE(user, NULL, 0);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* A leading '@' (no user text) is accepted with an empty user string. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://@127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, ""), 0);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
WFREE(user, NULL, 0);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* Non-URI "user@" with no host text: user set, hostname stays unset. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("tester@",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, "tester"), 0);
|
|
AssertTrue(hostname == NULL);
|
|
WFREE(user, NULL, 0);
|
|
|
|
/* Non-URI leading '@': empty user, hostname set (no ssh:// prefix). */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("@127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, ""), 0);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "127.0.0.1"), 0);
|
|
WFREE(user, NULL, 0);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* An out-of-range port is rejected (not silently truncated) and the
|
|
* caller's port and outputs are left untouched. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://127.0.0.1:70000",
|
|
&user, &hostname, &port), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(port, 22);
|
|
AssertTrue(user == NULL);
|
|
AssertTrue(hostname == NULL);
|
|
|
|
/* Non-numeric, trailing-garbage, and zero ports are rejected too. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://127.0.0.1:abc",
|
|
&user, &hostname, &port), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(port, 22);
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://127.0.0.1:22x",
|
|
&user, &hostname, &port), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(port, 22);
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://127.0.0.1:0",
|
|
&user, &hostname, &port), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(port, 22);
|
|
|
|
/* A valid in-range port is still accepted. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("ssh://127.0.0.1:65535",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertIntEQ(port, 65535);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* "ssh://" is only a prefix when it starts the string; a later occurrence
|
|
* is treated as ordinary host text, not a URI. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination("user@ssh://127.0.0.1",
|
|
&user, &hostname, &port), WS_SUCCESS);
|
|
AssertNotNull(user);
|
|
AssertIntEQ(WSTRCMP(user, "user"), 0);
|
|
AssertNotNull(hostname);
|
|
AssertIntEQ(WSTRCMP(hostname, "ssh://127.0.0.1"), 0);
|
|
AssertIntEQ(port, 22);
|
|
WFREE(user, NULL, 0);
|
|
WFREE(hostname, NULL, 0);
|
|
|
|
/* Each NULL output pointer (and a NULL input) is rejected. */
|
|
user = NULL; hostname = NULL; port = 22;
|
|
AssertIntEQ(ClientParseDestination(NULL, &user, &hostname, &port),
|
|
WS_BAD_ARGUMENT);
|
|
AssertIntEQ(ClientParseDestination("127.0.0.1", NULL, &hostname, &port),
|
|
WS_BAD_ARGUMENT);
|
|
AssertIntEQ(ClientParseDestination("127.0.0.1", &user, NULL, &port),
|
|
WS_BAD_ARGUMENT);
|
|
AssertIntEQ(ClientParseDestination("127.0.0.1", &user, &hostname, NULL),
|
|
WS_BAD_ARGUMENT);
|
|
/* No output should have been touched by the rejected calls. */
|
|
AssertTrue(user == NULL);
|
|
AssertTrue(hostname == NULL);
|
|
}
|
|
|
|
|
|
#ifdef WOLFSSH_TEST_INTERNAL
|
|
/* AppendKeyToFile must refuse a host name or key type that carries whitespace
|
|
* or control bytes, so an attacker-controlled value cannot inject extra fields
|
|
* or a forged entry into the known_hosts file. Exercised through the
|
|
* wolfSSH_TestAppendKeyToFile hook. */
|
|
static void TestAppendKeyToFile(void)
|
|
{
|
|
const char* path = "regress_known_hosts.tmp";
|
|
char buf[128];
|
|
word32 readSz;
|
|
|
|
/* A clean name and type write one well-formed, newline-terminated entry. */
|
|
(void)remove(path);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host.example.com",
|
|
"ssh-rsa", "AAAA"), WS_SUCCESS);
|
|
WMEMSET(buf, 0, sizeof(buf));
|
|
readSz = LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1);
|
|
AssertTrue(readSz > 0);
|
|
AssertIntEQ(WSTRCMP(buf, "host.example.com ssh-rsa AAAA\n"), 0);
|
|
|
|
/* A second call appends rather than truncating, preserving the first
|
|
* entry (the file is opened in append mode). */
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host2.example.com",
|
|
"ssh-ed25519", "BBBB"), WS_SUCCESS);
|
|
WMEMSET(buf, 0, sizeof(buf));
|
|
readSz = LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1);
|
|
AssertTrue(readSz > 0);
|
|
AssertIntEQ(WSTRCMP(buf,
|
|
"host.example.com ssh-rsa AAAA\n"
|
|
"host2.example.com ssh-ed25519 BBBB\n"), 0);
|
|
|
|
/* A newline in the name would forge an extra entry; it is rejected and
|
|
* nothing is written to the file. */
|
|
(void)remove(path);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path,
|
|
"127.0.0.1\nevil.example.com ssh-rsa BBBB", "ssh-rsa", "CCCC"),
|
|
WS_BAD_ARGUMENT);
|
|
AssertIntEQ(LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1), 0);
|
|
|
|
/* A space in the name would forge extra fields; it is rejected. */
|
|
(void)remove(path);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "real.example.com other",
|
|
"ssh-rsa", "CCCC"), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1), 0);
|
|
|
|
/* The key type is peer-supplied and is checked the same way: a newline in
|
|
* it is rejected and nothing is written. */
|
|
(void)remove(path);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host.example.com",
|
|
"ssh-rsa\nevil.example.com ssh-rsa DDDD", "EEEE"),
|
|
WS_BAD_ARGUMENT);
|
|
AssertIntEQ(LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1), 0);
|
|
|
|
/* Tab, carriage return, and DEL (0x7f) are rejected too, and each leaves
|
|
* the file unwritten. */
|
|
(void)remove(path);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host\tname", "ssh-rsa",
|
|
"CCCC"), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1), 0);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host\rname", "ssh-rsa",
|
|
"CCCC"), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1), 0);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host\x7fname", "ssh-rsa",
|
|
"CCCC"), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1), 0);
|
|
|
|
/* A NULL or empty name, type, or key is rejected and nothing is written.
|
|
* The key check guards fprintf against a NULL or empty value. */
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, NULL, "ssh-rsa", "CCCC"),
|
|
WS_BAD_ARGUMENT);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "", "ssh-rsa", "CCCC"),
|
|
WS_BAD_ARGUMENT);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host.example.com", "",
|
|
"CCCC"), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host.example.com", "ssh-rsa",
|
|
NULL), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "host.example.com", "ssh-rsa",
|
|
""), WS_BAD_ARGUMENT);
|
|
AssertIntEQ(LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1), 0);
|
|
|
|
/* High-bit (non-ASCII) bytes are only above the control range, so an
|
|
* internationalized host name is stored intact rather than rejected. This
|
|
* pins the unsigned-char handling: a signed-char compare would wrongly
|
|
* reject 0x80-0xff. */
|
|
(void)remove(path);
|
|
AssertIntEQ(wolfSSH_TestAppendKeyToFile(path, "h\xC3\xA9st", "ssh-rsa",
|
|
"AAAA"), WS_SUCCESS);
|
|
WMEMSET(buf, 0, sizeof(buf));
|
|
readSz = LoadFileBuffer(path, (byte*)buf, sizeof(buf) - 1);
|
|
AssertTrue(readSz > 0);
|
|
AssertIntEQ(WSTRCMP(buf, "h\xC3\xA9st ssh-rsa AAAA\n"), 0);
|
|
|
|
/* When the file cannot be opened (here, a path under a directory that does
|
|
* not exist), the function reports the failure rather than claiming
|
|
* success. */
|
|
AssertTrue(wolfSSH_TestAppendKeyToFile("regress_no_such_dir/known_hosts",
|
|
"host.example.com", "ssh-rsa", "AAAA") != WS_SUCCESS);
|
|
|
|
(void)remove(path);
|
|
}
|
|
#endif /* WOLFSSH_TEST_INTERNAL */
|
|
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
WOLFSSH_CTX* ctx;
|
|
WOLFSSH* ssh;
|
|
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
wolfSSH_Init();
|
|
|
|
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
|
|
AssertNotNull(ctx);
|
|
|
|
ssh = wolfSSH_new(ctx);
|
|
AssertNotNull(ssh);
|
|
|
|
TestClientParseDestination();
|
|
#ifdef WOLFSSH_TEST_INTERNAL
|
|
TestAppendKeyToFile();
|
|
#endif
|
|
TestAuthMessageBlockedDuringKeying(ssh);
|
|
TestUserauthFailureDuringKeying(ssh);
|
|
TestPasswordLeakAborts(ssh);
|
|
TestPrematureUserauthSuccess(ssh);
|
|
TestChannelSpoofSequence(ssh);
|
|
TestChannelSpoofAborts(ssh);
|
|
TestPublicKeyFailureBeforeRequest(ssh);
|
|
TestPublicKeyFailureAborts(ssh);
|
|
TestChannelBlockedBeforeAuth(ssh);
|
|
TestChannelAllowedAfterAuth(ssh);
|
|
TestChannelOpenCallbackRejectSendsOpenFail();
|
|
TestSecondSessionChannelRejected();
|
|
#ifdef WOLFSSH_FWD
|
|
TestDirectTcpipRejectSendsOpenFail();
|
|
TestDirectTcpipNoFwdCbSendsOpenFail();
|
|
TestForwardedTcpipOnServerSendsOpenFail();
|
|
TestGlobalRequestFwdNoCbSendsFailure();
|
|
TestGlobalRequestFwdNoCbNoReplyKeepsConnection();
|
|
TestGlobalRequestFwdWithCbSendsSuccess();
|
|
TestGlobalRequestFwdPort0ReturnsAllocatedPort();
|
|
TestGlobalRequestFwdPort0NoAllocSendsFailure();
|
|
TestGlobalRequestFwdRemoteSetupErrorSendsFailure();
|
|
TestGlobalRequestFwdPort0NoAllocNoReplyKeepsConnection();
|
|
TestGlobalRequestFwdCancelNoCbSendsFailure();
|
|
TestGlobalRequestFwdCancelWithCbSendsSuccess();
|
|
TestRequestSuccessWithPortParsesCorrectly();
|
|
#endif
|
|
#ifdef WOLFSSH_AGENT
|
|
TestAgentChannelNullAgentSendsOpenFail();
|
|
#endif
|
|
TestKexInitRejectedWhenKeying(ssh);
|
|
#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) && !defined(WOLFSSH_NO_RSA) \
|
|
&& !defined(WOLFSSH_NO_CURVE25519_SHA256) \
|
|
&& !defined(WOLFSSH_NO_RSA_SHA2_256)
|
|
TestFirstPacketFollows();
|
|
TestKexInitReservedNonZeroRejected();
|
|
TestDoKexInitRejectsWhenPeerIsKeying();
|
|
#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))
|
|
TestClientBuffersIdempotent();
|
|
#endif
|
|
TestPasswordEofNoCrash();
|
|
#ifndef WOLFSSH_TEST_BLOCK
|
|
TestWorkerReadsWhenSendWouldBlock();
|
|
#endif
|
|
|
|
#ifdef KEXDH_REPLY_REGRESS_KEX_ALGO
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_256
|
|
TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade();
|
|
#endif
|
|
#ifndef WOLFSSH_NO_RSA_SHA2_512
|
|
TestKexDhReplyRejectsRsaSha2_512SigNameDowngrade();
|
|
#endif
|
|
TestKexDhReplyRejectsNoPublicKeyCheck();
|
|
TestKexDhReplyRejectsWhenCallbackRejects();
|
|
#endif
|
|
|
|
#ifdef WOLFSSH_SFTP
|
|
TestOct2DecRejectsInvalidNonLeadingDigit();
|
|
TestSftpBufferSendPendingOutput();
|
|
#if !defined(NO_WOLFSSH_SERVER) && !defined(USE_WINDOWS_API) && \
|
|
!defined(NO_FILESYSTEM)
|
|
/* fenrir 4232/4343/4346/4349: forged SFTP file handles must be rejected */
|
|
TestSftpForgedHandleRejected();
|
|
#ifndef NO_WOLFSSH_DIR
|
|
/* file and directory handle IDs share one namespace and never cross-close */
|
|
TestSftpHandleNamespaceIsolation();
|
|
#endif
|
|
/* a failed close still drops the handle from the tracking list */
|
|
TestSftpCloseFailureRemovesHandle();
|
|
#endif
|
|
#if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME)
|
|
TestNucleusMonthConversion();
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef WOLFSSH_KEYBOARD_INTERACTIVE
|
|
TestKeyboardResponsePreparePacketFailure(ssh, ctx);
|
|
TestKeyboardResponseNoUserAuthCallback(ssh, ctx);
|
|
TestKeyboardResponseNullSsh();
|
|
TestKeyboardResponseNullCtx(ssh);
|
|
#endif
|
|
|
|
/* TODO: add app-level regressions that simulate stdin EOF/password
|
|
* prompts and mid-session socket closes once the test harness can
|
|
* drive the wolfssh client without real sockets/tty. */
|
|
|
|
ResetSession(ssh);
|
|
wolfSSH_free(ssh);
|
|
wolfSSH_CTX_free(ctx);
|
|
wolfSSH_Cleanup();
|
|
|
|
printf("regress: PASS\n");
|
|
return 0;
|
|
}
|