diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 1690587d1..9c3357a0d 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -2961,7 +2961,8 @@ static int RequestAuthentication(WS_UserAuthData* authData, wolfSSHD_ConfigGetPubKeyAuth(usrConf) != 1) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Public key authentication not " "allowed by configuration!"); - ret = WOLFSSH_USERAUTH_REJECTED; + /* FAILURE, not REJECTED, so the client can try another method. */ + ret = WOLFSSH_USERAUTH_FAILURE; } if (ret == WOLFSSH_USERAUTH_SUCCESS && @@ -2970,7 +2971,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, /* Forced command required. */ wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Public key login for root requires " "a forced command by configuration!"); - ret = WOLFSSH_USERAUTH_REJECTED; + ret = WOLFSSH_USERAUTH_FAILURE; } #if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) @@ -3093,7 +3094,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, "[SSHD] Per-user TrustedUserCAKeys override is not enforced " "for certificate authentication; rejecting (user=%.32s)", usr); - ret = WOLFSSH_USERAUTH_REJECTED; + ret = WOLFSSH_USERAUTH_FAILURE; } else { #if defined(WOLFSSH_CERTS) && defined(_WIN32) @@ -3131,7 +3132,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, "[SSHD] Certificate authentication cannot bind the requested " "user without FPKI or AuthorizedKeysFile; rejecting " "(user=%.32s)", usr); - ret = WOLFSSH_USERAUTH_REJECTED; + ret = WOLFSSH_USERAUTH_FAILURE; #endif } } diff --git a/apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh b/apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh index edd07f8f7..0e79e49f1 100755 --- a/apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh +++ b/apps/wolfsshd/test/sshd_permitroot_prohibit_password.sh @@ -52,8 +52,11 @@ fi TEST_CLIENT="../../../examples/client/client" -# Attempt password login as root, which should be rejected +# Attempt password login as root, which should be rejected. The client +# tries a key first, so offer one root does not authorize. timeout 10 $TEST_CLIENT -u "$USER" -P "somepassword" -c 'true' \ + -i $PWD/../../../keys/gretel-key-ecc.der \ + -j $PWD/../../../keys/gretel-key-ecc.pub \ -h "$TEST_HOST" -p "$TEST_PORT" > /dev/null 2>&1 PASS_RESULT=$? diff --git a/examples/client/common.c b/examples/client/common.c index ae2f4974b..6aaeed2b6 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -533,9 +533,7 @@ int ClientUserAuth(byte authType, WS_UserAuthData_PublicKey* pk = &authData->sf.publicKey; if (userPublicKeyType == NULL || userPublicKeySz == 0) { - /* Nothing to sign with. SendUserAuthRequest() turns this - * into WS_FATAL_ERROR rather than putting an untyped - * publickey request on the wire. */ + /* Nothing to sign with; the library tries another method. */ return WOLFSSH_USERAUTH_FAILURE; } diff --git a/examples/portfwd/portfwd.c b/examples/portfwd/portfwd.c index ee84d5cb3..665b659bb 100644 --- a/examples/portfwd/portfwd.c +++ b/examples/portfwd/portfwd.c @@ -193,7 +193,10 @@ static int wsUserAuth(byte authType, word32 passwordSz = 0; int ret = WOLFSSH_USERAUTH_SUCCESS; - (void)authType; + /* Password only, so it is asked for once. */ + if (authType != WOLFSSH_USERAUTH_PASSWORD) + return WOLFSSH_USERAUTH_INVALID_AUTHTYPE; + if (defaultPassword != NULL) { passwordSz = (word32)strlen(defaultPassword); if (passwordSz > (word32)sizeof(userPassword)) diff --git a/src/internal.c b/src/internal.c index 063846906..235e24433 100644 --- a/src/internal.c +++ b/src/internal.c @@ -12442,12 +12442,14 @@ static int DoUserAuthFailure(WOLFSSH* ssh, for (j = 0; j < sizeof(ssh->supportedAuth); j++) { if (authList[i] == ssh->supportedAuth[j]) { switch(authList[i]) { + /* As OpenSSH: the key once, the others thrice. */ case ID_USERAUTH_PASSWORD: - authType |= WOLFSSH_USERAUTH_PASSWORD; + if (ssh->pwAuthAttempts < 3) { + authType |= WOLFSSH_USERAUTH_PASSWORD; + } break; #ifdef WOLFSSH_KEYBOARD_INTERACTIVE case ID_USERAUTH_KEYBOARD: - /* try a different auth method if failing */ if (ssh->kbAuthAttempts < 3) { authType |= WOLFSSH_USERAUTH_KEYBOARD; } @@ -12455,7 +12457,9 @@ static int DoUserAuthFailure(WOLFSSH* ssh, #endif #ifndef WOLFSSH_NO_PUBKEY_AUTH case ID_USERAUTH_PUBLICKEY: - authType |= WOLFSSH_USERAUTH_PUBLICKEY; + if (!ssh->userAuthPkDone) { + authType |= WOLFSSH_USERAUTH_PUBLICKEY; + } break; #endif default: @@ -12465,9 +12469,9 @@ static int DoUserAuthFailure(WOLFSSH* ssh, } } - /* the auth type attempted was not in the list */ + /* nothing in the list is supported or left to try */ if (authType == 0) { - WLOG(WS_LOG_DEBUG, "Did not match any auth IDs in peers list"); + WLOG(WS_LOG_DEBUG, "No untried auth method in peer's list"); ret = WS_USER_AUTH_E; } } @@ -22863,6 +22867,43 @@ static int BuildUserAuthRequestMlDsa(WOLFSSH* ssh, #ifndef WOLFSSH_NO_PUBKEY_AUTH +/* Returns the signature algorithm for keyId, or ID_UNKNOWN if the peer + * accepts none. */ +static byte UserAuthSigId(WOLFSSH* ssh, byte keyId) +{ + byte algoId[4]; + word32 algoIdSz = 0; + + /* Typically a 1:1 match of key type to signature type. RSA uses + * a key type of ssh-rsa, but different signature types. */ + if (keyId == ID_SSH_RSA) { + #ifndef WOLFSSH_NO_RSA_SHA2_512 + algoId[algoIdSz++] = ID_RSA_SHA2_512; + #endif + #ifndef WOLFSSH_NO_RSA_SHA2_256 + algoId[algoIdSz++] = ID_RSA_SHA2_256; + #endif + #if !defined(WOLFSSH_NO_SSH_RSA_SHA1) \ + && defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) + algoId[algoIdSz++] = ID_SSH_RSA; + #endif + } + else { + algoId[algoIdSz++] = keyId; + } + + /* Is that in the peerSigId list? With no server-sig-algs, RFC 8308 + * section 2.2, use the canned order, which prefers rsa-sha2-* to + * ssh-rsa; RFC 8332 section 3.3 allows either. */ + if (ssh->peerSigAlgsSeen) { + return MatchIdLists(WOLFSSH_ENDPOINT_CLIENT, algoId, algoIdSz, + ssh->peerSigId, ssh->peerSigIdSz); + } + return MatchIdLists(WOLFSSH_ENDPOINT_CLIENT, algoId, algoIdSz, + cannedKeyAlgoClient, cannedKeyAlgoClientSz); +} + + static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, WS_UserAuthData* authData, WS_KeySignature* keySig) { @@ -22875,43 +22916,14 @@ static int PrepareUserAuthRequestPublicKey(WOLFSSH* ssh, word32* payloadSz, } if (ret == WS_SUCCESS) { - byte matchId, algoId[4]; - word32 algoIdSz = 0; + byte matchId; keySig->keyId = NameToId( (const char*)authData->sf.publicKey.publicKeyType, authData->sf.publicKey.publicKeyTypeSz); keySig->keyName = (const char*)authData->sf.publicKey.publicKeyType; keySig->keyNameSz = authData->sf.publicKey.publicKeyTypeSz; - /* Typically a 1:1 match of key type to signature type. RSA uses - * a key type of ssh-rsa, but different signature types. */ - if (keySig->keyId == ID_SSH_RSA) { - #ifndef WOLFSSH_NO_RSA_SHA2_512 - algoId[algoIdSz++] = ID_RSA_SHA2_512; - #endif - #ifndef WOLFSSH_NO_RSA_SHA2_256 - algoId[algoIdSz++] = ID_RSA_SHA2_256; - #endif - #if !defined(WOLFSSH_NO_SSH_RSA_SHA1) \ - && defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) - algoId[algoIdSz++] = ID_SSH_RSA; - #endif - } - else { - algoId[algoIdSz++] = keySig->keyId; - } - - /* Is that in the peerSigId list? With no server-sig-algs, RFC 8308 - * section 2.2, use the canned order, which prefers rsa-sha2-* to - * ssh-rsa; RFC 8332 section 3.3 allows either. */ - if (ssh->peerSigAlgsSeen) { - matchId = MatchIdLists(WOLFSSH_ENDPOINT_CLIENT, algoId, algoIdSz, - ssh->peerSigId, ssh->peerSigIdSz); - } - else { - matchId = MatchIdLists(WOLFSSH_ENDPOINT_CLIENT, algoId, algoIdSz, - cannedKeyAlgoClient, cannedKeyAlgoClientSz); - } + matchId = UserAuthSigId(ssh, keySig->keyId); if (matchId == ID_UNKNOWN) { ret = WS_MATCH_KEY_ALGO_E; } @@ -23390,80 +23402,80 @@ int SendUserAuthRequest(WOLFSSH* ssh, byte authType, int addSig) keySig_ptr->sigId = ID_NONE; keySig_ptr->heap = ssh->ctx->heap; -#ifdef WOLFSSH_TPM - /* When the client has a TPM key configured, prefer publickey auth so - * the TPM key is used even if the server also offers password or - * keyboard-interactive. Only strip the other methods on the first - * attempt; once publickey has been tried and rejected, allow fallback - * to password/keyboard on the next DoUserAuthFailure() retry. */ - if (ssh->ctx->tpmKey != NULL && !ssh->tpmPubkeyTried - && (authType & WOLFSSH_USERAUTH_PUBLICKEY)) { - ssh->tpmPubkeyTried = 1; - authType &= ~WOLFSSH_USERAUTH_PASSWORD; - #ifdef WOLFSSH_KEYBOARD_INTERACTIVE - authType &= ~WOLFSSH_USERAUTH_KEYBOARD; + authData.type = authType; + authData.username = (const byte*)ssh->userName; + authData.usernameSz = ssh->userNameSz; + + /* The client's order, as OpenSSH: publickey, + * keyboard-interactive, then password. */ + if ((authType & WOLFSSH_USERAUTH_PUBLICKEY) + && ssh->ctx->userAuthCb != NULL) { + WLOG(WS_LOG_DEBUG, "SUAR: Calling the userauth callback for key"); + /* Success without a key is a decline. */ + if (ssh->ctx->userAuthCb(WOLFSSH_USERAUTH_PUBLICKEY, + &authData, ssh->userAuthCtx) + == WOLFSSH_USERAUTH_SUCCESS + && authData.sf.publicKey.publicKeyType != NULL + && authData.sf.publicKey.publicKeyTypeSz != 0 + && authData.sf.publicKey.publicKey != NULL + && authData.sf.publicKey.publicKeySz != 0) { + WLOG(WS_LOG_DEBUG, "SUAR: Callback successful public key"); + authId = ID_USERAUTH_PUBLICKEY; + } + else { + WLOG(WS_LOG_DEBUG, "SUAR: Couldn't get key"); + ssh->userAuthPkDone = 1; /* a decline is the one key try */ + } + #ifndef WOLFSSH_NO_PUBKEY_AUTH + /* Skip a key the peer has no signature algorithm for. */ + if (authId == ID_USERAUTH_PUBLICKEY + && UserAuthSigId(ssh, NameToId( + (const char*)authData.sf.publicKey.publicKeyType, + authData.sf.publicKey.publicKeyTypeSz)) + == ID_UNKNOWN) { + WLOG(WS_LOG_DEBUG, "SUAR: No signature algorithm for key"); + authId = ID_NONE; + ssh->userAuthPkDone = 1; + } #endif + if (authId == ID_USERAUTH_PUBLICKEY) { + authData.type = WOLFSSH_USERAUTH_PUBLICKEY; + } + else { + WMEMSET(&authData.sf, 0, sizeof(authData.sf)); + authData.type = (byte)(authType & ~WOLFSSH_USERAUTH_PUBLICKEY); + } } -#endif - #ifdef WOLFSSH_KEYBOARD_INTERACTIVE /* Callback happens later for keyboard auth */ - if (authType & WOLFSSH_USERAUTH_KEYBOARD) { - authId = ID_USERAUTH_KEYBOARD; + if (authId == ID_NONE && (authType & WOLFSSH_USERAUTH_KEYBOARD)) { + authId = ID_USERAUTH_KEYBOARD; } - else #endif - if (ssh->ctx->userAuthCb != NULL) { - WLOG(WS_LOG_DEBUG, "SUAR: Calling the userauth callback"); - - authData.type = authType; - authData.username = (const byte*)ssh->userName; - authData.usernameSz = ssh->userNameSz; - - if (authType & WOLFSSH_USERAUTH_PASSWORD) { - ret = ssh->ctx->userAuthCb(WOLFSSH_USERAUTH_PASSWORD, - &authData, ssh->userAuthCtx); - if (ret != WOLFSSH_USERAUTH_SUCCESS) { - WLOG(WS_LOG_DEBUG, "SUAR: Couldn't get password"); - ret = WS_FATAL_ERROR; - authType &= ~WOLFSSH_USERAUTH_PASSWORD; - } - else { - WLOG(WS_LOG_DEBUG, "SUAR: Callback successful password"); - authId = ID_USERAUTH_PASSWORD; - authType = WOLFSSH_USERAUTH_PASSWORD; - authData.type = authType; - } + if (authId == ID_NONE && (authType & WOLFSSH_USERAUTH_PASSWORD) + && ssh->ctx->userAuthCb != NULL) { + WLOG(WS_LOG_DEBUG, "SUAR: Calling the userauth callback for " + "password"); + if (ssh->ctx->userAuthCb(WOLFSSH_USERAUTH_PASSWORD, + &authData, ssh->userAuthCtx) + == WOLFSSH_USERAUTH_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SUAR: Callback successful password"); + authId = ID_USERAUTH_PASSWORD; + authData.type = WOLFSSH_USERAUTH_PASSWORD; } - /* fall into public key case if keyboard or password case was not - * successful */ - if ((ret == WS_FATAL_ERROR || - -#ifdef WOLFSSH_KEYBOARD_INTERACTIVE - (!(authType & WOLFSSH_USERAUTH_PASSWORD) && - !(authType & WOLFSSH_USERAUTH_KEYBOARD))) && -#else - !(authType & WOLFSSH_USERAUTH_PASSWORD)) && -#endif - (authType & WOLFSSH_USERAUTH_PUBLICKEY)) { - ret = ssh->ctx->userAuthCb(WOLFSSH_USERAUTH_PUBLICKEY, - &authData, ssh->userAuthCtx); - if (ret != WOLFSSH_USERAUTH_SUCCESS) { - WLOG(WS_LOG_DEBUG, "SUAR: Couldn't get key"); - ret = WS_FATAL_ERROR; - } - else { - WLOG(WS_LOG_DEBUG, "SUAR: Callback successful public key"); - authData.type = WOLFSSH_USERAUTH_PUBLICKEY; - authId = ID_USERAUTH_PUBLICKEY; - } + else { + WLOG(WS_LOG_DEBUG, "SUAR: Couldn't get password"); } - } - else { + /* authType 0 is the opening "none" probe. */ + if (ssh->ctx->userAuthCb == NULL && authId == ID_NONE) { WLOG(WS_LOG_DEBUG, "SUAR: No user auth callback"); ret = WS_FATAL_ERROR; } + else if (authId == ID_NONE && authType != 0) { + WLOG(WS_LOG_DEBUG, "SUAR: Callback declined every method"); + ret = WS_USER_AUTH_E; + } } if (ret == WS_SUCCESS) { @@ -23524,6 +23536,7 @@ int SendUserAuthRequest(WOLFSSH* ssh, byte authType, int addSig) WOLFSSH_UNUSED(sigStartIdx); ret = BuildUserAuthRequestPassword(ssh, output, &idx, &authData); + ssh->pwAuthAttempts++; } #ifdef WOLFSSH_KEYBOARD_INTERACTIVE else if (authId == ID_USERAUTH_KEYBOARD) { diff --git a/tests/regress.c b/tests/regress.c index 07cc15ff5..7fd8fafdc 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -16885,18 +16885,18 @@ static void TestUserAuthPkWithoutServerSigAlgs(void) algName, sizeof(algName)), WS_SUCCESS); AssertStrEQ(algName, "rsa-sha2-256"); - /* Listed but nothing usable is still a mismatch. */ + /* Listed but nothing usable: the key is skipped, nothing is left. */ AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "bogus", - algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + algName, sizeof(algName)), WS_USER_AUTH_E); AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "", - algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + algName, sizeof(algName)), WS_USER_AUTH_E); /* Nor when it replaces a usable one, RFC 8308 section 2.4. */ pkCasePriorSigAlgs = "rsa-sha2-256"; AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "bogus", - algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + algName, sizeof(algName)), WS_USER_AUTH_E); AssertIntEQ(RunUserAuthPkSigAlgsCase("server-sig-algs", "", - algName, sizeof(algName)), WS_MATCH_KEY_ALGO_E); + algName, sizeof(algName)), WS_USER_AUTH_E); pkCasePriorSigAlgs = NULL; #if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_SSH_RSA_SHA1) \ @@ -16904,7 +16904,7 @@ static void TestUserAuthPkWithoutServerSigAlgs(void) /* Our own choice skips soft-disabled SHA-1. */ pkCaseKeyType = "x509v3-ssh-rsa"; AssertIntEQ(RunUserAuthPkSigAlgsCase(NULL, NULL, algName, - sizeof(algName)), WS_MATCH_KEY_ALGO_E); + sizeof(algName)), WS_USER_AUTH_E); pkCaseKeyType = NULL; #endif @@ -16934,6 +16934,309 @@ static void TestUserAuthPkWithoutServerSigAlgs(void) } #endif +#if !defined(NO_WOLFSSH_CLIENT) && !defined(WOLFSSH_NO_PUBKEY_AUTH) \ + && ((!defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_RSA_SHA2_256)) \ + || !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) +/* Each callback's authType and the authData->type it saw. */ +static byte authOrderCalls[8]; +static byte authOrderTypes[8]; +static word32 authOrderCount; + +/* Like the client apps: key from ClientUserAuth(), password refused + * while the key is still on offer. */ +static int AuthOrderUserAuth(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + static byte secret[] = "secret"; + + if (authOrderCount < (word32)sizeof(authOrderCalls)) { + authOrderCalls[authOrderCount] = authType; + authOrderTypes[authOrderCount] = authData->type; + } + authOrderCount++; + + if (authType == WOLFSSH_USERAUTH_PUBLICKEY) + return ClientUserAuth(authType, authData, ctx); + if (authType == WOLFSSH_USERAUTH_PASSWORD) { + if (authData->type & WOLFSSH_USERAUTH_PUBLICKEY) + return WOLFSSH_USERAUTH_FAILURE; + authData->sf.password.password = secret; + authData->sf.password.passwordSz = (word32)sizeof(secret) - 1; + return WOLFSSH_USERAUTH_SUCCESS; + } + return WOLFSSH_USERAUTH_FAILURE; +} + +typedef struct AuthOrderHarness { + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + MemIo io; + byte in[128]; + byte out[2048]; +} AuthOrderHarness; + +/* A client past its "none" probe with the gretel key loaded. */ +static void InitAuthOrderHarness(AuthOrderHarness* h) +{ + WMEMSET(h, 0, sizeof(*h)); +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + AssertIntEQ(ClientUsePubKey("keys/gretel-key-ecc.pub"), 0); + AssertIntEQ(ClientSetPrivateKey("keys/gretel-key-ecc.pem"), 0); +#else + AssertIntEQ(ClientUsePubKey("keys/gretel-key-rsa.pub"), 0); + AssertIntEQ(ClientSetPrivateKey("keys/gretel-key-rsa.pem"), 0); +#endif + + h->ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(h->ctx); + wolfSSH_SetIORecv(h->ctx, MemRecv); + wolfSSH_SetIOSend(h->ctx, MemSend); + wolfSSH_SetUserAuth(h->ctx, AuthOrderUserAuth); + h->ssh = wolfSSH_new(h->ctx); + AssertNotNull(h->ssh); + AssertIntEQ(wolfSSH_SetUsername(h->ssh, "gretel"), WS_SUCCESS); + + MemIoInit(&h->io, NULL, 0, h->out, (word32)sizeof(h->out)); + wolfSSH_SetIOReadCtx(h->ssh, &h->io); + wolfSSH_SetIOWriteCtx(h->ssh, &h->io); + AssertIntEQ(wolfSSH_SetHighwater(h->ssh, 0), WS_SUCCESS); + h->ssh->sessionIdSz = WC_SHA256_DIGEST_SIZE; + WMEMSET(h->ssh->sessionId, 0x33, h->ssh->sessionIdSz); + h->ssh->connectState = CONNECT_CLIENT_USERAUTH_SENT; + h->ssh->serverState = SERVER_USERAUTH_REQUEST_DONE; + + WMEMSET(authOrderCalls, 0, sizeof(authOrderCalls)); + WMEMSET(authOrderTypes, 0, sizeof(authOrderTypes)); + authOrderCount = 0; +} + +static void FreeAuthOrderHarness(AuthOrderHarness* h) +{ + wolfSSH_free(h->ssh); + wolfSSH_CTX_free(h->ctx); + ClientFreeBuffers(); +} + +/* Feed a USERAUTH_FAILURE listing methods; returns DoReceive(). */ +static int AuthOrderFail(AuthOrderHarness* h, const char* methods) +{ + byte payload[96]; + word32 payloadSz; + word32 inSz; + + payloadSz = AppendString(payload, sizeof(payload), 0, methods); + payloadSz = AppendByte(payload, sizeof(payload), payloadSz, 0); + inSz = WrapPacket(MSGID_USERAUTH_FAILURE, payload, payloadSz, + h->in, sizeof(h->in)); + + h->io.in = h->in; + h->io.inSz = inSz; + h->io.inOff = 0; + h->io.outSz = 0; + authOrderCount = 0; + return DoReceive(h->ssh); +} + +/* 1 if the one USERAUTH_REQUEST the client sent names method. */ +static int AuthOrderSent(AuthOrderHarness* h, const char* method) +{ + const byte* payload; + const byte* field = NULL; + word32 payloadSz; + word32 fieldSz = 0; + word32 idx = 0; + int i; + + AssertTrue(h->io.outSz > 0); + AssertIntEQ(ParseMsgId(h->io.out, h->io.outSz), MSGID_USERAUTH_REQUEST); + AssertIntEQ(NextPacketOffset(h->io.out, h->io.outSz), h->io.outSz); + payloadSz = ParsePayloadLen(h->io.out, h->io.outSz) - MSG_ID_SZ; + payload = h->io.out + UINT32_SZ + PAD_LENGTH_SZ + MSG_ID_SZ; + + /* user name, service name, method name */ + for (i = 0; i < 3; i++) { + AssertIntEQ(GetStringRef(&fieldSz, &field, payload, payloadSz, + &idx), WS_SUCCESS); + } + return fieldSz == (word32)WSTRLEN(method) + && WMEMCMP(field, method, fieldSz) == 0; +} + +/* Key first even when the server lists password first. */ +static void TestClientAuthTriesKeyFirst(void) +{ + AuthOrderHarness h; + + InitAuthOrderHarness(&h); + + AssertIntEQ(AuthOrderFail(&h, "password,publickey"), WS_SUCCESS); + AssertIntEQ(authOrderCount, 1); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PUBLICKEY); + AssertIntEQ(authOrderTypes[0], + WOLFSSH_USERAUTH_PUBLICKEY | WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "publickey")); + + FreeAuthOrderHarness(&h); +} + +/* Password-only server, key-loaded client. */ +static void TestClientAuthPasswordOnlyServer(void) +{ + AuthOrderHarness h; + + InitAuthOrderHarness(&h); + + AssertIntEQ(AuthOrderFail(&h, "password"), WS_SUCCESS); + AssertIntEQ(authOrderCount, 1); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PASSWORD); + AssertIntEQ(authOrderTypes[0], WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "password")); + + FreeAuthOrderHarness(&h); +} + +/* A callback answering every type alike gives no key: password next. */ +static int AuthOrderAnyTypeUserAuth(byte authType, WS_UserAuthData* authData, + void* ctx) +{ + static byte secret[] = "secret"; + + (void)ctx; + if (authOrderCount < (word32)sizeof(authOrderCalls)) + authOrderCalls[authOrderCount] = authType; + authOrderCount++; + authData->sf.password.password = secret; + authData->sf.password.passwordSz = (word32)sizeof(secret) - 1; + return WOLFSSH_USERAUTH_SUCCESS; +} + +static void TestClientAuthKeylessCallback(void) +{ + AuthOrderHarness h; + + InitAuthOrderHarness(&h); + wolfSSH_SetUserAuth(h.ctx, AuthOrderAnyTypeUserAuth); + + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_SUCCESS); + AssertIntEQ(authOrderCount, 2); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PUBLICKEY); + AssertIntEQ(authOrderCalls[1], WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "password")); + + /* The decline was the key try: not asked for again. */ + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_SUCCESS); + AssertIntEQ(authOrderCount, 1); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "password")); + + FreeAuthOrderHarness(&h); +} + +/* A refused key falls back to password; tries are bounded. */ +static void TestClientAuthFallbackAndBound(void) +{ + AuthOrderHarness h; + int i; + + InitAuthOrderHarness(&h); + + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_SUCCESS); + AssertTrue(AuthOrderSent(&h, "publickey")); + + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_SUCCESS); + AssertIntEQ(authOrderCount, 1); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PASSWORD); + AssertIntEQ(authOrderTypes[0], WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "password")); + + /* Three passwords in all, then nothing is left. */ + for (i = 0; i < 2; i++) { + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_SUCCESS); + AssertTrue(AuthOrderSent(&h, "password")); + } + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_FATAL_ERROR); + AssertIntEQ(h.ssh->error, WS_USER_AUTH_E); + AssertIntEQ(authOrderCount, 0); + AssertIntEQ(h.io.outSz, 0); + + FreeAuthOrderHarness(&h); + + /* Publickey-only: a refused key fails, no unsigned retry. */ + InitAuthOrderHarness(&h); + AssertIntEQ(AuthOrderFail(&h, "publickey"), WS_SUCCESS); + AssertTrue(AuthOrderSent(&h, "publickey")); + AssertIntEQ(AuthOrderFail(&h, "publickey"), WS_FATAL_ERROR); + AssertIntEQ(h.ssh->error, WS_USER_AUTH_E); + AssertIntEQ(authOrderCount, 0); + AssertIntEQ(h.io.outSz, 0); + FreeAuthOrderHarness(&h); +} + +/* A key with no usable signature algorithm is skipped. */ +static void TestClientAuthKeyNoSigAlgo(void) +{ + AuthOrderHarness h; + + InitAuthOrderHarness(&h); + /* server-sig-algs naming only an algorithm the key cannot use */ + h.ssh->peerSigId = (byte*)WMALLOC(1, h.ssh->ctx->heap, DYNTYPE_ID); + AssertNotNull(h.ssh->peerSigId); +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + h.ssh->peerSigId[0] = ID_ECDSA_SHA2_NISTP384; +#else + h.ssh->peerSigId[0] = ID_ECDSA_SHA2_NISTP256; +#endif + h.ssh->peerSigIdSz = 1; + h.ssh->peerSigAlgsSeen = 1; + + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_SUCCESS); + AssertIntEQ(authOrderCount, 2); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PUBLICKEY); + AssertIntEQ(authOrderCalls[1], WOLFSSH_USERAUTH_PASSWORD); + AssertIntEQ(authOrderTypes[1], WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "password")); + + /* Not asked for again. */ + AssertIntEQ(AuthOrderFail(&h, "publickey,password"), WS_SUCCESS); + AssertIntEQ(authOrderCount, 1); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "password")); + + FreeAuthOrderHarness(&h); +} + +#ifdef WOLFSSH_KEYBOARD_INTERACTIVE +/* All three: key, keyboard-interactive three times, then password. */ +static void TestClientAuthThreeMethodOrder(void) +{ + const char* all = "password,keyboard-interactive,publickey"; + AuthOrderHarness h; + int i; + + InitAuthOrderHarness(&h); + + AssertIntEQ(AuthOrderFail(&h, all), WS_SUCCESS); + AssertIntEQ(authOrderCount, 1); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PUBLICKEY); + AssertTrue(AuthOrderSent(&h, "publickey")); + + for (i = 0; i < 3; i++) { + AssertIntEQ(AuthOrderFail(&h, all), WS_SUCCESS); + AssertIntEQ(authOrderCount, 0); + AssertTrue(AuthOrderSent(&h, "keyboard-interactive")); + } + + AssertIntEQ(AuthOrderFail(&h, all), WS_SUCCESS); + AssertIntEQ(authOrderCount, 1); + AssertIntEQ(authOrderCalls[0], WOLFSSH_USERAUTH_PASSWORD); + AssertIntEQ(authOrderTypes[0], WOLFSSH_USERAUTH_PASSWORD); + AssertTrue(AuthOrderSent(&h, "password")); + + FreeAuthOrderHarness(&h); +} +#endif +#endif + /* Windows has no /dev/null; the null device there is "NUL". Keyed on * _WIN32, like the arpa/inet.h/direct.h include guard above: this is a * libc-availability question, not a wolfSSH API-selection one. */ @@ -23364,6 +23667,14 @@ int main(int argc, char** argv) && ((!defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_RSA_SHA2_256)) \ || !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) TestUserAuthPkWithoutServerSigAlgs(); + TestClientAuthTriesKeyFirst(); + TestClientAuthPasswordOnlyServer(); + TestClientAuthKeylessCallback(); + TestClientAuthFallbackAndBound(); + TestClientAuthKeyNoSigAlgo(); +#ifdef WOLFSSH_KEYBOARD_INTERACTIVE + TestClientAuthThreeMethodOrder(); +#endif #endif TestPasswordEofNoCrash(); TestPasswordBufferWiped(); diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 90afa378a..9f731b1e9 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1436,6 +1436,7 @@ struct WOLFSSH { void* publicKeyCheckCtx; byte sendTerminalRequest; byte userAuthPkDone; + byte pwAuthAttempts; byte sendExtInfo; byte extInfoSent; /* track if the ext info has already been sent */ byte sendStrictKex; /* offer strict KEX on initial KEXINIT */ @@ -1533,9 +1534,6 @@ struct WOLFSSH { byte kbRequestSent; /* client asked for keyboard-interactive, may be * prompted until the server's SUCCESS or FAILURE */ #endif -#ifdef WOLFSSH_TPM - byte tpmPubkeyTried; /* client tried TPM publickey; allow auth fallback */ -#endif #ifdef WOLFSSH_TEST_INTERNAL word32 testSftpSendCap; /* test hook: cap per-call SFTP buffer send */ word32 testSftpStallPending; /* test hook: force N flush-only resumes */ diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 19361f913..0be3ea02f 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -783,6 +783,12 @@ typedef struct WS_UserAuthData { * unchecked key authorizes an attacker-supplied key. The library verifies * the signature, not the key's authorization. * + * On a client it supplies credentials. Of the server's methods, the key is + * tried once, then keyboard-interactive and password up to three times + * each; a decline moves on, and authData->type holds the methods left. + * Keyboard-interactive is requested without asking the callback, and + * declining its prompts ends authentication. + * * Compare secrets with wolfSSH_ConstantCompare(), not memcmp(). */ typedef int (*WS_CallbackUserAuth)(byte authType, WS_UserAuthData* authData, void* ctx);