Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions apps/wolfsshd/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
#include <wolfssl/wolfcrypt/error-crypt.h>
#include <wolfssl/wolfcrypt/coding.h>
#include <wolfssl/wolfcrypt/asn_public.h>
#include <wolfssl/wolfcrypt/random.h>
#include <wolfssl/wolfcrypt/hash.h>

#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32))
/* Used to bind a client certificate to the requested user name: by UPN
Expand All @@ -80,6 +82,7 @@
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>
#include <time.h>
#ifdef WOLFSSH_OSSH_CERTS
#include <netinet/in.h> /* in6_addr, AF_INET6 */
#include <arpa/inet.h> /* inet_pton for source-address matching */
Expand Down Expand Up @@ -611,6 +614,37 @@ int wolfSSHD_GetCachedFakeHashCountForTest(void)
#endif /* WOLFSSHD_UNIT_TEST */
#endif /* HAVE_SHADOW */

/* Failed userauth replies take at least a per-user minimum, as OpenSSH sshd
* does: the floor plus 0 to 4.29 ms from a keyed hash of the username. */
#ifndef WOLFSSHD_MIN_FAIL_DELAY_US
#define WOLFSSHD_MIN_FAIL_DELAY_US 5000
#endif
#ifndef WOLFSSHD_MAX_FAIL_DELAY_US
#define WOLFSSHD_MAX_FAIL_DELAY_US 5000000
#endif

/* Keys the per-user delay. Written once by wolfSSHD_AuthInit() before any
* connection is forked. */
static byte failDelaySecret[16];

static void InitFailDelaySecret(void)
{
WC_RNG rng;
int ret;

ret = wc_InitRng(&rng);
if (ret == 0) {
ret = wc_RNG_GenerateBlock(&rng, failDelaySecret,
sizeof(failDelaySecret));
wc_FreeRng(&rng);
}
if (ret != 0) {
/* The delay still holds, but each user's share is predictable. */
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Error generating the auth failure delay key");
}
}

void wolfSSHD_AuthInit(void)
{
#ifdef HAVE_SHADOW
Expand Down Expand Up @@ -654,6 +688,8 @@ void wolfSSHD_AuthInit(void)
" fixed-cost fake hash instead of matching real crypt() cost");
}
#endif

InitFailDelaySecret();
}

#ifndef _WIN32
Expand Down Expand Up @@ -3037,11 +3073,118 @@ static int RequestAuthentication(WS_UserAuthData* authData,
}


/* Monotonic microseconds, modulo 2^32; only differences are used. */
static word32 AuthNowUs(void)
{
#ifdef _WIN32
/* GetTickCount64() ticks in 10-16 ms, coarser than the delay. */
LARGE_INTEGER cnt, freq;

if (!QueryPerformanceFrequency(&freq) || freq.QuadPart <= 0
|| !QueryPerformanceCounter(&cnt)) {
return 0;
}
return (word32)((cnt.QuadPart / freq.QuadPart) * 1000000
+ (cnt.QuadPart % freq.QuadPart) * 1000000 / freq.QuadPart);
#else
struct timespec ts;

if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return 0;
}
return (word32)ts.tv_sec * 1000000U + (word32)(ts.tv_nsec / 1000);
#endif
}


static void AuthSleepUs(word32 us)
{
#ifdef _WIN32
Sleep((DWORD)((us + 999) / 1000));
#else
struct timespec ts;

ts.tv_sec = (time_t)(us / 1000000U);
ts.tv_nsec = (long)(us % 1000000U) * 1000;
while (nanosleep(&ts, &ts) != 0 && errno == EINTR) {
/* resume the remaining time */
}
#endif
}


#ifdef WOLFSSHD_UNIT_TEST
void (*wsshd_authsleep_cb)(word32 us) = AuthSleepUs;
#define AuthSleepUs wsshd_authsleep_cb
#endif


/* The minimum time, in microseconds, a failed userauth for usr takes. */
WOLFSSHD_STATIC word32 UserFailDelayUs(const byte* usr, word32 usrSz)
{
word32 h = 0;
#if !defined(NO_SHA256)
#define FAIL_DELAY_HASH WC_HASH_TYPE_SHA256
#elif defined(WOLFSSL_SHA512)
#define FAIL_DELAY_HASH WC_HASH_TYPE_SHA512
#elif defined(WOLFSSL_SHA384)
#define FAIL_DELAY_HASH WC_HASH_TYPE_SHA384
#endif
#ifdef FAIL_DELAY_HASH
wc_HashAlg hash;
byte digest[WC_MAX_DIGEST_SIZE];

if (wc_HashInit(&hash, FAIL_DELAY_HASH) == 0) {
if (wc_HashUpdate(&hash, FAIL_DELAY_HASH, failDelaySecret,
sizeof(failDelaySecret)) == 0
&& (usr == NULL
|| wc_HashUpdate(&hash, FAIL_DELAY_HASH, usr, usrSz) == 0)
&& wc_HashFinal(&hash, FAIL_DELAY_HASH, digest) == 0) {
h = ((word32)digest[0] << 24) | ((word32)digest[1] << 16)
| ((word32)digest[2] << 8) | (word32)digest[3];
}
wc_HashFree(&hash, FAIL_DELAY_HASH);
WS_FORCEZERO(digest, sizeof(digest));
}
#undef FAIL_DELAY_HASH
#else
/* No hash to key the per-user share; the floor alone holds. */
(void)usr;
(void)usrSz;
#endif

return WOLFSSHD_MIN_FAIL_DELAY_US + h / 1000000U;
}


