mirror of https://github.com/wolfSSL/wolfssh.git
Mitigate user enumeration timing oracle using dummy hash cache
parent
48f1bd02ec
commit
492e8cd65d
|
|
@ -94,6 +94,15 @@
|
|||
#if !defined(_WIN32) && !(defined(__OSX__) || defined(__APPLE__))
|
||||
#include <shadow.h>
|
||||
#define HAVE_SHADOW
|
||||
|
||||
#if defined(_AIX) || defined(__TOS_AIX__)
|
||||
#define WSSHD_SHADOW_FILE "/etc/security/passwd"
|
||||
#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
|
||||
#define WSSHD_SHADOW_FILE "/etc/master.passwd"
|
||||
#else
|
||||
#define WSSHD_SHADOW_FILE "/etc/shadow"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(WOLFSSHD_UNIT_TEST) && !defined(_WIN32)
|
||||
|
|
@ -111,6 +120,10 @@ int (*wsshd_seteuid_cb)(WUID_T) = seteuid;
|
|||
struct passwd* (*wsshd_getpwnam_cb)(const char*) = getpwnam;
|
||||
#define getpwnam wsshd_getpwnam_cb
|
||||
int (*wsshd_setgroups_cb)(int, const WGID_T*) = wsshd_setgroups_default;
|
||||
#ifdef HAVE_SHADOW
|
||||
struct spwd* (*wsshd_getspnam_cb)(const char*) = getspnam;
|
||||
#define getspnam wsshd_getspnam_cb
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef WOLFSSH_OSSH_CERTS
|
||||
|
|
@ -318,6 +331,255 @@ static int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key,
|
|||
return ret;
|
||||
}
|
||||
|
||||
#ifdef HAVE_SHADOW
|
||||
/* Shared sizing for the dummy-hash buffers used to equalize crypt() timing
|
||||
* across real and fake password checks. Also used in CheckPasswordUnix to
|
||||
* size the real shadow hash copy buffer, so raising this changes both the
|
||||
* fake-hash template capacity and the real-hash fail-closed threshold. */
|
||||
#define WSSHD_FAKE_HASH_SZ 256
|
||||
/* Modular crypt bcrypt format: "$2<variant>$<cost>$" prefix is 7 bytes,
|
||||
* followed by a 22-byte base64-like salt, before the 31-byte digest. */
|
||||
#define WSSHD_BCRYPT_PREFIX_LEN 7
|
||||
#define WSSHD_BCRYPT_SALT_LEN 22
|
||||
/* Must be at least WSSHD_BCRYPT_SALT_LEN characters; indexed modulo its own
|
||||
* length below so a mismatch can't read out of bounds. */
|
||||
#define WSSHD_DUMMY_SALT_ALPHABET "ABCDEFGHIJKLMNOPQRSTUV"
|
||||
|
||||
#define WSSHD_MAX_FAKE_HASHES 8
|
||||
/* Oversized vs. real /etc/shadow lines so ordinary entries aren't truncated. */
|
||||
#define WSSHD_SHADOW_LINE_SZ 512
|
||||
/* Scratch buffer for draining an over-length line's remainder. */
|
||||
#define WSSHD_SHADOW_DUMP_SZ 256
|
||||
/* No lock needed: wolfsshd forks a fresh process per connection, so each
|
||||
* process's copy is written once by AuthInit() before any auth attempt. */
|
||||
static char cachedFakeHashes[WSSHD_MAX_FAKE_HASHES][WSSHD_FAKE_HASH_SZ] = {{0}};
|
||||
static int numCachedFakeHashes = 0;
|
||||
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
void GetFakeHashFromTemplate(const char* tmpl, char* out, word32 outSz)
|
||||
#else
|
||||
static void GetFakeHashFromTemplate(const char* tmpl, char* out, word32 outSz)
|
||||
#endif
|
||||
{
|
||||
word32 i;
|
||||
word32 dollarCount = 0;
|
||||
word32 lastDollarIdx = 0;
|
||||
|
||||
if (tmpl == NULL || out == NULL || outSz < 3) return;
|
||||
|
||||
/* Output will always be considered a "locked" account by prefixing '!' */
|
||||
out[0] = '!';
|
||||
|
||||
/* If it doesn't look like a modular crypt format, leave just "!" so
|
||||
* CheckPasswordHashUnix's storedSz > 1 check is false and it falls
|
||||
* through to the fixed-cost fakeHashSHA512 salt instead of reusing
|
||||
* a bare "*" that fails crypt() immediately and skips that cost. */
|
||||
if (tmpl[0] != '$') {
|
||||
out[1] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (XSTRNCMP(tmpl, "$2", 2) == 0) {
|
||||
/* bcrypt: copy only prefix+salt, excluding the digest. Legacy
|
||||
* "$2$NN$" has no variant letter, so its prefix is 1 byte shorter
|
||||
* than "$2a$NN$" etc. */
|
||||
word32 prefixLen = (tmpl[2] == '$') ?
|
||||
WSSHD_BCRYPT_PREFIX_LEN - 1 : WSSHD_BCRYPT_PREFIX_LEN;
|
||||
word32 saltEnd = prefixLen + WSSHD_BCRYPT_SALT_LEN;
|
||||
word32 copyLen = (saltEnd < outSz - 1) ? saltEnd : (outSz - 2);
|
||||
word32 tmplLen = (word32)XSTRLEN(tmpl);
|
||||
|
||||
if (copyLen > tmplLen) {
|
||||
copyLen = tmplLen;
|
||||
}
|
||||
|
||||
XMEMCPY(out + 1, tmpl, copyLen);
|
||||
out[1 + copyLen] = '\0';
|
||||
for (i = prefixLen; i < copyLen; i++) {
|
||||
/* Overwrite with a dummy alphanumeric salt */
|
||||
out[i + 1] = WSSHD_DUMMY_SALT_ALPHABET[
|
||||
(i - prefixLen) % (sizeof(WSSHD_DUMMY_SALT_ALPHABET) - 1)];
|
||||
}
|
||||
}
|
||||
else {
|
||||
word32 prevDollarIdx = 0;
|
||||
/* Others (MD5, SHA-256, SHA-512, yescrypt, etc.): salt ends at the
|
||||
* last '$' before the digest. yescrypt's non-standard encoding may
|
||||
* yield dollarCount < 3, producing '!*', which causes
|
||||
* CheckPasswordHashUnix to fall back to the fixed-cost SHA-512 salt. */
|
||||
for (i = 0; tmpl[i] != '\0'; i++) {
|
||||
if (tmpl[i] == '$') {
|
||||
dollarCount++;
|
||||
prevDollarIdx = lastDollarIdx;
|
||||
lastDollarIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
/* e.g., $6$rounds=5000$salt$hash -> prevDollarIdx is before 'salt' */
|
||||
if (dollarCount >= 3 && prevDollarIdx + 2 < outSz) {
|
||||
XMEMCPY(out + 1, tmpl, prevDollarIdx + 1);
|
||||
out[prevDollarIdx + 2] = '\0';
|
||||
XSTRNCAT(out, "wolfSSHFakeSalt$", outSz - XSTRLEN(out) - 1);
|
||||
}
|
||||
else {
|
||||
XSTRNCPY(out + 1, "*", outSz - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Parses a "user:hash:..." shadow line and adds its fake-hash template to
|
||||
* cachedFakeHashes, deduplicated and capped at WSSHD_MAX_FAKE_HASHES.
|
||||
* Mutates 'line' in place. Exposed unconditionally so unit tests can drive
|
||||
* it with synthetic lines. */
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
void AddShadowLineToFakeHashCache(char* line)
|
||||
#else
|
||||
static void AddShadowLineToFakeHashCache(char* line)
|
||||
#endif
|
||||
{
|
||||
char tmpl[WSSHD_FAKE_HASH_SZ];
|
||||
char* colon1;
|
||||
char* colon2;
|
||||
int duplicate;
|
||||
int i;
|
||||
|
||||
if (numCachedFakeHashes >= WSSHD_MAX_FAKE_HASHES) {
|
||||
return;
|
||||
}
|
||||
|
||||
colon1 = WSTRCHR(line, ':');
|
||||
if (colon1 != NULL) {
|
||||
colon2 = WSTRCHR(colon1 + 1, ':');
|
||||
if (colon2 != NULL) {
|
||||
*colon2 = '\0';
|
||||
GetFakeHashFromTemplate(colon1 + 1, tmpl, sizeof(tmpl));
|
||||
|
||||
if (tmpl[0] != '\0' && tmpl[1] != '\0' && tmpl[1] != '*') {
|
||||
duplicate = 0;
|
||||
for (i = 0; i < numCachedFakeHashes; i++) {
|
||||
if (XSTRNCMP(cachedFakeHashes[i], tmpl, sizeof(tmpl)) == 0) {
|
||||
duplicate = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicate) {
|
||||
XSTRNCPY(cachedFakeHashes[numCachedFakeHashes], tmpl,
|
||||
sizeof(cachedFakeHashes[0]));
|
||||
numCachedFakeHashes++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Reads an already-open shadow file line by line into the fake-hash cache.
|
||||
* Exposed unconditionally so unit tests can drive it with a synthetic
|
||||
* stream instead of a real /etc/shadow. */
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
void ScanShadowFile(WFILE* f)
|
||||
#else
|
||||
static void ScanShadowFile(WFILE* f)
|
||||
#endif
|
||||
{
|
||||
char line[WSSHD_SHADOW_LINE_SZ];
|
||||
|
||||
while (WFGETS(line, sizeof(line), f) != NULL &&
|
||||
numCachedFakeHashes < WSSHD_MAX_FAKE_HASHES) {
|
||||
/* If the line was truncated (no newline found), consume the remainder */
|
||||
if (WSTRCHR(line, '\n') == NULL) {
|
||||
char dump[WSSHD_SHADOW_DUMP_SZ];
|
||||
while (WFGETS(dump, sizeof(dump), f) != NULL) {
|
||||
if (WSTRCHR(dump, '\n') != NULL) break;
|
||||
}
|
||||
WS_FORCEZERO(dump, sizeof(dump));
|
||||
}
|
||||
|
||||
AddShadowLineToFakeHashCache(line);
|
||||
WS_FORCEZERO(line, sizeof(line));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
/* Test-only hook to seed cachedFakeHash without a real shadow file entry. */
|
||||
void wolfSSHD_SetCachedFakeHashForTest(const char* tmpl)
|
||||
{
|
||||
if (tmpl == NULL) {
|
||||
cachedFakeHashes[0][0] = '\0';
|
||||
numCachedFakeHashes = 0;
|
||||
}
|
||||
else {
|
||||
XSTRNCPY(cachedFakeHashes[0], tmpl, sizeof(cachedFakeHashes[0]));
|
||||
cachedFakeHashes[0][sizeof(cachedFakeHashes[0]) - 1] = '\0';
|
||||
numCachedFakeHashes = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test-only accessor so tests can verify wolfSSHD_AuthInit() actually
|
||||
* populated cachedFakeHash from a real shadow entry. */
|
||||
void wolfSSHD_GetCachedFakeHashForTest(char* out, word32 outSz)
|
||||
{
|
||||
if (out == NULL || outSz == 0) return;
|
||||
if (numCachedFakeHashes > 0) {
|
||||
XSTRNCPY(out, cachedFakeHashes[0], outSz);
|
||||
out[outSz - 1] = '\0';
|
||||
}
|
||||
else {
|
||||
out[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* Test-only accessor for the number of cached fake hashes, so tests can
|
||||
* verify AddShadowLineToFakeHashCache()'s dedup and cap behavior. */
|
||||
int wolfSSHD_GetCachedFakeHashCountForTest(void)
|
||||
{
|
||||
return numCachedFakeHashes;
|
||||
}
|
||||
#endif /* WOLFSSHD_UNIT_TEST */
|
||||
#endif /* HAVE_SHADOW */
|
||||
|
||||
void wolfSSHD_AuthInit(void)
|
||||
{
|
||||
#ifdef HAVE_SHADOW
|
||||
char tmpl[WSSHD_FAKE_HASH_SZ];
|
||||
struct spwd* rootShadow;
|
||||
|
||||
#ifndef WOLFSSHD_UNIT_TEST
|
||||
WFILE* f = NULL;
|
||||
#endif
|
||||
|
||||
#ifndef WOLFSSHD_UNIT_TEST
|
||||
/* /etc/shadow is commonly root:shadow 0640; don't reject group-readable. */
|
||||
if (wolfSSHD_OpenSecureFile(WSSHD_SHADOW_FILE, 0 /* ownerUid: root */,
|
||||
0 /* rejectReadable */, NULL, &f) == WS_SUCCESS && f != NULL) {
|
||||
ScanShadowFile(f);
|
||||
WFCLOSE(NULL, f);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (numCachedFakeHashes == 0) {
|
||||
rootShadow = getspnam("root");
|
||||
if (rootShadow != NULL && rootShadow->sp_pwdp != NULL) {
|
||||
GetFakeHashFromTemplate(rootShadow->sp_pwdp, tmpl, sizeof(tmpl));
|
||||
if (tmpl[0] != '\0' && tmpl[1] != '\0' && tmpl[1] != '*') {
|
||||
XSTRNCPY(cachedFakeHashes[0], tmpl, sizeof(cachedFakeHashes[0]));
|
||||
numCachedFakeHashes = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (numCachedFakeHashes == 0) {
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Error getting root password info");
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Possibly permissions level error?"
|
||||
" i.e SSHD not ran as sudo");
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Timing side-channel mitigation degraded: using a"
|
||||
" fixed-cost fake hash instead of matching real crypt() cost");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
#ifdef WOLFSSH_USE_PAM
|
||||
|
|
@ -392,7 +654,20 @@ int CheckPasswordHashUnix(const char* input, const char* stored)
|
|||
static int CheckPasswordHashUnix(const char* input, const char* stored)
|
||||
#endif
|
||||
{
|
||||
/* Fake salts for locked/empty accounts so crypt() is always invoked.
|
||||
* fakeHashSHA512 uses rounds=5000 (glibc default); real cost is matched
|
||||
* via the cached hash from wolfSSHD_AuthInit() on the normal path.
|
||||
* These constants are only used in degraded mode (empty cache). */
|
||||
static const char fakeHashSHA512[] =
|
||||
"$6$rounds=5000$wolfSSHdFakeSalt$";
|
||||
static const char fakeHashMD5[] = "$1$wolfSSHd$UkYLseEmSSXHYyxsWDQC80";
|
||||
static const char fakeHashDES[] = "wowolfSSHdUkYLs";
|
||||
/* Fallback salts tried in order when a locked-account salt is rejected
|
||||
* by crypt() (e.g. libc lacks that algorithm). */
|
||||
static const char* const fakeHashFallbacks[] =
|
||||
{ fakeHashMD5, fakeHashDES };
|
||||
int ret = WSSHD_AUTH_SUCCESS;
|
||||
int locked = 0;
|
||||
char* hashedInput = NULL;
|
||||
word32 hashedInputSz = 0, storedSz = 0;
|
||||
|
||||
|
|
@ -400,7 +675,8 @@ static int CheckPasswordHashUnix(const char* input, const char* stored)
|
|||
ret = WS_BAD_ARGUMENT;
|
||||
}
|
||||
|
||||
/* empty password case */
|
||||
/* Fast return for genuine empty passwords. The dummy caller
|
||||
* never passes an empty stored hash, avoiding a timing oracle. */
|
||||
if (ret == WSSHD_AUTH_SUCCESS && stored[0] == 0 && WSTRLEN(input) == 0) {
|
||||
wolfSSH_Log(WS_LOG_INFO,
|
||||
"[SSHD] User logged in with empty password");
|
||||
|
|
@ -408,16 +684,56 @@ static int CheckPasswordHashUnix(const char* input, const char* stored)
|
|||
}
|
||||
|
||||
if (ret == WSSHD_AUTH_SUCCESS) {
|
||||
hashedInput = crypt(input, stored);
|
||||
const char* salt = stored;
|
||||
|
||||
storedSz = (word32)WSTRLEN(stored);
|
||||
locked = (storedSz == 0 || stored[0] == '*' || stored[0] == '!');
|
||||
|
||||
if (locked) {
|
||||
/* Try to reuse the salt from the locked hash, but only if it's
|
||||
* a real modular crypt salt; otherwise crypt() fails on it
|
||||
* immediately and skips the cost this mitigation relies on. */
|
||||
if (storedSz > 1 && stored[0] == '!' && stored[1] == '$') {
|
||||
salt = stored + 1;
|
||||
}
|
||||
#ifdef HAVE_SHADOW
|
||||
/* Prefer the cached system hash (populated by AuthInit from
|
||||
* shadow file) so the work factor matches the real system. */
|
||||
else if (numCachedFakeHashes > 0 &&
|
||||
cachedFakeHashes[0][0] == '!' &&
|
||||
cachedFakeHashes[0][1] == '$') {
|
||||
salt = cachedFakeHashes[0] + 1;
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
salt = fakeHashSHA512;
|
||||
}
|
||||
}
|
||||
|
||||
hashedInput = crypt(input, salt);
|
||||
/* glibc signals an unsupported salt with a "*"-prefixed sentinel,
|
||||
* not NULL; check both so the fallback engages on every libc. */
|
||||
if (locked) {
|
||||
word32 fbIdx;
|
||||
for (fbIdx = 0;
|
||||
fbIdx < sizeof(fakeHashFallbacks) / sizeof(fakeHashFallbacks[0])
|
||||
&& (hashedInput == NULL || hashedInput[0] == '*');
|
||||
fbIdx++) {
|
||||
salt = fakeHashFallbacks[fbIdx];
|
||||
hashedInput = crypt(input, salt);
|
||||
}
|
||||
}
|
||||
|
||||
if (hashedInput == NULL) {
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
else if (locked) {
|
||||
ret = WSSHD_AUTH_FAILURE;
|
||||
}
|
||||
else {
|
||||
hashedInputSz = (word32)WSTRLEN(hashedInput);
|
||||
storedSz = (word32)WSTRLEN(stored);
|
||||
|
||||
if (storedSz == 0 || stored[0] == '*' ||
|
||||
hashedInputSz == 0 || hashedInput[0] == '*' ||
|
||||
if (hashedInputSz == 0 || hashedInput[0] == '*' ||
|
||||
hashedInputSz != storedSz ||
|
||||
ConstantCompare((const byte*)hashedInput,
|
||||
(const byte*)stored, storedSz) != 0) {
|
||||
|
|
@ -426,21 +742,27 @@ static int CheckPasswordHashUnix(const char* input, const char* stored)
|
|||
WS_FORCEZERO(hashedInput, hashedInputSz);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */
|
||||
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFSSHD_AUTH* authCtx)
|
||||
#else
|
||||
static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFSSHD_AUTH* authCtx)
|
||||
#endif
|
||||
{
|
||||
int ret = WS_SUCCESS;
|
||||
char* pwStr = NULL;
|
||||
struct passwd* pwInfo;
|
||||
#ifdef HAVE_SHADOW
|
||||
struct spwd* shadowInfo;
|
||||
/* getspnam() returns a static buffer; copy immediately before it can
|
||||
* be overwritten by any subsequent call. */
|
||||
char hashBuf[WSSHD_FAKE_HASH_SZ];
|
||||
#endif
|
||||
/* The hash of the user's password stored on the system. */
|
||||
char* storedHash;
|
||||
const char* storedHash = "*";
|
||||
char* storedHashCpy = NULL;
|
||||
|
||||
/* Allow zero length passwords, but not NULL pointers. */
|
||||
|
|
@ -464,38 +786,56 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS
|
|||
if (ret == WS_SUCCESS) {
|
||||
pwInfo = getpwnam((const char*)usr);
|
||||
if (pwInfo == NULL) {
|
||||
/* user name not found on system */
|
||||
ret = WS_FATAL_ERROR;
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
/* User not found: use dummy hash to equalize timing. */
|
||||
wolfSSH_Log(WS_LOG_INFO,
|
||||
"[SSHD] User name not found on system");
|
||||
}
|
||||
}
|
||||
|
||||
if (ret == WS_SUCCESS) {
|
||||
#ifdef HAVE_SHADOW
|
||||
if (pwInfo->pw_passwd[0] == 'x') {
|
||||
#ifdef WOLFSSH_HAVE_LIBCRYPT
|
||||
shadowInfo = getspnam((const char*)usr);
|
||||
#else
|
||||
shadowInfo = getspnam((char*)usr);
|
||||
#endif
|
||||
if (shadowInfo == NULL) {
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Error getting user password info");
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Possibly permissions level error?"
|
||||
" i.e SSHD not ran as sudo");
|
||||
ret = WS_FATAL_ERROR;
|
||||
else {
|
||||
#ifdef HAVE_SHADOW
|
||||
if (pwInfo->pw_passwd[0] == 'x') {
|
||||
#ifdef WOLFSSH_HAVE_LIBCRYPT
|
||||
shadowInfo = getspnam((const char*)usr);
|
||||
#else
|
||||
shadowInfo = getspnam((char*)usr);
|
||||
#endif
|
||||
if (shadowInfo == NULL) {
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Error getting user password info");
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Possibly permissions level error?"
|
||||
" i.e SSHD not ran as sudo");
|
||||
/* Fail closed: RequestAuthentication's error-branch
|
||||
* DoFakePasswordCheck() still equalizes timing for this
|
||||
* case, same as it does for the oversized-hash case. */
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
else if (shadowInfo->sp_pwdp == NULL) {
|
||||
/* Fail closed: some NSS backends (e.g. NIS/LDAP) can
|
||||
* return a spwd entry with a NULL sp_pwdp. */
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Shadow entry missing password hash");
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
else if (WSTRLEN(shadowInfo->sp_pwdp) >= WSSHD_FAKE_HASH_SZ) {
|
||||
/* Fail closed instead of silently truncating the hash. */
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Stored password hash too long for buffer");
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
else {
|
||||
/* Copy before any subsequent getspnam() call
|
||||
* overwrites the static buffer. */
|
||||
XSTRNCPY(hashBuf, shadowInfo->sp_pwdp, sizeof(hashBuf));
|
||||
hashBuf[sizeof(hashBuf) - 1] = '\0';
|
||||
storedHash = hashBuf;
|
||||
}
|
||||
}
|
||||
else {
|
||||
storedHash = shadowInfo->sp_pwdp;
|
||||
else
|
||||
#endif
|
||||
{
|
||||
storedHash = pwInfo->pw_passwd;
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
storedHash = pwInfo->pw_passwd;
|
||||
}
|
||||
}
|
||||
if (ret == WS_SUCCESS) {
|
||||
storedHashCpy = WSTRDUP(storedHash, NULL, DYNTYPE_STRING);
|
||||
|
|
@ -508,6 +848,8 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS
|
|||
|
||||
if (ret == WS_SUCCESS) {
|
||||
#if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN)
|
||||
/* Nonexistent users use "*" hash, so CheckPasswordHashUnix fails.
|
||||
* DoCheckUser() filters them earlier; this is defense-in-depth. */
|
||||
ret = CheckPasswordHashUnix(pwStr, storedHashCpy);
|
||||
#else
|
||||
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No compiled in password check");
|
||||
|
|
@ -523,6 +865,9 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS
|
|||
WS_FORCEZERO(storedHashCpy, (word32)WSTRLEN(storedHashCpy) + 1);
|
||||
WFREE(storedHashCpy, NULL, DYNTYPE_STRING);
|
||||
}
|
||||
#ifdef HAVE_SHADOW
|
||||
WS_FORCEZERO(hashBuf, sizeof(hashBuf));
|
||||
#endif
|
||||
|
||||
WOLFSSH_UNUSED(authCtx);
|
||||
return ret;
|
||||
|
|
@ -1942,6 +2287,82 @@ WOLFSSHD_STATIC int MatchUPNToUser(const char* usr, const char* name,
|
|||
#endif /* WOLFSSL_FPKI || WOLFSSHD_UNIT_TEST */
|
||||
|
||||
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
/* Test-only spy to assert if DoFakePasswordCheck() ran. */
|
||||
static int fakePasswordCheckCallCount = 0;
|
||||
|
||||
void wolfSSHD_ResetFakePasswordCheckCountForTest(void)
|
||||
{
|
||||
fakePasswordCheckCallCount = 0;
|
||||
}
|
||||
|
||||
int wolfSSHD_GetFakePasswordCheckCountForTest(void)
|
||||
{
|
||||
return fakePasswordCheckCallCount;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Runs a fake crypt() to equalize timing on failure paths that skip the
|
||||
* real password check. No-op under WOLFSSH_USE_PAM (PAM path needs its
|
||||
* own mitigation). */
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
void DoFakePasswordCheck(WS_UserAuthData* authData)
|
||||
#else
|
||||
static void DoFakePasswordCheck(WS_UserAuthData* authData)
|
||||
#endif
|
||||
{
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
fakePasswordCheckCallCount++;
|
||||
#endif
|
||||
#if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN)
|
||||
char* fakePwStr = NULL;
|
||||
const byte* fakePw = NULL;
|
||||
word32 fakePwSz = 0;
|
||||
const char* fakeHash = "*";
|
||||
|
||||
if (authData->type == WOLFSSH_USERAUTH_PASSWORD) {
|
||||
fakePw = authData->sf.password.password;
|
||||
fakePwSz = authData->sf.password.passwordSz;
|
||||
}
|
||||
|
||||
#ifdef HAVE_SHADOW
|
||||
if (numCachedFakeHashes > 0) {
|
||||
/* Byte-sum selects which cached hash to use. Deterministic but
|
||||
* attacker-predictable; impact is marginal on typical systems
|
||||
* where all accounts share one algorithm. */
|
||||
word32 hashIdx = 0;
|
||||
word32 i;
|
||||
if (authData->username != NULL) {
|
||||
for (i = 0; i < authData->usernameSz; i++) {
|
||||
hashIdx += authData->username[i];
|
||||
}
|
||||
}
|
||||
fakeHash = cachedFakeHashes[hashIdx % numCachedFakeHashes];
|
||||
}
|
||||
#endif
|
||||
|
||||
fakePwStr = (char*)WMALLOC(fakePwSz + 1, NULL, DYNTYPE_STRING);
|
||||
if (fakePwStr != NULL) {
|
||||
if (fakePwSz > 0 && fakePw != NULL) {
|
||||
XMEMCPY(fakePwStr, fakePw, fakePwSz);
|
||||
}
|
||||
fakePwStr[fakePwSz] = 0;
|
||||
|
||||
/* Return value ignored: fake check must not influence auth. */
|
||||
CheckPasswordHashUnix(fakePwStr, fakeHash);
|
||||
|
||||
WS_FORCEZERO(fakePwStr, fakePwSz + 1);
|
||||
WFREE(fakePwStr, NULL, DYNTYPE_STRING);
|
||||
}
|
||||
else {
|
||||
CheckPasswordHashUnix("", fakeHash);
|
||||
}
|
||||
#else
|
||||
WOLFSSH_UNUSED(authData);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* @TODO this will take a pipe or equivalent to talk to a privileged thread
|
||||
* rather than having WOLFSSHD_AUTH directly with privilege separation.
|
||||
|
|
@ -1970,6 +2391,7 @@ static int RequestAuthentication(WS_UserAuthData* authData,
|
|||
int ret;
|
||||
int rc;
|
||||
int isRoot;
|
||||
int needFakeCheck = 0;
|
||||
const char* usr;
|
||||
WOLFSSHD_CONFIG* usrConf = NULL;
|
||||
|
||||
|
|
@ -1987,27 +2409,22 @@ static int RequestAuthentication(WS_UserAuthData* authData,
|
|||
isRoot = IsRootUser(usr);
|
||||
ret = DoCheckUser(usr, authCtx, isRoot);
|
||||
|
||||
if (ret != WOLFSSH_USERAUTH_SUCCESS) {
|
||||
needFakeCheck = 1;
|
||||
}
|
||||
|
||||
/* temporarily elevate permissions */
|
||||
if (ret == WOLFSSH_USERAUTH_SUCCESS &&
|
||||
wolfSSHD_AuthRaisePermissions(authCtx) != WS_SUCCESS) {
|
||||
wolfSSH_Log(WS_LOG_ERROR,
|
||||
"[SSHD] Failure to raise permissions for auth");
|
||||
ret = WOLFSSH_USERAUTH_FAILURE;
|
||||
needFakeCheck = 1;
|
||||
}
|
||||
|
||||
/* Resolve the per-user configuration so that Match block overrides are
|
||||
* honored. wolfSSHD_AuthGetUserConf defaults to the global config when no
|
||||
* user-specific node applies, so this matches the existing behavior for
|
||||
* non-Match users while enforcing Match restrictions.
|
||||
*
|
||||
* A NULL return here means the user's configuration could not be resolved
|
||||
* (e.g. the user's group set could not be enumerated inside
|
||||
* wolfSSHD_AuthGetUserConf). DoCheckUser has already confirmed the user
|
||||
* exists, so this is a rare edge. Fail closed rather than fall back to the
|
||||
* permissive global node: such a user cannot complete a session anyway
|
||||
* (session setup in wolfsshd.c rejects an unresolvable user config with
|
||||
* WS_FATAL_ERROR), so denying auth here is safe and avoids evaluating
|
||||
* password/public-key authorization against the wrong config node. */
|
||||
/* Resolve per-user config to honor Match blocks. NULL means the group
|
||||
* set couldn't be enumerated; fail closed rather than fall back to the
|
||||
* global node and evaluate auth against the wrong config. */
|
||||
if (ret == WOLFSSH_USERAUTH_SUCCESS) {
|
||||
usrConf = wolfSSHD_AuthGetUserConf(authCtx, usr, NULL, NULL, NULL, NULL,
|
||||
NULL);
|
||||
|
|
@ -2016,6 +2433,7 @@ static int RequestAuthentication(WS_UserAuthData* authData,
|
|||
"[SSHD] Failure to get user configuration for auth (user=%s)",
|
||||
usr);
|
||||
ret = WOLFSSH_USERAUTH_FAILURE;
|
||||
needFakeCheck = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2039,8 +2457,13 @@ static int RequestAuthentication(WS_UserAuthData* authData,
|
|||
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Empty passwords not allowed by "
|
||||
"configuration!");
|
||||
ret = WOLFSSH_USERAUTH_FAILURE;
|
||||
/* Set explicitly; the trailing else also fires here, but
|
||||
* this makes the intent clear at the point of rejection. */
|
||||
needFakeCheck = 1;
|
||||
}
|
||||
else {
|
||||
|
||||
/* Only run password check when config allows it (avoids leaks). */
|
||||
if (ret == WOLFSSH_USERAUTH_SUCCESS) {
|
||||
rc = authCtx->checkPasswordCb(usr, authData->sf.password.password,
|
||||
authData->sf.password.passwordSz, authCtx);
|
||||
|
||||
|
|
@ -2061,8 +2484,18 @@ static int RequestAuthentication(WS_UserAuthData* authData,
|
|||
else {
|
||||
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error checking password.");
|
||||
ret = WOLFSSH_USERAUTH_FAILURE;
|
||||
needFakeCheck = 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
needFakeCheck = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Equalize crypt() cost for password auth across every failure path
|
||||
* above that never reached the real checkPasswordCb crypt() call. */
|
||||
if (needFakeCheck && authData->type == WOLFSSH_USERAUTH_PASSWORD) {
|
||||
DoFakePasswordCheck(authData);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -2664,12 +3097,20 @@ int wolfSSHD_AuthReducePermissions(WOLFSSHD_AUTH* auth)
|
|||
if (flag == WOLFSSHD_PRIV_SEPARAT || flag == WOLFSSHD_PRIV_SANDBOX) {
|
||||
wolfSSH_Log(WS_LOG_INFO, "[SSHD] Lowering permissions level");
|
||||
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
if (wsshd_setegid_cb(auth->gid) != 0) {
|
||||
#else
|
||||
if (setegid(auth->gid) != 0) {
|
||||
#endif
|
||||
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting sshd gid");
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
if (wsshd_seteuid_cb(auth->uid) != 0) {
|
||||
#else
|
||||
if (seteuid(auth->uid) != 0) {
|
||||
#endif
|
||||
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting sshd uid");
|
||||
ret = WS_FATAL_ERROR;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@
|
|||
#define WOLFSSH_USER_GET_STRING(x) #x
|
||||
#define WOLFSSH_USER_STRING(x) WOLFSSH_USER_GET_STRING(x)
|
||||
|
||||
/* Mirrors the condition auth.c uses to define its (translation-unit-local)
|
||||
* HAVE_SHADOW; kept as a single macro here so the two can't drift apart. */
|
||||
#if !defined(_WIN32) && !(defined(__OSX__) || defined(__APPLE__))
|
||||
#define WOLFSSHD_HAVE_SHADOW
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
|
||||
typedef struct USER_NODE USER_NODE;
|
||||
|
|
@ -69,6 +75,7 @@ typedef int (*CallbackCheckPublicKey)(const char* usr,
|
|||
const char* authorizedKeysFile,
|
||||
WOLFSSHD_AUTH* authCtx);
|
||||
|
||||
void wolfSSHD_AuthInit(void);
|
||||
WOLFSSHD_AUTH* wolfSSHD_AuthCreateUser(void* heap, const WOLFSSHD_CONFIG* conf);
|
||||
int wolfSSHD_AuthFreeUser(WOLFSSHD_AUTH* auth);
|
||||
int wolfSSHD_AuthReducePermissions(WOLFSSHD_AUTH* auth);
|
||||
|
|
@ -109,6 +116,9 @@ extern int (*wsshd_setegid_cb)(WGID_T);
|
|||
extern int (*wsshd_seteuid_cb)(WUID_T);
|
||||
extern struct passwd* (*wsshd_getpwnam_cb)(const char*);
|
||||
extern int (*wsshd_setgroups_cb)(int, const WGID_T*);
|
||||
#ifdef WOLFSSHD_HAVE_SHADOW
|
||||
extern struct spwd* (*wsshd_getspnam_cb)(const char*);
|
||||
#endif
|
||||
extern int (*wsshd_getgrouplist_cb)(const char*, WGID_T, WGID_T*, int*);
|
||||
int wolfSSHD_GetUserGroupNames(void* heap, const char* usr, WGID_T primaryGid,
|
||||
char*** outNames, word32* outCount);
|
||||
|
|
@ -121,6 +131,31 @@ int SearchForPubKey(const char* path, const char* authKeysFile,
|
|||
#if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN)
|
||||
int CheckPasswordHashUnix(const char* input, const char* stored);
|
||||
#endif
|
||||
#ifdef WOLFSSHD_HAVE_SHADOW
|
||||
void GetFakeHashFromTemplate(const char* tmpl, char* out, word32 outSz);
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
/* Pure test hooks under HAVE_SHADOW && WOLFSSHD_UNIT_TEST. */
|
||||
void wolfSSHD_SetCachedFakeHashForTest(const char* hash);
|
||||
void wolfSSHD_GetCachedFakeHashForTest(char* out, word32 outSz);
|
||||
int wolfSSHD_GetCachedFakeHashCountForTest(void);
|
||||
/* Parses one "user:hash:..." line into the fake-hash cache. */
|
||||
void AddShadowLineToFakeHashCache(char* line);
|
||||
/* Reads a shadow file stream line by line into the fake-hash cache. */
|
||||
void ScanShadowFile(WFILE* f);
|
||||
#endif
|
||||
#endif
|
||||
/* Not shadow-specific in auth.c, so not excluded on OSX/APPLE. */
|
||||
#if !defined(_WIN32) && !defined(WOLFSSH_USE_PAM)
|
||||
/* Returns WSSHD_AUTH_* for auth result, or WS_* for system errors. */
|
||||
int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz,
|
||||
WOLFSSHD_AUTH* authCtx);
|
||||
#endif
|
||||
#ifdef WOLFSSHD_UNIT_TEST
|
||||
void DoFakePasswordCheck(WS_UserAuthData* authData);
|
||||
/* Pure test hooks under WOLFSSHD_UNIT_TEST. */
|
||||
void wolfSSHD_ResetFakePasswordCheckCountForTest(void);
|
||||
int wolfSSHD_GetFakePasswordCheckCountForTest(void);
|
||||
#endif
|
||||
int CheckAuthKeysLine(char* line, word32 lineSz, const byte* key,
|
||||
word32 keySz);
|
||||
int ResolveAuthKeysPath(const char* homeDir, const char* pattern,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ apps_wolfsshd_wolfsshd_SOURCES = apps/wolfsshd/wolfsshd.c \
|
|||
apps_wolfsshd_wolfsshd_LDADD = src/libwolfssh.la
|
||||
apps_wolfsshd_wolfsshd_DEPENDENCIES = src/libwolfssh.la
|
||||
|
||||
noinst_PROGRAMS += apps/wolfsshd/test/test_configuration
|
||||
check_PROGRAMS += apps/wolfsshd/test/test_configuration
|
||||
apps_wolfsshd_test_test_configuration_SOURCES = apps/wolfsshd/test/test_configuration.c \
|
||||
apps/wolfsshd/configuration.c \
|
||||
apps/wolfsshd/configuration.h \
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2955,6 +2955,12 @@ static int StartSSHD(int argc, char** argv)
|
|||
}
|
||||
}
|
||||
|
||||
/* Must run before privilege drop so the shadow file is accessible.
|
||||
* Degrades to a fixed-cost fake hash if the shadow read fails. */
|
||||
if (ret == WS_SUCCESS && !testMode) {
|
||||
wolfSSHD_AuthInit();
|
||||
}
|
||||
|
||||
if (ret == WS_SUCCESS) {
|
||||
ret = wolfSSHD_ConfigLoad(conf, configFile);
|
||||
if (ret != WS_SUCCESS) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue