fix(a7-auth): address SCRAM auth review findings A-G
- tests: pass CREDENTIAL_KEY_LEN to unhex for the 32-byte KAT proof/sig (sizeof(expect) is 348, over-reading the 65-byte hex literal under ASan) - credentials: close the username-enumeration oracle with a store-wide dummy_key and a deterministic per-username dummy salt; make the store's iteration count uniform (reject intra-file and layered disagreements) and answer a miss with the store-wide count; run the constant-time key compare even when found=false and fold the decision with bitwise AND - credentials_compute_keys: enforce [CREDENTIAL_MIN_ITERS, CREDENTIAL_MAX_ITERS] - tests: recompute the whole KAT independently at CREDENTIAL_DEFAULT_ITERS (600000) and pin the golden store line; add non-uniform-store rejection, bound and deterministic-dummy-salt assertions - server: send exactly one generic STATUS_AUTH_FAILED on every failure path (including credentials_get_verifier failure); route all handshake exits through one burn path - credentials/server: burn the base64 decoders' scratch on error, the hash_store_line base64/line buffers on failure, and all handshake key/proof material - fuzz: guard the auth-offset scan against size_t underflow and use a found flag - docs: drop stale digest wording, use CREDENTIAL_MIN_ITERS as the --iterations bound, document 0600 output for --hash-credentials (plus a stderr warning on a group/other-accessible stdout file), and describe the deterministic dummy salt in the no-oracle claims
This commit is contained in:
+69
-55
@@ -73,65 +73,68 @@ typedef struct ModuleGateContext {
|
||||
* 2.19.0). Sends STATUS_AUTH_CHALLENGE (iteration count, base64 salt, base64
|
||||
* server nonce), expects STATUS_AUTH_RESPONSE (base64 client nonce, base64
|
||||
* ClientProof), verifies the proof constant-time and answers STATUS_AUTH_OK
|
||||
* with the base64 ServerSignature. On any failure it sends a single generic
|
||||
* with the base64 ServerSignature. On any failure it sends exactly one generic
|
||||
* STATUS_AUTH_FAILED and returns false. The verifier for an unknown/off-list
|
||||
* user is a dummy (random salt, dummy keys, found=false) so the same math runs
|
||||
* and no user-enumeration/timing oracle is exposed. */
|
||||
* user is a dummy (deterministic per-username salt, store-wide iterations, dummy
|
||||
* keys, found=false) so the same math runs and no user-enumeration/timing oracle
|
||||
* is exposed. */
|
||||
static bool server_auth_handshake(int fd, const Config* config, const DaemonModule* module) {
|
||||
if (!config->auth_user) {
|
||||
send_status(fd, STATUS_AUTH_FAILED);
|
||||
return false;
|
||||
}
|
||||
bool result = false;
|
||||
CredentialVerifier verifier;
|
||||
memset(&verifier, 0, sizeof(verifier));
|
||||
uint8_t snonce[CREDENTIAL_NONCE_LEN] = {0};
|
||||
char salt_b64[25] = {0};
|
||||
char snonce_b64[45] = {0};
|
||||
char* cnonce_b64 = NULL;
|
||||
char* proof_b64 = NULL;
|
||||
uint8_t cnonce[CREDENTIAL_NONCE_LEN] = {0};
|
||||
uint8_t proof[CREDENTIAL_KEY_LEN] = {0};
|
||||
uint8_t server_sig[CREDENTIAL_KEY_LEN] = {0};
|
||||
char sig_b64[45] = {0};
|
||||
size_t cnonce_len = 0;
|
||||
size_t proof_len = 0;
|
||||
|
||||
if (!config->auth_user)
|
||||
goto fail; /* no username: generic failure, no challenge */
|
||||
if (!credentials_get_verifier(g_credentials, config->auth_user,
|
||||
(const char* const*)module->auth_users, module->auth_user_count,
|
||||
&verifier))
|
||||
return false;
|
||||
uint8_t snonce[CREDENTIAL_NONCE_LEN];
|
||||
char salt_b64[25];
|
||||
char snonce_b64[45];
|
||||
bool ok =
|
||||
credentials_random_bytes(snonce, sizeof(snonce)) &&
|
||||
credentials_b64_encode(verifier.salt, CREDENTIAL_SALT_LEN, salt_b64, sizeof(salt_b64)) &&
|
||||
credentials_b64_encode(snonce, sizeof(snonce), snonce_b64, sizeof(snonce_b64));
|
||||
if (!ok) {
|
||||
send_status(fd, STATUS_AUTH_FAILED);
|
||||
return false;
|
||||
}
|
||||
ok = send_status(fd, STATUS_AUTH_CHALLENGE) && send_int(fd, (int)verifier.iters) &&
|
||||
send_str(fd, salt_b64) && send_str(fd, snonce_b64);
|
||||
goto fail; /* a crypto failure still owes the gate a terminal frame */
|
||||
if (!(credentials_random_bytes(snonce, sizeof(snonce)) &&
|
||||
credentials_b64_encode(verifier.salt, CREDENTIAL_SALT_LEN, salt_b64, sizeof(salt_b64)) &&
|
||||
credentials_b64_encode(snonce, sizeof(snonce), snonce_b64, sizeof(snonce_b64))))
|
||||
goto fail;
|
||||
if (!(send_status(fd, STATUS_AUTH_CHALLENGE) && send_int(fd, (int)verifier.iters) &&
|
||||
send_str(fd, salt_b64) && send_str(fd, snonce_b64)))
|
||||
goto fail;
|
||||
|
||||
Status status = STATUS_ERROR;
|
||||
char* cnonce_b64 = NULL;
|
||||
char* proof_b64 = NULL;
|
||||
uint8_t cnonce[CREDENTIAL_NONCE_LEN];
|
||||
uint8_t proof[CREDENTIAL_KEY_LEN];
|
||||
uint8_t server_sig[CREDENTIAL_KEY_LEN];
|
||||
size_t cnonce_len = 0;
|
||||
size_t proof_len = 0;
|
||||
bool verified = false;
|
||||
if (ok) {
|
||||
ok = receive_status(fd, &status) && status == STATUS_AUTH_RESPONSE;
|
||||
if (ok) {
|
||||
cnonce_b64 = receive_str_redacted(fd);
|
||||
proof_b64 = receive_str_redacted(fd);
|
||||
ok = cnonce_b64 && proof_b64 &&
|
||||
credentials_b64_decode(cnonce_b64, cnonce, sizeof(cnonce), &cnonce_len) &&
|
||||
cnonce_len == CREDENTIAL_NONCE_LEN &&
|
||||
credentials_b64_decode(proof_b64, proof, sizeof(proof), &proof_len) &&
|
||||
proof_len == CREDENTIAL_KEY_LEN;
|
||||
}
|
||||
verified = ok && credentials_verify_response(&verifier, config->auth_user, snonce, cnonce,
|
||||
proof, server_sig);
|
||||
}
|
||||
if (verified) {
|
||||
char sig_b64[45];
|
||||
ok = credentials_b64_encode(server_sig, sizeof(server_sig), sig_b64, sizeof(sig_b64)) &&
|
||||
send_status(fd, STATUS_AUTH_OK) && send_str_redacted(fd, sig_b64);
|
||||
credentials_burn(sig_b64, sizeof(sig_b64));
|
||||
} else {
|
||||
send_status(fd, STATUS_AUTH_FAILED);
|
||||
ok = false;
|
||||
}
|
||||
if (!(receive_status(fd, &status) && status == STATUS_AUTH_RESPONSE))
|
||||
goto fail;
|
||||
cnonce_b64 = receive_str_redacted(fd);
|
||||
proof_b64 = receive_str_redacted(fd);
|
||||
if (!(cnonce_b64 && proof_b64 &&
|
||||
credentials_b64_decode(cnonce_b64, cnonce, sizeof(cnonce), &cnonce_len) &&
|
||||
cnonce_len == CREDENTIAL_NONCE_LEN &&
|
||||
credentials_b64_decode(proof_b64, proof, sizeof(proof), &proof_len) &&
|
||||
proof_len == CREDENTIAL_KEY_LEN))
|
||||
goto fail;
|
||||
if (!credentials_verify_response(&verifier, config->auth_user, snonce, cnonce, proof, server_sig))
|
||||
goto fail;
|
||||
|
||||
/* Success writes exactly one terminal frame (STATUS_AUTH_OK). A broken pipe
|
||||
* while sending the signature just drops the connection; it must never emit a
|
||||
* second terminal status. */
|
||||
result = credentials_b64_encode(server_sig, sizeof(server_sig), sig_b64, sizeof(sig_b64)) &&
|
||||
send_status(fd, STATUS_AUTH_OK) && send_str_redacted(fd, sig_b64);
|
||||
goto cleanup;
|
||||
|
||||
fail:
|
||||
/* Every failure path writes exactly one generic terminal status, satisfying
|
||||
* the gate's CONFIG_VALIDATE_ALREADY_TERMINATED contract. */
|
||||
send_status(fd, STATUS_AUTH_FAILED);
|
||||
|
||||
cleanup:
|
||||
credentials_burn(cnonce_b64, cnonce_b64 ? strlen(cnonce_b64) : 0);
|
||||
credentials_burn(proof_b64, proof_b64 ? strlen(proof_b64) : 0);
|
||||
free(cnonce_b64);
|
||||
@@ -142,10 +145,11 @@ static bool server_auth_handshake(int fd, const Config* config, const DaemonModu
|
||||
credentials_burn((char*)cnonce, sizeof(cnonce));
|
||||
credentials_burn((char*)proof, sizeof(proof));
|
||||
credentials_burn((char*)server_sig, sizeof(server_sig));
|
||||
credentials_burn(sig_b64, sizeof(sig_b64));
|
||||
credentials_burn((char*)verifier.salt, sizeof(verifier.salt));
|
||||
credentials_burn((char*)verifier.stored_key, sizeof(verifier.stored_key));
|
||||
credentials_burn((char*)verifier.server_key, sizeof(verifier.server_key));
|
||||
return verified && ok;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Aggregate payload bytes the multithreaded receiver may buffer ahead of the
|
||||
@@ -364,7 +368,8 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
/* Auth-required module (A7, protocol 2.19.0): run the SCRAM challenge/
|
||||
* response BEFORE the module root is installed and before any data moves.
|
||||
* Fail closed: no store -> refuse (server misconfiguration, STATUS_ERROR);
|
||||
* a failed handshake already sent STATUS_AUTH_FAILED. The username may be
|
||||
* a failed handshake writes exactly one STATUS_AUTH_FAILED (on every
|
||||
* failure path) before signalling ALREADY_TERMINATED. The username may be
|
||||
* logged (never the password or any derived proof). */
|
||||
if (g_credentials == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
@@ -751,7 +756,8 @@ static void print_server_usage(void) {
|
||||
printf(" --allow-unauthenticated Allow plaintext/anonymous network clients\n");
|
||||
printf(" --hash-credentials <file> Read <file>'s user:password lines and print\n");
|
||||
printf(" PBKDF2 credential-store lines to stdout, then exit.\n");
|
||||
printf(" Use the output as --password-file for --daemon\n");
|
||||
printf(" Use the output as --password-file for --daemon;\n");
|
||||
printf(" redirect it to an owner-only (0600) file\n");
|
||||
printf(" --iterations N PBKDF2 iteration count for --hash-credentials\n");
|
||||
printf(" (default %u, range %u-%u)\n", CREDENTIAL_DEFAULT_ITERS,
|
||||
CREDENTIAL_MIN_ITERS, CREDENTIAL_MAX_ITERS);
|
||||
@@ -828,6 +834,14 @@ int main(int argc, char* argv[]) {
|
||||
* emit new-format credential-store lines, then exit. */
|
||||
if (opts.hash_credentials_file) {
|
||||
uint32_t iters = opts.hash_iterations_set ? opts.hash_iterations : CREDENTIAL_DEFAULT_ITERS;
|
||||
/* The output is secret material: if it is redirected to a regular file,
|
||||
* warn when that file is group/other-accessible (the store must be 0600). */
|
||||
struct stat out_st;
|
||||
if (fstat(STDOUT_FILENO, &out_st) == 0 && S_ISREG(out_st.st_mode) &&
|
||||
(out_st.st_mode & (S_IRWXG | S_IRWXO)) != 0)
|
||||
fprintf(stderr,
|
||||
"Warning: credential-store output is a group/other-accessible file; restrict it to "
|
||||
"mode 0600 (chmod 600)\n");
|
||||
char hash_err[512];
|
||||
if (credentials_hash_file(opts.hash_credentials_file, iters, stdout, hash_err,
|
||||
sizeof(hash_err)) != 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "server_cli.h"
|
||||
#include "charset.h"
|
||||
#include "credentials.h"
|
||||
#include "utils.h"
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
@@ -146,8 +147,10 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
||||
}
|
||||
char* end = NULL;
|
||||
long n = strtol(inline_value, &end, 10);
|
||||
if (!end || *end != '\0' || n < 0 || n > 10000000L) {
|
||||
set_error(err, err_size, "invalid --iterations '%s'", inline_value);
|
||||
if (!end || *end != '\0' || n < (long)CREDENTIAL_MIN_ITERS ||
|
||||
n > (long)CREDENTIAL_MAX_ITERS) {
|
||||
set_error(err, err_size, "--iterations must be in [%u,%u], got '%s'", CREDENTIAL_MIN_ITERS,
|
||||
CREDENTIAL_MAX_ITERS, inline_value);
|
||||
return -1;
|
||||
}
|
||||
opts->hash_iterations = (uint32_t)n;
|
||||
|
||||
+87
-19
@@ -30,6 +30,16 @@ struct CredentialStore {
|
||||
CredentialEntry* entries;
|
||||
int count;
|
||||
int capacity;
|
||||
/* Store-wide uniform PBKDF2 iteration count. Every entry must agree on it
|
||||
* (the parser refuses a store whose entries disagree), so a miss can be
|
||||
* challenged with the same count as a hit and the count itself never leaks
|
||||
* membership. Unused (0) for an empty store. */
|
||||
uint32_t iters;
|
||||
/* Random secret generated once at load. The dummy salt handed out for an
|
||||
* unknown/off-list user is HMAC-SHA256(dummy_key, username)[:SALT_LEN], so
|
||||
* repeated probes of the same username always see an identical challenge
|
||||
* while different usernames differ -- with no fresh-random tell. */
|
||||
uint8_t dummy_key[CREDENTIAL_KEY_LEN];
|
||||
};
|
||||
|
||||
/* Exact marker prefix of the new store verifier field. */
|
||||
@@ -179,15 +189,20 @@ bool credentials_b64_decode(const char* in, uint8_t* out, size_t out_sz, size_t*
|
||||
if (decoded_len > out_sz)
|
||||
return false;
|
||||
/* EVP_DecodeBlock writes the full (padded) quantum, so decode into a scratch
|
||||
* buffer sized for it and copy only the real bytes out. */
|
||||
uint8_t scratch[192];
|
||||
* buffer sized for it and copy only the real bytes out. The single `done`
|
||||
* path burns the scratch on failure as well as success, so no partial secret
|
||||
* survives an early return. */
|
||||
uint8_t scratch[192] = {0};
|
||||
bool ok = false;
|
||||
int n = EVP_DecodeBlock(scratch, (const unsigned char*)in, (int)len);
|
||||
if (n < 0 || (size_t)n != padded_len)
|
||||
return false;
|
||||
goto done;
|
||||
memcpy(out, scratch, decoded_len);
|
||||
credentials_burn((char*)scratch, sizeof(scratch));
|
||||
*out_len = decoded_len;
|
||||
return true;
|
||||
ok = true;
|
||||
done:
|
||||
credentials_burn((char*)scratch, sizeof(scratch));
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool credentials_random_bytes(uint8_t* out, size_t n) {
|
||||
@@ -230,10 +245,9 @@ bool credentials_compute_keys(const char* password, const uint8_t salt[CREDENTIA
|
||||
uint8_t server_key[CREDENTIAL_KEY_LEN]) {
|
||||
if (!password || !salt)
|
||||
return false;
|
||||
/* The caller (store parser / client clamp) is responsible for the
|
||||
* [MIN,MAX] policy; this primitive only refuses a zero/unbounded work
|
||||
* factor. Tests exercise the known-answer vector at a smaller count. */
|
||||
if (iters == 0 || iters > CREDENTIAL_MAX_ITERS)
|
||||
/* Enforce the full [MIN,MAX] policy here so no caller can derive a verifier
|
||||
* with a work factor outside the validated store range. */
|
||||
if (iters < CREDENTIAL_MIN_ITERS || iters > CREDENTIAL_MAX_ITERS)
|
||||
return false;
|
||||
size_t password_len = strlen(password);
|
||||
if (password_len > CREDENTIAL_MAX_PASSWORD_LEN || password_len > (size_t)INT_MAX)
|
||||
@@ -338,11 +352,15 @@ bool credentials_verify_response(const CredentialVerifier* v, const char* user,
|
||||
computed = hmac_sha256(v->server_key, CREDENTIAL_KEY_LEN, auth_msg, msg_len, server_sig);
|
||||
if (computed)
|
||||
memcpy(server_sig_out, server_sig, CREDENTIAL_KEY_LEN);
|
||||
/* Constant-time compare over the fixed 32-byte keys; a tampered nonce
|
||||
* changes the AuthMessage and so the recovered key. */
|
||||
bool accept = computed && v->found &&
|
||||
credentials_secure_equal((const char*)recovered, (const char*)v->stored_key,
|
||||
/* Always run the constant-time key compare (even when `found` is false) and
|
||||
* fold the accept decision with bitwise AND so no short-circuit reveals
|
||||
* whether the user was found. A tampered nonce changes the AuthMessage and
|
||||
* so the recovered key. */
|
||||
bool key_match = false;
|
||||
if (computed)
|
||||
key_match = credentials_secure_equal((const char*)recovered, (const char*)v->stored_key,
|
||||
CREDENTIAL_KEY_LEN);
|
||||
bool accept = computed & v->found & key_match;
|
||||
credentials_burn((char*)auth_msg, sizeof(auth_msg));
|
||||
credentials_burn((char*)client_sig, sizeof(client_sig));
|
||||
credentials_burn((char*)client_key, sizeof(client_key));
|
||||
@@ -526,6 +544,18 @@ static CredentialStore* load_store_file(const char* path, char* err, size_t err_
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
/* Every entry must agree on the iteration count, so a miss can be answered
|
||||
* with the store-wide count without leaking membership. */
|
||||
if (store->count == 0) {
|
||||
store->iters = parsed.iters;
|
||||
} else if (store->iters != parsed.iters) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' line %d: iteration count %u disagrees with the store-wide %u "
|
||||
"(the store must be uniform)",
|
||||
path, line_no, parsed.iters, store->iters);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (find_user(store, user) >= 0) {
|
||||
set_error(err, err_size, "credential file '%s' line %d: duplicate entry for user '%.*s'",
|
||||
path, line_no, (int)strlen(user), user);
|
||||
@@ -560,6 +590,15 @@ CredentialStore* credentials_load(const char* password_file, const char* early_i
|
||||
CredentialStore* store = load_store_file(password_file, err, err_size);
|
||||
if (!store)
|
||||
return NULL;
|
||||
/* Generate the store-wide dummy key once for the final (possibly merged)
|
||||
* store. It makes an unknown-user challenge deterministic, so fail the load
|
||||
* if the CSPRNG is unavailable rather than degrading the anti-enumeration
|
||||
* property. */
|
||||
if (!credentials_random_bytes(store->dummy_key, sizeof(store->dummy_key))) {
|
||||
set_error(err, err_size, "failed to generate the credential store dummy key");
|
||||
credentials_free(store);
|
||||
return NULL;
|
||||
}
|
||||
if (!early_input_file)
|
||||
return store;
|
||||
|
||||
@@ -568,6 +607,18 @@ CredentialStore* credentials_load(const char* password_file, const char* early_i
|
||||
credentials_free(store);
|
||||
return NULL;
|
||||
}
|
||||
/* A layered store must stay uniform too. */
|
||||
if (store->count > 0 && early->count > 0 && store->iters != early->iters) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' and early-input file '%s' disagree on the iteration count "
|
||||
"(%u vs %u); the store must be uniform",
|
||||
password_file, early_input_file, store->iters, early->iters);
|
||||
credentials_free(early);
|
||||
credentials_free(store);
|
||||
return NULL;
|
||||
}
|
||||
if (store->count == 0 && early->count > 0)
|
||||
store->iters = early->iters;
|
||||
/* Layer early input over the password file: an identical verifier dedupes, a
|
||||
* differing verifier for the same user is ambiguous and fails closed. */
|
||||
for (int i = 0; i < early->count; i++) {
|
||||
@@ -652,14 +703,23 @@ bool credentials_get_verifier(const CredentialStore* store, const char* user,
|
||||
if (!out)
|
||||
return false;
|
||||
memset(out, 0, sizeof(*out));
|
||||
/* Start from the dummy verifier: a fresh random salt and the default
|
||||
* iteration count, so a miss is shaped exactly like a hit. */
|
||||
if (!credentials_random_bytes(out->salt, CREDENTIAL_SALT_LEN))
|
||||
return false;
|
||||
out->iters = CREDENTIAL_DEFAULT_ITERS;
|
||||
const char* uname = user ? user : "";
|
||||
/* The dummy verifier is shaped exactly like a hit: the store-wide uniform
|
||||
* iteration count (default for an empty store) and fixed dummy keys. */
|
||||
out->iters = (store && store->count > 0) ? store->iters : CREDENTIAL_DEFAULT_ITERS;
|
||||
memcpy(out->stored_key, k_dummy_stored_key, CREDENTIAL_KEY_LEN);
|
||||
memcpy(out->server_key, k_dummy_server_key, CREDENTIAL_KEY_LEN);
|
||||
out->found = false;
|
||||
/* Deterministic per-username dummy salt: HMAC-SHA256(dummy_key, username)
|
||||
* truncated to the salt length. Two probes of the same unknown username see
|
||||
* an identical challenge; distinct usernames differ. A NULL store (never
|
||||
* reached in production) falls back to the all-zero static key. */
|
||||
const uint8_t* dummy_key = store ? store->dummy_key : k_dummy_stored_key;
|
||||
uint8_t mac[CREDENTIAL_KEY_LEN];
|
||||
if (!hmac_sha256(dummy_key, CREDENTIAL_KEY_LEN, (const uint8_t*)uname, strlen(uname), mac))
|
||||
return false;
|
||||
memcpy(out->salt, mac, CREDENTIAL_SALT_LEN);
|
||||
credentials_burn((char*)mac, sizeof(mac));
|
||||
if (!store || !user || n < 0)
|
||||
return true;
|
||||
/* Module-list membership: constant-time full scan, no early break, so the
|
||||
@@ -731,10 +791,17 @@ bool credentials_hash_store_line(const char* user, const char* password, uint32_
|
||||
credentials_burn((char*)stored_key, sizeof(stored_key));
|
||||
credentials_burn((char*)server_key, sizeof(server_key));
|
||||
credentials_burn((char*)salt, sizeof(salt));
|
||||
if (!ok)
|
||||
/* The base64 encodings of the salt/keys are secret material too (A7-4). */
|
||||
credentials_burn(salt_b64, sizeof(salt_b64));
|
||||
credentials_burn(stored_b64, sizeof(stored_b64));
|
||||
credentials_burn(server_b64, sizeof(server_b64));
|
||||
if (!ok) {
|
||||
credentials_burn(out, out_sz);
|
||||
return false;
|
||||
}
|
||||
if (written < 0 || (size_t)written >= out_sz) {
|
||||
set_error(err, err_size, "output buffer too small for the credential line");
|
||||
credentials_burn(out, out_sz);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -797,6 +864,7 @@ int credentials_hash_file(const char* path, uint32_t iters, FILE* out, char* err
|
||||
char store_line[CREDENTIAL_MAX_LINE];
|
||||
if (!credentials_hash_store_line(user, password, iters, store_line, sizeof(store_line), err,
|
||||
err_size)) {
|
||||
credentials_burn(store_line, sizeof(store_line));
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -55,9 +55,11 @@
|
||||
typedef struct CredentialStore CredentialStore;
|
||||
|
||||
/* One resolved verifier. `found` is false for an unknown user or a user not on
|
||||
* a module's auth list; the remaining fields then hold a fresh random salt, the
|
||||
* default iteration count and fixed dummy keys, so the server can run the same
|
||||
* challenge/response math with no enumeration/timing oracle. */
|
||||
* a module's auth list; the remaining fields then hold a deterministic dummy
|
||||
* salt (HMAC of the store-wide dummy key over the username), the store-wide
|
||||
* uniform iteration count (default for an empty store) and fixed dummy keys, so
|
||||
* the server can run the same challenge/response math with no enumeration or
|
||||
* timing oracle. */
|
||||
typedef struct {
|
||||
uint8_t salt[CREDENTIAL_SALT_LEN];
|
||||
uint32_t iters;
|
||||
@@ -73,8 +75,10 @@ typedef struct {
|
||||
* opened or that fails the strict grammar is a hard error (err filled, NULL
|
||||
* returned) -- the daemon fails CLOSED rather than serving an auth-required
|
||||
* module with a partial store. Both files may be NULL, which yields an empty
|
||||
* store (every auth-required module then refuses connections). When both are
|
||||
* given, the --early-input file is layered over --password-file: a duplicate
|
||||
* store (every auth-required module then refuses connections). Every entry in
|
||||
* the resulting store must agree on the iteration count; entries that disagree
|
||||
* (within one file or across the two layered sources) are rejected. When both
|
||||
* are given, the --early-input file is layered over --password-file: a duplicate
|
||||
* username whose verifier matches is deduplicated; one whose verifier differs
|
||||
* is an error (the two sources disagree), never a silent pick.
|
||||
*
|
||||
@@ -101,9 +105,10 @@ bool credentials_random_bytes(uint8_t* out, size_t n);
|
||||
|
||||
/* Resolve `user` against the store AND the module's auth-user list. The list
|
||||
* scan is a constant-time full-length comparison with no early break. On a
|
||||
* miss, *out is filled with a dummy verifier (fresh random salt, default
|
||||
* iterations, fixed dummy keys, found=false). Returns false only on invalid
|
||||
* arguments/allocation failure. */
|
||||
* miss, *out is filled with a dummy verifier (a deterministic per-username salt
|
||||
* derived from the store's dummy key, the store-wide uniform iteration count,
|
||||
* fixed dummy keys, found=false). Returns false on invalid arguments or an
|
||||
* HMAC/crypto primitive failure. */
|
||||
bool credentials_get_verifier(const CredentialStore* store, const char* user,
|
||||
const char* const* module_users, int n, CredentialVerifier* out);
|
||||
|
||||
@@ -111,7 +116,8 @@ bool credentials_get_verifier(const CredentialStore* store, const char* user,
|
||||
* K = PBKDF2-HMAC-SHA256(password, salt, iters, 32)
|
||||
* ClientKey = HMAC-SHA256(K, "Client Key"); StoredKey = SHA256(ClientKey)
|
||||
* ServerKey = HMAC-SHA256(K, "Server Key")
|
||||
* Any of client_key/stored_key/server_key may be NULL when not needed. */
|
||||
* Any of client_key/stored_key/server_key may be NULL when not needed.
|
||||
* `iters` must lie in [CREDENTIAL_MIN_ITERS, CREDENTIAL_MAX_ITERS]. */
|
||||
bool credentials_compute_keys(const char* password, const uint8_t salt[CREDENTIAL_SALT_LEN],
|
||||
uint32_t iters, uint8_t client_key[CREDENTIAL_KEY_LEN],
|
||||
uint8_t stored_key[CREDENTIAL_KEY_LEN],
|
||||
|
||||
@@ -427,9 +427,10 @@ static const char* status_to_string(Status status) {
|
||||
}
|
||||
|
||||
/* Shared string send/receive implementation. `redact` selects whether the
|
||||
* payload body is written to the LOG_DEBUG_PROTO debug log: secrets (daemon
|
||||
* auth username/digest) set it so a --verbose log never captures a replayable
|
||||
* credential, while every other string keeps its normal debug trace. */
|
||||
* payload body is written to the LOG_DEBUG_PROTO debug log: daemon auth material
|
||||
* (the username and the proof/signature fields) sets it so a --verbose log never
|
||||
* captures a replayable credential, while every other string keeps its normal
|
||||
* debug trace. */
|
||||
static bool protocol_send_str_impl(ProtocolSession* session, const char* data, bool redact) {
|
||||
if (data == NULL)
|
||||
return false;
|
||||
@@ -602,7 +603,8 @@ char* receive_str(int fd) {
|
||||
return protocol_receive_str(legacy_session(fd, -1));
|
||||
}
|
||||
/* Redacted variants: identical framing, but the string body is never written to
|
||||
the debug protocol log. Used for the daemon auth username/digest. */
|
||||
the debug protocol log. Used for daemon auth material (username, proof,
|
||||
signature). */
|
||||
bool send_str_redacted(int fd, const char* data) {
|
||||
return protocol_send_str_redacted(legacy_session(-1, fd), data);
|
||||
}
|
||||
|
||||
@@ -151,8 +151,9 @@ bool protocol_send_str(ProtocolSession* session, const char* data);
|
||||
char* protocol_receive_str(ProtocolSession* session);
|
||||
/* Redacted string variants: identical wire framing to protocol_send_str /
|
||||
* protocol_receive_str, but the payload body is replaced by `<redacted>` in the
|
||||
* LOG_DEBUG_PROTO debug log. Used for secrets (daemon auth username/digest) so
|
||||
* a --verbose log can never capture a replayable credential. */
|
||||
* LOG_DEBUG_PROTO debug log. Used for daemon auth material (the username and
|
||||
* the proof/signature fields) so a --verbose log can never capture a credential
|
||||
* that could be replayed. */
|
||||
bool protocol_send_str_redacted(ProtocolSession* session, const char* data);
|
||||
char* protocol_receive_str_redacted(ProtocolSession* session);
|
||||
bool protocol_send_data(ProtocolSession* session, const Data* data);
|
||||
|
||||
Reference in New Issue
Block a user