/* Sleeps until delayUs has passed since startUs, doubling delayUs while the
* time already spent exceeds it. Past the maximum it returns at once. A sleep
* may wake early, so the elapsed time is rechecked after each one. */
WOLFSSHD_STATIC void EnsureMinFailTime(word32 startUs, word32 delayUs)
{
word32 elapsed = AuthNowUs() - startUs;
int tries;

if (elapsed > WOLFSSHD_MAX_FAIL_DELAY_US) {
return;
}
while (delayUs < elapsed) {
delayUs *= 2;
}
/* The bound keeps a stalled clock from looping forever. */
for (tries = 0; tries < 64 && elapsed < delayUs; tries++) {
AuthSleepUs(delayUs - elapsed);
elapsed = AuthNowUs() - startUs;
}
}


/* return WOLFSSH_USERAUTH_SUCCESS on success */
int DefaultUserAuth(byte authType, WS_UserAuthData* authData, void* ctx)
{
int ret = WOLFSSH_USERAUTH_SUCCESS;
WOLFSSHD_AUTH* authCtx;
word32 startUs = AuthNowUs();

if (ctx == NULL) {
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No auth callbacks passed in");
Expand Down Expand Up @@ -3071,6 +3214,11 @@ int DefaultUserAuth(byte authType, WS_UserAuthData* authData, void* ctx)
ret = RequestAuthentication(authData, authCtx);
}

if (ret != WOLFSSH_USERAUTH_SUCCESS) {
EnsureMinFailTime(startUs,
UserFailDelayUs(authData->username, authData->usernameSz));
}

return ret;
}

Expand Down
6 changes: 6 additions & 0 deletions apps/wolfsshd/auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ int IsRootLoginDenied(int isRoot, WOLFSSHD_CONFIG* usrConf);
int IsRootPasswordAuthBlocked(int isRoot, WOLFSSHD_CONFIG* usrConf);
int IsRootPubKeyForcedCmdMissing(int isRoot, WOLFSSHD_CONFIG* usrConf);
int wolfSSHD_GetUserAuthTypes(const WOLFSSHD_CONFIG* usrConf);
word32 UserFailDelayUs(const byte* usr, word32 usrSz);
void EnsureMinFailTime(word32 startUs, word32 delayUs);
#ifdef WOLFSSHD_UNIT_TEST
/* EnsureMinFailTime() sleeps through this; a test may wake it early. */
extern void (*wsshd_authsleep_cb)(word32 us);
#endif
#if defined(WOLFSSH_OSSH_CERTS) && !defined(_WIN32)
int OsshPrefixMatch(const byte* a, const byte* b, int bits, int len);
int OsshSourceAddrMatch(const byte* list, word32 listSz, const char* peerIp);
Expand Down
152 changes: 152 additions & 0 deletions apps/wolfsshd/test/test_configuration.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <grp.h>
#include <time.h>
#endif

#ifndef WOLFSSH_DEFAULT_LOG_WIDTH
Expand Down Expand Up @@ -4358,6 +4359,153 @@ static int test_AuthCreateUser_privSepOff(void)
return ret;
}

static word32 TestNowUs(void)
{
struct timespec ts;

if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return 0;
}
return (word32)ts.tv_sec * 1000000U + (word32)(ts.tv_nsec / 1000);
}

/* The per-user failure delay is stable for a user and stays within OpenSSH's
* range: a 5 ms floor plus at most 4.29 ms. */
static int test_UserFailDelayUs(void)
{
static const byte alice[] = "alice";
static const byte bob[] = "bob";
word32 a, b;

a = UserFailDelayUs(alice, (word32)sizeof(alice) - 1);
b = UserFailDelayUs(bob, (word32)sizeof(bob) - 1);

if (a < 5000 || a > 5000 + 4295 || b < 5000 || b > 5000 + 4295) {
Log(" delay out of range: %u, %u\n", a, b);
return WS_FATAL_ERROR;
}
if (UserFailDelayUs(alice, (word32)sizeof(alice) - 1) != a) {
Log(" delay not stable for one user\n");
return WS_FATAL_ERROR;
}
if (UserFailDelayUs(NULL, 0) < 5000) {
return WS_FATAL_ERROR;
}
return WS_SUCCESS;
}

