From 0cbdae43fa1ad6d5d930ff16e4be677e87ea0f7d Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Tue, 23 Jun 2026 11:43:25 +0900 Subject: [PATCH] tests: exercise DoKexDhReply host-key signature verify with corrupted signatures --- configure.ac | 5 +- tests/regress.c | 246 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 212 insertions(+), 39 deletions(-) diff --git a/configure.ac b/configure.ac index 07731f0b3..32ea8139f 100644 --- a/configure.ac +++ b/configure.ac @@ -312,9 +312,12 @@ AS_IF([test "x$ENABLED_SSHD" = "xyes"],[ AC_CONFIG_LINKS([keys/gretel-key-rsa.pub:keys/gretel-key-rsa.pub keys/gretel-key-rsa.pem:keys/gretel-key-rsa.pem + keys/gretel-key-ecc.pub:keys/gretel-key-ecc.pub + keys/gretel-key-ecc.pem:keys/gretel-key-ecc.pem keys/server-key-rsa.der:keys/server-key-rsa.der keys/server-key-ecc.der:keys/server-key-ecc.der - keys/server-key-ecc-521.der:keys/server-key-ecc-521.der]) + keys/server-key-ecc-521.der:keys/server-key-ecc-521.der + keys/server-key-ed25519.der:keys/server-key-ed25519.der]) # Set the automake conditionals. AM_CONDITIONAL([BUILD_EXAMPLE_SERVERS],[test "x$ENABLED_EXAMPLES" = "xyes"]) diff --git a/tests/regress.c b/tests/regress.c index 00a407fcd..6c30a1aaa 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -336,8 +336,13 @@ static void FreeChannelOpenHarness(ChannelOpenHarness* harness) wolfSSH_CTX_free(harness->ctx); } +/* Needs a server, a client, the key files, and at least one of the host key + * algorithms the tests below cover. */ #if !defined(NO_WOLFSSH_SERVER) && !defined(NO_WOLFSSH_CLIENT) && \ - !defined(WOLFSSH_NO_RSA) && !defined(NO_FILESYSTEM) + !defined(NO_FILESYSTEM) && \ + (!defined(WOLFSSH_NO_RSA_SHA2_256) || \ + !defined(WOLFSSH_NO_RSA_SHA2_512) || \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) || !defined(WOLFSSH_NO_ED25519)) #if !defined(WOLFSSH_NO_DH_GROUP14_SHA256) #define KEXDH_REPLY_REGRESS_KEX_ALGO "diffie-hellman-group14-sha256" #elif !defined(WOLFSSH_NO_DH_GROUP16_SHA512) @@ -391,12 +396,33 @@ static word32 LoadFileBuffer(const char* path, byte* buf, word32 bufSz) #define REGRESS_DUPLEX_QUEUE_SZ 32768U #define REGRESS_MUTATION_SCRATCH_SZ 4096U #define REGRESS_SERVER_KEY_PATH "keys/server-key-rsa.der" +#define REGRESS_SERVER_KEY_ECC_PATH "keys/server-key-ecc.der" +#define REGRESS_SERVER_KEY_ED25519_PATH "keys/server-key-ed25519.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 +/* Host key used by the tests that are not tied to one key algorithm. */ +#if !defined(WOLFSSH_NO_RSA_SHA2_256) + #define REGRESS_DEFAULT_KEY_ALGO "rsa-sha2-256" + #define REGRESS_DEFAULT_KEY_PATH REGRESS_SERVER_KEY_PATH +#elif !defined(WOLFSSH_NO_RSA_SHA2_512) + #define REGRESS_DEFAULT_KEY_ALGO "rsa-sha2-512" + #define REGRESS_DEFAULT_KEY_PATH REGRESS_SERVER_KEY_PATH +#elif !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + #define REGRESS_DEFAULT_KEY_ALGO "ecdsa-sha2-nistp256" + #define REGRESS_DEFAULT_KEY_PATH REGRESS_SERVER_KEY_ECC_PATH +#else + #define REGRESS_DEFAULT_KEY_ALGO "ssh-ed25519" + #define REGRESS_DEFAULT_KEY_PATH REGRESS_SERVER_KEY_ED25519_PATH +#endif + +/* KEXDH_REPLY mutation modes for the duplex mutator. */ +#define REGRESS_MUTATE_SIG_NAME 0 +#define REGRESS_MUTATE_SIG_DATA 1 + typedef struct { byte data[REGRESS_DUPLEX_QUEUE_SZ]; word32 len; @@ -409,6 +435,7 @@ typedef struct { word32 mutatedPackets; byte scratch[REGRESS_MUTATION_SCRATCH_SZ]; word32 scratchSz; + byte mode; } KexReplyMutator; typedef struct DuplexEndpoint { @@ -545,8 +572,12 @@ static int QueueAppend(DuplexQueue* queue, const byte* data, word32 dataSz) return WS_SUCCESS; } +/* Rebuild a single KEXDH_REPLY packet: REGRESS_MUTATE_SIG_NAME replaces the + * signature name (data intact); REGRESS_MUTATE_SIG_DATA keeps the name and + * flips one byte of the signature data to reach the host-key verify. */ static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz, - const char* replacement, byte* out, word32 outSz, word32* outLen) + byte mode, const char* replacement, byte* out, word32 outSz, + word32* outLen) { const byte* payload; const byte* pubKey; @@ -564,7 +595,10 @@ static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz, byte payloadBuf[REGRESS_MUTATION_SCRATCH_SZ]; byte innerSig[REGRESS_MUTATION_SCRATCH_SZ]; - if (replacement == NULL || out == NULL || outLen == NULL) { + if (out == NULL || outLen == NULL) { + return WS_BAD_ARGUMENT; + } + if (mode == REGRESS_MUTATE_SIG_NAME && replacement == NULL) { return WS_BAD_ARGUMENT; } @@ -614,14 +648,40 @@ static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz, 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); + if (mode == REGRESS_MUTATE_SIG_DATA) { + word32 dataStart; + word32 flipIdx; + + if (sigDataSz <= LENGTH_SZ) { + return WS_PARSE_E; + } + innerSigSz = AppendBlob(innerSig, sizeof(innerSig), innerSigSz, + sigName, sigNameSz); + /* Signature data value bytes start after their length prefix. */ + dataStart = innerSigSz + LENGTH_SZ; + innerSigSz = AppendBlob(innerSig, sizeof(innerSig), innerSigSz, + sigData, sigDataSz); + + if (sigNameSz == sizeof("ssh-ed25519") - 1 + && WMEMCMP(sigName, "ssh-ed25519", sigNameSz) == 0) { + /* Raw R || S: flip a byte of S. wc_ed25519_verify_msg rejects the + * tampered signature -- ret == SIG_VERIFY_E here, or ret == 0 with + * res != 1 on other builds; DoKexDhReply maps both to WS_ED25519_E. */ + flipIdx = dataStart + sigDataSz / 2; + } + else { + /* RSA raw signature or ECC mpint r: flip an interior data byte. */ + flipIdx = dataStart + LENGTH_SZ; + } + innerSig[flipIdx] ^= 0xFF; + } + else { + innerSigSz = AppendString(innerSig, sizeof(innerSig), innerSigSz, + replacement); + innerSigSz = AppendBlob(innerSig, sizeof(innerSig), innerSigSz, + sigData, sigDataSz); + } outerIdx = 0; outerIdx = AppendBlob(payloadBuf, sizeof(payloadBuf), outerIdx, @@ -634,12 +694,13 @@ static int RewriteSingleKexDhReplyPacket(const byte* packet, word32 packetSz, return 1; } -static int RewriteKexDhReplySignatureName(const byte* packet, word32 packetSz, - const char* replacement, byte* out, word32 outSz, word32* outLen) +static int RewriteKexDhReplyPacket(const byte* packet, word32 packetSz, + byte mode, const char* replacement, byte* out, word32 outSz, + word32* outLen) { word32 offset = 0; - if (packet == NULL || replacement == NULL || out == NULL || outLen == NULL) { + if (packet == NULL || out == NULL || outLen == NULL) { return WS_BAD_ARGUMENT; } @@ -653,7 +714,7 @@ static int RewriteKexDhReplySignatureName(const byte* packet, word32 packetSz, if (packet[offset + UINT32_SZ + PAD_LENGTH_SZ] == MSGID_KEXDH_REPLY) { rewriteRet = RewriteSingleKexDhReplyPacket(packet + offset, - curPacketSz, replacement, out, outSz, outLen); + curPacketSz, mode, replacement, out, outSz, outLen); if (rewriteRet <= 0) { return rewriteRet; } @@ -728,7 +789,8 @@ static int DuplexSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) word32 mutatedSz = 0; int mutateRet; - mutateRet = RewriteKexDhReplySignatureName(output, outputSz, "ssh-rsa", + mutateRet = RewriteKexDhReplyPacket(output, outputSz, + endpoint->mutator->mode, "ssh-rsa", endpoint->mutator->scratch, (word32)sizeof(endpoint->mutator->scratch), &mutatedSz); if (mutateRet < 0) { @@ -781,7 +843,8 @@ static void FreeKexReplyHarness(KexReplyHarness* harness) } static void InitKexReplyHarnessEx(KexReplyHarness* harness, - const char* keyAlgo, byte mutateReply, byte skipPublicKeyCheck) + const char* keyAlgo, const char* keyPath, byte mutateReply, + byte mutateMode, byte skipPublicKeyCheck) { byte keyBuf[2048]; word32 keySz; @@ -790,6 +853,7 @@ static void InitKexReplyHarnessEx(KexReplyHarness* harness, InitDuplexPair(&harness->clientIo, &harness->serverIo, &harness->mutator); harness->mutator.enabled = mutateReply; + harness->mutator.mode = mutateMode; harness->clientCtx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(harness->clientCtx); @@ -816,7 +880,7 @@ static void InitKexReplyHarnessEx(KexReplyHarness* harness, wolfSSH_CTX_SetPublicKeyCheck(harness->clientCtx, AcceptAnyServerHostKey); } - keySz = LoadFileBuffer(REGRESS_SERVER_KEY_PATH, keyBuf, sizeof(keyBuf)); + keySz = LoadFileBuffer(keyPath, keyBuf, sizeof(keyBuf)); AssertTrue(keySz > 0); AssertIntEQ(wolfSSH_CTX_UsePrivateKey_buffer(harness->serverCtx, keyBuf, keySz, WOLFSSH_FORMAT_ASN1), WS_SUCCESS); @@ -836,9 +900,10 @@ static void InitKexReplyHarnessEx(KexReplyHarness* harness, } static void InitKexReplyHarness(KexReplyHarness* harness, - const char* keyAlgo, byte mutateReply) + const char* keyAlgo, const char* keyPath, byte mutateReply) { - InitKexReplyHarnessEx(harness, keyAlgo, mutateReply, 0); + InitKexReplyHarnessEx(harness, keyAlgo, keyPath, + mutateReply, REGRESS_MUTATE_SIG_NAME, 0); } static int IsHandshakeRetryable(int err) @@ -890,12 +955,13 @@ static void RunKexReplyHandshake(KexReplyHarness* harness, result->steps = REGRESS_MAX_HANDSHAKE_STEPS; } -static void AssertHandshakeSucceeds(const char* keyAlgo) +static void AssertHandshakeSucceeds(const char* keyAlgo, const char* keyPath) { KexReplyHarness harness; KexReplyRunResult result; - InitKexReplyHarness(&harness, keyAlgo, 0); + InitKexReplyHarnessEx(&harness, keyAlgo, keyPath, 0, + REGRESS_MUTATE_SIG_NAME, 0); RunKexReplyHandshake(&harness, &result); AssertTrue(result.clientSuccess); @@ -907,12 +973,13 @@ static void AssertHandshakeSucceeds(const char* keyAlgo) FreeKexReplyHarness(&harness); } -static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo) +static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo, + const char* keyPath) { KexReplyHarness harness; KexReplyRunResult result; - InitKexReplyHarness(&harness, keyAlgo, 1); + InitKexReplyHarness(&harness, keyAlgo, keyPath, 1); RunKexReplyHandshake(&harness, &result); AssertIntEQ(harness.mutator.parseError, 0); @@ -929,25 +996,45 @@ static void AssertHandshakeRejectsMutatedReply(const char* keyAlgo) #ifndef WOLFSSH_NO_RSA_SHA2_256 static void TestKexDhReplyRejectsRsaSha2_256SigNameDowngrade(void) { - AssertHandshakeSucceeds("rsa-sha2-256"); - AssertHandshakeRejectsMutatedReply("rsa-sha2-256"); + AssertHandshakeSucceeds("rsa-sha2-256", REGRESS_SERVER_KEY_PATH); + AssertHandshakeRejectsMutatedReply("rsa-sha2-256", REGRESS_SERVER_KEY_PATH); } #endif #ifndef WOLFSSH_NO_RSA_SHA2_512 static void TestKexDhReplyRejectsRsaSha2_512SigNameDowngrade(void) { - AssertHandshakeSucceeds("rsa-sha2-512"); - AssertHandshakeRejectsMutatedReply("rsa-sha2-512"); + AssertHandshakeSucceeds("rsa-sha2-512", REGRESS_SERVER_KEY_PATH); + AssertHandshakeRejectsMutatedReply("rsa-sha2-512", REGRESS_SERVER_KEY_PATH); } #endif -static void AssertHandshakeRejectsWithNoPublicKeyCheck(const char* keyAlgo) +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 +static void TestKexDhReplyRejectsEccSigNameDowngrade(void) +{ + AssertHandshakeSucceeds("ecdsa-sha2-nistp256", REGRESS_SERVER_KEY_ECC_PATH); + AssertHandshakeRejectsMutatedReply("ecdsa-sha2-nistp256", + REGRESS_SERVER_KEY_ECC_PATH); +} +#endif + +#ifndef WOLFSSH_NO_ED25519 +static void TestKexDhReplyRejectsEd25519SigNameDowngrade(void) +{ + AssertHandshakeSucceeds("ssh-ed25519", REGRESS_SERVER_KEY_ED25519_PATH); + AssertHandshakeRejectsMutatedReply("ssh-ed25519", + REGRESS_SERVER_KEY_ED25519_PATH); +} +#endif + +static void AssertHandshakeRejectsWithNoPublicKeyCheck(const char* keyAlgo, + const char* keyPath) { KexReplyHarness harness; KexReplyRunResult result; - InitKexReplyHarnessEx(&harness, keyAlgo, 0, 1 /* skipPublicKeyCheck */); + InitKexReplyHarnessEx(&harness, keyAlgo, keyPath, 0, + REGRESS_MUTATE_SIG_NAME, 1 /* skipPublicKeyCheck */); RunKexReplyHandshake(&harness, &result); AssertFalse(result.clientSuccess); @@ -961,20 +1048,21 @@ static void AssertHandshakeRejectsWithNoPublicKeyCheck(const char* keyAlgo) 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"); + AssertHandshakeRejectsWithNoPublicKeyCheck(REGRESS_DEFAULT_KEY_ALGO, + REGRESS_DEFAULT_KEY_PATH); +#if !defined(WOLFSSH_NO_RSA_SHA2_256) && !defined(WOLFSSH_NO_RSA_SHA2_512) + AssertHandshakeRejectsWithNoPublicKeyCheck("rsa-sha2-512", + REGRESS_SERVER_KEY_PATH); #endif } -static void AssertHandshakeRejectsWhenCallbackRejects(const char* keyAlgo) +static void AssertHandshakeRejectsWhenCallbackRejects(const char* keyAlgo, + const char* keyPath) { KexReplyHarness harness; KexReplyRunResult result; - InitKexReplyHarness(&harness, keyAlgo, 0); + InitKexReplyHarness(&harness, keyAlgo, keyPath, 0); wolfSSH_CTX_SetPublicKeyCheck(harness.clientCtx, RejectAnyServerHostKey); RunKexReplyHandshake(&harness, &result); @@ -989,13 +1077,77 @@ static void AssertHandshakeRejectsWhenCallbackRejects(const char* keyAlgo) static void TestKexDhReplyRejectsWhenCallbackRejects(void) { + AssertHandshakeRejectsWhenCallbackRejects(REGRESS_DEFAULT_KEY_ALGO, + REGRESS_DEFAULT_KEY_PATH); +#if !defined(WOLFSSH_NO_RSA_SHA2_256) && !defined(WOLFSSH_NO_RSA_SHA2_512) + AssertHandshakeRejectsWhenCallbackRejects("rsa-sha2-512", + REGRESS_SERVER_KEY_PATH); +#endif +} + +/* Flip a byte of the host-key signature data while keeping the signature name + * valid, so the client reaches the cryptographic verify and must reject the + * tampered signature with the host-key-type specific error. */ +static void AssertHandshakeRejectsCorruptedSig(const char* keyAlgo, + const char* keyPath, int expectedErr) +{ + KexReplyHarness harness; + KexReplyRunResult result; + + InitKexReplyHarnessEx(&harness, keyAlgo, keyPath, 1, + REGRESS_MUTATE_SIG_DATA, 0); + 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); + AssertIntEQ(result.clientErr, expectedErr); + + FreeKexReplyHarness(&harness); +} + #ifndef WOLFSSH_NO_RSA_SHA2_256 - AssertHandshakeRejectsWhenCallbackRejects("rsa-sha2-256"); +static void TestKexDhReplyRejectsRsaSha2_256CorruptSig(void) +{ + AssertHandshakeSucceeds("rsa-sha2-256", REGRESS_SERVER_KEY_PATH); + AssertHandshakeRejectsCorruptedSig("rsa-sha2-256", + REGRESS_SERVER_KEY_PATH, WS_RSA_E); +} #endif + #ifndef WOLFSSH_NO_RSA_SHA2_512 - AssertHandshakeRejectsWhenCallbackRejects("rsa-sha2-512"); +static void TestKexDhReplyRejectsRsaSha2_512CorruptSig(void) +{ + AssertHandshakeSucceeds("rsa-sha2-512", REGRESS_SERVER_KEY_PATH); + AssertHandshakeRejectsCorruptedSig("rsa-sha2-512", + REGRESS_SERVER_KEY_PATH, WS_RSA_E); +} #endif + +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 +/* The byte flip preserves the mpint length of r, so wc_ecc_rs_raw_to_sig still + * succeeds and the failure surfaces as WS_ECC_E from the verify step. */ +static void TestKexDhReplyRejectsEccCorruptSig(void) +{ + AssertHandshakeSucceeds("ecdsa-sha2-nistp256", REGRESS_SERVER_KEY_ECC_PATH); + AssertHandshakeRejectsCorruptedSig("ecdsa-sha2-nistp256", + REGRESS_SERVER_KEY_ECC_PATH, WS_ECC_E); +} +#endif + +#ifndef WOLFSSH_NO_ED25519 +static void TestKexDhReplyRejectsEd25519CorruptSig(void) +{ + AssertHandshakeSucceeds("ssh-ed25519", REGRESS_SERVER_KEY_ED25519_PATH); + AssertHandshakeRejectsCorruptedSig("ssh-ed25519", + REGRESS_SERVER_KEY_ED25519_PATH, WS_ED25519_E); } +#endif #endif /* KEXDH_REPLY_REGRESS_KEX_ALGO */ @@ -4695,8 +4847,26 @@ int main(int argc, char** argv) #ifndef WOLFSSH_NO_RSA_SHA2_512 TestKexDhReplyRejectsRsaSha2_512SigNameDowngrade(); #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + TestKexDhReplyRejectsEccSigNameDowngrade(); + #endif + #ifndef WOLFSSH_NO_ED25519 + TestKexDhReplyRejectsEd25519SigNameDowngrade(); + #endif TestKexDhReplyRejectsNoPublicKeyCheck(); TestKexDhReplyRejectsWhenCallbackRejects(); + #ifndef WOLFSSH_NO_RSA_SHA2_256 + TestKexDhReplyRejectsRsaSha2_256CorruptSig(); + #endif + #ifndef WOLFSSH_NO_RSA_SHA2_512 + TestKexDhReplyRejectsRsaSha2_512CorruptSig(); + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + TestKexDhReplyRejectsEccCorruptSig(); + #endif + #ifndef WOLFSSH_NO_ED25519 + TestKexDhReplyRejectsEd25519CorruptSig(); + #endif #endif #ifdef WOLFSSH_SFTP