Skip to content

Commit 4b88768

Browse files
ejohnstownphilljj
authored andcommitted
fix: always send USERAUTH_FAILURE on reject
RFC 4252 section 5.1 has the server answer a request it does not accept with USERAUTH_FAILURE. A callback returning WOLFSSH_USERAUTH_REJECTED now always gets that reply, and always ends the session. NO_FAILURE_ON_REJECTED suppressed the reply. The macro was never set by configure, named in a header, or documented, and it guarded all four method handlers alike. - drop the macro and its four guards - end the session on a rejected keyboard-interactive setup, the one rejection that used to leave the peer retrying to the cap - state the guarantee on WOLFSSH_USERAUTH_REJECTED in wolfssh/ssh.h - add test_UserAuthRejectedSendsFailure() over the dispatched methods and the keyboard-interactive response, asserting on the wire since the handlers return WS_USER_AUTH_E anyway Issue: F-11672
1 parent 231772c commit 4b88768

3 files changed

Lines changed: 295 additions & 17 deletions

File tree

src/internal.c

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8524,11 +8524,9 @@ static int DoUserAuthRequestNone(WOLFSSH* ssh, WS_UserAuthData* authData,
85248524
}
85258525
else if (ret == WOLFSSH_USERAUTH_REJECTED) {
85268526
WLOG(WS_LOG_DEBUG, "DUARN: none rejected");
8527-
#ifndef NO_FAILURE_ON_REJECTED
85288527
/* Count before failing: a send that blocks returns
85298528
* WS_WANT_WRITE, which isn't fatal on its own. */
85308529
(void)SendUserAuthFailureCount(ssh, 0, countIt);
8531-
#endif
85328530
ret = WS_USER_AUTH_E;
85338531
}
85348532
else if (ret == WOLFSSH_USERAUTH_WOULD_BLOCK) {
@@ -8657,9 +8655,7 @@ static int DoUserAuthInfoResponse(WOLFSSH* ssh,
86578655
}
86588656
else if (ret == WOLFSSH_USERAUTH_REJECTED) {
86598657
WLOG(WS_LOG_DEBUG, "DUARKB: keyboard rejected");
8660-
#ifndef NO_FAILURE_ON_REJECTED
8661-
authFailure = 1;
8662-
#endif
8658+
authFailure = 1;
86638659
authRejected = 1;
86648660
ret = WS_USER_AUTH_E;
86658661
}
@@ -8772,9 +8768,7 @@ static int DoUserAuthRequestPassword(WOLFSSH* ssh, WS_UserAuthData* authData,
87728768
}
87738769
else if (ret == WOLFSSH_USERAUTH_REJECTED) {
87748770
WLOG(WS_LOG_DEBUG, "DUARPW: password rejected");
8775-
#ifndef NO_FAILURE_ON_REJECTED
8776-
authFailure = 1;
8777-
#endif
8771+
authFailure = 1;
87788772
authRejected = 1;
87798773
ret = WS_USER_AUTH_E;
87808774
}
@@ -10204,9 +10198,7 @@ static int DoUserAuthRequestPublicKey(WOLFSSH* ssh, WS_UserAuthData* authData,
1020410198
ret = WS_AUTH_PENDING;
1020510199
}
1020610200
else if (ret == WOLFSSH_USERAUTH_REJECTED) {
10207-
#ifndef NO_FAILURE_ON_REJECTED
10208-
authFailure = 1;
10209-
#endif
10201+
authFailure = 1;
1021010202
authRejected = 1;
1021110203
ret = WS_USER_AUTH_E;
1021210204
}
@@ -17459,6 +17451,15 @@ int SendUserAuthKeyboardRequest(WOLFSSH* ssh, WS_UserAuthData* authData)
1745917451
ssh->kbSetupPending = 0;
1746017452
return WS_AUTH_PENDING;
1746117453
}
17454+
else if (ret == WOLFSSH_USERAUTH_REJECTED) {
17455+
/* A hard rejection ends the session here as it does in the other
17456+
* methods. Count before failing: a send that blocks returns
17457+
* WS_WANT_WRITE, which isn't fatal on its own. */
17458+
WLOG(WS_LOG_DEBUG, "SUAKR: keyboard setup rejected");
17459+
ssh->kbSetupPending = 0;
17460+
(void)SendUserAuthFailureCount(ssh, 0, 1);
17461+
return WS_USER_AUTH_E;
17462+
}
1746217463
else {
1746317464
WLOG(WS_LOG_DEBUG, "Issue with keyboard auth setup, try another "
1746417465
"auth type");

tests/unit.c

Lines changed: 280 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7673,13 +7673,17 @@ static int UnitAuthKbSetupOk(byte authType, WS_UserAuthData* authData,
76737673
}
76747674
return WOLFSSH_USERAUTH_INVALID_PASSWORD;
76757675
}
7676+
#endif /* WOLFSSH_KEYBOARD_INTERACTIVE */
76767677

