feat(a7): SCRAM-SHA-256 daemon auth to replace replayable digest
Replace the challenge-less static-SHA-256 daemon bearer credential with a SCRAM-SHA-256-style challenge/response and a salted PBKDF2 verifier store. PROTOCOL_VERSION 2.18.0 -> 2.19.0; legacy user:SHA256HEX stores hard-reject. - credentials: b64/rand/PBKDF2/HMAC primitives, verifier store parser, constant-time proof verify + ServerSignature, --hash-credentials helper - config: auth block is now [present][username]; client runs the challenge exchange; config_burn_auth wipes plaintext/derived secrets (A7-4) - server: gate drives the challenge, dummy verifier for unknown/off-list users - tests: independent Python KAT, replay + legacy integration tests, fuzz paths - docs: new store format, --hash-credentials, 2.19.0 bump TLS verification behavior (A7-3/S1) is intentionally unchanged.
This commit is contained in:
+10
-27
@@ -1573,14 +1573,14 @@ static int read_patterns_from_file(const char* filepath, char*** patterns, int*
|
||||
}
|
||||
|
||||
#ifndef FASTSYNC_TEST_BUILD
|
||||
/* Daemon auth (Wave B): read --password-file and derive the wire credentials
|
||||
* (username + SHA-256 hex digest of the password). Runs once the destination
|
||||
* form is known: the credentials only make sense for a daemon
|
||||
* (host::module/path) destination, so a --password-file without one is a hard
|
||||
* error here rather than a silently-ignored flag. The literal password is
|
||||
* hashed immediately and wiped from memory; only the digest (and username) are
|
||||
* kept on the Config for config_send. Returns 0 on success, -1 on error (the
|
||||
* reason is logged; neither the password nor its digest is ever logged). */
|
||||
/* Daemon auth (A7, protocol 2.19.0): read --password-file and keep the
|
||||
* username plus the LITERAL password (client-only, never serialized). Runs
|
||||
* once the destination form is known: the credentials only make sense for a
|
||||
* daemon (host::module/path) destination, so a --password-file without one is a
|
||||
* hard error here rather than a silently-ignored flag. The password is handed
|
||||
* to the SCRAM challenge/response in config_send and burned by
|
||||
* config_burn_auth/config_delete at teardown. Returns 0 on success, -1 on
|
||||
* error (the reason is logged; the password is never logged). */
|
||||
static int load_daemon_credentials(Config* config) {
|
||||
if (!config->password_file)
|
||||
return 0;
|
||||
@@ -1597,27 +1597,10 @@ static int load_daemon_credentials(Config* config) {
|
||||
log_message(LOG_LEVEL_ERROR, "%s", err);
|
||||
return -1;
|
||||
}
|
||||
char hash[CREDENTIAL_HASH_HEX_LEN + 1];
|
||||
if (!credentials_hash_password(password, hash)) {
|
||||
log_message(LOG_LEVEL_ERROR, "failed to hash the password from '%s'", config->password_file);
|
||||
credentials_burn(password, strlen(password));
|
||||
free(password);
|
||||
free(user);
|
||||
return -1;
|
||||
}
|
||||
credentials_burn(password, strlen(password));
|
||||
free(password);
|
||||
|
||||
free(config->auth_user);
|
||||
free(config->auth_password_hash);
|
||||
config_burn_auth(config);
|
||||
config->auth_user = user;
|
||||
config->auth_password_hash = str_dup(hash);
|
||||
if (!config->auth_password_hash) {
|
||||
log_message(LOG_LEVEL_ERROR, "memory allocation failed reading '%s'", config->password_file);
|
||||
free(config->auth_user);
|
||||
config->auth_user = NULL;
|
||||
return -1;
|
||||
}
|
||||
config->auth_password = password;
|
||||
log_info_message(LOG_INFO_MISC, "Loaded daemon credentials for user '%s'", config->auth_user);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+126
-22
@@ -60,12 +60,94 @@ static CredentialStore* g_credentials = NULL;
|
||||
* SUPER_MODE_OFF here and the handler applies it exactly once after acceptance. */
|
||||
typedef struct ModuleGateContext {
|
||||
SSL* ssl;
|
||||
/* The connection descriptor, so the gate can drive the SCRAM auth handshake
|
||||
* while it still owns the config-frame exchange (before the STATUS_OK ack). */
|
||||
int fd;
|
||||
/* SUPER_MODE_OFF when this connection must not attempt any super-user
|
||||
activity (operator --no-super, or a daemon module without the
|
||||
`client owner = yes` opt-in); -1 when the config's own mode stands. */
|
||||
int super_mode_override;
|
||||
} ModuleGateContext;
|
||||
|
||||
/* Server half of the SCRAM challenge/response (A7 remediation, protocol
|
||||
* 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
|
||||
* 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. */
|
||||
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;
|
||||
}
|
||||
CredentialVerifier verifier;
|
||||
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);
|
||||
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;
|
||||
}
|
||||
credentials_burn(cnonce_b64, cnonce_b64 ? strlen(cnonce_b64) : 0);
|
||||
credentials_burn(proof_b64, proof_b64 ? strlen(proof_b64) : 0);
|
||||
free(cnonce_b64);
|
||||
free(proof_b64);
|
||||
credentials_burn((char*)snonce, sizeof(snonce));
|
||||
credentials_burn(salt_b64, sizeof(salt_b64));
|
||||
credentials_burn(snonce_b64, sizeof(snonce_b64));
|
||||
credentials_burn((char*)cnonce, sizeof(cnonce));
|
||||
credentials_burn((char*)proof, sizeof(proof));
|
||||
credentials_burn((char*)server_sig, sizeof(server_sig));
|
||||
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;
|
||||
}
|
||||
|
||||
/* Aggregate payload bytes the multithreaded receiver may buffer ahead of the
|
||||
slow disk writer. Receiving one more chunk adds up to ~2 * MAX_CHUNK_SIZE
|
||||
of transient wire/decompression buffers on top of the queued payloads, so
|
||||
@@ -279,10 +361,11 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||
}
|
||||
if (module->auth_user_count > 0) {
|
||||
/* Auth-required module (Wave B): verify the presented credentials against
|
||||
* the store BEFORE the module root is installed and before any data moves.
|
||||
* Fail closed: no store -> refuse; no/invalid credentials -> refuse. The
|
||||
* username may be logged (never the digest/password). */
|
||||
/* 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
|
||||
* logged (never the password or any derived proof). */
|
||||
if (g_credentials == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"daemon module '%s' requires authentication but no credential store is "
|
||||
@@ -291,28 +374,25 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
return "requested daemon module requires authentication and no credential "
|
||||
"store is configured";
|
||||
}
|
||||
if (!config->auth_user || !config->auth_password_hash) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"daemon module '%s' requires authentication; the client "
|
||||
"presented no credentials",
|
||||
config->module);
|
||||
return "requested daemon module requires authentication";
|
||||
}
|
||||
if (gate_ctx && !gate_ctx->ssl) {
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"daemon module '%s' is authenticating over a plaintext connection (no --tls); "
|
||||
"the credential exchange is not encrypted",
|
||||
config->module);
|
||||
}
|
||||
if (!credentials_gate_allows(g_credentials, (const char* const*)module->auth_users,
|
||||
module->auth_user_count, config->auth_user,
|
||||
config->auth_password_hash)) {
|
||||
char* escaped_user = output_escape(config->auth_user, config->eight_bit_output);
|
||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': authentication failed for user '%s'",
|
||||
config->module, escaped_user ? escaped_user : "<allocation failed>");
|
||||
free(escaped_user);
|
||||
if (!gate_ctx || gate_ctx->fd < 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available",
|
||||
config->module);
|
||||
return "authentication failed for the requested daemon module";
|
||||
}
|
||||
if (!server_auth_handshake(gate_ctx->fd, config, module)) {
|
||||
char* escaped_user =
|
||||
config->auth_user ? output_escape(config->auth_user, config->eight_bit_output) : NULL;
|
||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': authentication failed for user '%s'",
|
||||
config->module, escaped_user ? escaped_user : "(none)");
|
||||
free(escaped_user);
|
||||
return CONFIG_VALIDATE_ALREADY_TERMINATED;
|
||||
}
|
||||
char* escaped_user = output_escape(config->auth_user, config->eight_bit_output);
|
||||
log_message(LOG_LEVEL_INFO, "daemon module '%s': user '%s' authenticated", config->module,
|
||||
escaped_user ? escaped_user : "<allocation failed>");
|
||||
@@ -334,6 +414,7 @@ void handler(int file_descriptor) {
|
||||
protocol_session_bind(&session);
|
||||
ModuleGateContext gate_ctx;
|
||||
gate_ctx.ssl = ssl;
|
||||
gate_ctx.fd = file_descriptor;
|
||||
gate_ctx.super_mode_override = -1;
|
||||
Config* config = config_receive_with_validate(file_descriptor, server_module_gate, &gate_ctx);
|
||||
if (config == NULL) {
|
||||
@@ -638,10 +719,12 @@ static void print_server_usage(void) {
|
||||
printf(" --no-detach Stay in the foreground (default detaches to\n");
|
||||
printf(" background when running --daemon)\n");
|
||||
printf(" --password-file=FILE Credential store for modules that declare\n");
|
||||
printf(" 'auth users' (line format: user:SHA256HEX where\n");
|
||||
printf(" SHA256HEX is the lowercase hex SHA-256 of the\n");
|
||||
printf(" user's password). Requires --daemon; an auth-\n");
|
||||
printf(" required module with no store refuses to start\n");
|
||||
printf(" 'auth users' (line format:\n");
|
||||
printf(" user:$fastsync$1$pbkdf2-sha256$iters$salt$stored$server,\n");
|
||||
printf(" generated by --hash-credentials). Legacy\n");
|
||||
printf(" user:SHA256HEX lines are rejected. Requires\n");
|
||||
printf(" --daemon; an auth-required module with no store\n");
|
||||
printf(" refuses to start\n");
|
||||
printf(" --early-input=FILE Second credential store layered over\n");
|
||||
printf(" --password-file (same format); usually a secrets-\n");
|
||||
printf(" manager/process-substitution file. Requires --daemon\n");
|
||||
@@ -666,6 +749,12 @@ static void print_server_usage(void) {
|
||||
printf(" client's CONVERT_SPEC). A name that cannot be\n");
|
||||
printf(" represented fails the run cleanly\n");
|
||||
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(" --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);
|
||||
printf(" -v, --verbose Enable debug logging\n");
|
||||
printf(" --help Show this help\n");
|
||||
}
|
||||
@@ -735,6 +824,21 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* --hash-credentials: standalone offline tool; read user:password lines and
|
||||
* 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;
|
||||
char hash_err[512];
|
||||
if (credentials_hash_file(opts.hash_credentials_file, iters, stdout, hash_err,
|
||||
sizeof(hash_err)) != 0) {
|
||||
fprintf(stderr, "Error: %s\n", hash_err);
|
||||
server_cli_options_free(&opts);
|
||||
return 1;
|
||||
}
|
||||
server_cli_options_free(&opts);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int exit_code = 0;
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
if (opts.verbose) {
|
||||
|
||||
@@ -127,6 +127,31 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
||||
inline_value = argv[++i];
|
||||
}
|
||||
opts->early_input_file = inline_value;
|
||||
} else if (arg_has_value(argv[i], "--hash-credentials", &inline_value)) {
|
||||
if (!inline_value) {
|
||||
if (i + 1 >= argc) {
|
||||
set_error(err, err_size, "missing argument for --hash-credentials");
|
||||
return -1;
|
||||
}
|
||||
inline_value = argv[++i];
|
||||
}
|
||||
opts->hash_credentials_file = inline_value;
|
||||
} else if (arg_has_value(argv[i], "--iterations", &inline_value)) {
|
||||
if (!inline_value) {
|
||||
if (i + 1 >= argc) {
|
||||
set_error(err, err_size, "missing argument for --iterations");
|
||||
return -1;
|
||||
}
|
||||
inline_value = argv[++i];
|
||||
}
|
||||
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);
|
||||
return -1;
|
||||
}
|
||||
opts->hash_iterations = (uint32_t)n;
|
||||
opts->hash_iterations_set = true;
|
||||
} else if (arg_is(argv[i], "--address")) {
|
||||
if (i + 1 >= argc) {
|
||||
set_error(err, err_size, "missing argument for --address");
|
||||
@@ -227,6 +252,14 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
||||
"--daemon");
|
||||
return -1;
|
||||
}
|
||||
if (opts->hash_credentials_file != NULL && (opts->daemon_mode || opts->stdio_mode)) {
|
||||
set_error(err, err_size, "--hash-credentials cannot be combined with --daemon or --stdio");
|
||||
return -1;
|
||||
}
|
||||
if (opts->hash_iterations_set && opts->hash_credentials_file == NULL) {
|
||||
set_error(err, err_size, "--iterations requires --hash-credentials");
|
||||
return -1;
|
||||
}
|
||||
/* --iconv: reject a malformed CONVERT_SPEC or an unsupported charset name at
|
||||
startup (a probe iconv_open is attempted). */
|
||||
if (opts->iconv_spec != NULL && !charset_spec_valid(opts->iconv_spec)) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Parsed fastsync-server command line. All string members are borrowed
|
||||
* pointers into the original argv (valid for the life of the argv array the
|
||||
@@ -26,7 +27,13 @@ typedef struct ServerCliOptions {
|
||||
const char* config_path; /* --config value, or NULL */
|
||||
const char* password_file; /* --password-file value, or NULL (daemon) */
|
||||
const char* early_input_file; /* --early-input value, or NULL (daemon) */
|
||||
const char** dparams; /* raw --dparam override strings */
|
||||
/* --hash-credentials=FILE: read `user:password` lines from FILE and print
|
||||
* new-format credential-store lines to stdout, then exit. Standalone mode
|
||||
* (mutually exclusive with --daemon/--stdio). */
|
||||
const char* hash_credentials_file;
|
||||
bool hash_iterations_set; /* an explicit --iterations was given */
|
||||
uint32_t hash_iterations; /* --iterations value (default CREDENTIAL_DEFAULT_ITERS) */
|
||||
const char** dparams; /* raw --dparam override strings */
|
||||
int dparam_count;
|
||||
const char* bind_address; /* --address */
|
||||
int bind_family; /* AF_UNSPEC / AF_INET / AF_INET6 */
|
||||
|
||||
+130
-29
@@ -41,7 +41,7 @@ static void config_set_defaults(Config* config) {
|
||||
config->ssh_destination = NULL;
|
||||
config->module = NULL;
|
||||
config->auth_user = NULL;
|
||||
config->auth_password_hash = NULL;
|
||||
config->auth_password = NULL;
|
||||
config->password_file = NULL;
|
||||
config->iconv_spec = NULL;
|
||||
config->fastsync_server_path = NULL;
|
||||
@@ -630,6 +630,20 @@ void config_parse_ssh_dest(Config* config) {
|
||||
config->receive_root_directory = path;
|
||||
}
|
||||
|
||||
void config_burn_auth(Config* config) {
|
||||
if (!config)
|
||||
return;
|
||||
if (config->auth_password) {
|
||||
credentials_burn(config->auth_password, strlen(config->auth_password));
|
||||
free(config->auth_password);
|
||||
config->auth_password = NULL;
|
||||
}
|
||||
if (config->auth_user) {
|
||||
free(config->auth_user);
|
||||
config->auth_user = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void config_delete(Config* config) {
|
||||
if (config == NULL)
|
||||
return;
|
||||
@@ -642,8 +656,7 @@ void config_delete(Config* config) {
|
||||
free(config->receive_root_directory);
|
||||
free(config->ssh_destination);
|
||||
free(config->module);
|
||||
free(config->auth_user);
|
||||
free(config->auth_password_hash);
|
||||
config_burn_auth(config);
|
||||
free(config->password_file);
|
||||
free(config->iconv_spec);
|
||||
free(config->write_batch);
|
||||
@@ -1128,23 +1141,18 @@ static bool receive_daemon_module(int fd, Config* c) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Daemon password credentials (Wave B, within protocol 2.15.0 -- see the
|
||||
* PROTOCOL_VERSION note in config.h: this rides the Wave A trailing-string
|
||||
* area, symmetric sender+receiver in every 2.15.0 build, so it is not a frame
|
||||
* layout that needs its own bump). A single presence int is followed, when
|
||||
* set, by the username and the SHA-256 hex digest of the password. The
|
||||
* literal password never crosses the wire. */
|
||||
/* Daemon password credentials (A7 remediation, protocol 2.19.0). A single
|
||||
* presence int is followed, when set, by ONLY the username; the password is
|
||||
* never serialized. The daemon answers an auth-required module with the SCRAM
|
||||
* challenge (see the auth exchange below). */
|
||||
static bool send_daemon_auth(int fd, const Config* c) {
|
||||
bool present = c->auth_user != NULL && c->auth_password_hash != NULL && c->auth_user[0] != '\0' &&
|
||||
c->auth_password_hash[0] != '\0';
|
||||
bool present = c->auth_user != NULL && c->auth_user[0] != '\0';
|
||||
if (!send_int(fd, present ? 1 : 0))
|
||||
return false;
|
||||
if (!present)
|
||||
return true;
|
||||
/* Redacted send: the username and hard-wired digest must never reach a
|
||||
* --verbose debug log (they are replayable), while normal protocol strings
|
||||
* keep their debug trace. */
|
||||
return send_str_redacted(fd, c->auth_user) && send_str_redacted(fd, c->auth_password_hash);
|
||||
/* Redacted send: the username must never reach a --verbose debug log. */
|
||||
return send_str_redacted(fd, c->auth_user);
|
||||
}
|
||||
|
||||
static bool receive_daemon_auth(int fd, Config* c) {
|
||||
@@ -1153,27 +1161,107 @@ static bool receive_daemon_auth(int fd, Config* c) {
|
||||
return false;
|
||||
if (!present)
|
||||
return true;
|
||||
/* Redacted receive: never log the incoming username/digest bodies. */
|
||||
/* Redacted receive: never log the incoming username body. */
|
||||
char* user = receive_str_redacted(fd);
|
||||
char* hash = receive_str_redacted(fd);
|
||||
if (!user || !hash) {
|
||||
free(user);
|
||||
free(hash);
|
||||
if (!user)
|
||||
return false;
|
||||
}
|
||||
size_t user_len = strlen(user);
|
||||
bool valid = user_len > 0 && user_len <= CREDENTIAL_MAX_USER_LEN && credentials_hash_valid(hash);
|
||||
if (!valid) {
|
||||
if (!credentials_username_valid(user)) {
|
||||
free(user);
|
||||
free(hash);
|
||||
log_message(LOG_LEVEL_WARNING, "Daemon client sent malformed auth credentials");
|
||||
return false;
|
||||
}
|
||||
c->auth_user = user;
|
||||
c->auth_password_hash = hash;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Client half of the SCRAM challenge/response (A7 remediation). Called by
|
||||
* config_send after the config frame is written and the server answered
|
||||
* STATUS_AUTH_CHALLENGE. The plaintext password lives only in
|
||||
* config->auth_password and every derived buffer is wiped on the way out. */
|
||||
static bool client_auth_exchange(int fd, const Config* c) {
|
||||
if (!c->auth_user || !c->auth_password)
|
||||
return false;
|
||||
int iters = 0;
|
||||
if (!receive_int(fd, &iters))
|
||||
return false;
|
||||
if (iters < (int)CREDENTIAL_MIN_ITERS || iters > (int)CREDENTIAL_MAX_ITERS) {
|
||||
log_message(LOG_LEVEL_ERROR, "Daemon sent an out-of-range auth iteration count");
|
||||
return false;
|
||||
}
|
||||
char* salt_b64 = receive_str(fd);
|
||||
char* snonce_b64 = receive_str(fd);
|
||||
uint8_t salt[CREDENTIAL_SALT_LEN];
|
||||
uint8_t snonce[CREDENTIAL_NONCE_LEN];
|
||||
uint8_t cnonce[CREDENTIAL_NONCE_LEN];
|
||||
size_t salt_len = 0;
|
||||
size_t snonce_len = 0;
|
||||
bool ok = salt_b64 && snonce_b64 &&
|
||||
credentials_b64_decode(salt_b64, salt, sizeof(salt), &salt_len) &&
|
||||
salt_len == CREDENTIAL_SALT_LEN &&
|
||||
credentials_b64_decode(snonce_b64, snonce, sizeof(snonce), &snonce_len) &&
|
||||
snonce_len == CREDENTIAL_NONCE_LEN && credentials_random_bytes(cnonce, sizeof(cnonce));
|
||||
credentials_burn(salt_b64, salt_b64 ? strlen(salt_b64) : 0);
|
||||
credentials_burn(snonce_b64, snonce_b64 ? strlen(snonce_b64) : 0);
|
||||
free(salt_b64);
|
||||
free(snonce_b64);
|
||||
if (!ok) {
|
||||
log_message(LOG_LEVEL_ERROR, "Daemon sent a malformed auth challenge");
|
||||
return false;
|
||||
}
|
||||
uint8_t client_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t stored_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t server_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t auth_msg[CREDENTIAL_AUTH_MESSAGE_MAX];
|
||||
size_t msg_len = 0;
|
||||
uint8_t proof[CREDENTIAL_KEY_LEN];
|
||||
uint8_t expected_sig[CREDENTIAL_KEY_LEN];
|
||||
ok = credentials_compute_keys(c->auth_password, salt, (uint32_t)iters, client_key, stored_key,
|
||||
server_key) &&
|
||||
credentials_build_auth_message(c->auth_user, snonce, cnonce, auth_msg, sizeof(auth_msg),
|
||||
&msg_len) &&
|
||||
credentials_client_proof(client_key, stored_key, server_key, auth_msg, msg_len, proof,
|
||||
expected_sig);
|
||||
char cnonce_b64[45];
|
||||
char proof_b64[45];
|
||||
if (ok)
|
||||
ok = credentials_b64_encode(cnonce, sizeof(cnonce), cnonce_b64, sizeof(cnonce_b64)) &&
|
||||
credentials_b64_encode(proof, sizeof(proof), proof_b64, sizeof(proof_b64));
|
||||
if (!ok) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to compute the daemon auth response");
|
||||
} else {
|
||||
ok = send_status(fd, STATUS_AUTH_RESPONSE) && send_str_redacted(fd, cnonce_b64) &&
|
||||
send_str_redacted(fd, proof_b64);
|
||||
}
|
||||
if (ok) {
|
||||
Status status = STATUS_ERROR;
|
||||
char* sig_b64 = NULL;
|
||||
uint8_t sig[CREDENTIAL_KEY_LEN];
|
||||
size_t sig_len = 0;
|
||||
ok = receive_status(fd, &status) && status == STATUS_AUTH_OK &&
|
||||
(sig_b64 = receive_str_redacted(fd)) != NULL &&
|
||||
credentials_b64_decode(sig_b64, sig, sizeof(sig), &sig_len) &&
|
||||
sig_len == CREDENTIAL_KEY_LEN &&
|
||||
credentials_secure_equal((const char*)sig, (const char*)expected_sig, CREDENTIAL_KEY_LEN);
|
||||
if (!ok)
|
||||
log_message(LOG_LEVEL_ERROR, "Daemon authentication failed");
|
||||
credentials_burn(sig_b64, sig_b64 ? strlen(sig_b64) : 0);
|
||||
credentials_burn((char*)sig, sizeof(sig));
|
||||
free(sig_b64);
|
||||
}
|
||||
credentials_burn((char*)client_key, sizeof(client_key));
|
||||
credentials_burn((char*)stored_key, sizeof(stored_key));
|
||||
credentials_burn((char*)server_key, sizeof(server_key));
|
||||
credentials_burn((char*)auth_msg, sizeof(auth_msg));
|
||||
credentials_burn((char*)proof, sizeof(proof));
|
||||
credentials_burn((char*)expected_sig, sizeof(expected_sig));
|
||||
credentials_burn((char*)salt, sizeof(salt));
|
||||
credentials_burn((char*)snonce, sizeof(snonce));
|
||||
credentials_burn((char*)cnonce, sizeof(cnonce));
|
||||
credentials_burn(cnonce_b64, sizeof(cnonce_b64));
|
||||
credentials_burn(proof_b64, sizeof(proof_b64));
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* --iconv CONVERT_SPEC (protocol 2.16.0). Trailing string on the config frame,
|
||||
* sent after the Wave A/B daemon-auth block and before the ack, so the
|
||||
* receiver knows the wire charset before the first file name arrives. The full
|
||||
@@ -1268,6 +1356,14 @@ bool config_send(int file_descriptor, const Config* config) {
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
return false;
|
||||
if (status == STATUS_AUTH_CHALLENGE) {
|
||||
/* Daemon auth (protocol 2.19.0): run the SCRAM exchange, then wait for the
|
||||
* ordinary STATUS_OK the server sends once authentication succeeded. */
|
||||
if (!client_auth_exchange(file_descriptor, config))
|
||||
return false;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
return false;
|
||||
}
|
||||
if (status != STATUS_OK) {
|
||||
log_message(LOG_LEVEL_ERROR, "Error transmitting config");
|
||||
return false;
|
||||
@@ -1329,9 +1425,14 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
if (rejection != NULL) {
|
||||
/* Daemon module gate (unknown module / read-only module / auth-required
|
||||
* module): refuse BEFORE the STATUS_OK so the client aborts at the
|
||||
* config handshake and no file data is ever exchanged. */
|
||||
fprintf(stderr, "%s\n", rejection);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
* config handshake and no file data is ever exchanged. The auth
|
||||
* handshake already sent STATUS_AUTH_FAILED when it failed, signalled by
|
||||
* the CONFIG_VALIDATE_ALREADY_TERMINATED sentinel, so no second status is
|
||||
* written. */
|
||||
if (rejection != CONFIG_VALIDATE_ALREADY_TERMINATED) {
|
||||
fprintf(stderr, "%s\n", rejection);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
}
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
+47
-19
@@ -96,19 +96,18 @@ typedef struct Config {
|
||||
* string so the daemon can look the module up in its own config and confine
|
||||
* the connection to the module's root (never a client-chosen root). */
|
||||
char* module;
|
||||
/* Daemon password authentication (Wave B, protocol 2.15.0, WITHIN the Wave A
|
||||
* frame layout -- see the PROTOCOL_VERSION note below for why this is not a
|
||||
* bump). Client-composed from a --password-file whose first meaningful line
|
||||
* is `user:password`: the client sends ONLY the username and a SHA-256 hex
|
||||
* digest of the password (auth_user + auth_password_hash), never the literal
|
||||
* password. Both are NULL when the client has no credentials to present; a
|
||||
* module WITHOUT `auth users` stays open and the server ignores any
|
||||
* credentials that do arrive (the client sends them opportunistically and
|
||||
* the server decides). */
|
||||
/* Daemon password authentication (A7 remediation, protocol 2.19.0).
|
||||
* Client-composed from a --password-file whose first meaningful line is
|
||||
* `user:password`: the client sends ONLY the username in the config frame
|
||||
* (auth_user); the literal password is kept in auth_password CLIENT-SIDE for
|
||||
* the duration of the SCRAM challenge/response and is NEVER serialized. Both
|
||||
* are NULL when the client has no credentials to present; a module WITHOUT
|
||||
* `auth users` stays open and the server ignores any credentials that do
|
||||
* arrive (the client sends them opportunistically and the server decides). */
|
||||
char* auth_user;
|
||||
char* auth_password_hash;
|
||||
char* auth_password;
|
||||
/* Client-only path of --password-file (never crosses the wire; it is read to
|
||||
* populate auth_user/auth_password_hash before connecting). */
|
||||
* populate auth_user/auth_password before connecting). */
|
||||
char* password_file;
|
||||
char* fastsync_server_path;
|
||||
/* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the
|
||||
@@ -546,8 +545,8 @@ typedef struct Config {
|
||||
* is what keeps a 2.15 client and a 2.14 server from ever reaching that state.
|
||||
*
|
||||
* NOTE: daemon module-selection bump owned by Wave A (2.15.0); later daemon
|
||||
* waves (auth, motd) must not bump PROTOCOL_VERSION. Wave B (auth) adds the
|
||||
* credential fields (auth_user/auth_password_hash) as further trailing
|
||||
* waves (auth, motd) must not bump PROTOCOL_VERSION. Wave B (auth) added the
|
||||
* credential fields (auth_user + password digest) as further trailing
|
||||
* config-frame strings AFTER the Wave A module string, with a presence int
|
||||
* prefix. This is not a new frame version: sender and receiver of a 2.15.0
|
||||
* build always read and write the same full layout (the strict same-version
|
||||
@@ -617,8 +616,22 @@ typedef struct Config {
|
||||
* (config_receive rejects a mismatched version before parsing anything else) is
|
||||
* what keeps a 2.18 client and a 2.17 server from ever reaching that state.
|
||||
* --super never elevates privileges; it only permits a confined attempt, and
|
||||
* --copy-as never switches process credentials (see RSYNC_COMPAT.md). */
|
||||
#define PROTOCOL_VERSION "2.18.0"
|
||||
* --copy-as never switches process credentials (see RSYNC_COMPAT.md).
|
||||
*
|
||||
* A7 Auth Wave: 2.18.0 -> 2.19.0.
|
||||
*
|
||||
* WHY the bump, grounded in the wire: the daemon auth block on the config frame
|
||||
* loses the hard-wired password digest (it becomes `[int present][str_redacted
|
||||
* username]`), and the frame stream gains the SCRAM challenge/response
|
||||
* (STATUS_AUTH_CHALLENGE -> STATUS_AUTH_RESPONSE -> STATUS_AUTH_OK) between the
|
||||
* config frame and the STATUS_OK ack. A 2.18 peer would desynchronize on both
|
||||
* the shorter auth block and the new status frames, so the strict same-version
|
||||
* handshake (config_receive rejects a mismatched version before parsing
|
||||
* anything else) is what keeps a 2.19 client and a 2.18 server from ever
|
||||
* reaching that state. SECURITY: a 2.19 store holds a salted PBKDF2 verifier
|
||||
* and cannot verify (and refuses to load) a legacy unsalted-SHA-256 store line,
|
||||
* so an old bearer digest can never be replayed against a 2.19 daemon. */
|
||||
#define PROTOCOL_VERSION "2.19.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
@@ -642,21 +655,36 @@ typedef struct Config {
|
||||
|
||||
Config* config_create(void);
|
||||
void config_delete(Config* config);
|
||||
|
||||
/* Wipe the client-side plaintext auth password (and username) from a Config
|
||||
* before it is freed or handed off. Safe on a NULL/empty Config and idempotent
|
||||
* (it clears the pointers after burning). config_delete calls this
|
||||
* automatically; a caller that drops a Config earlier may call it explicitly. */
|
||||
void config_burn_auth(Config* config);
|
||||
|
||||
bool config_send(int file_descriptor, const Config* config);
|
||||
Config* config_receive(int file_descriptor);
|
||||
bool config_is_remote_dest(const char* s);
|
||||
void config_parse_ssh_dest(Config* config);
|
||||
|
||||
/* A ConfigValidateFunc may return this sentinel to tell
|
||||
* config_receive_with_validate that the callback ALREADY sent a terminal status
|
||||
* frame (e.g. STATUS_AUTH_FAILED, then closed) and the frame must be abandoned
|
||||
* without an additional STATUS_ERROR. A normal rejection returns a message
|
||||
* string (logged, then STATUS_ERROR); NULL accepts. */
|
||||
#define CONFIG_VALIDATE_ALREADY_TERMINATED ((const char*)-1)
|
||||
|
||||
/* Server-side config-frame gate (daemon module selection, Wave A). A server
|
||||
* that needs to make an accept/reject decision about a received Config BEFORE
|
||||
* it sends the STATUS_OK ack (so a rejected connection is refused cleanly with
|
||||
* no data transferred) passes a callback here; it runs after the frame parses
|
||||
* and validates but before the STATUS_OK/STATUS_ERROR ack. Return NULL to
|
||||
* accept the connection; return a non-NULL message to reject it (the message
|
||||
* is logged server-side and STATUS_ERROR is sent in place of STATUS_OK). The
|
||||
* callback runs in the connection's own process, so it may set up per-module
|
||||
* process state (e.g. the authorized root). context is an opaque caller
|
||||
* pointer. */
|
||||
* is logged server-side and STATUS_ERROR is sent in place of STATUS_OK), or the
|
||||
* CONFIG_VALIDATE_ALREADY_TERMINATED sentinel when the callback already sent
|
||||
* its own terminal status. The callback runs in the connection's own process,
|
||||
* so it may set up per-module process state (e.g. the authorized root) and
|
||||
* drive the daemon auth handshake. context is an opaque caller pointer. */
|
||||
typedef const char* (*ConfigValidateFunc)(const Config* config, void* context);
|
||||
Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc validate,
|
||||
void* context);
|
||||
|
||||
+527
-108
@@ -3,7 +3,10 @@
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/params.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -12,11 +15,15 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* One store entry: a username and its password's SHA-256 hex digest. The
|
||||
* plaintext password never appears here (and never on the daemon host). */
|
||||
/* One store entry: a username and its salted PBKDF2 verifier. The plaintext
|
||||
* password never appears here (and never on the daemon host); the verifier is
|
||||
* not replayable because the proof is bound to a per-connection nonce. */
|
||||
typedef struct CredentialEntry {
|
||||
char* user;
|
||||
char* password_hex; /* CREDENTIAL_HASH_HEX_LEN lowercase hex chars */
|
||||
uint8_t salt[CREDENTIAL_SALT_LEN];
|
||||
uint32_t iters;
|
||||
uint8_t stored_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t server_key[CREDENTIAL_KEY_LEN];
|
||||
} CredentialEntry;
|
||||
|
||||
struct CredentialStore {
|
||||
@@ -25,6 +32,15 @@ struct CredentialStore {
|
||||
int capacity;
|
||||
};
|
||||
|
||||
/* Exact marker prefix of the new store verifier field. */
|
||||
#define CREDENTIAL_STORE_PREFIX "$fastsync$1$pbkdf2-sha256$"
|
||||
#define CREDENTIAL_AUTH_PREFIX "FastSync-Auth-v1"
|
||||
|
||||
/* Fixed dummy keys used when a user is unknown or off the module's list. They
|
||||
* can never authenticate because acceptance additionally requires found=true. */
|
||||
static const uint8_t k_dummy_stored_key[CREDENTIAL_KEY_LEN] = {0};
|
||||
static const uint8_t k_dummy_server_key[CREDENTIAL_KEY_LEN] = {0};
|
||||
|
||||
static void set_error(char* err, size_t err_size, const char* fmt, ...) {
|
||||
if (!err || err_size == 0)
|
||||
return;
|
||||
@@ -106,6 +122,10 @@ static bool username_wellformed(const char* user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool credentials_username_valid(const char* user) {
|
||||
return username_wellformed(user);
|
||||
}
|
||||
|
||||
static int hex_value(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
@@ -114,17 +134,235 @@ static int hex_value(char c) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool credentials_hash_valid(const char* hash_hex) {
|
||||
if (!hash_hex)
|
||||
/* True for the OLD `user:SHA256HEX` secret form: exactly 64 lowercase hex
|
||||
* digits. Such a line is refused loudly (and never accepted) so an operator
|
||||
* cannot keep a replayable bearer digest in place after the protocol bump. */
|
||||
static bool secret_is_legacy_hex(const char* s) {
|
||||
if (!s)
|
||||
return false;
|
||||
for (int i = 0; i < CREDENTIAL_HASH_HEX_LEN; i++) {
|
||||
if (hex_value(hash_hex[i]) < 0)
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (hex_value(s[i]) < 0)
|
||||
return false;
|
||||
}
|
||||
return hash_hex[CREDENTIAL_HASH_HEX_LEN] == '\0';
|
||||
return s[64] == '\0';
|
||||
}
|
||||
|
||||
static bool append_entry(CredentialStore* store, const char* user, const char* password_hex) {
|
||||
bool credentials_b64_encode(const uint8_t* in, size_t n, char* out, size_t out_sz) {
|
||||
if (!in || !out)
|
||||
return false;
|
||||
if (n > (size_t)INT_MAX)
|
||||
return false;
|
||||
size_t encoded_len = 4 * ((n + 2) / 3);
|
||||
if (out_sz < encoded_len + 1)
|
||||
return false;
|
||||
int written = EVP_EncodeBlock((unsigned char*)out, in, (int)n);
|
||||
if (written < 0 || (size_t)written != encoded_len)
|
||||
return false;
|
||||
out[encoded_len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
bool credentials_b64_decode(const char* in, uint8_t* out, size_t out_sz, size_t* out_len) {
|
||||
if (!in || !out || !out_len)
|
||||
return false;
|
||||
size_t len = strlen(in);
|
||||
/* Every value we decode is short (a 32-byte key is 44 chars); refusing long
|
||||
* input keeps the scratch buffer fixed and bounds a hostile frame. */
|
||||
if (len == 0 || (len % 4) != 0 || len > 256)
|
||||
return false;
|
||||
size_t padded_len = (len / 4) * 3;
|
||||
size_t decoded_len = padded_len;
|
||||
if (in[len - 1] == '=')
|
||||
decoded_len--;
|
||||
if (len >= 2 && in[len - 2] == '=')
|
||||
decoded_len--;
|
||||
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];
|
||||
int n = EVP_DecodeBlock(scratch, (const unsigned char*)in, (int)len);
|
||||
if (n < 0 || (size_t)n != padded_len)
|
||||
return false;
|
||||
memcpy(out, scratch, decoded_len);
|
||||
credentials_burn((char*)scratch, sizeof(scratch));
|
||||
*out_len = decoded_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool credentials_random_bytes(uint8_t* out, size_t n) {
|
||||
if (!out || n == 0 || n > (size_t)INT_MAX)
|
||||
return false;
|
||||
return RAND_bytes(out, (int)n) == 1;
|
||||
}
|
||||
|
||||
/* HMAC-SHA256 via the OpenSSL 3 EVP_MAC API (HMAC() is deprecated). */
|
||||
static bool hmac_sha256(const uint8_t* key, size_t key_len, const uint8_t* data, size_t data_len,
|
||||
uint8_t out[CREDENTIAL_KEY_LEN]) {
|
||||
EVP_MAC* mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
|
||||
if (!mac)
|
||||
return false;
|
||||
EVP_MAC_CTX* ctx = EVP_MAC_CTX_new(mac);
|
||||
EVP_MAC_free(mac);
|
||||
if (!ctx)
|
||||
return false;
|
||||
OSSL_PARAM params[2];
|
||||
params[0] = OSSL_PARAM_construct_utf8_string("digest", (char*)"SHA256", 0);
|
||||
params[1] = OSSL_PARAM_construct_end();
|
||||
size_t out_len = 0;
|
||||
bool ok =
|
||||
EVP_MAC_init(ctx, key, key_len, params) == 1 && EVP_MAC_update(ctx, data, data_len) == 1 &&
|
||||
EVP_MAC_final(ctx, out, &out_len, CREDENTIAL_KEY_LEN) == 1 && out_len == CREDENTIAL_KEY_LEN;
|
||||
EVP_MAC_CTX_free(ctx);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool sha256(const uint8_t* data, size_t len, uint8_t out[CREDENTIAL_KEY_LEN]) {
|
||||
unsigned int out_len = 0;
|
||||
if (EVP_Digest(data, len, out, &out_len, EVP_sha256(), NULL) != 1)
|
||||
return false;
|
||||
return out_len == CREDENTIAL_KEY_LEN;
|
||||
}
|
||||
|
||||
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],
|
||||
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)
|
||||
return false;
|
||||
size_t password_len = strlen(password);
|
||||
if (password_len > CREDENTIAL_MAX_PASSWORD_LEN || password_len > (size_t)INT_MAX)
|
||||
return false;
|
||||
uint8_t k[CREDENTIAL_KEY_LEN];
|
||||
if (PKCS5_PBKDF2_HMAC(password, (int)password_len, salt, CREDENTIAL_SALT_LEN, (int)iters,
|
||||
EVP_sha256(), CREDENTIAL_KEY_LEN, k) != 1) {
|
||||
credentials_burn((char*)k, sizeof(k));
|
||||
return false;
|
||||
}
|
||||
uint8_t derived_client[CREDENTIAL_KEY_LEN];
|
||||
uint8_t derived_server[CREDENTIAL_KEY_LEN];
|
||||
bool ok = hmac_sha256(k, sizeof(k), (const uint8_t*)"Client Key", 10, derived_client) &&
|
||||
hmac_sha256(k, sizeof(k), (const uint8_t*)"Server Key", 10, derived_server);
|
||||
if (ok && stored_key)
|
||||
ok = sha256(derived_client, sizeof(derived_client), stored_key);
|
||||
if (ok && client_key)
|
||||
memcpy(client_key, derived_client, CREDENTIAL_KEY_LEN);
|
||||
if (ok && server_key)
|
||||
memcpy(server_key, derived_server, CREDENTIAL_KEY_LEN);
|
||||
credentials_burn((char*)k, sizeof(k));
|
||||
credentials_burn((char*)derived_client, sizeof(derived_client));
|
||||
credentials_burn((char*)derived_server, sizeof(derived_server));
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void write_be32(uint8_t* out, uint32_t value) {
|
||||
out[0] = (uint8_t)(value >> 24);
|
||||
out[1] = (uint8_t)(value >> 16);
|
||||
out[2] = (uint8_t)(value >> 8);
|
||||
out[3] = (uint8_t)value;
|
||||
}
|
||||
|
||||
bool credentials_build_auth_message(const char* user, const uint8_t* snonce, const uint8_t* cnonce,
|
||||
uint8_t* out, size_t out_sz, size_t* out_len) {
|
||||
if (!user || !snonce || !cnonce || !out || !out_len)
|
||||
return false;
|
||||
size_t user_len = strlen(user);
|
||||
if (user_len > CREDENTIAL_MAX_USER_LEN)
|
||||
return false;
|
||||
size_t total = 16 + 4 + user_len + 4 + CREDENTIAL_NONCE_LEN + 4 + CREDENTIAL_NONCE_LEN;
|
||||
if (out_sz < total)
|
||||
return false;
|
||||
size_t off = 0;
|
||||
memcpy(out + off, CREDENTIAL_AUTH_PREFIX, 16);
|
||||
off += 16;
|
||||
write_be32(out + off, (uint32_t)user_len);
|
||||
off += 4;
|
||||
memcpy(out + off, user, user_len);
|
||||
off += user_len;
|
||||
write_be32(out + off, CREDENTIAL_NONCE_LEN);
|
||||
off += 4;
|
||||
memcpy(out + off, snonce, CREDENTIAL_NONCE_LEN);
|
||||
off += CREDENTIAL_NONCE_LEN;
|
||||
write_be32(out + off, CREDENTIAL_NONCE_LEN);
|
||||
off += 4;
|
||||
memcpy(out + off, cnonce, CREDENTIAL_NONCE_LEN);
|
||||
off += CREDENTIAL_NONCE_LEN;
|
||||
*out_len = off;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool credentials_client_proof(const uint8_t client_key[CREDENTIAL_KEY_LEN],
|
||||
const uint8_t stored_key[CREDENTIAL_KEY_LEN],
|
||||
const uint8_t server_key[CREDENTIAL_KEY_LEN], const uint8_t* auth_msg,
|
||||
size_t msg_len, uint8_t proof[CREDENTIAL_KEY_LEN],
|
||||
uint8_t server_sig[CREDENTIAL_KEY_LEN]) {
|
||||
if (!client_key || !stored_key || !server_key || !auth_msg || !proof || !server_sig)
|
||||
return false;
|
||||
uint8_t client_sig[CREDENTIAL_KEY_LEN];
|
||||
bool ok = hmac_sha256(stored_key, CREDENTIAL_KEY_LEN, auth_msg, msg_len, client_sig);
|
||||
if (ok) {
|
||||
for (size_t i = 0; i < CREDENTIAL_KEY_LEN; i++)
|
||||
proof[i] = client_key[i] ^ client_sig[i];
|
||||
ok = hmac_sha256(server_key, CREDENTIAL_KEY_LEN, auth_msg, msg_len, server_sig);
|
||||
}
|
||||
credentials_burn((char*)client_sig, sizeof(client_sig));
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool credentials_verify_response(const CredentialVerifier* v, const char* user,
|
||||
const uint8_t* snonce, const uint8_t* cnonce,
|
||||
const uint8_t proof[CREDENTIAL_KEY_LEN],
|
||||
uint8_t server_sig_out[CREDENTIAL_KEY_LEN]) {
|
||||
if (!v || !user || !snonce || !cnonce || !proof || !server_sig_out)
|
||||
return false;
|
||||
uint8_t auth_msg[CREDENTIAL_AUTH_MESSAGE_MAX];
|
||||
size_t msg_len = 0;
|
||||
if (!credentials_build_auth_message(user, snonce, cnonce, auth_msg, sizeof(auth_msg), &msg_len))
|
||||
return false;
|
||||
uint8_t client_sig[CREDENTIAL_KEY_LEN];
|
||||
uint8_t client_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t recovered[CREDENTIAL_KEY_LEN];
|
||||
uint8_t server_sig[CREDENTIAL_KEY_LEN];
|
||||
bool computed = hmac_sha256(v->stored_key, CREDENTIAL_KEY_LEN, auth_msg, msg_len, client_sig);
|
||||
if (computed) {
|
||||
for (size_t i = 0; i < CREDENTIAL_KEY_LEN; i++)
|
||||
client_key[i] = proof[i] ^ client_sig[i];
|
||||
computed = sha256(client_key, CREDENTIAL_KEY_LEN, recovered);
|
||||
}
|
||||
if (computed)
|
||||
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,
|
||||
CREDENTIAL_KEY_LEN);
|
||||
credentials_burn((char*)auth_msg, sizeof(auth_msg));
|
||||
credentials_burn((char*)client_sig, sizeof(client_sig));
|
||||
credentials_burn((char*)client_key, sizeof(client_key));
|
||||
credentials_burn((char*)recovered, sizeof(recovered));
|
||||
credentials_burn((char*)server_sig, sizeof(server_sig));
|
||||
return accept;
|
||||
}
|
||||
|
||||
static bool entries_equal(const CredentialEntry* a, const CredentialEntry* b) {
|
||||
return a->iters == b->iters &&
|
||||
credentials_secure_equal((const char*)a->salt, (const char*)b->salt,
|
||||
CREDENTIAL_SALT_LEN) &&
|
||||
credentials_secure_equal((const char*)a->stored_key, (const char*)b->stored_key,
|
||||
CREDENTIAL_KEY_LEN) &&
|
||||
credentials_secure_equal((const char*)a->server_key, (const char*)b->server_key,
|
||||
CREDENTIAL_KEY_LEN);
|
||||
}
|
||||
|
||||
static bool append_entry(CredentialStore* store, const char* user, const uint8_t* salt,
|
||||
uint32_t iters, const uint8_t* stored_key, const uint8_t* server_key) {
|
||||
if (store->count == store->capacity) {
|
||||
int new_capacity = store->capacity == 0 ? 8 : store->capacity * 2;
|
||||
CredentialEntry* grown =
|
||||
@@ -134,15 +372,15 @@ static bool append_entry(CredentialStore* store, const char* user, const char* p
|
||||
store->entries = grown;
|
||||
store->capacity = new_capacity;
|
||||
}
|
||||
store->entries[store->count].user = str_dup(user);
|
||||
store->entries[store->count].password_hex = str_dup(password_hex);
|
||||
if (!store->entries[store->count].user || !store->entries[store->count].password_hex) {
|
||||
free(store->entries[store->count].user);
|
||||
free(store->entries[store->count].password_hex);
|
||||
store->entries[store->count].user = NULL;
|
||||
store->entries[store->count].password_hex = NULL;
|
||||
CredentialEntry* entry = &store->entries[store->count];
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->user = str_dup(user);
|
||||
if (!entry->user)
|
||||
return false;
|
||||
}
|
||||
memcpy(entry->salt, salt, CREDENTIAL_SALT_LEN);
|
||||
entry->iters = iters;
|
||||
memcpy(entry->stored_key, stored_key, CREDENTIAL_KEY_LEN);
|
||||
memcpy(entry->server_key, server_key, CREDENTIAL_KEY_LEN);
|
||||
store->count++;
|
||||
return true;
|
||||
}
|
||||
@@ -155,9 +393,75 @@ static int find_user(const CredentialStore* store, const char* user) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Parse one credential store file (user:SHA256HEX per line) into a fresh
|
||||
* store. Duplicate usernames WITHIN one file are an error (ambiguous). A
|
||||
* NULL path yields an empty store. */
|
||||
/* Parse the new `$fastsync$1$pbkdf2-sha256$...` verifier field in place. */
|
||||
static bool parse_verifier_secret(char* secret, CredentialEntry* entry, const char* path,
|
||||
int line_no, const char* user, char* err, size_t err_size) {
|
||||
if (secret_is_legacy_hex(secret)) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' line %d: legacy unsalted SHA-256 secret for user '%s' is not "
|
||||
"accepted (protocol 2.19.0 uses a salted PBKDF2 verifier); regenerate the store "
|
||||
"with --hash-credentials",
|
||||
path, line_no, user);
|
||||
return false;
|
||||
}
|
||||
const char* prefix = CREDENTIAL_STORE_PREFIX;
|
||||
size_t prefix_len = strlen(prefix);
|
||||
if (strncmp(secret, prefix, prefix_len) != 0) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' line %d: expected a '%s...' verifier for user '%s' (regenerate "
|
||||
"a legacy line with --hash-credentials)",
|
||||
path, line_no, prefix, user);
|
||||
return false;
|
||||
}
|
||||
char* cursor = secret + prefix_len;
|
||||
const char* iters_str = cursor;
|
||||
char* sep = strchr(cursor, '$');
|
||||
if (!sep)
|
||||
goto malformed;
|
||||
*sep = '\0';
|
||||
const char* salt_str = sep + 1;
|
||||
sep = strchr(salt_str, '$');
|
||||
if (!sep)
|
||||
goto malformed;
|
||||
*sep = '\0';
|
||||
const char* stored_str = sep + 1;
|
||||
sep = strchr(stored_str, '$');
|
||||
if (!sep)
|
||||
goto malformed;
|
||||
*sep = '\0';
|
||||
const char* server_str = sep + 1;
|
||||
if (*iters_str == '\0' || *salt_str == '\0' || *stored_str == '\0' || *server_str == '\0')
|
||||
goto malformed;
|
||||
|
||||
char* end = NULL;
|
||||
unsigned long parsed = strtoul(iters_str, &end, 10);
|
||||
if (!end || *end != '\0' || parsed < CREDENTIAL_MIN_ITERS || parsed > CREDENTIAL_MAX_ITERS)
|
||||
goto malformed;
|
||||
entry->iters = (uint32_t)parsed;
|
||||
|
||||
size_t decoded = 0;
|
||||
if (!credentials_b64_decode(salt_str, entry->salt, CREDENTIAL_SALT_LEN, &decoded) ||
|
||||
decoded != CREDENTIAL_SALT_LEN)
|
||||
goto malformed;
|
||||
if (!credentials_b64_decode(stored_str, entry->stored_key, CREDENTIAL_KEY_LEN, &decoded) ||
|
||||
decoded != CREDENTIAL_KEY_LEN)
|
||||
goto malformed;
|
||||
if (!credentials_b64_decode(server_str, entry->server_key, CREDENTIAL_KEY_LEN, &decoded) ||
|
||||
decoded != CREDENTIAL_KEY_LEN)
|
||||
goto malformed;
|
||||
return true;
|
||||
|
||||
malformed:
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' line %d: malformed verifier for user '%s' (expected "
|
||||
"'%s<iters>$<salt_b64>$<stored_key_b64>$<server_key_b64>')",
|
||||
path, line_no, user, prefix);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Parse one credential store file into a fresh store. Duplicate usernames
|
||||
* WITHIN one file are an error (ambiguous). A NULL path yields an empty
|
||||
* store. */
|
||||
static CredentialStore* load_store_file(const char* path, char* err, size_t err_size) {
|
||||
CredentialStore* store = calloc(1, sizeof(CredentialStore));
|
||||
if (!store) {
|
||||
@@ -200,14 +504,14 @@ static CredentialStore* load_store_file(const char* path, char* err, size_t err_
|
||||
char* colon = strchr(cursor, ':');
|
||||
if (!colon) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' line %d: expected 'user:SHA256HEX' (no ':' found)", path,
|
||||
"credential file '%s' line %d: expected 'user:$fastsync$...' (no ':' found)", path,
|
||||
line_no);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
*colon = '\0';
|
||||
const char* user = trim_space(cursor);
|
||||
const char* secret = trim_space(colon + 1);
|
||||
char* secret = trim_space(colon + 1);
|
||||
if (!username_wellformed(user)) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' line %d: invalid username (must be 1-%d "
|
||||
@@ -216,11 +520,9 @@ static CredentialStore* load_store_file(const char* path, char* err, size_t err_
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (!credentials_hash_valid(secret)) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' line %d: secret for user '%s' must be %d "
|
||||
"lowercase hex characters (the SHA-256 of the password)",
|
||||
path, line_no, user, CREDENTIAL_HASH_HEX_LEN);
|
||||
CredentialEntry parsed;
|
||||
memset(&parsed, 0, sizeof(parsed));
|
||||
if (!parse_verifier_secret(secret, &parsed, path, line_no, user, err, err_size)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
@@ -230,7 +532,8 @@ static CredentialStore* load_store_file(const char* path, char* err, size_t err_
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (!append_entry(store, user, secret)) {
|
||||
if (!append_entry(store, user, parsed.salt, parsed.iters, parsed.stored_key,
|
||||
parsed.server_key)) {
|
||||
set_error(err, err_size, "out of memory reading credential file '%s'", path);
|
||||
ok = false;
|
||||
break;
|
||||
@@ -242,6 +545,7 @@ static CredentialStore* load_store_file(const char* path, char* err, size_t err_
|
||||
ok = false;
|
||||
}
|
||||
fclose(fp);
|
||||
credentials_burn(line, sizeof(line));
|
||||
if (!ok) {
|
||||
credentials_free(store);
|
||||
return NULL;
|
||||
@@ -264,14 +568,14 @@ CredentialStore* credentials_load(const char* password_file, const char* early_i
|
||||
credentials_free(store);
|
||||
return NULL;
|
||||
}
|
||||
/* Layer early input over the password file: same secret dedupes, a differing
|
||||
* secret for the same user is ambiguous and fails closed. */
|
||||
/* 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++) {
|
||||
int existing = find_user(store, early->entries[i].user);
|
||||
if (existing >= 0) {
|
||||
if (strcmp(store->entries[existing].password_hex, early->entries[i].password_hex) != 0) {
|
||||
if (!entries_equal(&store->entries[existing], &early->entries[i])) {
|
||||
set_error(err, err_size,
|
||||
"credential file '%s' and early-input file '%s' disagree on the secret for "
|
||||
"credential file '%s' and early-input file '%s' disagree on the verifier for "
|
||||
"user '%s'",
|
||||
password_file, early_input_file, early->entries[i].user);
|
||||
credentials_free(early);
|
||||
@@ -280,7 +584,9 @@ CredentialStore* credentials_load(const char* password_file, const char* early_i
|
||||
}
|
||||
continue; /* identical; nothing to merge */
|
||||
}
|
||||
if (!append_entry(store, early->entries[i].user, early->entries[i].password_hex)) {
|
||||
if (!append_entry(store, early->entries[i].user, early->entries[i].salt,
|
||||
early->entries[i].iters, early->entries[i].stored_key,
|
||||
early->entries[i].server_key)) {
|
||||
set_error(err, err_size, "out of memory merging early-input credentials");
|
||||
credentials_free(early);
|
||||
credentials_free(store);
|
||||
@@ -295,8 +601,11 @@ void credentials_free(CredentialStore* store) {
|
||||
if (!store)
|
||||
return;
|
||||
for (int i = 0; i < store->count; i++) {
|
||||
/* Wipe the derived keys before releasing the entry (A7-4). */
|
||||
credentials_burn((char*)store->entries[i].salt, CREDENTIAL_SALT_LEN);
|
||||
credentials_burn((char*)store->entries[i].stored_key, CREDENTIAL_KEY_LEN);
|
||||
credentials_burn((char*)store->entries[i].server_key, CREDENTIAL_KEY_LEN);
|
||||
free(store->entries[i].user);
|
||||
free(store->entries[i].password_hex);
|
||||
}
|
||||
free(store->entries);
|
||||
free(store);
|
||||
@@ -317,24 +626,197 @@ bool credentials_secure_equal(const char* a, const char* b, size_t len) {
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
bool credentials_hash_password(const char* password, char* out_hex) {
|
||||
if (!password || !out_hex)
|
||||
/* 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;
|
||||
uint8_t digest[EVP_MAX_MD_SIZE];
|
||||
unsigned int digest_len = 0;
|
||||
if (EVP_Digest(password, strlen(password), digest, &digest_len, EVP_sha256(), NULL) != 1)
|
||||
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;
|
||||
}
|
||||
|
||||
bool credentials_get_verifier(const CredentialStore* store, const char* user,
|
||||
const char* const* module_users, int n, CredentialVerifier* out) {
|
||||
if (!out)
|
||||
return false;
|
||||
if (digest_len != 32)
|
||||
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;
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (unsigned int i = 0; i < digest_len; i++) {
|
||||
out_hex[2 * i] = hex[digest[i] >> 4];
|
||||
out_hex[2 * i + 1] = hex[digest[i] & 0x0f];
|
||||
out->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;
|
||||
if (!store || !user || n < 0)
|
||||
return true;
|
||||
/* Module-list membership: constant-time full scan, no early break, so the
|
||||
* list is not a username-enumeration oracle. */
|
||||
bool on_list = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (module_users && module_users[i] && username_secure_equal(module_users[i], user))
|
||||
on_list = true;
|
||||
}
|
||||
if (!on_list)
|
||||
return true;
|
||||
/* Store lookup is also a constant-time full scan. */
|
||||
const CredentialEntry* match = NULL;
|
||||
for (int i = 0; i < store->count; i++) {
|
||||
if (username_secure_equal(store->entries[i].user, user))
|
||||
match = &store->entries[i];
|
||||
}
|
||||
if (match) {
|
||||
memcpy(out->salt, match->salt, CREDENTIAL_SALT_LEN);
|
||||
out->iters = match->iters;
|
||||
memcpy(out->stored_key, match->stored_key, CREDENTIAL_KEY_LEN);
|
||||
memcpy(out->server_key, match->server_key, CREDENTIAL_KEY_LEN);
|
||||
out->found = true;
|
||||
}
|
||||
out_hex[2 * digest_len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
bool credentials_hash_store_line(const char* user, const char* password, uint32_t iters, char* out,
|
||||
size_t out_sz, char* err, size_t err_size) {
|
||||
if (err && err_size)
|
||||
err[0] = '\0';
|
||||
if (!username_wellformed(user)) {
|
||||
set_error(err, err_size, "invalid username (1-%d non-whitespace characters)",
|
||||
CREDENTIAL_MAX_USER_LEN);
|
||||
return false;
|
||||
}
|
||||
if (!password || !out || out_sz == 0) {
|
||||
set_error(err, err_size, "missing password or output buffer");
|
||||
return false;
|
||||
}
|
||||
if (strlen(password) > CREDENTIAL_MAX_PASSWORD_LEN) {
|
||||
set_error(err, err_size, "password exceeds %d characters", CREDENTIAL_MAX_PASSWORD_LEN);
|
||||
return false;
|
||||
}
|
||||
if (iters < CREDENTIAL_MIN_ITERS || iters > CREDENTIAL_MAX_ITERS) {
|
||||
set_error(err, err_size, "iterations %u out of range [%u,%u]", iters, CREDENTIAL_MIN_ITERS,
|
||||
CREDENTIAL_MAX_ITERS);
|
||||
return false;
|
||||
}
|
||||
uint8_t salt[CREDENTIAL_SALT_LEN];
|
||||
uint8_t client_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t stored_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t server_key[CREDENTIAL_KEY_LEN];
|
||||
char salt_b64[25];
|
||||
char stored_b64[45];
|
||||
char server_b64[45];
|
||||
bool ok =
|
||||
credentials_random_bytes(salt, sizeof(salt)) &&
|
||||
credentials_compute_keys(password, salt, iters, client_key, stored_key, server_key) &&
|
||||
credentials_b64_encode(salt, sizeof(salt), salt_b64, sizeof(salt_b64)) &&
|
||||
credentials_b64_encode(stored_key, sizeof(stored_key), stored_b64, sizeof(stored_b64)) &&
|
||||
credentials_b64_encode(server_key, sizeof(server_key), server_b64, sizeof(server_b64));
|
||||
int written = -1;
|
||||
if (ok) {
|
||||
written = snprintf(out, out_sz, "%s:%s%u$%s$%s$%s", user, CREDENTIAL_STORE_PREFIX, iters,
|
||||
salt_b64, stored_b64, server_b64);
|
||||
}
|
||||
credentials_burn((char*)client_key, sizeof(client_key));
|
||||
credentials_burn((char*)stored_key, sizeof(stored_key));
|
||||
credentials_burn((char*)server_key, sizeof(server_key));
|
||||
credentials_burn((char*)salt, sizeof(salt));
|
||||
if (!ok)
|
||||
return false;
|
||||
if (written < 0 || (size_t)written >= out_sz) {
|
||||
set_error(err, err_size, "output buffer too small for the credential line");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int credentials_hash_file(const char* path, uint32_t iters, FILE* out, char* err, size_t err_size) {
|
||||
if (err && err_size)
|
||||
err[0] = '\0';
|
||||
if (!path || !out) {
|
||||
set_error(err, err_size, "missing plaintext file or output stream");
|
||||
return -1;
|
||||
}
|
||||
if (iters < CREDENTIAL_MIN_ITERS || iters > CREDENTIAL_MAX_ITERS) {
|
||||
set_error(err, err_size, "iterations %u out of range [%u,%u]", iters, CREDENTIAL_MIN_ITERS,
|
||||
CREDENTIAL_MAX_ITERS);
|
||||
return -1;
|
||||
}
|
||||
FILE* fp = secret_file_open(path, err, err_size);
|
||||
if (!fp)
|
||||
return -1;
|
||||
int line_no = 0;
|
||||
int result = 0;
|
||||
char line[CREDENTIAL_MAX_LINE + 2];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line_no++;
|
||||
size_t len = strlen(line);
|
||||
if (len == CREDENTIAL_MAX_LINE + 1 && line[len - 1] != '\n' && !feof(fp)) {
|
||||
set_error(err, err_size, "plaintext file '%s' line %d exceeds the %d-byte limit", path,
|
||||
line_no, CREDENTIAL_MAX_LINE);
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
|
||||
line[--len] = '\0';
|
||||
char* cursor = line;
|
||||
while (*cursor == ' ' || *cursor == '\t')
|
||||
cursor++;
|
||||
if (*cursor == '\0' || is_comment_char(*cursor))
|
||||
continue;
|
||||
char* colon = strchr(cursor, ':');
|
||||
if (!colon) {
|
||||
set_error(err, err_size, "plaintext file '%s' line %d: expected 'user:password'", path,
|
||||
line_no);
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
*colon = '\0';
|
||||
const char* user = trim_space(cursor);
|
||||
const char* password = colon + 1;
|
||||
if (!username_wellformed(user)) {
|
||||
set_error(err, err_size, "plaintext file '%s' line %d: invalid username", path, line_no);
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
if (*password == '\0') {
|
||||
set_error(err, err_size, "plaintext file '%s' line %d: empty password", path, line_no);
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
char store_line[CREDENTIAL_MAX_LINE];
|
||||
if (!credentials_hash_store_line(user, password, iters, store_line, sizeof(store_line), err,
|
||||
err_size)) {
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
if (fprintf(out, "%s\n", store_line) < 0) {
|
||||
set_error(err, err_size, "cannot write hashed credentials: %s", strerror(errno));
|
||||
credentials_burn(store_line, sizeof(store_line));
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
credentials_burn(store_line, sizeof(store_line));
|
||||
}
|
||||
if (result == 0 && ferror(fp)) {
|
||||
set_error(err, err_size, "error reading plaintext file '%s': %s", path, strerror(errno));
|
||||
result = -1;
|
||||
}
|
||||
credentials_burn(line, sizeof(line));
|
||||
fclose(fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
int credentials_read_secret_file(const char* path, char** user_out, char** password_out, char* err,
|
||||
size_t err_size) {
|
||||
if (user_out)
|
||||
@@ -447,66 +929,3 @@ void credentials_burn(char* secret, size_t len) {
|
||||
for (size_t i = 0; i < len; i++)
|
||||
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
|
||||
* authenticate because a real store entry is preferred when it exists. */
|
||||
static const char k_dummy_hash[CREDENTIAL_HASH_HEX_LEN + 1] =
|
||||
"0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
bool credentials_verify(const CredentialStore* store, const char* user,
|
||||
const char* presented_hash_hex) {
|
||||
if (!store || !user || !presented_hash_hex || !credentials_hash_valid(presented_hash_hex))
|
||||
return false;
|
||||
const char* stored = k_dummy_hash;
|
||||
for (int i = 0; i < store->count; i++) {
|
||||
/* 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);
|
||||
}
|
||||
|
||||
bool credentials_gate_allows(const CredentialStore* store, const char* const* module_users,
|
||||
int module_user_count, const char* presented_user,
|
||||
const char* presented_hash_hex) {
|
||||
if (!store || module_user_count < 0)
|
||||
return false; /* fail closed: an auth-required module without a store refuses */
|
||||
if (!presented_user || !presented_hash_hex)
|
||||
return false; /* no credentials presented */
|
||||
bool on_module_list = false;
|
||||
for (int i = 0; i < module_user_count; i++) {
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
if (!on_module_list)
|
||||
return false;
|
||||
return credentials_verify(store, presented_user, presented_hash_hex);
|
||||
}
|
||||
|
||||
+130
-71
@@ -3,46 +3,69 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* Daemon password authentication (Wave B).
|
||||
/* Daemon password authentication (A7 remediation, protocol 2.19.0).
|
||||
*
|
||||
* FastSync authenticates a daemon connection with a username plus a SHA-256
|
||||
* hex digest of that username's password. The digest is what crosses the
|
||||
* wire: a challenge-less credential exchange, so the literal password is never
|
||||
* transmitted (and never stored on the daemon host). A module that declares
|
||||
* `auth users` demands that the presented username is on its list AND that the
|
||||
* presented digest matches the credential store's entry for that username.
|
||||
* The digest comparison is constant-time; a module with `auth users` whose
|
||||
* store is missing/misconfigured fails CLOSED (never falls open).
|
||||
* FastSync authenticates a daemon connection with a SCRAM-SHA-256-style
|
||||
* challenge/response handshake. The daemon stores only a salted PBKDF2
|
||||
* verifier (never the password, and never a value that can be replayed as a
|
||||
* bearer credential): the client proves knowledge of the password against a
|
||||
* per-connection server nonce, and the server proves the same shared secret
|
||||
* back. See credentials.c for the exact derivation.
|
||||
*
|
||||
* Credential store format (server --password-file and --early-input): one
|
||||
* `user:SHA256HEX` entry per line. SHA256HEX is the lowercase hex SHA-256 of
|
||||
* the user's password -- the exact value a FastSync client transmits. Blank
|
||||
* lines and lines whose first non-space character is '#' or ';' are comments.
|
||||
* The parser is STRICT: a malformed line (no ':', an empty/whitespace user, a
|
||||
* secret that is not 64 lowercase hex chars, a line longer than
|
||||
* CREDENTIAL_MAX_LINE) fails the whole load so a typo can never silently
|
||||
* change who may log in.
|
||||
* Server credential store format (--password-file and --early-input): one line
|
||||
* per entry,
|
||||
* user:$fastsync$1$pbkdf2-sha256$<iters>$<salt_b64>$<stored_key_b64>$<server_key_b64>
|
||||
* with standard base64, a 16-byte salt and 32-byte keys, and iters in
|
||||
* [CREDENTIAL_MIN_ITERS, CREDENTIAL_MAX_ITERS]. Blank lines and lines whose
|
||||
* first non-space character is '#' or ';' are comments. The parser is STRICT:
|
||||
* a malformed line fails the whole load so a typo can never silently change who
|
||||
* may log in. A line holding the legacy (unsalted SHA-256 hex) secret is
|
||||
* hard-rejected with an actionable "legacy" error; there is no auto-upgrade.
|
||||
* Use `fastsync-server --hash-credentials` to generate new-format lines.
|
||||
*
|
||||
* Client --password-file format: the FIRST meaningful (non-comment, non-blank)
|
||||
* line is `user:password`, holding the literal password. The client hashes it
|
||||
* and sends only the digest; the file should be mode 0600 and readable only by
|
||||
* its owner.
|
||||
*/
|
||||
* line is `user:password`, holding the literal password. The client keeps it
|
||||
* only for the duration of the handshake and wipes it at teardown; the file
|
||||
* should be mode 0600 and readable only by its owner. */
|
||||
|
||||
/* Lowercase hex length of a SHA-256 digest (what travels on the wire and what
|
||||
* the server store holds). */
|
||||
#define CREDENTIAL_HASH_HEX_LEN 64
|
||||
/* Longest accepted credential-file line (excluding the trailing newline). */
|
||||
#define CREDENTIAL_MAX_LINE 4096
|
||||
/* Upper bound on a username in a credential file and on the wire. Kept well
|
||||
* below MAX_STRING_SIZE so a wire username can never exhaust anything. */
|
||||
#define CREDENTIAL_MAX_USER_LEN 256
|
||||
/* Upper bound on a client-file password (before hashing). */
|
||||
/* Upper bound on a client-file password (before derivation). */
|
||||
#define CREDENTIAL_MAX_PASSWORD_LEN 1024
|
||||
|
||||
/* SCRAM-SHA-256 parameters. Salt and client nonce sizes are fixed by the
|
||||
* shared-auth-message framing; keys are always 32 bytes (SHA-256). */
|
||||
#define CREDENTIAL_SALT_LEN 16
|
||||
#define CREDENTIAL_NONCE_LEN 32
|
||||
#define CREDENTIAL_KEY_LEN 32
|
||||
#define CREDENTIAL_DEFAULT_ITERS 600000u
|
||||
#define CREDENTIAL_MIN_ITERS 100000u
|
||||
#define CREDENTIAL_MAX_ITERS 10000000u
|
||||
/* Buffer size for the full AuthMessage (prefix + three length-prefixed fields).
|
||||
* Worst case: 16 + 4 + 256 + 4 + 32 + 4 + 32. */
|
||||
#define CREDENTIAL_AUTH_MESSAGE_MAX \
|
||||
(16 + 4 + CREDENTIAL_MAX_USER_LEN + 4 + CREDENTIAL_NONCE_LEN + 4 + CREDENTIAL_NONCE_LEN)
|
||||
|
||||
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. */
|
||||
typedef struct {
|
||||
uint8_t salt[CREDENTIAL_SALT_LEN];
|
||||
uint32_t iters;
|
||||
uint8_t stored_key[CREDENTIAL_KEY_LEN];
|
||||
uint8_t server_key[CREDENTIAL_KEY_LEN];
|
||||
bool found;
|
||||
} CredentialVerifier;
|
||||
|
||||
/* Load the daemon credential store.
|
||||
*
|
||||
* password_file and early_input_file are both NULL-or-path, matching the
|
||||
@@ -52,70 +75,106 @@ typedef struct CredentialStore CredentialStore;
|
||||
* 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
|
||||
* username whose secret matches is deduplicated; one whose secret differs is
|
||||
* an error (the two sources disagree), never a silent pick.
|
||||
* username whose verifier matches is deduplicated; one whose verifier differs
|
||||
* is an error (the two sources disagree), never a silent pick.
|
||||
*
|
||||
* The returned store is heap-owned; free it with credentials_free. */
|
||||
CredentialStore* credentials_load(const char* password_file, const char* early_input_file,
|
||||
char* err, size_t err_size);
|
||||
|
||||
/* Wipe every stored key/salt and free the store. */
|
||||
void credentials_free(CredentialStore* store);
|
||||
|
||||
/* True when `hash_hex` is exactly CREDENTIAL_HASH_HEX_LEN lowercase hex digits
|
||||
* (the wire/store digest form). Used to reject a malformed presented digest
|
||||
* before it reaches the comparison. */
|
||||
bool credentials_hash_valid(const char* hash_hex);
|
||||
/* True when `user` is a single bounded token free of whitespace/control bytes
|
||||
* (the rule applied to store users, client-file users and the module list). */
|
||||
bool credentials_username_valid(const char* user);
|
||||
|
||||
/* Compute the lowercase hex SHA-256 of `password` into out_hex, which must
|
||||
* hold at least CREDENTIAL_HASH_HEX_LEN + 1 bytes. Returns false on a NULL
|
||||
* password or a hashing failure. The output is NUL-terminated. */
|
||||
bool credentials_hash_password(const char* password, char* out_hex);
|
||||
/* Standard base64. encode writes NUL-terminated output to out (size out_sz).
|
||||
* decode writes the raw bytes to out (capacity out_sz) and stores the length;
|
||||
* the input must be a well-formed padded base64 string. Both return false on
|
||||
* NULL arguments, a bad character/length, or insufficient output space. */
|
||||
bool credentials_b64_encode(const uint8_t* in, size_t n, char* out, size_t out_sz);
|
||||
bool credentials_b64_decode(const char* in, uint8_t* out, size_t out_sz, size_t* out_len);
|
||||
|
||||
/* Fill out[0..n) from the CSPRNG (RAND_bytes). Returns false on failure. */
|
||||
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. */
|
||||
bool credentials_get_verifier(const CredentialStore* store, const char* user,
|
||||
const char* const* module_users, int n, CredentialVerifier* out);
|
||||
|
||||
/* Derive the SCRAM keys from a plaintext password:
|
||||
* 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. */
|
||||
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],
|
||||
uint8_t server_key[CREDENTIAL_KEY_LEN]);
|
||||
|
||||
/* Serialize the shared AuthMessage:
|
||||
* "FastSync-Auth-v1" || be32(len(user)) || user
|
||||
* || be32(32) || server_nonce
|
||||
* || be32(32) || client_nonce
|
||||
* out must hold at least CREDENTIAL_AUTH_MESSAGE_MAX bytes. *out_len receives
|
||||
* the number of bytes written. */
|
||||
bool credentials_build_auth_message(const char* user, const uint8_t* snonce, const uint8_t* cnonce,
|
||||
uint8_t* out, size_t out_sz, size_t* out_len);
|
||||
|
||||
/* Client side: ClientProof = ClientKey XOR HMAC(StoredKey, AuthMessage), and
|
||||
* the expected ServerSignature = HMAC(ServerKey, AuthMessage). */
|
||||
bool credentials_client_proof(const uint8_t client_key[CREDENTIAL_KEY_LEN],
|
||||
const uint8_t stored_key[CREDENTIAL_KEY_LEN],
|
||||
const uint8_t server_key[CREDENTIAL_KEY_LEN], const uint8_t* auth_msg,
|
||||
size_t msg_len, uint8_t proof[CREDENTIAL_KEY_LEN],
|
||||
uint8_t server_sig[CREDENTIAL_KEY_LEN]);
|
||||
|
||||
/* Server side: recompute ClientSig' = HMAC(StoredKey, AuthMessage) and
|
||||
* ClientKey' = proof XOR ClientSig', then accept iff v->found AND
|
||||
* SHA256(ClientKey') equals StoredKey (constant-time over the 32-byte keys).
|
||||
* Always computes server_sig_out = HMAC(ServerKey, AuthMessage). Returns the
|
||||
* accept decision. */
|
||||
bool credentials_verify_response(const CredentialVerifier* v, const char* user,
|
||||
const uint8_t* snonce, const uint8_t* cnonce,
|
||||
const uint8_t proof[CREDENTIAL_KEY_LEN],
|
||||
uint8_t server_sig_out[CREDENTIAL_KEY_LEN]);
|
||||
|
||||
/* Derive a new-format store line for `user`/`password` and write it (without a
|
||||
* trailing newline) into out. A random 16-byte salt is used. On failure err is
|
||||
* filled. Used by --hash-credentials and by tests. */
|
||||
bool credentials_hash_store_line(const char* user, const char* password, uint32_t iters, char* out,
|
||||
size_t out_sz, char* err, size_t err_size);
|
||||
|
||||
/* Read `user:password` lines from `path` (the same owner-only check as the
|
||||
* other secret files) and write one new-format store line per entry to `out`.
|
||||
* Blank/comment lines are skipped; a malformed line fails the whole run.
|
||||
* Returns 0 on success, -1 on error (err filled). Used by
|
||||
* `--hash-credentials`. */
|
||||
int credentials_hash_file(const char* path, uint32_t iters, FILE* out, char* err, size_t err_size);
|
||||
|
||||
/* Read the CLIENT-side secret file: the first meaningful line is
|
||||
* `user:password` (the literal password). *user_out and *password_out are
|
||||
* freshly allocated on success (password is plaintext -- the caller hashes it
|
||||
* and then burns/frees it); both are NULL on error. Returns 0 on success, -1
|
||||
* on failure (err filled: the path is named, never the credential itself).
|
||||
* Only the line's trailing CR/LF are stripped: the password's bytes are
|
||||
* otherwise preserved exactly, so a password with leading/trailing whitespace
|
||||
* (after the ':') is kept usable. The username is trimmed of surrounding
|
||||
* space/tabs. */
|
||||
* freshly allocated on success (password is plaintext -- the caller derives the
|
||||
* proof and then burns/frees it); both are NULL on error. Returns 0 on
|
||||
* success, -1 on failure (err filled: the path is named, never the credential
|
||||
* itself). Only the line's trailing CR/LF are stripped: the password's bytes
|
||||
* are otherwise preserved exactly, so a password with leading/trailing
|
||||
* whitespace (after the ':') is kept usable. The username is trimmed of
|
||||
* surrounding space/tabs. */
|
||||
int credentials_read_secret_file(const char* path, char** user_out, char** password_out, char* err,
|
||||
size_t err_size);
|
||||
|
||||
/* Constant-time equality over exactly len bytes. Returns true when the two
|
||||
* buffers match. No early exit: the whole length is always scanned, so a
|
||||
* timing side-channel cannot reveal how many leading bytes matched. */
|
||||
/* Constant-time equality over exactly len bytes. */
|
||||
bool credentials_secure_equal(const char* a, const char* b, size_t len);
|
||||
|
||||
/* Overwrite secret[0..len) with zeros (best-effort wipe of a plaintext
|
||||
* password that is about to be freed). */
|
||||
/* Overwrite secret[0..len) with zeros (best-effort wipe). */
|
||||
void credentials_burn(char* secret, size_t len);
|
||||
|
||||
/* Verify a presented (user, digest) against the store. Returns true only when
|
||||
* the store holds an entry for `user` whose stored digest equals the presented
|
||||
* one. A NULL store, NULL user/digest, unknown user and wrong digest all
|
||||
* return false. The digest comparison runs over a fixed dummy whenever the
|
||||
* user is absent, and the username lookup is a single constant-time
|
||||
* full-length compare (no byte-wise early exit), so neither "unknown user" vs
|
||||
* "wrong password" nor a username prefix match can be distinguished by timing
|
||||
* (no user-enumeration oracle in the comparison path). */
|
||||
bool credentials_verify(const CredentialStore* store, const char* user,
|
||||
const char* presented_hash_hex);
|
||||
|
||||
/* The daemon's per-module auth decision, in one pure, unit-testable function.
|
||||
* `module_users`/`module_user_count` are the module's `auth users` list; a
|
||||
* module that declares auth users requires the presented user to be ON that
|
||||
* list AND to verify against the store. Returns false (fail closed) when the
|
||||
* store is NULL, when no credential was presented, when the user is not on the
|
||||
* module's list, or when verification fails. This is the single decision the
|
||||
* server_module_gate seam applies to an auth-required module. Like
|
||||
* credentials_verify, username matches here use a constant-time full-length
|
||||
* compare rather than a byte-wise-short-circuiting strcmp. */
|
||||
bool credentials_gate_allows(const CredentialStore* store, const char* const* module_users,
|
||||
int module_user_count, const char* presented_user,
|
||||
const char* presented_hash_hex);
|
||||
|
||||
/* Number of entries currently in the store (tests/introspection). */
|
||||
int credentials_store_size(const CredentialStore* store);
|
||||
|
||||
|
||||
@@ -413,6 +413,14 @@ static const char* status_to_string(Status status) {
|
||||
return "SPECIAL";
|
||||
case STATUS_DIR_TIMES:
|
||||
return "DIR_TIMES";
|
||||
case STATUS_AUTH_CHALLENGE:
|
||||
return "AUTH_CHALLENGE";
|
||||
case STATUS_AUTH_RESPONSE:
|
||||
return "AUTH_RESPONSE";
|
||||
case STATUS_AUTH_OK:
|
||||
return "AUTH_OK";
|
||||
case STATUS_AUTH_FAILED:
|
||||
return "AUTH_FAILED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
+14
-1
@@ -113,7 +113,20 @@ enum NET_STATUS {
|
||||
* than MAX_MANIFEST_ENTRIES is split across repeated frames. The receiver
|
||||
* defers the actual utimensat until its own delete/publish phase has
|
||||
* committed, then skips the whole set when -O/--omit-dir-times is set. */
|
||||
STATUS_DIR_TIMES
|
||||
STATUS_DIR_TIMES,
|
||||
/* Daemon SCRAM-SHA-256 authentication (A7 remediation, protocol 2.19.0).
|
||||
* STATUS_AUTH_CHALLENGE: the server requires auth and is about to send the
|
||||
* iteration count, the base64 salt and the base64 server nonce.
|
||||
* STATUS_AUTH_RESPONSE: the client's reply, followed by the base64 client
|
||||
* nonce and the base64 ClientProof. STATUS_AUTH_OK: the client proof
|
||||
* verified, followed by the base64 ServerSignature. STATUS_AUTH_FAILED:
|
||||
* a single generic refusal (unknown user, off-list user, wrong proof,
|
||||
* missing/malformed credentials) after which the server closes without
|
||||
* writing any data. */
|
||||
STATUS_AUTH_CHALLENGE,
|
||||
STATUS_AUTH_RESPONSE,
|
||||
STATUS_AUTH_OK,
|
||||
STATUS_AUTH_FAILED
|
||||
};
|
||||
|
||||
void io_set_fds(int read_fd, int write_fd);
|
||||
|
||||
Reference in New Issue
Block a user