internal: send a disconnect when key exchange fails

- DoKexDhInit() sends SSH_MSG_DISCONNECT with KEY_EXCHANGE_FAILED on
  WS_CRYPTO_FAILED and WS_PUBKEY_REJECTED_E, DoKexDhGexGroup() on
  WS_CRYPTO_FAILED and WS_DH_SIZE_E.
- DoKexDhReply() sends KEY_EXCHANGE_FAILED on WS_CRYPTO_FAILED and
  HOST_KEY_NOT_VERIFIABLE on WS_PUBKEY_REJECTED_E.
- DuplexEndpoint records the reason code of a plaintext outbound
  disconnect, and InitKexReplyHarnessKex() takes an explicit KEX
  algorithm.
- New mutator modes shorten f and e, write a zero-length e, cut the
  GEX prime below the requested floor and set the GEX generator to 1;
  LocateSinglePacketPayload() finds the payload for all three
  single-packet rewriters.
- The harness KEX algorithm falls back to curve25519-sha256, then
  ecdh-sha2-nistp256, when no plain diffie-hellman-group is built.
- Tests assert the reason code on the wire for each new mode and for
  host key rejection, and assert no disconnect on a successful
  handshake.

Issue: F-8838
pull/1211/head
Yosuke Shimizu 2026-08-14 15:52:27 +09:00 committed by John Safranek
parent 78633a1a23
commit 40f9400f94
2 changed files with 459 additions and 47 deletions

View File