/* Time already spent past the delay doubles it, as OpenSSH does: 7 ms spent
* against a 5 ms delay waits until 10 ms. */
static int test_EnsureMinFailTime(void)
{
word32 start, spent;

start = TestNowUs() - 7000;
EnsureMinFailTime(start, 5000);
spent = TestNowUs() - start;
if (spent < 10000) {
Log(" returned after %u us, expected at least 10000\n", spent);
return WS_FATAL_ERROR;
}

/* Past the maximum it does not wait. */
start = TestNowUs() - 6000000;
EnsureMinFailTime(start, 5000);
spent = TestNowUs() - start;
if (spent >= 6000000 + 5000000) {
return WS_FATAL_ERROR;
}
return WS_SUCCESS;
}

static int s_shortSleepCalls = 0;

/* Sleeps a quarter of the request, as a sleep that wakes early would. */
static void ShortSleepUs(word32 us)
{
struct timespec ts;

s_shortSleepCalls++;
us /= 4;
ts.tv_sec = (time_t)(us / 1000000U);
ts.tv_nsec = (long)(us % 1000000U) * 1000;
(void)nanosleep(&ts, NULL);
}

/* A sleep that returns early is rechecked and topped up, so the minimum
* still holds. */
static int test_EnsureMinFailTime_earlyWake(void)
{
int ret = WS_SUCCESS;
void (*savedSleep)(word32) = wsshd_authsleep_cb;
word32 start, spent;

wsshd_authsleep_cb = ShortSleepUs;
s_shortSleepCalls = 0;
start = TestNowUs();
EnsureMinFailTime(start, 20000);
spent = TestNowUs() - start;
wsshd_authsleep_cb = savedSleep;

if (spent < 20000) {
Log(" returned after %u us in %d sleeps, expected at least"
" 20000\n", spent, s_shortSleepCalls);
ret = WS_FATAL_ERROR;
}
return ret;
}

/* A failed userauth reply is held to the minimum delay, even on a path that
* fails at once without looking the user up. */
static int test_DefaultUserAuth_minFailDelay(void)
{
int ret = WS_SUCCESS;
int rc;
WOLFSSHD_CONFIG* conf;
WOLFSSHD_AUTH* authCtx;
WS_UserAuthData authData;
word32 start, spent;
static const char line[] = "UsePrivilegeSeparation no";

conf = wolfSSHD_ConfigNew(NULL);
if (conf == NULL) {
return WS_MEMORY_E;
}
if (ParseConfigLine(&conf, line, (int)WSTRLEN(line), 0) != WS_SUCCESS) {
wolfSSHD_ConfigFree(conf);
return WS_FATAL_ERROR;
}
authCtx = wolfSSHD_AuthCreateUser(NULL, conf);
if (authCtx == NULL) {
wolfSSHD_ConfigFree(conf);
return WS_FATAL_ERROR;
}

WMEMSET(&authData, 0, sizeof(authData));
authData.type = WOLFSSH_USERAUTH_KEYBOARD;
authData.username = (const byte*)"delay_test_user";
authData.usernameSz = (word32)WSTRLEN((const char*)authData.username);

start = TestNowUs();
rc = DefaultUserAuth(WOLFSSH_USERAUTH_KEYBOARD, &authData, authCtx);
spent = TestNowUs() - start;

if (rc != WOLFSSH_USERAUTH_INVALID_AUTHTYPE) {
Log(" unexpected result %d\n", rc);
ret = WS_FATAL_ERROR;
}
else if (spent < UserFailDelayUs(authData.username,
authData.usernameSz)) {
Log(" failure returned after %u us, expected at least %u\n",
spent, UserFailDelayUs(authData.username, authData.usernameSz));
ret = WS_FATAL_ERROR;
}

wolfSSHD_AuthFreeUser(authCtx);
wolfSSHD_ConfigFree(conf);
return ret;
}

#if (defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN)) && \
defined(WOLFSSHD_HAVE_SHADOW)
/* PasswordAuthentication no must reject through RequestAuthentication's
Expand Down Expand Up @@ -7811,6 +7959,10 @@ const TEST_CASE testCases[] = {
TEST_DECL(test_AuthReducePermissionsUser_gid_fail),
TEST_DECL(test_AuthReducePermissionsUser_uid_fail),
TEST_DECL(test_AuthCreateUser_privSepOff),
TEST_DECL(test_UserFailDelayUs),
TEST_DECL(test_EnsureMinFailTime),
TEST_DECL(test_EnsureMinFailTime_earlyWake),
TEST_DECL(test_DefaultUserAuth_minFailDelay),
#if (defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN)) && \
defined(WOLFSSHD_HAVE_SHADOW)
TEST_DECL(test_RequestAuth_pwAuthNoRejectsBeforePasswordCheck),
Expand Down
Loading
Loading