7678+
#if defined(WOLFSSH_KEYBOARD_INTERACTIVE) || \
7679+
defined(WOLFSSH_ALLOW_USERAUTH_NONE) || !defined(WOLFSSH_NO_PUBKEY_AUTH)
76777680
/* Build a USERAUTH_REQUEST payload for `service` and auth method `method`
76787681
* with no method-specific fields, as DoUserAuthRequest() sees it with the
76797682
* message id already consumed. The keyboard and none dispatch paths don't
7680-
* parse trailing fields, so three strings (user, service, method) suffice. A
7681-
* NULL method omits that field, which a bad service name never reaches.
7682-
* Returns the payload size, 0 on overflow. */
7683+
* parse trailing fields, so three strings (user, service, method) suffice,
7684+
* and the publickey path appends its own fields to this. A NULL method omits
7685+
* that field, which a bad service name never reaches. Returns the payload
7686+
* size, 0 on overflow. */
76837687
static word32 BuildAuthMethodRequest(byte* buf, word32 bufSz,
76847688
const char* service, const char* method)
76857689
{
@@ -7703,7 +7707,54 @@ static word32 BuildAuthMethodRequest(byte* buf, word32 bufSz,
77037707
}
77047708
return idx;
77057709
}
7706-
#endif /* WOLFSSH_KEYBOARD_INTERACTIVE */
7710+
7711+
#ifndef WOLFSSH_NO_PUBKEY_AUTH
7712+
/* A public key algorithm the server offers, so the request gets past the
7713+
* algorithm match and reaches the userauth callback. */
7714+
#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256
7715+
#define UNIT_AUTH_PK_ALGO "ecdsa-sha2-nistp256"
7716+
#elif !defined(WOLFSSH_NO_RSA_SHA2_256)
7717+
#define UNIT_AUTH_PK_ALGO "rsa-sha2-256"
7718+
#elif !defined(WOLFSSH_NO_ED25519)
7719+
#define UNIT_AUTH_PK_ALGO "ssh-ed25519"
7720+
#endif
7721+
7722+
#ifdef UNIT_AUTH_PK_ALGO
7723+
/* Build the no-signature "publickey" USERAUTH_REQUEST, the PK_OK probe. The
7724+
* handler reads the key blob only as far as its format string before calling
7725+
* the userauth callback, so the algorithm name stands in for the key. Returns
7726+
* the payload size, 0 on overflow. */
7727+
static word32 BuildAuthPkRequest(byte* buf, word32 bufSz)
7728+
{
7729+
word32 idx, algoSz = (word32)WSTRLEN(UNIT_AUTH_PK_ALGO);
7730+
7731+
idx = BuildAuthMethodRequest(buf, bufSz, "ssh-connection", "publickey");
7732+
if (idx == 0)
7733+
return 0;
7734+
if (idx + BOOLEAN_SZ + 3 * UINT32_SZ + 2 * algoSz > bufSz)
7735+
return 0;
7736+
7737+
buf[idx++] = 0; /* has signature FALSE */
7738+
7739+
PutU32BE(buf + idx, algoSz);
7740+
idx += UINT32_SZ;
7741+
WMEMCPY(buf + idx, UNIT_AUTH_PK_ALGO, algoSz);
7742+
idx += algoSz;
7743+
7744+
/* Key blob, itself a string holding the key format. */
7745+
PutU32BE(buf + idx, UINT32_SZ + algoSz);
7746+
idx += UINT32_SZ;
7747+
PutU32BE(buf + idx, algoSz);
7748+
idx += UINT32_SZ;
7749+
WMEMCPY(buf + idx, UNIT_AUTH_PK_ALGO, algoSz);
7750+
idx += algoSz;
7751+
7752+
return idx;
7753+
}
7754+
#endif /* UNIT_AUTH_PK_ALGO */
7755+
#endif /* !WOLFSSH_NO_PUBKEY_AUTH */
7756+
#endif /* WOLFSSH_KEYBOARD_INTERACTIVE || WOLFSSH_ALLOW_USERAUTH_NONE
7757+
* || !WOLFSSH_NO_PUBKEY_AUTH */
77077758