@ -5955,6 +5955,12 @@ static int DoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
ret = SendKexDhReply(ssh);
}
/* RFC 8731 sec. 3: a rejected key exchange input aborts with a
* disconnect */
if (ret == WS_CRYPTO_FAILED || ret == WS_PUBKEY_REJECTED_E) {
(void)SendDisconnect(ssh, WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
}
return ret;
}
@ -7638,6 +7644,15 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
if (sigKeyBlock_ptr)
WFREE(sigKeyBlock_ptr, ssh->ctx->heap, DYNTYPE_PRIVKEY);
/* RFC 4253 11.1: WS_PUBKEY_REJECTED_E here is only the host key check,
* which is server authentication, so it gets its own reason. */
if (ret == WS_CRYPTO_FAILED) {
(void)SendDisconnect(ssh, WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
}
else if (ret == WS_PUBKEY_REJECTED_E) {
(void)SendDisconnect(ssh,
WOLFSSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
}
WLOG(WS_LOG_DEBUG, "Leaving DoKexDhReply(), ret = %d", ret);
return ret;
}
@ -8042,6 +8057,12 @@ static int DoKexDhGexGroup(WOLFSSH* ssh,
ret = SendKexDhInit(ssh);
}
/* A group under the enforced floor (RFC 8270) or a bad generator ends
* the key exchange, so tell the peer why. */
if (ret == WS_CRYPTO_FAILED || ret == WS_DH_SIZE_E) {
(void)SendDisconnect(ssh, WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
}
return ret;
}
@ -16300,9 +16321,8 @@ int SendKexDhGexRequest(WOLFSSH* ssh)
}
}
/* RFC 4419 sec. 3 requires min <= preferred <= max on the wire. Clamp
* preferred into that range in both directions; riding it up with a raised
* min or down under a lowered max keeps the advertised triple ordered. */
/* Keep the advertised triple ordered: clamp preferred into [min, max]
* in both directions. */
if (ret == WS_SUCCESS) {
if (ssh->handshake->dhGexPreferredSz < ssh->handshake->dhGexMinSz)
ssh->handshake->dhGexPreferredSz = ssh->handshake->dhGexMinSz;

View File

@ -494,6 +494,10 @@ static void TestKbInfoResponseMismatchKeepsFraming(void)
#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"
#elif !defined(WOLFSSH_NO_CURVE25519_SHA256)
#define KEXDH_REPLY_REGRESS_KEX_ALGO "curve25519-sha256"
#elif !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256)
#define KEXDH_REPLY_REGRESS_KEX_ALGO "ecdh-sha2-nistp256"
#endif
#endif
@ -565,10 +569,29 @@ static word32 LoadFileBuffer(const char* path, byte* buf, word32 bufSz)
#define REGRESS_DEFAULT_KEY_PATH REGRESS_SERVER_KEY_ED25519_PATH
#endif
/* KEXDH_REPLY mutation modes for the duplex mutator. */
/* KEX algorithm for the truncated peer-key tests */
#if !defined(WOLFSSH_NO_CURVE25519_SHA256)
#define REGRESS_TRUNC_KEX_ALGO "curve25519-sha256"
#elif !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256)
#define REGRESS_TRUNC_KEX_ALGO "ecdh-sha2-nistp256"
#endif
/* KEX algorithm for the GEX group test */
#ifndef WOLFSSH_NO_DH_GEX_SHA256
#define REGRESS_GEX_KEX_ALGO "diffie-hellman-group-exchange-sha256"
/* Well under WOLFSSH_DH_GEX_MIN_BITS, so the client's range check
* rejects the group before any primality work. */
#define REGRESS_GEX_SHRUNK_PRIME_SZ 128U
#endif
#define REGRESS_MUTATE_SIG_NAME 0
#define REGRESS_MUTATE_SIG_DATA 1
#define REGRESS_MUTATE_SIG_NAME_OVERRUN 2
#define REGRESS_MUTATE_F_TRUNC 3
#define REGRESS_MUTATE_E_TRUNC 4
#define REGRESS_MUTATE_E_EMPTY 5
#define REGRESS_MUTATE_GEX_GROUP_SHRINK 6
#define REGRESS_MUTATE_GEX_GEN_BAD 7
typedef struct {
byte data[REGRESS_DUPLEX_QUEUE_SZ];
@ -590,7 +613,9 @@ typedef struct DuplexEndpoint {
DuplexQueue inbound;
struct DuplexEndpoint* peer;
KexReplyMutator* mutator;
word32 disconnectReason;
byte isServer;
byte sawDisconnect;
} DuplexEndpoint;
typedef struct {
@ -720,33 +745,12 @@ static int QueueAppend(DuplexQueue* queue, const byte* data, word32 dataSz)
return WS_SUCCESS;
}
/* SIG_NAME replaces the signature name; SIG_DATA flips a signature byte. */
static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz,
byte mode, const char* replacement, byte* out, word32 outSz,
word32* outLen)
/* Locate the payload of an unbundled packet carrying msgId. Returns 1 with
* payload set, 0 when the packet does not match, negative on bad framing. */
static int LocateSinglePacketPayload(const byte* packet, word32 packetSz,
byte msgId, const byte** payload, word32* payloadSz)
{
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 (out == NULL || outLen == NULL) {
return WS_BAD_ARGUMENT;
}
if (mode == REGRESS_MUTATE_SIG_NAME && replacement == NULL) {
return WS_BAD_ARGUMENT;
}
word32 packetLen, padLen;
if (packetSz < UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ) {
return 0;
@ -762,12 +766,50 @@ static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz,
return WS_PARSE_E;
}
if (packet[UINT32_SZ + PAD_LENGTH_SZ] != MSGID_KEXDH_REPLY) {
if (packet[UINT32_SZ + PAD_LENGTH_SZ] != msgId) {
return 0;
}
payload = packet + UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ;
payloadSz = packetSz - UINT32_SZ - PAD_LENGTH_SZ - MSG_ID_SZ - padLen;
*payload = packet + UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ;
*payloadSz = packetSz - UINT32_SZ - PAD_LENGTH_SZ - MSG_ID_SZ - padLen;
return 1;
}
/* SIG_NAME replaces the signature name; SIG_DATA flips a signature byte. */
static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz,
byte mode, 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 payloadSz;
word32 pubKeySz, fSz, sigBlobSz;
word32 sigNameSz, sigDataSz;
word32 idx = 0;
word32 innerIdx = 0;
word32 outerIdx = 0;
word32 innerSigSz;
int ret;
byte payloadBuf[REGRESS_MUTATION_SCRATCH_SZ];
byte innerSig[REGRESS_MUTATION_SCRATCH_SZ];
if (out == NULL || outLen == NULL) {
return WS_BAD_ARGUMENT;
}
if (mode == REGRESS_MUTATE_SIG_NAME && replacement == NULL) {
return WS_BAD_ARGUMENT;
}
ret = LocateSinglePacketPayload(packet, packetSz, MSGID_KEXDH_REPLY,
&payload, &payloadSz);
if (ret != 1) {
return ret;
}
if (ReadStringRef(&pubKeySz, &pubKey, payload, payloadSz, &idx) !=
WS_SUCCESS) {
@ -826,6 +868,17 @@ static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz,
innerSigSz = AppendUint32(innerSig, sizeof(innerSig), innerSigSz,
sigNameSz);
}
else if (mode == REGRESS_MUTATE_F_TRUNC) {
/* Signature blob is copied through untouched; only f shrinks below. */
if (fSz == 0) {
return WS_PARSE_E;
}
fSz--;
innerSigSz = AppendBlob(innerSig, sizeof(innerSig), innerSigSz,
sigName, sigNameSz);
innerSigSz = AppendBlob(innerSig, sizeof(innerSig), innerSigSz,
sigData, sigDataSz);
}
else {
innerSigSz = AppendString(innerSig, sizeof(innerSig), innerSigSz,
replacement);
@ -886,6 +939,159 @@ static int RewriteKexDhReplyPacket(const byte* packet, word32 packetSz,
return 0;
}
/* Drop one byte from the peer public key string e of a KEXDH_INIT. */
static int RewriteSingleKexDhInitPacket(const byte* packet, word32 packetSz,
byte mode, byte* out, word32 outSz, word32* outLen)
{
const byte* payload;
const byte* e;
word32 payloadSz;
word32 eSz;
word32 idx = 0;
word32 outerIdx = 0;
int ret;
byte payloadBuf[REGRESS_MUTATION_SCRATCH_SZ];
if (out == NULL || outLen == NULL) {
return WS_BAD_ARGUMENT;
}
ret = LocateSinglePacketPayload(packet, packetSz, MSGID_KEXDH_INIT,
&payload, &payloadSz);
if (ret != 1) {
return ret;
}
if (ReadStringRef(&eSz, &e, payload, payloadSz, &idx) != WS_SUCCESS) {
return WS_PARSE_E;
}
if (eSz == 0) {
return WS_PARSE_E;
}
/* E_EMPTY keeps the framing valid but leaves e zero length, which is what
* DoKexDhInit's range check rejects. E_TRUNC drops one byte instead. */
if (mode == REGRESS_MUTATE_E_EMPTY) {
outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx, e, 0);
}
else {
outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx, e,
eSz - 1);
}
*outLen = WrapPacket(MSGID_KEXDH_INIT, payloadBuf, outerIdx, out, outSz);
return 1;
}
#ifdef REGRESS_GEX_KEX_ALGO
/* Rewrite a KEXDH_GEX_GROUP: SHRINK puts the prime under the client's
* requested floor, GEN_BAD drops the generator below 2. */
static int RewriteSingleKexDhGexGroupPacket(const byte* packet,
word32 packetSz, byte mode, byte* out, word32 outSz, word32* outLen)
{
const byte* payload;
const byte* primeGroup;
const byte* generator;
word32 payloadSz;
word32 primeGroupSz, generatorSz;
word32 idx = 0;
word32 outerIdx = 0;
int ret;
byte badGenerator = 1;
byte payloadBuf[REGRESS_MUTATION_SCRATCH_SZ];
if (out == NULL || outLen == NULL) {
return WS_BAD_ARGUMENT;
}
ret = LocateSinglePacketPayload(packet, packetSz, MSGID_KEXDH_GEX_GROUP,
&payload, &payloadSz);
if (ret != 1) {
return ret;
}
if (ReadStringRef(&primeGroupSz, &primeGroup, payload, payloadSz, &idx)
!= WS_SUCCESS) {
return WS_PARSE_E;
}
if (ReadStringRef(&generatorSz, &generator, payload, payloadSz, &idx)
!= WS_SUCCESS) {
return WS_PARSE_E;
}
if (mode == REGRESS_MUTATE_GEX_GROUP_SHRINK) {
/* A group already at the target size would leave the test asserting
* nothing, so fail loudly rather than pass the group through. */
if (primeGroupSz <= REGRESS_GEX_SHRUNK_PRIME_SZ) {
return WS_PARSE_E;
}
primeGroupSz = REGRESS_GEX_SHRUNK_PRIME_SZ;
}
else {
/* Leave the prime valid so the range check passes and the generator
* check is what rejects the group. */
generator = &badGenerator;
generatorSz = 1;
}
outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx,
primeGroup, primeGroupSz);
outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx,
generator, generatorSz);
*outLen = WrapPacket(MSGID_KEXDH_GEX_GROUP, payloadBuf, outerIdx,
out, outSz);
return 1;
}
#endif /* REGRESS_GEX_KEX_ALGO */
/* SIG_*, F_TRUNC and the GEX_* modes rewrite the server's messages;
* E_TRUNC and E_EMPTY the client's init. */
static int MutatorTargetsEndpoint(byte mode, byte isServer)
{
if (mode == REGRESS_MUTATE_E_TRUNC || mode == REGRESS_MUTATE_E_EMPTY) {
return !isServer;
}
return isServer != 0;
}
/* Record a plaintext DISCONNECT leaving this endpoint. KEX-time traffic is
* unencrypted, so the reason code is readable straight off the wire. */
static void NoteOutboundDisconnect(DuplexEndpoint* endpoint,
const byte* packet, word32 packetSz)
{
word32 offset = 0;
word32 packetLen;
word32 padLen;
while (packetSz - offset >= UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ) {
word32 curPacketSz = ReadUint32(packet + offset) + UINT32_SZ;
/* Valid framing carries a pad length, a message id and the minimum
* padding, so a shorter declared packet cannot hold the id read
* below. */
if (curPacketSz > packetSz - offset ||
curPacketSz < UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ +
MIN_PAD_LENGTH) {
return;
}
/* The reason code lives in the payload, so bound it against the
* payload rather than the buffer: padding is not readable content. */
packetLen = curPacketSz - UINT32_SZ;
padLen = packet[offset + UINT32_SZ];
if (packet[offset + UINT32_SZ + PAD_LENGTH_SZ] == MSGID_DISCONNECT &&
packetLen >= padLen + PAD_LENGTH_SZ + MSG_ID_SZ + UINT32_SZ) {
endpoint->sawDisconnect = 1;
endpoint->disconnectReason = ReadUint32(packet + offset +
UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ);
return;
}
offset += curPacketSz;
}
}
static int DuplexRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
{
DuplexEndpoint* endpoint = (DuplexEndpoint*)ctx;
@ -923,14 +1129,14 @@ static int DuplexSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
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 &&
if (endpoint->mutator != NULL &&
endpoint->mutator->enabled &&
MutatorTargetsEndpoint(endpoint->mutator->mode,
endpoint->isServer) &&
endpoint->mutator->mutatedPackets == 0 &&
outputSz >= UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ &&
!(outputSz >= REGRESS_SSH_PROTO_PREFIX_SZ &&
@ -939,10 +1145,28 @@ static int DuplexSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
word32 mutatedSz = 0;
int mutateRet;
mutateRet = RewriteKexDhReplyPacket(output, outputSz,
endpoint->mutator->mode, endpoint->mutator->replaceName,
endpoint->mutator->scratch,
(word32)sizeof(endpoint->mutator->scratch), &mutatedSz);
if (endpoint->mutator->mode == REGRESS_MUTATE_E_TRUNC ||
endpoint->mutator->mode == REGRESS_MUTATE_E_EMPTY) {
/* KEXDH_INIT is never bundled, so no packet scan is needed. */
mutateRet = RewriteSingleKexDhInitPacket(output, outputSz,
endpoint->mutator->mode, endpoint->mutator->scratch,
(word32)sizeof(endpoint->mutator->scratch), &mutatedSz);
}
#ifdef REGRESS_GEX_KEX_ALGO
else if (endpoint->mutator->mode == REGRESS_MUTATE_GEX_GROUP_SHRINK ||
endpoint->mutator->mode == REGRESS_MUTATE_GEX_GEN_BAD) {
/* GEX_GROUP answers a request of its own, so it is not bundled. */
mutateRet = RewriteSingleKexDhGexGroupPacket(output, outputSz,
endpoint->mutator->mode, endpoint->mutator->scratch,
(word32)sizeof(endpoint->mutator->scratch), &mutatedSz);
}
#endif
else {
mutateRet = RewriteKexDhReplyPacket(output, outputSz,
endpoint->mutator->mode, endpoint->mutator->replaceName,
endpoint->mutator->scratch,
(word32)sizeof(endpoint->mutator->scratch), &mutatedSz);
}
if (mutateRet < 0) {
endpoint->mutator->parseError = mutateRet;
return WS_CBIO_ERR_GENERAL;
@ -956,6 +1180,16 @@ static int DuplexSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
}
}
/* Only scan plaintext records. AEAD modes send the packet length as
* cleartext AAD, so an encrypted payload byte can look like a message
* id and trip the sniffer. Every disconnect under test predates NEWKEYS. */
if (ssh != NULL && ssh->encryptId == ID_NONE &&
!(outputSz >= REGRESS_SSH_PROTO_PREFIX_SZ &&
WMEMCMP(output, REGRESS_SSH_PROTO_PREFIX,
REGRESS_SSH_PROTO_PREFIX_SZ) == 0)) {
NoteOutboundDisconnect(endpoint, output, outputSz);
}
ret = QueueAppend(&endpoint->peer->inbound, output, outputSz);
if (ret != WS_SUCCESS) {
return WS_CBIO_ERR_GENERAL;
@ -972,6 +1206,7 @@ static void InitDuplexPair(DuplexEndpoint* client, DuplexEndpoint* server,
client->peer = server;
server->peer = client;
client->mutator = mutator;
server->mutator = mutator;
server->isServer = 1;
}
@ -992,13 +1227,18 @@ static void FreeKexReplyHarness(KexReplyHarness* harness)
}
}
static void InitKexReplyHarnessEx(KexReplyHarness* harness,
const char* keyAlgo, const char* keyPath, byte mutateReply,
byte mutateMode, const char* replaceName, byte skipPublicKeyCheck)
static void InitKexReplyHarnessKex(KexReplyHarness* harness,
const char* kexAlgo, const char* keyAlgo, const char* keyPath,
byte mutateReply, byte mutateMode, const char* replaceName,
byte skipPublicKeyCheck)
{
byte keyBuf[2048];
word32 keySz;
if (kexAlgo == NULL) {
kexAlgo = KEXDH_REPLY_REGRESS_KEX_ALGO;
}
WMEMSET(harness, 0, sizeof(*harness));
InitDuplexPair(&harness->clientIo, &harness->serverIo, &harness->mutator);
@ -1011,10 +1251,10 @@ static void InitKexReplyHarnessEx(KexReplyHarness* harness,
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_SetAlgoListKex(harness->clientCtx, kexAlgo),
WS_SUCCESS);
AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(harness->serverCtx, kexAlgo),
WS_SUCCESS);
AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(harness->clientCtx, keyAlgo),
WS_SUCCESS);
AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(harness->serverCtx, keyAlgo),
@ -1050,6 +1290,14 @@ static void InitKexReplyHarnessEx(KexReplyHarness* harness,
WS_SUCCESS);
}
static void InitKexReplyHarnessEx(KexReplyHarness* harness,
const char* keyAlgo, const char* keyPath, byte mutateReply,
byte mutateMode, const char* replaceName, byte skipPublicKeyCheck)
{
InitKexReplyHarnessKex(harness, NULL, keyAlgo, keyPath, mutateReply,
mutateMode, replaceName, skipPublicKeyCheck);
}
static void InitKexReplyHarness(KexReplyHarness* harness,
const char* keyAlgo, const char* keyPath, byte mutateReply,
const char* replaceName)
@ -1121,6 +1369,10 @@ static void AssertHandshakeSucceeds(const char* keyAlgo, const char* keyPath)
AssertIntEQ(harness.mutator.mutatedPackets, 0);
AssertIntEQ(harness.client->connectState, CONNECT_SERVER_CHANNEL_REQUEST_DONE);
AssertIntEQ(harness.server->acceptState, ACCEPT_CLIENT_SESSION_ESTABLISHED);
/* A clean handshake sends no disconnect, so the sniffer the rejection
* tests rely on is proven not to fire on its own. */
AssertFalse(harness.clientIo.sawDisconnect);
AssertFalse(harness.serverIo.sawDisconnect);
FreeKexReplyHarness(&harness);
}
@ -1199,6 +1451,9 @@ static void AssertHandshakeRejectsWithNoPublicKeyCheck(const char* keyAlgo,
AssertTrue(result.clientErr != WS_WANT_READ && result.clientErr != WS_WANT_WRITE);
AssertIntEQ(result.clientErr, WS_PUBKEY_REJECTED_E);
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
AssertTrue(harness.clientIo.sawDisconnect);
AssertIntEQ(harness.clientIo.disconnectReason,
WOLFSSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
FreeKexReplyHarness(&harness);
}
@ -1228,6 +1483,9 @@ static void AssertHandshakeRejectsWhenCallbackRejects(const char* keyAlgo,
AssertTrue(result.clientErr != WS_WANT_READ && result.clientErr != WS_WANT_WRITE);
AssertIntEQ(result.clientErr, WS_PUBKEY_REJECTED_E);
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
AssertTrue(harness.clientIo.sawDisconnect);
AssertIntEQ(harness.clientIo.disconnectReason,
WOLFSSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
FreeKexReplyHarness(&harness);
}
@ -1322,6 +1580,131 @@ static void TestKexDhReplyRejectsSigNameOverrun(void)
FreeKexReplyHarness(&harness);
}
#ifdef REGRESS_TRUNC_KEX_ALGO
/* RFC 8731 sec. 3: a peer public key of the wrong length aborts with a
* disconnect. The truncated key is rejected during key agreement, before
* signature verification. */
static void TestKexDhReplyTruncatedFSendsDisconnect(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
InitKexReplyHarnessKex(&harness, REGRESS_TRUNC_KEX_ALGO,
REGRESS_DEFAULT_KEY_ALGO, REGRESS_DEFAULT_KEY_PATH, 1,
REGRESS_MUTATE_F_TRUNC, NULL, 0);
RunKexReplyHandshake(&harness, &result);
AssertIntEQ(harness.mutator.parseError, 0);
AssertIntEQ(harness.mutator.mutatedPackets, 1);
AssertFalse(result.clientSuccess);
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
AssertTrue(result.clientRet == WS_FATAL_ERROR);
AssertIntEQ(result.clientErr, WS_CRYPTO_FAILED);
AssertTrue(harness.clientIo.sawDisconnect);
AssertIntEQ(harness.clientIo.disconnectReason,
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
FreeKexReplyHarness(&harness);
}
static void TestKexDhInitTruncatedESendsDisconnect(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
InitKexReplyHarnessKex(&harness, REGRESS_TRUNC_KEX_ALGO,
REGRESS_DEFAULT_KEY_ALGO, REGRESS_DEFAULT_KEY_PATH, 1,
REGRESS_MUTATE_E_TRUNC, NULL, 0);
RunKexReplyHandshake(&harness, &result);
AssertIntEQ(harness.mutator.parseError, 0);
AssertIntEQ(harness.mutator.mutatedPackets, 1);
AssertFalse(result.serverSuccess);
AssertTrue(result.serverRet == WS_FATAL_ERROR);
AssertIntEQ(result.serverErr, WS_CRYPTO_FAILED);
AssertTrue(harness.serverIo.sawDisconnect);
AssertIntEQ(harness.serverIo.disconnectReason,
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
FreeKexReplyHarness(&harness);
}
/* Covers the WS_PUBKEY_REJECTED_E arm of the server's disconnect guard, which
* the truncated-e case cannot reach. */
static void TestKexDhInitEmptyESendsDisconnect(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
InitKexReplyHarnessKex(&harness, REGRESS_TRUNC_KEX_ALGO,
REGRESS_DEFAULT_KEY_ALGO, REGRESS_DEFAULT_KEY_PATH, 1,
REGRESS_MUTATE_E_EMPTY, NULL, 0);
RunKexReplyHandshake(&harness, &result);
AssertIntEQ(harness.mutator.parseError, 0);
AssertIntEQ(harness.mutator.mutatedPackets, 1);
AssertFalse(result.serverSuccess);
AssertIntEQ(result.serverErr, WS_PUBKEY_REJECTED_E);
AssertTrue(harness.serverIo.sawDisconnect);
AssertIntEQ(harness.serverIo.disconnectReason,
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
FreeKexReplyHarness(&harness);
}
#endif /* REGRESS_TRUNC_KEX_ALGO */
#ifdef REGRESS_GEX_KEX_ALGO
/* A GEX group below the floor this client enforces (RFC 8270) ends the key
* exchange. Covers the WS_DH_SIZE_E arm of the client's guard, which no
* other mutator reaches. */
static void TestKexDhGexGroupShrunkPrimeSendsDisconnect(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
InitKexReplyHarnessKex(&harness, REGRESS_GEX_KEX_ALGO,
REGRESS_DEFAULT_KEY_ALGO, REGRESS_DEFAULT_KEY_PATH, 1,
REGRESS_MUTATE_GEX_GROUP_SHRINK, NULL, 0);
RunKexReplyHandshake(&harness, &result);
AssertIntEQ(harness.mutator.parseError, 0);
AssertIntEQ(harness.mutator.mutatedPackets, 1);
AssertFalse(result.clientSuccess);
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
AssertTrue(result.clientRet == WS_FATAL_ERROR);
AssertIntEQ(result.clientErr, WS_DH_SIZE_E);
AssertTrue(harness.clientIo.sawDisconnect);
AssertIntEQ(harness.clientIo.disconnectReason,
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
FreeKexReplyHarness(&harness);
}
/* Covers the WS_CRYPTO_FAILED arm of the client's GEX guard, which the
* shrunk-prime case cannot reach. */
static void TestKexDhGexGroupBadGeneratorSendsDisconnect(void)
{
KexReplyHarness harness;
KexReplyRunResult result;
InitKexReplyHarnessKex(&harness, REGRESS_GEX_KEX_ALGO,
REGRESS_DEFAULT_KEY_ALGO, REGRESS_DEFAULT_KEY_PATH, 1,
REGRESS_MUTATE_GEX_GEN_BAD, NULL, 0);
RunKexReplyHandshake(&harness, &result);
AssertIntEQ(harness.mutator.parseError, 0);
AssertIntEQ(harness.mutator.mutatedPackets, 1);
AssertFalse(result.clientSuccess);
AssertFalse(harness.client->connectState >= CONNECT_KEYED);
AssertTrue(result.clientRet == WS_FATAL_ERROR);
AssertIntEQ(result.clientErr, WS_CRYPTO_FAILED);
AssertTrue(harness.clientIo.sawDisconnect);
AssertIntEQ(harness.clientIo.disconnectReason,
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
FreeKexReplyHarness(&harness);
}
#endif /* REGRESS_GEX_KEX_ALGO */
#endif /* KEXDH_REPLY_REGRESS_KEX_ALGO */
#ifndef NO_WOLFSSH_SERVER
@ -7749,6 +8132,15 @@ int main(int argc, char** argv)
TestKexDhReplyRejectsEd25519CorruptSig();
#endif
TestKexDhReplyRejectsSigNameOverrun();
#ifdef REGRESS_TRUNC_KEX_ALGO
TestKexDhReplyTruncatedFSendsDisconnect();
TestKexDhInitTruncatedESendsDisconnect();
TestKexDhInitEmptyESendsDisconnect();
#endif
#ifdef REGRESS_GEX_KEX_ALGO
TestKexDhGexGroupShrunkPrimeSendsDisconnect();
TestKexDhGexGroupBadGeneratorSendsDisconnect();
#endif
#endif
#ifdef WOLFSSH_SFTP