fix(d5-daemon-auth): address auth review findings (Wave B)
- test_credentials.c: NUL-terminate the overlong-line stack buffer before make_tmp_file's strlen() (was a stack-buffer-overflow READ under ASan); still exercises the overlong-rejection path. - Add redacted protocol string variants (protocol_send_str_redacted / receive + fd send_str_redacted/receive_str_redacted) and use them for the daemon auth username/digest so --verbose / LOG_DEBUG_ALL never logs a replayable credential while other protocol strings keep their debug trace. - credentials_verify/gate: replace byte-wise-short-circuiting strcmp with a fixed-length constant-time username compare (closes user-enumeration oracle); update doc comment to match. - read_secret_file: preserve password exact bytes (only strip trailing CR/LF) and burn the stack line buffer; document the whitespace behavior. - test_server_cli.c: note the parser zero-inits opts on failure. - Add debug-level daemon test asserting the digest never appears under --verbose. PROTOCOL_VERSION stays 2.15.0.
This commit is contained in:
@@ -343,7 +343,10 @@ int credentials_read_secret_file(const char* path, char** user_out, char** passw
|
||||
}
|
||||
*colon = '\0';
|
||||
const char* user = trim_space(cursor);
|
||||
const char* password = trim_space(colon + 1);
|
||||
/* Preserve the password's exact bytes: only the line's trailing CR/LF was
|
||||
* already stripped above. Trimming leading/trailing space here would make
|
||||
* a password that legitimately begins or ends with whitespace unusable. */
|
||||
const char* password = colon + 1;
|
||||
if (!username_wellformed(user)) {
|
||||
set_error(err, err_size,
|
||||
"password file '%s' line %d: invalid username (must be 1-%d "
|
||||
@@ -389,6 +392,10 @@ int credentials_read_secret_file(const char* path, char** user_out, char** passw
|
||||
set_error(err, err_size, "password file '%s' contains no 'user:password' line", path);
|
||||
|
||||
done:
|
||||
/* Wipe the stack line (which may hold the literal password) before return.
|
||||
* user/password were str_dup'd into their outputs on success, so the stack
|
||||
* copy is the only remaining plaintext. */
|
||||
credentials_burn(line, sizeof(line));
|
||||
fclose(fp);
|
||||
return result;
|
||||
}
|
||||
@@ -401,6 +408,27 @@ void credentials_burn(char* secret, size_t len) {
|
||||
p[i] = '\0';
|
||||
}
|
||||
|
||||
/* Constant-time equality over two usernames. Compares a fixed
|
||||
* CREDENTIAL_MAX_USER_LEN-byte window (padding with zeros past each string's
|
||||
* own length) and folds the length difference into the accumulator, so no byte
|
||||
* returns early. This closes the byte-wise username-enumeration timing oracle
|
||||
* that a plain strcmp (which short-circuits on the first differing byte)
|
||||
* would otherwise expose. Over-long inputs are refused (length differs), which
|
||||
* is a non-secret branch: usernames are bounded in every caller anyway. */
|
||||
static bool username_secure_equal(const char* a, const char* b) {
|
||||
size_t alen = strlen(a);
|
||||
size_t blen = strlen(b);
|
||||
if (alen > CREDENTIAL_MAX_USER_LEN || blen > CREDENTIAL_MAX_USER_LEN)
|
||||
return false;
|
||||
size_t diff = alen ^ blen;
|
||||
for (size_t i = 0; i < CREDENTIAL_MAX_USER_LEN; i++) {
|
||||
unsigned char ac = i < alen ? (unsigned char)a[i] : 0u;
|
||||
unsigned char bc = i < blen ? (unsigned char)b[i] : 0u;
|
||||
diff |= (size_t)(ac ^ bc);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
/* Fixed 64-lowercase-hex dummy used for a constant-time digest comparison when
|
||||
* the presented user is unknown, so the verify path takes the same time for an
|
||||
* unknown user and a wrong password. Value chosen arbitrarily; it can never
|
||||
@@ -414,7 +442,9 @@ bool credentials_verify(const CredentialStore* store, const char* user,
|
||||
return false;
|
||||
const char* stored = k_dummy_hash;
|
||||
for (int i = 0; i < store->count; i++) {
|
||||
if (strcmp(store->entries[i].user, user) == 0)
|
||||
/* Constant-time username match: no early return, so time depends on the
|
||||
* fixed compare window and a byte-wise prefix match cannot be observed. */
|
||||
if (username_secure_equal(store->entries[i].user, user))
|
||||
stored = store->entries[i].password_hex;
|
||||
}
|
||||
return credentials_secure_equal(presented_hash_hex, stored, CREDENTIAL_HASH_HEX_LEN);
|
||||
@@ -429,7 +459,9 @@ bool credentials_gate_allows(const CredentialStore* store, const char* const* mo
|
||||
return false; /* no credentials presented */
|
||||
bool on_module_list = false;
|
||||
for (int i = 0; i < module_user_count; i++) {
|
||||
if (module_users[i] && strcmp(module_users[i], presented_user) == 0) {
|
||||
/* Constant-time match against the module's auth-users list, for the same
|
||||
* reason as credentials_verify, so the list is not an enumeration oracle. */
|
||||
if (module_users[i] && username_secure_equal(module_users[i], presented_user)) {
|
||||
on_module_list = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user