77087759
/* Build a "password" USERAUTH_REQUEST payload, as DoUserAuthRequest() sees it
77097760
* with the message id already consumed. Returns the payload size. */
@@ -7969,6 +8020,226 @@ static int CaptureIoSendAuthSvc(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
79698020
return (int)sz;
79708021
}
79718022

8023+
/* Userauth callback that rejects outright. */
8024+
static int UnitAuthAlwaysReject(byte authType, WS_UserAuthData* authData,
8025+
void* ctx)
8026+
{
8027+
(void)authType;
8028+
(void)authData;
8029+
(void)ctx;
8030+
return WOLFSSH_USERAUTH_REJECTED;
8031+
}
8032+
8033+
8034+
#ifdef WOLFSSH_KEYBOARD_INTERACTIVE
8035+
/* Accept the setup so the exchange gets past SendUserAuthKeyboardRequest(),
8036+
* then reject the response, which is the DoUserAuthInfoResponse() branch. */
8037+
static int UnitAuthKbRespReject(byte authType, WS_UserAuthData* authData,
8038+
void* ctx)
8039+
{
8040+
(void)ctx;
8041+
if (authType == WOLFSSH_USERAUTH_KEYBOARD_SETUP) {
8042+
authData->sf.keyboard.promptCount = 0;
8043+
return WOLFSSH_USERAUTH_SUCCESS;
8044+
}
8045+
return WOLFSSH_USERAUTH_REJECTED;
8046+
}
8047+
8048+
8049+
/* Drive an accepted keyboard-interactive setup, then a rejected
8050+
* INFO_RESPONSE, and check the rejection drew a USERAUTH_FAILURE. Returns 0,
8051+
* or 1..5 naming what failed. */
8052+
static int RunRejectedInfoResponse(void)
8053+
{
8054+
WOLFSSH_CTX* ctx = NULL;
8055+
WOLFSSH* ssh = NULL;
8056+
byte request[64];
8057+
byte infoRsp[UINT32_SZ];
8058+
word32 requestSz, idx = 0;
8059+
int ret;
8060+
int result = 0;
8061+
8062+
requestSz = BuildAuthMethodRequest(request, (word32)sizeof(request),
8063+
"ssh-connection", "keyboard-interactive");
8064+
if (requestSz == 0)
8065+
return 1;
8066+
8067+
/* INFO_RESPONSE body: response count of 0, matching promptCount. */
8068+
PutU32BE(infoRsp, 0);
8069+
8070+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
8071+
if (ctx == NULL)
8072+
return 1;
8073+
wolfSSH_SetUserAuth(ctx, UnitAuthKbRespReject);
8074+
wolfSSH_SetIOSend(ctx, CaptureIoSendAuthSvc);
8075+
8076+
ssh = wolfSSH_new(ctx);
8077+
if (ssh == NULL) {
8078+
wolfSSH_CTX_free(ctx);
8079+
return 1;
8080+
}
8081+
8082+
ret = wolfSSH_TestDoUserAuthRequest(ssh, request, requestSz, &idx);
8083+
if (ret != WS_SUCCESS) {
8084+
printf("UserAuthRejectedSendsFailure: kb setup ret=%d expected %d\n",
8085+
ret, WS_SUCCESS);
8086+
result = 2;
8087+
}
8088+
8089+
if (result == 0) {
8090+
/* Drop the INFO_REQUEST the setup sent; the assertion is on what the
8091+
* response draws. */
8092+
s_authSvcCaptureSz = 0;
8093+
s_authSvcSendCount = 0;
8094+
WMEMSET(s_authSvcCapture, 0, sizeof(s_authSvcCapture));
8095+
8096+
idx = 0;
8097+
ret = wolfSSH_TestDoUserAuthInfoResponse(ssh, infoRsp,
8098+
(word32)sizeof(infoRsp), &idx);
8099+
8100+
if (ret != WS_USER_AUTH_E) {
8101+
printf("UserAuthRejectedSendsFailure: info response ret=%d "
8102+
"expected %d\n", ret, WS_USER_AUTH_E);
8103+
result = 3;
8104+
}
8105+
else if (s_authSvcSendCount == 0) {
8106+
printf("UserAuthRejectedSendsFailure: info response sent nothing "
8107+
"on rejection\n");
8108+
result = 4;
8109+
}
8110+
else if (CaptureMsgId(s_authSvcCapture, s_authSvcCaptureSz)
8111+
!= MSGID_USERAUTH_FAILURE) {
8112+
printf("UserAuthRejectedSendsFailure: info response msgId=%d "
8113+
"expected USERAUTH_FAILURE\n",
8114+
CaptureMsgId(s_authSvcCapture, s_authSvcCaptureSz));
8115+
result = 5;
8116+
}
8117+
}
8118+
8119+
wolfSSH_free(ssh);
8120+
wolfSSH_CTX_free(ctx);
8121+
8122+
return result;
8123+
}
8124+
#endif /* WOLFSSH_KEYBOARD_INTERACTIVE */
8125+
8126+
8127+
/* Drive one rejected USERAUTH_REQUEST on a fresh session and check that a
8128+
* USERAUTH_FAILURE went out. Returns 0, or 1..3 naming what failed. */
8129+
static int RunRejectedRequest(const byte* request, word32 requestSz,
8130+
const char* method)
8131+
{
8132+
WOLFSSH_CTX* ctx = NULL;
8133+
WOLFSSH* ssh = NULL;
8134+
word32 idx = 0;
8135+
int ret;
8136+
int result = 0;
8137+
8138+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
8139+
if (ctx == NULL)
8140+
return 1;
8141+
wolfSSH_SetUserAuth(ctx, UnitAuthAlwaysReject);
8142+
wolfSSH_SetIOSend(ctx, CaptureIoSendAuthSvc);
8143+
8144+
ssh = wolfSSH_new(ctx);
8145+
if (ssh == NULL) {
8146+
wolfSSH_CTX_free(ctx);
8147+
return 1;
8148+
}
8149+
8150+
s_authSvcCaptureSz = 0;
8151+
s_authSvcSendCount = 0;
8152+
WMEMSET(s_authSvcCapture, 0, sizeof(s_authSvcCapture));
8153+
8154+
ret = wolfSSH_TestDoUserAuthRequest(ssh, (byte*)request, requestSz, &idx);
8155+
8156+
if (ret != WS_USER_AUTH_E) {
8157+
printf("UserAuthRejectedSendsFailure: %s ret=%d expected %d\n",
8158+
method, ret, WS_USER_AUTH_E);
8159+
result = 1;
8160+
}
8161+
else if (s_authSvcSendCount == 0) {
8162+
printf("UserAuthRejectedSendsFailure: %s sent nothing on rejection\n",
8163+
method);
8164+
result = 2;
8165+
}
8166+
else if (CaptureMsgId(s_authSvcCapture, s_authSvcCaptureSz)
8167+
!= MSGID_USERAUTH_FAILURE) {
8168+
printf("UserAuthRejectedSendsFailure: %s msgId=%d expected"
8169+
" USERAUTH_FAILURE\n", method,
8170+
CaptureMsgId(s_authSvcCapture, s_authSvcCaptureSz));
8171+
result = 3;
8172+
}
8173+
8174+
wolfSSH_free(ssh);
8175+
wolfSSH_CTX_free(ctx);
8176+
8177+
return result;
8178+
}
8179+
8180+
8181+
/* RFC 4252 section 5.1: a rejected request is answered with
8182+
* USERAUTH_FAILURE. The handler returns WS_USER_AUTH_E either way, so the
8183+
* assertion is on what went out on the wire. Every method the build dispatches
8184+
* has its own reject branch, so each is driven here.
8185+
*
8186+
* The keyboard method has two: the setup callback, which answers the request
8187+
* before any prompt goes out, and the response callback, driven separately
8188+
* through the INFO_RESPONSE shim. The publickey case needs one of the offered
8189+
* key algorithms compiled in. */
8190+
static int test_UserAuthRejectedSendsFailure(void)
8191+
{
8192+
byte request[128];
8193+
word32 requestSz;
8194+
int ret;
8195+
8196+
requestSz = BuildAuthPwRequest(request, (word32)sizeof(request));
8197+
if (requestSz == 0)
8198+
return -790;
8199+
ret = RunRejectedRequest(request, requestSz, "password");
8200+
if (ret != 0)
8201+
return -790 - ret;
8202+
8203+
#ifdef WOLFSSH_KEYBOARD_INTERACTIVE
8204+
requestSz = BuildAuthMethodRequest(request, (word32)sizeof(request),
8205+
"ssh-connection", "keyboard-interactive");
8206+
if (requestSz == 0)
8207+
return -800;
8208+
ret = RunRejectedRequest(request, requestSz, "keyboard-interactive");
8209+
if (ret != 0)
8210+
return -800 - ret;
8211+
8212+
ret = RunRejectedInfoResponse();
8213+
if (ret != 0)
8214+
return -810 - ret;
8215+
#endif
8216+
8217+
#ifdef WOLFSSH_ALLOW_USERAUTH_NONE
8218+
requestSz = BuildAuthMethodRequest(request, (word32)sizeof(request),
8219+
"ssh-connection", "none");
8220+
if (requestSz == 0)
8221+
return -795;
8222+
/* The opening probe is exempt from the failure count, not from the
8223+
* reply. */
8224+
ret = RunRejectedRequest(request, requestSz, "none");
8225+
if (ret != 0)
8226+
return -795 - ret;
8227+
#endif
8228+
8229+
#ifdef UNIT_AUTH_PK_ALGO
8230+
requestSz = BuildAuthPkRequest(request, (word32)sizeof(request));
8231+
if (requestSz == 0)
8232+
return -805;
8233+
/* No signature, so the callback answers the PK_OK probe. */
8234+
ret = RunRejectedRequest(request, requestSz, "publickey");
8235+
if (ret != 0)
8236+
return -805 - ret;
8237+
#endif
8238+
8239+
return 0;
8240+
}
8241+
8242+
79728243
/* Verify DoUserAuthRequest rejects non-"ssh-connection" service names per
79738244
* RFC 4252 Section 5. For each case we assert:
79748245
* 1. ret == WS_SUCCESS (connection stays open for retry)
@@ -17840,6 +18111,11 @@ int wolfSSH_UnitTest(int argc, char** argv)
1784018111
(unitResult == 0 ? "SUCCESS" : "FAILED"));
1784118112
testResult = testResult || unitResult;
1784218113

18114+
unitResult = test_UserAuthRejectedSendsFailure();
18115+
printf("UserAuthRejectedSendsFailure: %s\n",
18116+
(unitResult == 0 ? "SUCCESS" : "FAILED"));
18117+
testResult = testResult || unitResult;
18118+
1784318119
#if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && \
1784418120
!defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384)
1784518121
unitResult = test_EccUserAuthCurveMismatch();

wolfssh/ssh.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,9 @@ typedef struct WS_UserAuthData {
466466
* a multi-method authentication passed, WOLFSSH_USERAUTH_SUCCESS_ANOTHER
467467
* reports that a keyboard-interactive round passed and asks for the next
468468
* round, WOLFSSH_USERAUTH_WOULD_BLOCK asks for the request to be
469-
* retried, and WOLFSSH_USERAUTH_REJECTED is a hard rejection that ends
470-
* the session; any other value is treated as an ordinary failure.
469+
* retried, and WOLFSSH_USERAUTH_REJECTED is a hard rejection: the server
470+
* answers with USERAUTH_FAILURE, then ends the session. Any other value
471+
* is treated as an ordinary failure.
471472
*
472473
* WARNING: WOLFSSH_USERAUTH_SUCCESS has the value 0, the same as
473474
* WS_SUCCESS and the C "no error" idiom. A bare "return 0;", a forwarded

0 commit comments

Comments
 (0)