diff --git a/configure.ac b/configure.ac index 32ea8139f..49ffa9acb 100644 --- a/configure.ac +++ b/configure.ac @@ -225,6 +225,11 @@ AC_ARG_ENABLE([smallstack], [AS_HELP_STRING([--enable-smallstack],[Enable small stack (default: disabled)])], [ENABLED_SMALLSTACK=$enableval],[ENABLED_SMALLSTACK=no]) +# Allow "none" cipher/MAC +AC_ARG_ENABLE([none-cipher], + [AS_HELP_STRING([--enable-none-cipher],[Allow negotiating the insecure "none" cipher/MAC, disabling encryption/integrity (default: disabled)])], + [ENABLED_NONE_CIPHER=$enableval],[ENABLED_NONE_CIPHER=no]) + # use PAM lib AC_ARG_WITH([pam], [AS_HELP_STRING([--with-pam=PATH],[PATH to directory with the PAM library])], @@ -278,6 +283,8 @@ AS_IF([test "x$ENABLED_CERTS" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_CERTS"]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) +AS_IF([test "x$ENABLED_NONE_CIPHER" = "xyes"], + [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_ALLOW_NONE_CIPHER"]) AS_IF([test "x$ENABLED_SSHCLIENT" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SSHCLIENT"]) AS_IF([test "x$ENABLED_TPM" = "xyes"], @@ -377,6 +384,7 @@ AS_ECHO([" * echoserver shell support: $ENABLED_SHELL"]) AS_ECHO([" * scp: $ENABLED_SCP"]) AS_ECHO([" * sftp: $ENABLED_SFTP"]) AS_ECHO([" * sftp buffer zeroize: $ENABLED_SFTP_ZEROIZE"]) +AS_ECHO([" * allow none cipher/MAC: $ENABLED_NONE_CIPHER"]) AS_ECHO([" * sshd: $ENABLED_SSHD"]) AS_ECHO([" * ssh client: $ENABLED_SSHCLIENT"]) AS_ECHO([" * agent: $ENABLED_AGENT"]) diff --git a/src/internal.c b/src/internal.c index a5fa0ee3b..636cde839 100644 --- a/src/internal.c +++ b/src/internal.c @@ -208,6 +208,7 @@ static const char sshProtoIdStr[] = "SSH-2.0-wolfSSHv" LIBWOLFSSH_VERSION_STRING "\r\n"; +static const char sshProtoIdPrefix[] = "SSH-2.0-"; static const char OpenSSH[] = "SSH-2.0-OpenSSH"; @@ -1127,6 +1128,7 @@ WOLFSSH_CTX* CtxInit(WOLFSSH_CTX* ctx, byte side, void* heap) #endif /* WOLFSSH_CERTS */ ctx->windowSz = DEFAULT_WINDOW_SZ; ctx->maxPacketSz = DEFAULT_MAX_PACKET_SZ; + ctx->maxAuthAttempts = DEFAULT_MAX_AUTH_ATTEMPTS; ctx->sshProtoIdStr = sshProtoIdStr; ctx->algoListKex = cannedKexAlgoNames; if (side == WOLFSSH_ENDPOINT_CLIENT) { @@ -1462,6 +1464,7 @@ WOLFSSH* SshInit(WOLFSSH* ssh, WOLFSSH_CTX* ctx) ssh->ioWriteCtx = &ssh->wfd; /* set */ ssh->highwaterMark = ctx->highwaterMark; ssh->msgHighwaterMark = ctx->msgHighwaterMark; + ssh->maxAuthAttempts = ctx->maxAuthAttempts; ssh->highwaterCtx = (void*)ssh; ssh->reqSuccessCtx = (void*)ssh; ssh->fs = NULL; @@ -3195,22 +3198,23 @@ static int GenerateKeys(WOLFSSH* ssh, byte hashId, byte doKeyPad) } } - if (ret == WS_SUCCESS) + /* The "none" cipher has no key or IV; skip zero-length derivations. */ + if (ret == WS_SUCCESS && cK->ivSz > 0) ret = GenerateKey(hashId, 'A', cK->iv, cK->ivSz, ssh->k, ssh->kSz, ssh->h, ssh->hSz, ssh->sessionId, ssh->sessionIdSz, doKeyPad); - if (ret == WS_SUCCESS) + if (ret == WS_SUCCESS && sK->ivSz > 0) ret = GenerateKey(hashId, 'B', sK->iv, sK->ivSz, ssh->k, ssh->kSz, ssh->h, ssh->hSz, ssh->sessionId, ssh->sessionIdSz, doKeyPad); - if (ret == WS_SUCCESS) + if (ret == WS_SUCCESS && cK->encKeySz > 0) ret = GenerateKey(hashId, 'C', cK->encKey, cK->encKeySz, ssh->k, ssh->kSz, ssh->h, ssh->hSz, ssh->sessionId, ssh->sessionIdSz, doKeyPad); - if (ret == WS_SUCCESS) + if (ret == WS_SUCCESS && sK->encKeySz > 0) ret = GenerateKey(hashId, 'D', sK->encKey, sK->encKeySz, ssh->k, ssh->kSz, ssh->h, ssh->hSz, @@ -3434,7 +3438,9 @@ static const NameIdPair NameIdMap[] = { }; -byte NameToId(const char* name, word32 nameSz) +/* Look up a name and, on a match, return its category via 'type'. Returns the + * ID, or ID_UNKNOWN if the name is not in the table. 'type' may be NULL. */ +static byte NameToIdType(const char* name, word32 nameSz, byte* type) { byte id = ID_UNKNOWN; word32 i; @@ -3447,6 +3453,8 @@ byte NameToId(const char* name, word32 nameSz) XMEMCMP(name, NameIdMap[i].name, nameSz) == 0) { id = NameIdMap[i].id; + if (type != NULL) + *type = NameIdMap[i].type; break; } } @@ -3455,6 +3463,12 @@ byte NameToId(const char* name, word32 nameSz) } +byte NameToId(const char* name, word32 nameSz) +{ + return NameToIdType(name, nameSz, NULL); +} + + const char* IdToName(byte id) { const char* name = "unknown"; @@ -3494,6 +3508,79 @@ const char* NameByIndexType(byte type, word32* idx) } +int CheckAlgoList(const char* list, byte type) +{ + const char* name; + word32 nameSz, listSz, i; + int usableCount = 0; + int ret = WS_SUCCESS; + + if (list == NULL) + return WS_INVALID_ALGO_ID; + + listSz = (word32)WSTRLEN(list); + name = list; + nameSz = 0; + + for (i = 0; i <= listSz; i++) { + if (i == listSz || list[i] == ',') { + if (nameSz == 0) { + /* Only the empty element a single trailing comma leaves is + * legal; AlgoListSz() strips that one. Others would reach + * KEXINIT as a zero-length name, banned by RFC 4251. */ + if (i != listSz || i == 0) { + ret = WS_INVALID_ALGO_ID; + break; + } + } + else { + byte tokenType = TYPE_OTHER; + byte id = NameToIdType(name, nameSz, &tokenType); + + if (id == ID_NONE) { + int noneOk = 0; + + /* "none" names the null cipher/MAC, not a host key. It + * disables the transport, so it needs the build flag. */ +#ifdef WOLFSSH_ALLOW_NONE_CIPHER + if (type == TYPE_CIPHER || type == TYPE_MAC) { + noneOk = 1; + } +#endif + if (!noneOk) { + ret = WS_INVALID_ALGO_ID; + break; + } + usableCount++; + } + else if (id != ID_UNKNOWN) { + /* Wrong category is a caller mistake, not a build + * difference. */ + if (tokenType != type) { + ret = WS_INVALID_ALGO_ID; + break; + } + usableCount++; + } + /* Unknown names are skipped, not rejected: callers pass + * portable supersets, and negotiation ignores them too. */ + } + name = list + i + 1; + nameSz = 0; + } + else { + nameSz++; + } + } + + /* Nothing usable left; fail here, not as a later KEX failure. */ + if (ret == WS_SUCCESS && usableCount == 0) + ret = WS_INVALID_ALGO_ID; + + return ret; +} + + WOLFSSH_CHANNEL* ChannelNew(WOLFSSH* ssh, byte channelType, word32 initialWindowSz, word32 maxPacketSz) { @@ -3560,6 +3647,11 @@ void ChannelDelete(WOLFSSH_CHANNEL* channel, void* heap) if (channel->origin) WFREE(channel->origin, heap, DYNTYPE_STRING); #endif /* WOLFSSH_FWD */ + /* Scrub decrypted channel data before releasing the buffer. */ + if (channel->inputBuffer.buffer != NULL) { + WS_FORCEZERO(channel->inputBuffer.buffer, + channel->inputBuffer.bufferSz); + } WFREE(channel->inputBuffer.buffer, channel->inputBuffer.heap, DYNTYPE_BUFFER); if (channel->command) @@ -3829,9 +3921,16 @@ int GrowBuffer(WOLFSSH_BUFFER* buf, word32 sz) } if (!buf->dynamicFlag) { + /* Promoting off the static buffer; scrub its plaintext. */ + WS_FORCEZERO(buf->staticBuffer, STATIC_BUFFER_LEN); buf->dynamicFlag = 1; } else { + /* Scrub decrypted traffic before releasing the old buffer. + * Must cover bufferSz, not length: the compacting WMEMMOVE + * below lowers length while leaving a stale copy of the + * shifted-out bytes above it. */ + WS_FORCEZERO(buf->buffer, buf->bufferSz); WFREE(buf->buffer, buf->heap, DYNTYPE_BUFFER); } @@ -3876,8 +3975,16 @@ void ShrinkBuffer(WOLFSSH_BUFFER* buf, int forcedFree) if (buf->dynamicFlag) { WLOG(WS_LOG_DEBUG, "SB: releasing dynamic buffer"); + /* Scrub decrypted traffic before releasing the dynamic buffer. + * Must cover bufferSz, not length: GrowBuffer()'s compaction and + * the shift-down above both leave plaintext above length. */ + WS_FORCEZERO(buf->buffer, buf->bufferSz); WFREE(buf->buffer, buf->heap, DYNTYPE_BUFFER); } + if (forcedFree) { + /* Scrub plaintext a prior shift-down left in the static buffer. */ + WS_FORCEZERO(buf->staticBuffer, STATIC_BUFFER_LEN); + } buf->dynamicFlag = 0; buf->buffer = buf->staticBuffer; buf->bufferSz = STATIC_BUFFER_LEN; @@ -4217,6 +4324,10 @@ int GetString(char* s, word32* sSz, const byte* buf, word32 len, word32 *idx) if (s == NULL || sSz == NULL) result = WS_BAD_ARGUMENT; + /* Need room for the null char, and *sSz - 1 must not wrap. */ + if (result == WS_SUCCESS && *sSz == 0) + result = WS_BUFFER_E; + if (result == WS_SUCCESS) result = GetStringRef(&strSz, &str, buf, len, idx); @@ -4540,6 +4651,10 @@ static byte MatchIdLists(int side, const byte* left, word32 leftSz, static INLINE byte BlockSzForId(byte id) { switch (id) { + case ID_NONE: + /* RFC 4253 still pads to 8 under the null cipher, and a 0 here + * divides by zero in BundlePacket() once NEWKEYS installs it. */ + return MIN_BLOCK_SZ; #ifndef WOLFSSH_NO_AES_CBC case ID_AES128_CBC: case ID_AES192_CBC: @@ -7772,11 +7887,16 @@ static int DoExtInfoServerSigAlgs(WOLFSSH* ssh, byte algoId; peerSigIdSz = CountNameList(names, namesSz); - if (peerSigIdSz > 0) { - peerSigId = (byte*)WMALLOC(peerSigIdSz, ssh->ctx->heap, DYNTYPE_ID); - if (peerSigId == NULL) { - ret = WS_MEMORY_E; - } + if (peerSigIdSz == 0) { + /* Treat an empty list as no extension. GetNameListRaw() would reject + * the NULL idList and drop the connection. */ + WLOG(WS_LOG_DEBUG, "DEISSA: peer sent an empty server-sig-algs"); + return WS_SUCCESS; + } + + peerSigId = (byte*)WMALLOC(peerSigIdSz, ssh->ctx->heap, DYNTYPE_ID); + if (peerSigId == NULL) { + ret = WS_MEMORY_E; } if (ret == WS_SUCCESS) { @@ -7837,6 +7957,46 @@ static int DoExtInfo(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) } +/* Count a non-authenticating server-side userauth attempt and enforce the + * per-session limit. On reaching it, disconnect the peer and return a fatal + * error so the accept loop stops. Not for pending (would-block) or successful + * attempts; partial success and rejection are both charged. */ +static int CountUserAuthFailure(WOLFSSH* ssh) +{ + int ret = WS_SUCCESS; + + ssh->authFailures++; + if (ssh->authFailures >= ssh->maxAuthAttempts) { + WLOG(WS_LOG_DEBUG, "Max userauth attempts reached, disconnecting"); + (void)SendDisconnect(ssh, + WOLFSSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE); + ret = WS_USER_AUTH_E; + } + + return ret; +} + + +/* Send a userauth failure, counting the attempt when count is set. Counts + * even when the send returns WS_WANT_WRITE, which DoReceive() treats as + * non-fatal; a stalled peer would otherwise pipeline uncounted guesses. */ +static int SendUserAuthFailureCount(WOLFSSH* ssh, byte partialSuccess, + int count) +{ + int ret; + + ret = SendUserAuthFailure(ssh, partialSuccess); + if (count) { + int cntRet = CountUserAuthFailure(ssh); + + if (cntRet != WS_SUCCESS) + ret = cntRet; + } + + return ret; +} + + #ifdef WOLFSSH_ALLOW_USERAUTH_NONE /* Utility for DoUserAuthRequest() */ static int DoUserAuthRequestNone(WOLFSSH* ssh, WS_UserAuthData* authData, @@ -7865,14 +8025,13 @@ static int DoUserAuthRequestNone(WOLFSSH* ssh, WS_UserAuthData* authData, ret = WS_SUCCESS; } else if (ret == WOLFSSH_USERAUTH_REJECTED) { - WLOG(WS_LOG_DEBUG, "DUARN: password rejected"); + WLOG(WS_LOG_DEBUG, "DUARN: none rejected"); #ifndef NO_FAILURE_ON_REJECTED - ret = SendUserAuthFailure(ssh, 0); - if (ret == WS_SUCCESS) - ret = WS_USER_AUTH_E; - #else - ret = WS_USER_AUTH_E; + /* Count before failing: a send that blocks returns + * WS_WANT_WRITE, which isn't fatal on its own. */ + (void)SendUserAuthFailureCount(ssh, 0, 1); #endif + ret = WS_USER_AUTH_E; } else if (ret == WOLFSSH_USERAUTH_WOULD_BLOCK) { WLOG(WS_LOG_DEBUG, "DUARN: userauth callback would block"); @@ -7880,14 +8039,13 @@ static int DoUserAuthRequestNone(WOLFSSH* ssh, WS_UserAuthData* authData, } else { WLOG(WS_LOG_DEBUG, "DUARN: none check failed, retry"); - ret = SendUserAuthFailure(ssh, 0); + ret = SendUserAuthFailureCount(ssh, 0, 1); } } else { WLOG(WS_LOG_DEBUG, "DUARN: No user auth callback"); - ret = SendUserAuthFailure(ssh, 0); - if (ret == WS_SUCCESS) - ret = WS_FATAL_ERROR; + (void)SendUserAuthFailureCount(ssh, 0, 1); + ret = WS_FATAL_ERROR; } } @@ -7923,6 +8081,8 @@ static int DoUserAuthInfoResponse(WOLFSSH* ssh, } if (ret == WS_SUCCESS) { + /* Response arrived; the exchange is answered. */ + ssh->kbSetupPending = 0; WMEMSET(&authData, 0, sizeof(authData)); begin = *idx; kb = &authData.sf.keyboard; @@ -8033,13 +8193,16 @@ static int DoUserAuthInfoResponse(WOLFSSH* ssh, } if (authFailure || partialSuccess) { - ret = SendUserAuthFailure(ssh, partialSuccess); - if (ret == WS_SUCCESS && authRejected) { + /* Charge every non-authenticating outcome: an uncounted partial + * success replays forever, and a rejection isn't fatal on its own if + * the send blocks. */ + ret = SendUserAuthFailureCount(ssh, partialSuccess, 1); + if (authRejected) { ret = WS_USER_AUTH_E; } } else if (ret == WOLFSSH_USERAUTH_SUCCESS_ANOTHER) { - ret = SendUserAuthKeyboardRequest(ssh, &authData); + ret = SendUserAuthKeyboardRequest(ssh, &authData, 0); } else if (ret == WS_SUCCESS) { ssh->clientState = CLIENT_USERAUTH_DONE; @@ -8140,8 +8303,11 @@ static int DoUserAuthRequestPassword(WOLFSSH* ssh, WS_UserAuthData* authData, *idx = begin; if (authFailure || partialSuccess) { - ret = SendUserAuthFailure(ssh, partialSuccess); - if (ret == WS_SUCCESS && authRejected) { + /* Charge every non-authenticating outcome: an uncounted partial + * success replays forever, and a rejection isn't fatal on its own if + * the send blocks. */ + ret = SendUserAuthFailureCount(ssh, partialSuccess, 1); + if (authRejected) { ret = WS_USER_AUTH_E; } } @@ -9533,13 +9699,15 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData, } if (authFailure) { - ret = SendUserAuthFailure(ssh, 0); - if (ret == WS_SUCCESS && authRejected) { + /* Count even with partialSuccess set: the callback can grant partial + * success and the signature still fail. */ + ret = SendUserAuthFailureCount(ssh, 0, 1); + if (authRejected) { ret = WS_USER_AUTH_E; } } else if (partialSuccess && hasSig) { - ret = SendUserAuthFailure(ssh, 1); + ret = SendUserAuthFailureCount(ssh, 1, 1); } WLOG(WS_LOG_DEBUG, "Leaving DoUserAuthRequestPublicKey(), ret = %d", ret); @@ -9579,7 +9747,7 @@ static int DoUserAuthRequest(WOLFSSH* ssh, != ID_SERVICE_CONNECTION) { WLOG(WS_LOG_DEBUG, "DUAR: Invalid service name"); serviceValid = 0; - ret = SendUserAuthFailure(ssh, 0); + ret = SendUserAuthFailureCount(ssh, 0, 1); /* Consume all remaining data */ *idx = len; } @@ -9611,12 +9779,27 @@ static int DoUserAuthRequest(WOLFSSH* ssh, if (ret == WS_SUCCESS && serviceValid) { authNameId = NameToId((const char*)authData.authName, authData.authNameSz); ssh->authId = authNameId; + ssh->authRequests++; + /* Each method branch counts its own failures; it can't be centralized + * here since handlers return WS_SUCCESS for both success and + * failure-sent. */ if (authNameId == ID_USERAUTH_PASSWORD) ret = DoUserAuthRequestPassword(ssh, &authData, buf, len, &begin); #ifdef WOLFSSH_KEYBOARD_INTERACTIVE else if (authNameId == ID_USERAUTH_KEYBOARD) { - ret = SendUserAuthKeyboardRequest(ssh, &authData); + int counted = 0; + + /* Restarting before answering the prior INFO_REQUEST counts as a + * failure; else a peer loops here without ever responding. Clear + * the flag first so the send below doesn't charge it twice. */ + if (ssh->kbSetupPending) { + ssh->kbSetupPending = 0; + counted = 1; + ret = CountUserAuthFailure(ssh); + } + if (ret == WS_SUCCESS) + ret = SendUserAuthKeyboardRequest(ssh, &authData, counted); } #endif #if !defined(WOLFSSH_NO_RSA) || !defined(WOLFSSH_NO_ECDSA) \ @@ -9632,9 +9815,13 @@ static int DoUserAuthRequest(WOLFSSH* ssh, } #endif else { + /* Don't charge the opening "none" probe every client sends to + * learn the method list; OpenSSH exempts it too. */ + int countIt = !(authNameId == ID_NONE && ssh->authRequests == 1); + WLOG(WS_LOG_DEBUG, "DUAR: invalid userauth type: %s", IdToName(authNameId)); - ret = SendUserAuthFailure(ssh, 0); + ret = SendUserAuthFailureCount(ssh, 0, countIt); /* Consume all remaining data */ begin = len; } @@ -12136,7 +12323,7 @@ int DoProtoId(WOLFSSH* ssh) if (ssh->inputBuffer.length >= SSH_PROTO_SZ && WSTRNCMP((char*)ssh->inputBuffer.buffer, - ssh->ctx->sshProtoIdStr, SSH_PROTO_SZ) == 0) { + sshProtoIdPrefix, SSH_PROTO_SZ) == 0) { if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) ssh->clientState = CLIENT_VERSION_DONE; @@ -15847,8 +16034,11 @@ static int SendExtInfoServer(WOLFSSH* ssh) c32toa(keyAlgoNamesSz, output + idx); idx += LENGTH_SZ; - WMEMCPY(output + idx, ssh->algoListKeyAccepted, keyAlgoNamesSz); - idx += keyAlgoNamesSz; + /* List may be cleared (NULL). */ + if (keyAlgoNamesSz > 0) { + WMEMCPY(output + idx, ssh->algoListKeyAccepted, keyAlgoNamesSz); + idx += keyAlgoNamesSz; + } ssh->outputBuffer.length = idx; @@ -16052,7 +16242,8 @@ static int BuildUserAuthRequestKeyboard(WOLFSSH* ssh, byte* output, word32* idx, return ret; } -int SendUserAuthKeyboardRequest(WOLFSSH* ssh, WS_UserAuthData* authData) +int SendUserAuthKeyboardRequest(WOLFSSH* ssh, WS_UserAuthData* authData, + int counted) { byte* output; word32 idx; @@ -16078,10 +16269,20 @@ int SendUserAuthKeyboardRequest(WOLFSSH* ssh, WS_UserAuthData* authData) if (ret == WOLFSSH_USERAUTH_SUCCESS) { ret = WS_SUCCESS; } + else if (ret == WOLFSSH_USERAUTH_WOULD_BLOCK) { + /* Not a decline; retry on the next pass like the other + * handlers, uncharged. */ + WLOG(WS_LOG_DEBUG, "SUAKR: keyboard setup callback would block"); + ssh->kbSetupPending = 0; + return WS_AUTH_PENDING; + } else { WLOG(WS_LOG_DEBUG, "Issue with keyboard auth setup, try another " "auth type"); - return SendUserAuthFailure(ssh, 0); + /* No INFO_REQUEST sent, so charge the rejection here unless the + * caller already did; else a peer loops unbounded. */ + ssh->kbSetupPending = 0; + return SendUserAuthFailureCount(ssh, 0, !counted); } } @@ -16091,8 +16292,9 @@ int SendUserAuthKeyboardRequest(WOLFSSH* ssh, WS_UserAuthData* authData) } } - if (ret == WS_SUCCESS) + if (ret == WS_SUCCESS) { ssh->kbAuth.promptCount = authData->sf.keyboard.promptCount; + } payloadSz = MSG_ID_SZ; if (ret == WS_SUCCESS) { @@ -16121,7 +16323,11 @@ int SendUserAuthKeyboardRequest(WOLFSSH* ssh, WS_UserAuthData* authData) ret = wolfSSH_SendPacket(ssh); } - if ((ret != WS_WANT_WRITE) && (ret != WS_SUCCESS)) { + if ((ret == WS_WANT_WRITE) || (ret == WS_SUCCESS)) { + /* INFO_REQUEST sent or buffered; the exchange is now outstanding. */ + ssh->kbSetupPending = 1; + } + else { PurgePacket(ssh); } diff --git a/src/ssh.c b/src/ssh.c index 9f686975b..be0acf497 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -2339,8 +2339,10 @@ int wolfSSH_CTX_SetAlgoListKex(WOLFSSH_CTX* ctx, const char* list) int ret = WS_SSH_CTX_NULL_E; if (ctx) { - ctx->algoListKex = list; - ret = WS_SUCCESS; + ret = CheckAlgoList(list, TYPE_KEX); + if (ret == WS_SUCCESS) { + ctx->algoListKex = list; + } } return ret; @@ -2364,8 +2366,10 @@ int wolfSSH_SetAlgoListKex(WOLFSSH* ssh, const char* list) int ret = WS_SSH_NULL_E; if (ssh) { - ssh->algoListKex = list; - ret = WS_SUCCESS; + ret = CheckAlgoList(list, TYPE_KEX); + if (ret == WS_SUCCESS) { + ssh->algoListKex = list; + } } return ret; @@ -2389,8 +2393,17 @@ int wolfSSH_CTX_SetAlgoListKey(WOLFSSH_CTX* ctx, const char* list) int ret = WS_SSH_CTX_NULL_E; if (ctx) { - ctx->algoListKey = list; - ret = WS_SUCCESS; + /* NULL restores the server's auto-derived list; a client has none. */ + if (list == NULL) { + ret = (ctx->side == WOLFSSH_ENDPOINT_SERVER) ? + WS_SUCCESS : WS_INVALID_ALGO_ID; + } + else { + ret = CheckAlgoList(list, TYPE_KEY); + } + if (ret == WS_SUCCESS) { + ctx->algoListKey = list; + } } return ret; @@ -2414,8 +2427,18 @@ int wolfSSH_SetAlgoListKey(WOLFSSH* ssh, const char* list) int ret = WS_SSH_NULL_E; if (ssh) { - ssh->algoListKey = list; - ret = WS_SUCCESS; + /* NULL is server-only. */ + if (list == NULL) { + ret = (ssh->ctx != NULL + && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) ? + WS_SUCCESS : WS_INVALID_ALGO_ID; + } + else { + ret = CheckAlgoList(list, TYPE_KEY); + } + if (ret == WS_SUCCESS) { + ssh->algoListKey = list; + } } return ret; @@ -2439,8 +2462,10 @@ int wolfSSH_CTX_SetAlgoListCipher(WOLFSSH_CTX* ctx, const char* list) int ret = WS_SSH_CTX_NULL_E; if (ctx) { - ctx->algoListCipher = list; - ret = WS_SUCCESS; + ret = CheckAlgoList(list, TYPE_CIPHER); + if (ret == WS_SUCCESS) { + ctx->algoListCipher = list; + } } return ret; @@ -2464,8 +2489,10 @@ int wolfSSH_SetAlgoListCipher(WOLFSSH* ssh, const char* list) int ret = WS_SSH_NULL_E; if (ssh) { - ssh->algoListCipher = list; - ret = WS_SUCCESS; + ret = CheckAlgoList(list, TYPE_CIPHER); + if (ret == WS_SUCCESS) { + ssh->algoListCipher = list; + } } return ret; @@ -2489,8 +2516,10 @@ int wolfSSH_CTX_SetAlgoListMac(WOLFSSH_CTX* ctx, const char* list) int ret = WS_SSH_CTX_NULL_E; if (ctx) { - ctx->algoListMac = list; - ret = WS_SUCCESS; + ret = CheckAlgoList(list, TYPE_MAC); + if (ret == WS_SUCCESS) { + ctx->algoListMac = list; + } } return ret; @@ -2514,8 +2543,10 @@ int wolfSSH_SetAlgoListMac(WOLFSSH* ssh, const char* list) int ret = WS_SSH_NULL_E; if (ssh) { - ssh->algoListMac = list; - ret = WS_SUCCESS; + ret = CheckAlgoList(list, TYPE_MAC); + if (ret == WS_SUCCESS) { + ssh->algoListMac = list; + } } return ret; @@ -2539,8 +2570,11 @@ int wolfSSH_CTX_SetAlgoListKeyAccepted(WOLFSSH_CTX* ctx, const char* list) int ret = WS_SSH_CTX_NULL_E; if (ctx) { - ctx->algoListKeyAccepted = list; - ret = WS_SUCCESS; + /* NULL empties the advertised list, it does not restore a default. */ + ret = (list == NULL) ? WS_SUCCESS : CheckAlgoList(list, TYPE_KEY); + if (ret == WS_SUCCESS) { + ctx->algoListKeyAccepted = list; + } } return ret; @@ -2564,8 +2598,11 @@ int wolfSSH_SetAlgoListKeyAccepted(WOLFSSH* ssh, const char* list) int ret = WS_SSH_NULL_E; if (ssh) { - ssh->algoListKeyAccepted = list; - ret = WS_SUCCESS; + /* NULL empties the advertised list, it does not restore a default. */ + ret = (list == NULL) ? WS_SUCCESS : CheckAlgoList(list, TYPE_KEY); + if (ret == WS_SUCCESS) { + ssh->algoListKeyAccepted = list; + } } return ret; @@ -2644,6 +2681,51 @@ int wolfSSH_CTX_SetBanner(WOLFSSH_CTX* ctx, return WS_SUCCESS; } +int wolfSSH_CTX_SetMaxAuthAttempts(WOLFSSH_CTX* ctx, int value) +{ + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_SetMaxAuthAttempts()"); + + if (ctx == NULL) + return WS_BAD_ARGUMENT; + + /* A value <= 0 restores the built-in default. */ + if (value <= 0) + ctx->maxAuthAttempts = DEFAULT_MAX_AUTH_ATTEMPTS; + else + ctx->maxAuthAttempts = (word32)value; + + return WS_SUCCESS; +} + + +int wolfSSH_CTX_GetMaxAuthAttempts(WOLFSSH_CTX* ctx) +{ + return (ctx == NULL) ? WS_BAD_ARGUMENT : (int)ctx->maxAuthAttempts; +} + + +int wolfSSH_SetMaxAuthAttempts(WOLFSSH* ssh, int value) +{ + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_SetMaxAuthAttempts()"); + + if (ssh == NULL) + return WS_BAD_ARGUMENT; + + /* A value <= 0 restores the built-in default. */ + if (value <= 0) + ssh->maxAuthAttempts = DEFAULT_MAX_AUTH_ATTEMPTS; + else + ssh->maxAuthAttempts = (word32)value; + + return WS_SUCCESS; +} + + +int wolfSSH_GetMaxAuthAttempts(WOLFSSH* ssh) +{ + return (ssh == NULL) ? WS_BAD_ARGUMENT : (int)ssh->maxAuthAttempts; +} + int wolfSSH_CTX_SetSshProtoIdStr(WOLFSSH_CTX* ctx, const char* protoIdStr) { diff --git a/tests/api.c b/tests/api.c index bda465eef..88d0955b7 100644 --- a/tests/api.c +++ b/tests/api.c @@ -3654,13 +3654,58 @@ static void test_wolfSSH_RealPath(void) { ; } #endif +static void test_wolfSSH_SetMaxAuthAttempts(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + int defaultValue; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + /* NULL is rejected. */ + AssertIntEQ(wolfSSH_CTX_SetMaxAuthAttempts(NULL, 3), WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_CTX_GetMaxAuthAttempts(NULL), WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_SetMaxAuthAttempts(NULL, 3), WS_BAD_ARGUMENT); + AssertIntEQ(wolfSSH_GetMaxAuthAttempts(NULL), WS_BAD_ARGUMENT); + + defaultValue = wolfSSH_CTX_GetMaxAuthAttempts(ctx); + AssertIntGT(defaultValue, 0); + + /* A positive value is accepted. */ + AssertIntEQ(wolfSSH_CTX_SetMaxAuthAttempts(ctx, 3), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_GetMaxAuthAttempts(ctx), 3); + + /* Zero and negative values restore the default without error. */ + AssertIntEQ(wolfSSH_CTX_SetMaxAuthAttempts(ctx, 0), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_GetMaxAuthAttempts(ctx), defaultValue); + AssertIntEQ(wolfSSH_CTX_SetMaxAuthAttempts(ctx, -1), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_GetMaxAuthAttempts(ctx), defaultValue); + + /* A session inherits the CTX value and can override it. */ + AssertIntEQ(wolfSSH_CTX_SetMaxAuthAttempts(ctx, 4), WS_SUCCESS); + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_GetMaxAuthAttempts(ssh), 4); + AssertIntEQ(wolfSSH_SetMaxAuthAttempts(ssh, 2), WS_SUCCESS); + AssertIntEQ(wolfSSH_GetMaxAuthAttempts(ssh), 2); + AssertIntEQ(wolfSSH_CTX_GetMaxAuthAttempts(ctx), 4); + AssertIntEQ(wolfSSH_SetMaxAuthAttempts(ssh, 0), WS_SUCCESS); + AssertIntEQ(wolfSSH_GetMaxAuthAttempts(ssh), defaultValue); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + static void test_wolfSSH_SetAlgoList(void) { - const char* newKexList = "diffie-hellman-group1-sha1,ecdh-sha2-nistp521"; - const char* newKeyList = "rsa-sha2-512,ecdsa-sha2-nistp521"; - const char* newCipherList = "aes128-ctr,aes128-cbc"; - const char* newMacList = "hmac-sha1"; - const char* newKeyAccList = "ssh-rsa"; + const char* newKexList; + const char* newKeyList; + const char* newCipherList; + const char* newMacList; + const char* newKeyAccList; + word32 queryIdx; const char* defaultKexList = NULL; const char* defaultKeyList = NULL; const char* defaultCipherList = NULL; @@ -3677,6 +3722,19 @@ static void test_wolfSSH_SetAlgoList(void) byte* key; word32 keySz; + /* Use algorithms compiled into this build so reduced-crypto configs don't + * break the test; Query* returns a stable name pointer. KeyAccepted is a + * TYPE_KEY list. */ + queryIdx = 0; newKexList = wolfSSH_QueryKex(&queryIdx); + queryIdx = 0; newKeyList = wolfSSH_QueryKey(&queryIdx); + queryIdx = 0; newCipherList = wolfSSH_QueryCipher(&queryIdx); + queryIdx = 0; newMacList = wolfSSH_QueryMac(&queryIdx); + newKeyAccList = newKeyList; + AssertNotNull(newKexList); + AssertNotNull(newKeyList); + AssertNotNull(newCipherList); + AssertNotNull(newMacList); + /* Create a ctx object. */ ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); AssertNotNull(ctx); @@ -3718,23 +3776,23 @@ static void test_wolfSSH_SetAlgoList(void) AssertPtrEq(checkKeyAccList, defaultKeyAccList); /* Set the ssh's algo lists, check they match new value. */ - wolfSSH_SetAlgoListKex(ssh, newKexList); + AssertIntEQ(wolfSSH_SetAlgoListKex(ssh, newKexList), WS_SUCCESS); checkKexList = wolfSSH_GetAlgoListKex(ssh); AssertPtrEq(checkKexList, newKexList); - wolfSSH_SetAlgoListKey(ssh, newKeyList); + AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, newKeyList), WS_SUCCESS); checkKeyList = wolfSSH_GetAlgoListKey(ssh); AssertPtrEq(checkKeyList, newKeyList); - wolfSSH_SetAlgoListCipher(ssh, newCipherList); + AssertIntEQ(wolfSSH_SetAlgoListCipher(ssh, newCipherList), WS_SUCCESS); checkCipherList = wolfSSH_GetAlgoListCipher(ssh); AssertPtrEq(checkCipherList, newCipherList); - wolfSSH_SetAlgoListMac(ssh, newMacList); + AssertIntEQ(wolfSSH_SetAlgoListMac(ssh, newMacList), WS_SUCCESS); checkMacList = wolfSSH_GetAlgoListMac(ssh); AssertPtrEq(checkMacList, newMacList); - wolfSSH_SetAlgoListKeyAccepted(ssh, newKeyAccList); + AssertIntEQ(wolfSSH_SetAlgoListKeyAccepted(ssh, newKeyAccList), WS_SUCCESS); checkKeyAccList = wolfSSH_GetAlgoListKeyAccepted(ssh); AssertPtrEq(checkKeyAccList, newKeyAccList); @@ -3742,23 +3800,24 @@ static void test_wolfSSH_SetAlgoList(void) wolfSSH_free(ssh); /* Set new algo lists on the ctx. */ - wolfSSH_CTX_SetAlgoListKex(ctx, newKexList); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(ctx, newKexList), WS_SUCCESS); defaultKexList = wolfSSH_CTX_GetAlgoListKex(ctx); AssertPtrEq(defaultKexList, newKexList); - wolfSSH_CTX_SetAlgoListKey(ctx, newKeyList); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(ctx, newKeyList), WS_SUCCESS); defaultKeyList = wolfSSH_CTX_GetAlgoListKey(ctx); - AssertPtrEq(checkKeyList, newKeyList); + AssertPtrEq(defaultKeyList, newKeyList); - wolfSSH_CTX_SetAlgoListCipher(ctx, newCipherList); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, newCipherList), WS_SUCCESS); defaultCipherList = wolfSSH_CTX_GetAlgoListCipher(ctx); AssertNotNull(defaultCipherList); - wolfSSH_CTX_SetAlgoListMac(ctx, newMacList); + AssertIntEQ(wolfSSH_CTX_SetAlgoListMac(ctx, newMacList), WS_SUCCESS); defaultMacList = wolfSSH_CTX_GetAlgoListMac(ctx); AssertNotNull(defaultMacList); - wolfSSH_CTX_SetAlgoListKeyAccepted(ctx, newKeyAccList); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKeyAccepted(ctx, newKeyAccList), + WS_SUCCESS); defaultKeyAccList = wolfSSH_CTX_GetAlgoListKeyAccepted(ctx); AssertNotNull(defaultKeyAccList); @@ -3834,10 +3893,16 @@ static void test_wolfSSH_SetAlgoList(void) AssertNull(checkKeyList); /* Set a new list on ssh. */ - wolfSSH_SetAlgoListKey(ssh, newKeyList); + AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, newKeyList), WS_SUCCESS); checkKeyList = wolfSSH_GetAlgoListKey(ssh); AssertPtrEq(checkKeyList, newKeyList); + /* NULL restores the server auto-derive default for the key lists. */ + AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, NULL), WS_SUCCESS); + AssertNull(wolfSSH_GetAlgoListKey(ssh)); + AssertIntEQ(wolfSSH_SetAlgoListKeyAccepted(ssh, NULL), WS_SUCCESS); + AssertNull(wolfSSH_GetAlgoListKeyAccepted(ssh)); + /* Cleanup */ wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); @@ -3845,6 +3910,136 @@ static void test_wolfSSH_SetAlgoList(void) } +/* Exercise CheckAlgoList()'s rejection paths through the public setters. */ +static void test_wolfSSH_CheckAlgoList(void) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + const char* validCipher; + const char* secondCipher; + const char* aKexName; + char listBuf[128]; + word32 queryIdx; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + AssertNotNull(ctx); + + queryIdx = 0; + validCipher = wolfSSH_QueryCipher(&queryIdx); + secondCipher = wolfSSH_QueryCipher(&queryIdx); + queryIdx = 0; aKexName = wolfSSH_QueryKex(&queryIdx); + AssertNotNull(validCipher); + AssertNotNull(aKexName); + + /* Every built-in default must pass its own setter, in any build config. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(ctx, + wolfSSH_CTX_GetAlgoListKex(ctx)), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, + wolfSSH_CTX_GetAlgoListCipher(ctx)), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetAlgoListMac(ctx, + wolfSSH_CTX_GetAlgoListMac(ctx)), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(ctx, + wolfSSH_CTX_GetAlgoListKey(ctx)), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKeyAccepted(ctx, + wolfSSH_CTX_GetAlgoListKeyAccepted(ctx)), WS_SUCCESS); + + /* A list with no usable name in it is rejected. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, "not-an-algorithm"), + WS_INVALID_ALGO_ID); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, "not-one,nor-this"), + WS_INVALID_ALGO_ID); + + /* Category mismatch: a KEX name in the cipher slot is rejected. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, aKexName), + WS_INVALID_ALGO_ID); + + /* Empty and all-comma lists are rejected. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, ""), WS_INVALID_ALGO_ID); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, ",,,"), WS_INVALID_ALGO_ID); + + /* A rejected list must leave the previous value intact. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, validCipher), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, "bogus"), + WS_INVALID_ALGO_ID); + AssertPtrEq(wolfSSH_CTX_GetAlgoListCipher(ctx), validCipher); + + /* One trailing comma is tolerated; the canned lists carry one. */ + WSNPRINTF(listBuf, sizeof(listBuf), "%s,", validCipher); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), WS_SUCCESS); + + /* Any other empty element would reach KEXINIT as a zero-length name. */ + WSNPRINTF(listBuf, sizeof(listBuf), ",%s", validCipher); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), + WS_INVALID_ALGO_ID); + + WSNPRINTF(listBuf, sizeof(listBuf), "%s,,", validCipher); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), + WS_INVALID_ALGO_ID); + + /* An unknown name is skipped, so one superset list works on any build. */ + WSNPRINTF(listBuf, sizeof(listBuf), "%s,bogus", validCipher); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), WS_SUCCESS); + + /* A known name in the wrong category still fails the whole list. */ + WSNPRINTF(listBuf, sizeof(listBuf), "%s,%s", validCipher, aKexName); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), + WS_INVALID_ALGO_ID); + + /* Reduced-crypto builds may have only one cipher. */ + if (secondCipher != NULL) { + WSNPRINTF(listBuf, sizeof(listBuf), "%s,%s", + validCipher, secondCipher); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), WS_SUCCESS); + + WSNPRINTF(listBuf, sizeof(listBuf), "bogus,%s", secondCipher); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), WS_SUCCESS); + + WSNPRINTF(listBuf, sizeof(listBuf), "%s,,%s", + validCipher, secondCipher); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, listBuf), + WS_INVALID_ALGO_ID); + } + + /* "none" names no host key, and needs the build flag for cipher/MAC. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(ctx, "none"), WS_INVALID_ALGO_ID); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKeyAccepted(ctx, "none"), + WS_INVALID_ALGO_ID); +#ifndef WOLFSSH_ALLOW_NONE_CIPHER + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, "none"), WS_INVALID_ALGO_ID); + AssertIntEQ(wolfSSH_CTX_SetAlgoListMac(ctx, "none"), WS_INVALID_ALGO_ID); +#endif + + /* Restore a static list; the setters store the caller's pointer and + * listBuf goes out of scope at return. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, validCipher), WS_SUCCESS); + + /* NULL is rejected for kex/cipher/mac, accepted for the key lists. */ + AssertIntEQ(wolfSSH_CTX_SetAlgoListKex(ctx, NULL), WS_INVALID_ALGO_ID); + AssertIntEQ(wolfSSH_CTX_SetAlgoListCipher(ctx, NULL), WS_INVALID_ALGO_ID); + AssertIntEQ(wolfSSH_CTX_SetAlgoListMac(ctx, NULL), WS_INVALID_ALGO_ID); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(ctx, NULL), WS_SUCCESS); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKeyAccepted(ctx, NULL), WS_SUCCESS); + + wolfSSH_CTX_free(ctx); + + /* Client: NULL rejected for Key, accepted for KeyAccepted. */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + AssertIntEQ(wolfSSH_CTX_SetAlgoListKey(ctx, NULL), WS_INVALID_ALGO_ID); + AssertNotNull(wolfSSH_CTX_GetAlgoListKey(ctx)); + AssertIntEQ(wolfSSH_CTX_SetAlgoListKeyAccepted(ctx, NULL), WS_SUCCESS); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, NULL), WS_INVALID_ALGO_ID); + AssertNotNull(wolfSSH_GetAlgoListKey(ssh)); + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); +} + + static void test_wolfSSH_QueryAlgoList(void) { const char* name; @@ -4125,7 +4320,9 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_ReadKey_shortBuffer(); test_wolfSSH_ReadKey_noTrailingNewline(); test_wolfSSH_QueryAlgoList(); + test_wolfSSH_SetMaxAuthAttempts(); test_wolfSSH_SetAlgoList(); + test_wolfSSH_CheckAlgoList(); #ifdef WOLFSSH_AGENT test_wolfSSH_agent_signrequest_partial_write(); test_wolfSSH_agent_signrequest_wrong_message(); diff --git a/tests/unit.c b/tests/unit.c index 0f57c728f..903122bda 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -230,6 +230,31 @@ static const ProtoIdTestVector protoIdTestVectors[] = { { "server rejects non-SSH protocol", "GET / HTTP/1.1\r\n", 0, WS_VERSION_E, WOLFSSH_ENDPOINT_SERVER }, + + /* Protoversion is exactly "2.0" plus a '-'. A 7 byte compare lets + * "SSH-2.01-..." pass. (RFC 4253 4.2) */ + { "protoversion 2.01 rejected", + "SSH-2.01-Test\r\n", + 0, WS_VERSION_E, WOLFSSH_ENDPOINT_CLIENT }, + { "server rejects protoversion 2.01", + "SSH-2.01-Test\r\n", + 0, WS_VERSION_E, WOLFSSH_ENDPOINT_SERVER }, + + /* Protoversion runs into the terminator, no delimiter. */ + { "protoversion with no delimiter CRLF", + "SSH-2.0\r\n", + 0, WS_VERSION_E, WOLFSSH_ENDPOINT_CLIENT }, + { "protoversion with no delimiter LF", + "SSH-2.0\n", + 0, WS_VERSION_E, WOLFSSH_ENDPOINT_CLIENT }, + + /* Controls. */ + { "protoversion 2.0 accepted", + "SSH-2.0-Test\r\n", + 0, WS_SUCCESS, WOLFSSH_ENDPOINT_CLIENT }, + { "server accepts protoversion 2.0", + "SSH-2.0-Test\r\n", + 0, WS_SUCCESS, WOLFSSH_ENDPOINT_SERVER }, }; static int RecvFromPtr(WOLFSSH* ssh, void* data, word32 sz, void* ctx) @@ -472,6 +497,45 @@ static int test_DoProtoId(void) } } + /* A non-conforming local proto ID doesn't reject a conforming peer. */ + { + static const ProtoIdTestVector customTv = { + "custom local proto ID accepts peer", + "SSH-2.0-OpenSSH_8.9\r\n", + 0, WS_SUCCESS, WOLFSSH_ENDPOINT_CLIENT + }; + ProtoIdTestState state; + + state.tv = &customTv; + state.offset = 0; + + wolfSSH_SetIORecv(clientCtx, RecvFromPtr); + if (wolfSSH_CTX_SetSshProtoIdStr(clientCtx, + "SSH-2.0_NonConforming\r\n") != WS_SUCCESS) { + fprintf(stderr, "\t\"%s\" FAIL: SetSshProtoIdStr failed\n", + customTv.name); + failures++; + } + else { + ssh = wolfSSH_new(clientCtx); + if (ssh == NULL) { + fprintf(stderr, "\t\"%s\" FAIL: wolfSSH_new returned NULL\n", + customTv.name); + failures++; + } + else { + wolfSSH_SetIOReadCtx(ssh, &state); + ret = wolfSSH_TestDoProtoId(ssh); + if (ret != customTv.expected) { + fprintf(stderr, "\t\"%s\" FAIL: got %d, expected %d\n", + customTv.name, ret, customTv.expected); + failures++; + } + wolfSSH_free(ssh); + } + } + } + wolfSSH_CTX_free(serverCtx); wolfSSH_CTX_free(clientCtx); @@ -1369,6 +1433,25 @@ static int test_DoReceive_RejectsShortPadding(void) wolfSSH_CTX_free(ctx); return result; } + +/* Send sink, so a test can run a real send path (build the packet, drain it) + * without a live transport. */ +static int UnitIoSendSink(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + (void)ssh; + (void)buf; + (void)ctx; + return (int)sz; +} + +/* Write a big-endian uint32 without the internal-only c32toa(). */ +static void PutU32BE(byte* out, word32 v) +{ + out[0] = (byte)(v >> 24); + out[1] = (byte)(v >> 16); + out[2] = (byte)(v >> 8); + out[3] = (byte)(v); +} #endif /* WOLFSSH_TEST_INTERNAL */ @@ -1712,26 +1795,6 @@ static int test_DhGexGroupSelect(void) return 0; } -/* Send sink so SendKexDhGexGroup can run the real server path (select the - * group, build the KEXDH_GEX_GROUP packet, drain it) without a live transport. - * We only care that the group it selects gets cached on the handshake. */ -static int GexSinkSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) -{ - (void)ssh; - (void)buf; - (void)ctx; - return (int)sz; -} - -/* Write a big-endian uint32 without the internal-only c32toa(). */ -static void PutU32BE(byte* out, word32 v) -{ - out[0] = (byte)(v >> 24); - out[1] = (byte)(v >> 16); - out[2] = (byte)(v >> 8); - out[3] = (byte)(v); -} - /* One send/hash consistency case. Runs the real server request path for the * given client window, then confirms GetDHPrimeGroup hands the exchange-hash * and key-agreement path the exact group SendKexDhGexGroup cached and put on @@ -1754,7 +1817,7 @@ static int DhGexSendHashConsistencyCase(word32 minBits, word32 prefBits, ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) return base; - wolfSSH_SetIOSend(ctx, GexSinkSend); + wolfSSH_SetIOSend(ctx, UnitIoSendSink); ssh = wolfSSH_new(ctx); if (ssh == NULL || ssh->handshake == NULL) { result = base - 1; @@ -1856,7 +1919,7 @@ static int test_DhGexGroupCacheMissFallback(void) ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) return -240; - wolfSSH_SetIOSend(ctx, GexSinkSend); + wolfSSH_SetIOSend(ctx, UnitIoSendSink); ssh = wolfSSH_new(ctx); if (ssh == NULL || ssh->handshake == NULL) { result = -241; @@ -1922,7 +1985,7 @@ static int test_DhGexServerRejectsUnsatisfiableWindow(void) ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) return -280; - wolfSSH_SetIOSend(ctx, GexSinkSend); + wolfSSH_SetIOSend(ctx, UnitIoSendSink); ssh = wolfSSH_new(ctx); if (ssh == NULL || ssh->handshake == NULL) { result = -281; @@ -1995,7 +2058,7 @@ static int test_DhGexRequestFloorClamp(void) ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); if (ctx == NULL) return -290; - wolfSSH_SetIOSend(ctx, GexSinkSend); + wolfSSH_SetIOSend(ctx, UnitIoSendSink); /* A sub-floor min is raised to the floor, and the preferred size rides up * with it so the advertised triple stays min <= preferred. */ @@ -2159,7 +2222,7 @@ static int test_DhGexGroup16KeyAgree(void) ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) return -250; - wolfSSH_SetIOSend(ctx, GexSinkSend); + wolfSSH_SetIOSend(ctx, UnitIoSendSink); ssh = wolfSSH_new(ctx); if (ssh == NULL || ssh->handshake == NULL) { result = -251; @@ -2282,7 +2345,7 @@ static int test_DhGexGroupAcceptHonorsGenerator(void) sctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (sctx == NULL) return -300; - wolfSSH_SetIOSend(sctx, GexSinkSend); + wolfSSH_SetIOSend(sctx, UnitIoSendSink); sssh = wolfSSH_new(sctx); if (sssh == NULL || sssh->handshake == NULL) { result = -301; @@ -2328,7 +2391,7 @@ static int test_DhGexGroupAcceptHonorsGenerator(void) result = -305; goto out; } - wolfSSH_SetIOSend(cctx, GexSinkSend); + wolfSSH_SetIOSend(cctx, UnitIoSendSink); cssh = wolfSSH_new(cctx); if (cssh == NULL || cssh->handshake == NULL) { result = -306; @@ -2409,7 +2472,7 @@ static int test_DhGexGroupSendRecache(void) ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) return -320; - wolfSSH_SetIOSend(ctx, GexSinkSend); + wolfSSH_SetIOSend(ctx, UnitIoSendSink); ssh = wolfSSH_new(ctx); if (ssh == NULL || ssh->handshake == NULL) { result = -321; @@ -3238,6 +3301,85 @@ static int test_DoChannelExtendedData_overflow(void) return result; } +/* GetString() reserves a byte for the null. A zero-sized destination has + * no room for it and must be rejected, not wrap the subtraction. */ +static int test_GetString_zeroDestSz(void) +{ + /* SSH string: length 5, then "hello" */ + static const byte wire[] = { 0x00, 0x00, 0x00, 0x05, 'h','e','l','l','o' }; + /* SSH string: length 0 */ + static const byte wireEmpty[] = { 0x00, 0x00, 0x00, 0x00 }; + char dest[16]; + word32 destSz; + word32 idx; + int result = 0; + int ret; + int i; + + /* NULL args. The zero-size check below dereferences sSz, so it must + * stay after this one. */ + destSz = (word32)sizeof(dest); + idx = 0; + ret = GetString(NULL, &destSz, wire, (word32)sizeof(wire), &idx); + if (ret != WS_BAD_ARGUMENT) { result = -1048; goto done; } + idx = 0; + ret = GetString(dest, NULL, wire, (word32)sizeof(wire), &idx); + if (ret != WS_BAD_ARGUMENT) { result = -1049; goto done; } + + /* zero destination size, non-empty string: reject, touch nothing */ + WMEMSET(dest, 0xAA, sizeof(dest)); + destSz = 0; + idx = 0; + ret = GetString(dest, &destSz, wire, (word32)sizeof(wire), &idx); + if (ret != WS_BUFFER_E) { result = -1050; goto done; } + if (destSz != 0) { result = -1051; goto done; } + if (idx != 0) { result = -1052; goto done; } + for (i = 0; i < (int)sizeof(dest); i++) { + if ((byte)dest[i] != 0xAA) { result = -1053; goto done; } + } + + /* zero destination size, empty string: same rejection */ + destSz = 0; + idx = 0; + ret = GetString(dest, &destSz, wireEmpty, (word32)sizeof(wireEmpty), &idx); + if (ret != WS_BUFFER_E) { result = -1054; goto done; } + if (destSz != 0) { result = -1055; goto done; } + for (i = 0; i < (int)sizeof(dest); i++) { + if ((byte)dest[i] != 0xAA) { result = -1056; goto done; } + } + + /* room for only the null: empty result, no copy */ + WMEMSET(dest, 0xAA, sizeof(dest)); + destSz = 1; + idx = 0; + ret = GetString(dest, &destSz, wire, (word32)sizeof(wire), &idx); + if (ret != WS_SUCCESS) { result = -1057; goto done; } + if (destSz != 0 || dest[0] != 0) { result = -1058; goto done; } + + /* truncating copy still reserves the null */ + WMEMSET(dest, 0xAA, sizeof(dest)); + destSz = 3; + idx = 0; + ret = GetString(dest, &destSz, wire, (word32)sizeof(wire), &idx); + if (ret != WS_SUCCESS) { result = -1059; goto done; } + if (destSz != 2 || WSTRCMP(dest, "he") != 0) { result = -1060; goto done; } + + /* whole string fits */ + WMEMSET(dest, 0xAA, sizeof(dest)); + destSz = (word32)sizeof(dest); + idx = 0; + ret = GetString(dest, &destSz, wire, (word32)sizeof(wire), &idx); + if (ret != WS_SUCCESS) { result = -1061; goto done; } + if (destSz != 5 || WSTRCMP(dest, "hello") != 0) { + result = -1062; + goto done; + } + if (idx != (word32)sizeof(wire)) { result = -1063; goto done; } + +done: + return result; +} + /* DoChannelWindowAdjust adds the peer's advertised bytes to peerWindowSz. * A crafted bytesToAdd that would wrap the word32 must be rejected with * WS_OVERFLOW_E and leave the window untouched; a value that fits must be @@ -3627,6 +3769,114 @@ static int test_DoChannelRequest(void) return result; } +/* IO send that always wants write, the state a peer forces by not reading. */ +static int UnitIoSendWantWrite(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + (void)ssh; (void)buf; (void)sz; (void)ctx; + return WS_CBIO_ERR_WANT_WRITE; +} + +static int UnitAuthAlwaysFail(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + (void)authType; (void)authData; (void)ctx; + return WOLFSSH_USERAUTH_INVALID_PASSWORD; +} + +/* Build a "password" USERAUTH_REQUEST payload, as DoUserAuthRequest() sees it + * with the message id already consumed. Returns the payload size. */ +static word32 BuildAuthPwRequest(byte* buf, word32 bufSz) +{ + static const char* fields[] = { "jill", "ssh-connection", "password" }; + word32 idx = 0, i, fieldSz; + + for (i = 0; i < sizeof(fields)/sizeof(fields[0]); i++) { + fieldSz = (word32)WSTRLEN(fields[i]); + if (idx + UINT32_SZ + fieldSz > bufSz) + return 0; + PutU32BE(buf + idx, fieldSz); + idx += UINT32_SZ; + WMEMCPY(buf + idx, fields[i], fieldSz); + idx += fieldSz; + } + + fieldSz = (word32)WSTRLEN("badpass"); + if (idx + 1 + UINT32_SZ + fieldSz > bufSz) + return 0; + buf[idx++] = 0; /* no password change */ + PutU32BE(buf + idx, fieldSz); + idx += UINT32_SZ; + WMEMCPY(buf + idx, "badpass", fieldSz); + idx += fieldSz; + + return idx; +} + +/* Verify the failed-userauth cap actually fires, and that it fires whether or + * not the USERAUTH_FAILURE send completes. The want-write case is the + * regression: WS_WANT_WRITE is non-fatal in DoReceive(), so a peer that stops + * reading its socket must not be able to pipeline uncounted guesses. + * + * Asserts, with the limit set to 3: attempts 1 and 2 leave the connection up + * (WS_SUCCESS, or WS_WANT_WRITE when the send blocks) and attempt 3 returns + * WS_USER_AUTH_E, ending the accept loop. */ +static int test_MaxAuthAttempts(void) +{ + byte request[128]; + word32 requestSz; + int result = 0; + int i; + struct { + WS_CallbackIOSend send; + int expectRet; /* return for the under-the-limit attempts */ + const char* label; + } cases[] = { + { UnitIoSendSink, WS_SUCCESS, "send completes" }, + { UnitIoSendWantWrite, WS_WANT_WRITE, "send wants write" }, + }; + + requestSz = BuildAuthPwRequest(request, (word32)sizeof(request)); + if (requestSz == 0) + return -700; + + for (i = 0; i < (int)(sizeof(cases)/sizeof(cases[0])); i++) { + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + int attempt; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { result = -701; break; } + wolfSSH_SetUserAuth(ctx, UnitAuthAlwaysFail); + wolfSSH_SetIOSend(ctx, cases[i].send); + if (wolfSSH_CTX_SetMaxAuthAttempts(ctx, 3) != WS_SUCCESS) { + wolfSSH_CTX_free(ctx); + result = -702; + break; + } + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { wolfSSH_CTX_free(ctx); result = -703; break; } + + for (attempt = 1; attempt <= 3 && result == 0; attempt++) { + word32 idx = 0; + int expect = (attempt < 3) ? cases[i].expectRet : WS_USER_AUTH_E; + int ret = wolfSSH_TestDoUserAuthRequest(ssh, request, requestSz, + &idx); + + if (ret != expect) { + printf("MaxAuthAttempts[%s]: attempt %d ret=%d expected %d\n", + cases[i].label, attempt, ret, expect); + result = -710 - (i * 10) - attempt; + } + } + + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + } + + return result; +} + /* Capture buffer for the service-name unit test. Separate from the channel- * request capture so the two tests can run independently in any order. */ static byte s_authSvcCapture[256]; @@ -6174,7 +6424,10 @@ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) return -1; } #endif - rewind(f); + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return -1; + } *buf = (byte*)malloc((size_t)sz); if (*buf == NULL) { @@ -6696,7 +6949,6 @@ static int test_SshResourceFree_zeroesSecrets(void) const byte* keysBytes; const byte* peerKeysBytes; int result = 0; - int retainInstalled = 0; wolfSSL_Malloc_cb prevMf = NULL; wolfSSL_Free_cb prevFf = NULL; wolfSSL_Realloc_cb prevRf = NULL; @@ -6723,10 +6975,8 @@ static int test_SshResourceFree_zeroesSecrets(void) result = -702; goto out; } - retainInstalled = 1; wolfSSH_free(ssh); wolfSSL_SetAllocators(prevMf, prevFf, prevRf); - retainInstalled = 0; if (!IsRetained(ssh)) { result = -703; @@ -6757,8 +7007,6 @@ static int test_SshResourceFree_zeroesSecrets(void) } out: - if (retainInstalled) - wolfSSL_SetAllocators(prevMf, prevFf, prevRf); DrainRetained(); if (ctx != NULL) wolfSSH_CTX_free(ctx); @@ -6783,7 +7031,6 @@ static int test_KeyAgreeDh_client_zeroesEphemeralPrivKey(void) word32 markedSz; word32 i; int result = 0; - int dhInited = 0; ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); if (ctx == NULL) @@ -6806,7 +7053,6 @@ static int test_KeyAgreeDh_client_zeroesEphemeralPrivKey(void) result = -713; goto cleanup; } - dhInited = 1; markedSz = (word32)sizeof(hs->x); WMEMSET(hs->x, 0xA5, markedSz); @@ -6818,7 +7064,6 @@ static int test_KeyAgreeDh_client_zeroesEphemeralPrivKey(void) (void)wolfSSH_TestKeyAgreeDh_client(ssh, WC_HASH_TYPE_SHA256, bogusF, (word32)sizeof(bogusF)); /* wc_FreeDhKey was called inside the test hook; do not free again. */ - dhInited = 0; for (i = 0; i < markedSz; i++) { if (hs->x[i] != 0) { @@ -6828,8 +7073,6 @@ static int test_KeyAgreeDh_client_zeroesEphemeralPrivKey(void) } cleanup: - if (dhInited) - wc_FreeDhKey(&ssh->handshake->privKey.dh); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); return result; @@ -9323,6 +9566,11 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + unitResult = test_GetString_zeroDestSz(); + printf("GetString_zeroDestSz: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + unitResult = test_SendChannelData_eofTxd(); printf("SendChannelData_eofTxd: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; @@ -9438,6 +9686,10 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + unitResult = test_MaxAuthAttempts(); + printf("MaxAuthAttempts: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + unitResult = test_IdentifyAsn1Key(); printf("IdentifyAsn1Key: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 56c5abb04..3f4408d5d 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -151,6 +151,15 @@ extern "C" { defined(WOLFSSH_NO_HMAC_SHA2_512) #error "You need at least one MAC algorithm." #endif +/* The SHA-1 MACs only join the default algorithm list when the soft disable + * is lifted. Without them there is nothing left to offer, and an empty + * default list can never negotiate. */ +#if defined(WOLFSSH_NO_HMAC_SHA2_256) && \ + defined(WOLFSSH_NO_HMAC_SHA2_512) && \ + !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) + #error "The only MAC algorithms left are soft-disabled. Define " \ + "WOLFSSH_NO_SHA1_SOFT_DISABLE to offer them." +#endif #if defined(WOLFSSH_NO_DH) || defined(WOLFSSH_NO_SHA1) #undef WOLFSSH_NO_DH_GROUP1_SHA1 @@ -233,6 +242,21 @@ extern "C" { defined(WOLFSSH_NO_CURVE25519_SHA256) #error "You need at least one key agreement algorithm." #endif +/* Likewise, the SHA-1 groups are the only soft-disabled KEX names. */ +#if defined(WOLFSSH_NO_DH_GROUP14_SHA256) && \ + defined(WOLFSSH_NO_DH_GROUP16_SHA512) && \ + defined(WOLFSSH_NO_DH_GEX_SHA256) && \ + defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) && \ + defined(WOLFSSH_NO_ECDH_SHA2_NISTP384) && \ + defined(WOLFSSH_NO_ECDH_SHA2_NISTP521) && \ + defined(WOLFSSH_NO_NISTP256_MLKEM768_SHA256) && \ + defined(WOLFSSH_NO_NISTP384_MLKEM1024_SHA384) && \ + defined(WOLFSSH_NO_CURVE25519_MLKEM768_SHA256) && \ + defined(WOLFSSH_NO_CURVE25519_SHA256) && \ + !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) + #error "The only key agreement algorithms left are soft-disabled. " \ + "Define WOLFSSH_NO_SHA1_SOFT_DISABLE to offer them." +#endif #if defined(WOLFSSH_NO_DH_GROUP1_SHA1) && \ defined(WOLFSSH_NO_DH_GROUP14_SHA1) && \ @@ -291,6 +315,23 @@ extern "C" { defined(WOLFSSH_NO_MLDSA87) #error "You need at least one signing algorithm." #endif +/* ssh-rsa is the only soft-disabled signing name. Client-only: a server + * negotiates host keys from ctx->publicKeyAlgo, not cannedKeyAlgoNames, so + * an empty canned list only costs it the server-sig-algs advertisement. */ +#if !defined(NO_WOLFSSH_CLIENT) && \ + defined(WOLFSSH_NO_RSA_SHA2_256) && \ + defined(WOLFSSH_NO_RSA_SHA2_512) && \ + defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && \ + defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) && \ + defined(WOLFSSH_NO_ECDSA_SHA2_NISTP521) && \ + defined(WOLFSSH_NO_ED25519) && \ + defined(WOLFSSH_NO_MLDSA44) && \ + defined(WOLFSSH_NO_MLDSA65) && \ + defined(WOLFSSH_NO_MLDSA87) && \ + !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) + #error "The only signing algorithms left are soft-disabled. Define " \ + "WOLFSSH_NO_SHA1_SOFT_DISABLE to offer them." +#endif #if defined(WOLFSSH_NO_SSH_RSA_SHA1) && \ defined(WOLFSSH_NO_RSA_SHA2_256) && \ @@ -329,6 +370,13 @@ extern "C" { defined(WOLFSSH_NO_AES_GCM) #error "You need at least one encryption algorithm." #endif +/* AES-CBC is the only soft-disabled cipher. */ +#if defined(WOLFSSH_NO_AES_CTR) && \ + defined(WOLFSSH_NO_AES_GCM) && \ + !defined(WOLFSSH_NO_AES_CBC_SOFT_DISABLE) + #error "The only encryption algorithms left are soft-disabled. Define " \ + "WOLFSSH_NO_AES_CBC_SOFT_DISABLE to offer them." +#endif #if defined(WOLFSSH_NO_AES_GCM) #undef WOLFSSH_NO_AEAD @@ -473,7 +521,7 @@ enum NameIdType { #define SHA1_96_SZ 12 #define UINT32_SZ 4 #define LENGTH_SZ UINT32_SZ -#define SSH_PROTO_SZ 7 /* "SSH-2.0" */ +#define SSH_PROTO_SZ 8 /* "SSH-2.0-" */ #define TERMINAL_MODE_SZ 5 /* opcode byte + argument uint32 */ #define AEAD_IMP_IV_SZ 4 #define AEAD_EXP_IV_SZ 8 @@ -498,6 +546,11 @@ enum NameIdType { #ifndef DEFAULT_NEXT_CHANNEL #define DEFAULT_NEXT_CHANNEL 0 #endif +#ifndef DEFAULT_MAX_AUTH_ATTEMPTS + /* Server-side cap on failed userauth attempts, same value as the OpenSSH + * MaxAuthTries default. */ + #define DEFAULT_MAX_AUTH_ATTEMPTS 6 +#endif #ifndef MAX_PACKET_SZ /* This is from RFC 4253 section 6.1. */ #define MAX_PACKET_SZ 35000 @@ -607,6 +660,16 @@ enum NameIdType { WOLFSSH_LOCAL byte NameToId(const char* name, word32 nameSz); WOLFSSH_LOCAL const char* IdToName(byte id); WOLFSSH_LOCAL const char* NameByIndexType(byte type, word32* idx); +/* Validate a comma-separated algorithm list against a category (a TYPE_* value). + * Unknown names are skipped, so a caller can pass a portable superset; a known + * name of the wrong category is rejected, and at least one usable name must + * remain. One trailing comma is allowed, as the canned lists carry one; any + * other empty element is rejected, since only that one is stripped before the + * list goes on the wire. "none" is rejected for TYPE_KEY, and accepted for + * TYPE_CIPHER/TYPE_MAC only under WOLFSSH_ALLOW_NONE_CIPHER + * (--enable-none-cipher) -- an insecure, testing-only plaintext transport. + * Returns WS_SUCCESS or WS_INVALID_ALGO_ID; a NULL list is invalid. */ +WOLFSSH_LOCAL int CheckAlgoList(const char* list, byte type); /* For cases when openssl coexist is used */ @@ -709,6 +772,7 @@ struct WOLFSSH_CTX { word32 bannerSz; word32 windowSz; word32 maxPacketSz; + word32 maxAuthAttempts; /* server cap on failed userauth */ byte side; /* client or server */ byte showBanner; #ifdef WOLFSSH_AGENT @@ -1080,6 +1144,7 @@ struct WOLFSSH { #ifdef WOLFSSH_KEYBOARD_INTERACTIVE WS_UserAuthData_Keyboard kbAuth; byte kbAuthAttempts; + byte kbSetupPending; /* server sent INFO_REQUEST, awaiting a response */ #endif #ifdef WOLFSSH_TPM byte tpmPubkeyTried; /* client tried TPM publickey; allow auth fallback */ @@ -1089,6 +1154,9 @@ struct WOLFSSH { word32 testSftpStallPending; /* test hook: force N flush-only resumes */ #endif byte userAuthSeen; /* a userauth request has bound the username */ + word32 maxAuthAttempts; /* server cap on failed userauth attempts */ + word32 authFailures; /* count of failed userauth attempts */ + word32 authRequests; /* count of userauth requests received */ }; @@ -1250,8 +1318,11 @@ WOLFSSH_LOCAL int SendExtInfo(WOLFSSH* ssh); WOLFSSH_LOCAL int SendUserAuthRequest(WOLFSSH* ssh, byte authType, int addSig); #ifdef WOLFSSH_KEYBOARD_INTERACTIVE WOLFSSH_LOCAL int SendUserAuthKeyboardResponse(WOLFSSH* ssh); +/* Send an INFO_REQUEST for keyboard-interactive auth. Set counted when the + * caller already charged a failure for this message, so a declined setup + * callback doesn't charge it twice. */ WOLFSSH_LOCAL int SendUserAuthKeyboardRequest(WOLFSSH* ssh, - WS_UserAuthData* authData); + WS_UserAuthData* authData, int counted); #endif WOLFSSH_LOCAL int SendUserAuthSuccess(WOLFSSH* ssh); WOLFSSH_LOCAL int SendUserAuthFailure(WOLFSSH* ssh, byte partialSuccess); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 6ce19aa9d..8d28c6431 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -104,6 +104,14 @@ WOLFSSH_API int wolfSSH_ReadKey_file(const char* name, byte** out, word32* outSz, const byte** outType, word32* outTypeSz, byte* isPrivate, void* heap); +/* SetAlgoList* validate the list, returning WS_SUCCESS, WS_INVALID_ALGO_ID for + * a bad list, or WS_SSH_CTX_NULL_E / WS_SSH_NULL_E for a NULL ctx / ssh. + * Kex/Cipher/Mac reject NULL. Key accepts NULL only on a server, restoring the + * default of deriving the host key list from the loaded private keys; a client + * has no such fallback. KeyAccepted accepts NULL on either side, but that + * empties the list rather than restoring a default: the server then advertises + * an empty RFC 8308 "server-sig-algs", telling clients it accepts no signature + * algorithms, which stops OpenSSH clients offering rsa-sha2-256/512. */ WOLFSSH_API int wolfSSH_CTX_SetAlgoListKex(WOLFSSH_CTX* ctx, const char* list); WOLFSSH_API const char* wolfSSH_CTX_GetAlgoListKex(WOLFSSH_CTX* ctx); WOLFSSH_API int wolfSSH_SetAlgoListKex(WOLFSSH* ssh, const char* list); @@ -437,6 +445,19 @@ WOLFSSH_API char* wolfSSH_GetUsername(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_CTX_SetBanner(WOLFSSH_CTX* ctx, const char* newBanner); WOLFSSH_API int wolfSSH_CTX_SetSshProtoIdStr(WOLFSSH_CTX* ctx, const char* protoIdStr); +/* Set the server-side limit on failed userauth attempts per connection. The + * default is DEFAULT_MAX_AUTH_ATTEMPTS (6), the same value as the OpenSSH + * MaxAuthTries default. When the limit is reached the server sends an + * SSH_MSG_DISCONNECT and drops the connection. A value <= 0 restores the + * built-in default; there is no "unlimited" setting. Every request that does + * not fully authenticate is charged, including a partial success; only the + * opening "none" probe clients use to learn the method list is exempt. + * The WOLFSSH setter overrides the value inherited from the CTX for one + * session; the getters return the current value or WS_BAD_ARGUMENT. */ +WOLFSSH_API int wolfSSH_CTX_SetMaxAuthAttempts(WOLFSSH_CTX* ctx, int value); +WOLFSSH_API int wolfSSH_CTX_GetMaxAuthAttempts(WOLFSSH_CTX* ctx); +WOLFSSH_API int wolfSSH_SetMaxAuthAttempts(WOLFSSH* ssh, int value); +WOLFSSH_API int wolfSSH_GetMaxAuthAttempts(WOLFSSH* ssh); WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz, int format);