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:
@@ -516,9 +516,13 @@ Client and server versions must currently match exactly.
|
|||||||
|
|
||||||
Daemon modules that declare `auth users` authenticate with a SCRAM-SHA-256-style
|
Daemon modules that declare `auth users` authenticate with a SCRAM-SHA-256-style
|
||||||
challenge/response against a salted PBKDF2 verifier store: no password and no
|
challenge/response against a salted PBKDF2 verifier store: no password and no
|
||||||
replayable bearer credential crosses the wire or is stored on the daemon. Store
|
replayable bearer credential crosses the wire or is stored on the daemon. All
|
||||||
lines are generated with `fastsync-server --hash-credentials <plaintext-file>`
|
store entries share one iteration count, and an unknown user is answered with a
|
||||||
(see `RSYNC_COMPAT.md`); legacy `user:SHA256HEX` stores are rejected.
|
deterministic per-username dummy challenge, so probing the daemon cannot
|
||||||
|
enumerate users. Store lines are generated with
|
||||||
|
`fastsync-server --hash-credentials <plaintext-file>` (see `RSYNC_COMPAT.md`);
|
||||||
|
redirect that output to an owner-only (mode 0600) file, and note that legacy
|
||||||
|
`user:SHA256HEX` stores are rejected.
|
||||||
|
|
||||||
TLS provides encrypted TCP transport. Supplying `--ca` enables certificate
|
TLS provides encrypted TCP transport. Supplying `--ca` enables certificate
|
||||||
verification; without it, traffic is encrypted but peer identity is not
|
verification; without it, traffic is encrypted but peer identity is not
|
||||||
|
|||||||
+2
-2
@@ -639,8 +639,8 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
|||||||
- **Module selection & confinement:** the client requests a module with an rsync-style `host::module[/path]` destination. The module name crosses the wire as a trailing string on the config frame (bumping `PROTOCOL_VERSION` 2.14.0 → 2.15.0; the bump is required because the config-frame layout changed and the strict same-version handshake is what prevents a peer from desynchronizing on the new trailing field). The daemon looks the module up in ITS OWN config and uses the module's `path` as the authorized root through the exact same `configure_authorization` confinement the standalone server applies to `--destination-root` (`file_open_secure_parent`, `has_path_traversal`, `path_is_within`); the client never supplies the root, every client-chosen-ownership/super-user request is refused unless the module declares `client owner = yes` (the daemon's per-module opt-in, see below), and the operator `--no-super` veto forces super-user activities off for every daemon connection. The client's `/path` part is relative inside the module and is rejected if absolute or if it contains `..`. Unknown modules are refused before any data moves (the run fails cleanly at the config handshake). An absolute destination and a module request against a non-daemon server are also refused.
|
- **Module selection & confinement:** the client requests a module with an rsync-style `host::module[/path]` destination. The module name crosses the wire as a trailing string on the config frame (bumping `PROTOCOL_VERSION` 2.14.0 → 2.15.0; the bump is required because the config-frame layout changed and the strict same-version handshake is what prevents a peer from desynchronizing on the new trailing field). The daemon looks the module up in ITS OWN config and uses the module's `path` as the authorized root through the exact same `configure_authorization` confinement the standalone server applies to `--destination-root` (`file_open_secure_parent`, `has_path_traversal`, `path_is_within`); the client never supplies the root, every client-chosen-ownership/super-user request is refused unless the module declares `client owner = yes` (the daemon's per-module opt-in, see below), and the operator `--no-super` veto forces super-user activities off for every daemon connection. The client's `/path` part is relative inside the module and is rejected if absolute or if it contains `..`. Unknown modules are refused before any data moves (the run fails cleanly at the config handshake). An absolute destination and a module request against a non-daemon server are also refused.
|
||||||
- **`client owner` (client-chosen-ownership opt-in):** by default a daemon module refuses every request that would let the client pick an owner or ask for super-user activities — `--numeric-ids`, `--chown`, `--usermap`/`--groupmap`, `--fake-super`, `--copy-as`, and an explicit `--super` — at the config handshake (before `STATUS_OK`), because a daemon has no per-module opt-in for client-chosen ownership and any anonymous client could otherwise force arbitrary owner ids inside the module root. `client owner = yes` opts a single module in, allowing those requests within that module's root (the standalone listener and the SSH `--stdio` server always honor them for their single operator-authorized root). Without the opt-in the daemon also forces super-user **device** activity off for that connection — char/block device-node creation (`--devices`) and `--write-devices` — even under the default `AUTO` mode, so a non-opted module can never be made to `mknod` or write a raw device; those entries are skipped (not refused) so an ordinary `-a` push still succeeds without device nodes. The opt-in does **not** lift the privilege requirement: `--copy-as` still needs a root receiver, and the operator `--no-super` veto still forces super-user activities off for every connection. The daemon logs a prominent startup warning for each `client owner = yes` module so the operator's deliberate choice is visible.
|
- **`client owner` (client-chosen-ownership opt-in):** by default a daemon module refuses every request that would let the client pick an owner or ask for super-user activities — `--numeric-ids`, `--chown`, `--usermap`/`--groupmap`, `--fake-super`, `--copy-as`, and an explicit `--super` — at the config handshake (before `STATUS_OK`), because a daemon has no per-module opt-in for client-chosen ownership and any anonymous client could otherwise force arbitrary owner ids inside the module root. `client owner = yes` opts a single module in, allowing those requests within that module's root (the standalone listener and the SSH `--stdio` server always honor them for their single operator-authorized root). Without the opt-in the daemon also forces super-user **device** activity off for that connection — char/block device-node creation (`--devices`) and `--write-devices` — even under the default `AUTO` mode, so a non-opted module can never be made to `mknod` or write a raw device; those entries are skipped (not refused) so an ordinary `-a` push still succeeds without device nodes. The opt-in does **not** lift the privilege requirement: `--copy-as` still needs a root receiver, and the operator `--no-super` veto still forces super-user activities off for every connection. The daemon logs a prominent startup warning for each `client owner = yes` module so the operator's deliberate choice is visible.
|
||||||
- **`read only` safe default:** every network transfer FastSync currently supports is a push that writes under the module root, so a `read only` module refuses the connection (clear server log "module is read only"; the client exits non-zero, nothing is transferred). A future pull/list operation can be opened up when it exists; the knob is already stored.
|
- **`read only` safe default:** every network transfer FastSync currently supports is a push that writes under the module root, so a `read only` module refuses the connection (clear server log "module is read only"; the client exits non-zero, nothing is transferred). A future pull/list operation can be opened up when it exists; the knob is already stored.
|
||||||
- **`auth users` (A7 SCRAM-SHA-256 authentication):** a module that declares `auth users` requires the client to present credentials. The config frame carries ONLY the username; the daemon answers an auth-required module with `STATUS_AUTH_CHALLENGE` (PBKDF2 iteration count, 16-byte salt, 32-byte server nonce), the client answers with `STATUS_AUTH_RESPONSE` (fresh 32-byte client nonce + a 32-byte ClientProof), and the daemon accepts only when the proof verifies **and** the username is **on the module's `auth users` list** and has a store entry, replying `STATUS_AUTH_OK` with a 32-byte ServerSignature the client verifies before proceeding. Verification is constant-time over fixed 32-byte keys, username membership uses a constant-time full-length scan, and an unknown/off-list user still receives a challenge and runs the same math against a dummy verifier (fresh random salt, default iterations, dummy keys), so there is no user-enumeration or timing oracle. The daemon logs the username but **never the password, proof or keys**. A module WITHOUT `auth users` stays open (legitimate rsync configuration); credentials sent to such a module are ignored. Read-only is orthogonal: even a correctly authenticated push to a `read only` module is still refused (all FastSync network transfers write). Fail-closed policy: a daemon whose config declares `auth users` on any module refuses to start unless a credential store was given (`--password-file` and/or `--early-input`); a missing or empty store is never silently treated as "open". A failed handshake (missing credentials, unknown/off-list user, wrong proof or malformed data) yields a single generic `STATUS_AUTH_FAILED` and the daemon closes before any data moves.
|
- **`auth users` (A7 SCRAM-SHA-256 authentication):** a module that declares `auth users` requires the client to present credentials. The config frame carries ONLY the username; the daemon answers an auth-required module with `STATUS_AUTH_CHALLENGE` (PBKDF2 iteration count, 16-byte salt, 32-byte server nonce), the client answers with `STATUS_AUTH_RESPONSE` (fresh 32-byte client nonce + a 32-byte ClientProof), and the daemon accepts only when the proof verifies **and** the username is **on the module's `auth users` list** and has a store entry, replying `STATUS_AUTH_OK` with a 32-byte ServerSignature the client verifies before proceeding. Verification is constant-time over fixed 32-byte keys (the compare runs even for a miss), username membership uses a constant-time full-length scan, and an unknown/off-list user still receives a challenge and runs the same math against a dummy verifier: a deterministic per-username salt (`HMAC-SHA256(store dummy key, username)`), the store-wide uniform iteration count and dummy keys. Re-probing the same unknown username therefore yields an identical salt and iteration count while a different username yields a different salt, so there is no user-enumeration or timing oracle. The daemon logs the username but **never the password, proof or keys**. A module WITHOUT `auth users` stays open (legitimate rsync configuration); credentials sent to such a module are ignored. Read-only is orthogonal: even a correctly authenticated push to a `read only` module is still refused (all FastSync network transfers write). Fail-closed policy: a daemon whose config declares `auth users` on any module refuses to start unless a credential store was given (`--password-file` and/or `--early-input`); a missing or empty store is never silently treated as "open". A failed handshake (missing credentials, unknown/off-list user, wrong proof or malformed data) yields a single generic `STATUS_AUTH_FAILED` and the daemon closes before any data moves.
|
||||||
- **Credential store format:** server `--password-file`/`--early-input` files are line-based `user:$fastsync$1$pbkdf2-sha256$<iters>$<salt_b64>$<stored_key_b64>$<server_key_b64>`, one per line (standard base64; 16-byte salt, 32-byte keys; `iters` in `[100000, 10000000]`, default 600000). Generate lines with `fastsync-server --hash-credentials FILE [--iterations N]`. Blank lines and lines starting with `#`/`;` are comments; the parser is strict (a malformed line fails the whole load, so a typo can never let a different set of users in). **The legacy `user:SHA256HEX` form is hard-rejected** with an actionable "legacy" error; there is no auto-upgrade, so a replayable bearer digest can never be loaded by a 2.19.0 daemon. The client `--password-file` holds `user:password` on its first meaningful line (the literal password, used only for the handshake then burned); keep both files readable only by their owner (mode 0600). Per-username wire length is bounded (256 chars) and every decoded salt/key length is validated.
|
- **Credential store format:** server `--password-file`/`--early-input` files are line-based `user:$fastsync$1$pbkdf2-sha256$<iters>$<salt_b64>$<stored_key_b64>$<server_key_b64>`, one per line (standard base64; 16-byte salt, 32-byte keys; `iters` in `[100000, 10000000]`, default 600000). Every entry in the resulting store must agree on `iters` (a store whose entries disagree, or where a layered `--early-input` disagrees with `--password-file`, is rejected). Generate lines with `fastsync-server --hash-credentials FILE [--iterations N]`; the emitted lines are secret material, so redirect them to an owner-only (mode 0600) file (the tool warns on stderr if stdout is a group/other-accessible regular file). Blank lines and lines starting with `#`/`;` are comments; the parser is strict (a malformed line fails the whole load, so a typo can never let a different set of users in). **The legacy `user:SHA256HEX` form is hard-rejected** with an actionable "legacy" error; there is no auto-upgrade, so a replayable bearer digest can never be loaded by a 2.19.0 daemon. The client `--password-file` holds `user:password` on its first meaningful line (the literal password, used only for the handshake then burned); keep both files readable only by their owner (mode 0600). Per-username wire length is bounded (256 chars) and every decoded salt/key length is validated.
|
||||||
- **Plaintext caveat:** over a plaintext (non-TLS) daemon a sniffer can read the challenge and response and mount an **offline dictionary attack** against a weak password, so use `--tls` for any real deployment. Unlike the old challenge-less exchange there is **no replay**: the proof is bound to the fresh per-connection server nonce, so a captured `STATUS_AUTH_RESPONSE` cannot be reused on another connection (an integration test proxies the daemon and proves this). The daemon logs a warning when an auth-required module is reached over plaintext. TLS client-CN (`--client-cn`) is an independent transport identity check and composes with password auth: both may be required on the same connection.
|
- **Plaintext caveat:** over a plaintext (non-TLS) daemon a sniffer can read the challenge and response and mount an **offline dictionary attack** against a weak password, so use `--tls` for any real deployment. Unlike the old challenge-less exchange there is **no replay**: the proof is bound to the fresh per-connection server nonce, so a captured `STATUS_AUTH_RESPONSE` cannot be reused on another connection (an integration test proxies the daemon and proves this). The daemon logs a warning when an auth-required module is reached over plaintext. TLS client-CN (`--client-cn`) is an independent transport identity check and composes with password auth: both may be required on the same connection.
|
||||||
- **Wire/protocol:** the config-frame auth block is now `[int present][str_redacted username]` (the old digest field is gone), and the frame stream gains the challenge/response (`STATUS_AUTH_CHALLENGE` → `STATUS_AUTH_RESPONSE` → `STATUS_AUTH_OK`/`STATUS_AUTH_FAILED`) between the config frame and the `STATUS_OK` ack. Both are wire-layout changes, so `PROTOCOL_VERSION` is bumped **2.18.0 → 2.19.0** (see the A7 note in `src/shared/config.h`); the strict same-version handshake keeps a 2.19 client and a 2.18 server from desynchronizing.
|
- **Wire/protocol:** the config-frame auth block is now `[int present][str_redacted username]` (the old digest field is gone), and the frame stream gains the challenge/response (`STATUS_AUTH_CHALLENGE` → `STATUS_AUTH_RESPONSE` → `STATUS_AUTH_OK`/`STATUS_AUTH_FAILED`) between the config frame and the `STATUS_OK` ack. Both are wire-layout changes, so `PROTOCOL_VERSION` is bumped **2.18.0 → 2.19.0** (see the A7 note in `src/shared/config.h`); the strict same-version handshake keeps a 2.19 client and a 2.18 server from desynchronizing.
|
||||||
- **Client side:** `host::module/path` selects the TCP transport and connects to `--server-port`; `host:path` stays the SSH transport; plain paths stay local TCP. The daemon username comes from `--password-file` (first `user:password` line), and `--password-file` without a `host::module/path` destination is a client error (fail fast). A `user@host::module` form is rejected with a pointer to `--password-file`. The client's plaintext password is wiped from memory (`config_burn_auth`) at transfer teardown.
|
- **Client side:** `host::module/path` selects the TCP transport and connects to `--server-port`; `host:path` stays the SSH transport; plain paths stay local TCP. The daemon username comes from `--password-file` (first `user:password` line), and `--password-file` without a `host::module/path` destination is a client error (fail fast). A `user@host::module` form is rejected with a pointer to `--password-file`. The client's plaintext password is wiped from memory (`config_burn_auth`) at transfer teardown.
|
||||||
|
|||||||
+61
-47
@@ -73,65 +73,68 @@ typedef struct ModuleGateContext {
|
|||||||
* 2.19.0). Sends STATUS_AUTH_CHALLENGE (iteration count, base64 salt, base64
|
* 2.19.0). Sends STATUS_AUTH_CHALLENGE (iteration count, base64 salt, base64
|
||||||
* server nonce), expects STATUS_AUTH_RESPONSE (base64 client nonce, base64
|
* server nonce), expects STATUS_AUTH_RESPONSE (base64 client nonce, base64
|
||||||
* ClientProof), verifies the proof constant-time and answers STATUS_AUTH_OK
|
* 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
|
* 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
|
* user is a dummy (deterministic per-username salt, store-wide iterations, dummy
|
||||||
* and no user-enumeration/timing oracle is exposed. */
|
* 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) {
|
static bool server_auth_handshake(int fd, const Config* config, const DaemonModule* module) {
|
||||||
if (!config->auth_user) {
|
bool result = false;
|
||||||
send_status(fd, STATUS_AUTH_FAILED);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
CredentialVerifier verifier;
|
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,
|
if (!credentials_get_verifier(g_credentials, config->auth_user,
|
||||||
(const char* const*)module->auth_users, module->auth_user_count,
|
(const char* const*)module->auth_users, module->auth_user_count,
|
||||||
&verifier))
|
&verifier))
|
||||||
return false;
|
goto fail; /* a crypto failure still owes the gate a terminal frame */
|
||||||
uint8_t snonce[CREDENTIAL_NONCE_LEN];
|
if (!(credentials_random_bytes(snonce, sizeof(snonce)) &&
|
||||||
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(verifier.salt, CREDENTIAL_SALT_LEN, salt_b64, sizeof(salt_b64)) &&
|
||||||
credentials_b64_encode(snonce, sizeof(snonce), snonce_b64, sizeof(snonce_b64));
|
credentials_b64_encode(snonce, sizeof(snonce), snonce_b64, sizeof(snonce_b64))))
|
||||||
if (!ok) {
|
goto fail;
|
||||||
send_status(fd, STATUS_AUTH_FAILED);
|
if (!(send_status(fd, STATUS_AUTH_CHALLENGE) && send_int(fd, (int)verifier.iters) &&
|
||||||
return false;
|
send_str(fd, salt_b64) && send_str(fd, snonce_b64)))
|
||||||
}
|
goto fail;
|
||||||
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;
|
Status status = STATUS_ERROR;
|
||||||
char* cnonce_b64 = NULL;
|
if (!(receive_status(fd, &status) && status == STATUS_AUTH_RESPONSE))
|
||||||
char* proof_b64 = NULL;
|
goto fail;
|
||||||
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);
|
cnonce_b64 = receive_str_redacted(fd);
|
||||||
proof_b64 = receive_str_redacted(fd);
|
proof_b64 = receive_str_redacted(fd);
|
||||||
ok = cnonce_b64 && proof_b64 &&
|
if (!(cnonce_b64 && proof_b64 &&
|
||||||
credentials_b64_decode(cnonce_b64, cnonce, sizeof(cnonce), &cnonce_len) &&
|
credentials_b64_decode(cnonce_b64, cnonce, sizeof(cnonce), &cnonce_len) &&
|
||||||
cnonce_len == CREDENTIAL_NONCE_LEN &&
|
cnonce_len == CREDENTIAL_NONCE_LEN &&
|
||||||
credentials_b64_decode(proof_b64, proof, sizeof(proof), &proof_len) &&
|
credentials_b64_decode(proof_b64, proof, sizeof(proof), &proof_len) &&
|
||||||
proof_len == CREDENTIAL_KEY_LEN;
|
proof_len == CREDENTIAL_KEY_LEN))
|
||||||
}
|
goto fail;
|
||||||
verified = ok && credentials_verify_response(&verifier, config->auth_user, snonce, cnonce,
|
if (!credentials_verify_response(&verifier, config->auth_user, snonce, cnonce, proof, server_sig))
|
||||||
proof, server_sig);
|
goto fail;
|
||||||
}
|
|
||||||
if (verified) {
|
/* Success writes exactly one terminal frame (STATUS_AUTH_OK). A broken pipe
|
||||||
char sig_b64[45];
|
* while sending the signature just drops the connection; it must never emit a
|
||||||
ok = credentials_b64_encode(server_sig, sizeof(server_sig), sig_b64, sizeof(sig_b64)) &&
|
* 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);
|
send_status(fd, STATUS_AUTH_OK) && send_str_redacted(fd, sig_b64);
|
||||||
credentials_burn(sig_b64, sizeof(sig_b64));
|
goto cleanup;
|
||||||
} else {
|
|
||||||
|
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);
|
send_status(fd, STATUS_AUTH_FAILED);
|
||||||
ok = false;
|
|
||||||
}
|
cleanup:
|
||||||
credentials_burn(cnonce_b64, cnonce_b64 ? strlen(cnonce_b64) : 0);
|
credentials_burn(cnonce_b64, cnonce_b64 ? strlen(cnonce_b64) : 0);
|
||||||
credentials_burn(proof_b64, proof_b64 ? strlen(proof_b64) : 0);
|
credentials_burn(proof_b64, proof_b64 ? strlen(proof_b64) : 0);
|
||||||
free(cnonce_b64);
|
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*)cnonce, sizeof(cnonce));
|
||||||
credentials_burn((char*)proof, sizeof(proof));
|
credentials_burn((char*)proof, sizeof(proof));
|
||||||
credentials_burn((char*)server_sig, sizeof(server_sig));
|
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.salt, sizeof(verifier.salt));
|
||||||
credentials_burn((char*)verifier.stored_key, sizeof(verifier.stored_key));
|
credentials_burn((char*)verifier.stored_key, sizeof(verifier.stored_key));
|
||||||
credentials_burn((char*)verifier.server_key, sizeof(verifier.server_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
|
/* 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/
|
/* Auth-required module (A7, protocol 2.19.0): run the SCRAM challenge/
|
||||||
* response BEFORE the module root is installed and before any data moves.
|
* response BEFORE the module root is installed and before any data moves.
|
||||||
* Fail closed: no store -> refuse (server misconfiguration, STATUS_ERROR);
|
* 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). */
|
* logged (never the password or any derived proof). */
|
||||||
if (g_credentials == NULL) {
|
if (g_credentials == NULL) {
|
||||||
log_message(LOG_LEVEL_ERROR,
|
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(" --allow-unauthenticated Allow plaintext/anonymous network clients\n");
|
||||||
printf(" --hash-credentials <file> Read <file>'s user:password lines and print\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(" 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(" --iterations N PBKDF2 iteration count for --hash-credentials\n");
|
||||||
printf(" (default %u, range %u-%u)\n", CREDENTIAL_DEFAULT_ITERS,
|
printf(" (default %u, range %u-%u)\n", CREDENTIAL_DEFAULT_ITERS,
|
||||||
CREDENTIAL_MIN_ITERS, CREDENTIAL_MAX_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. */
|
* emit new-format credential-store lines, then exit. */
|
||||||
if (opts.hash_credentials_file) {
|
if (opts.hash_credentials_file) {
|
||||||
uint32_t iters = opts.hash_iterations_set ? opts.hash_iterations : CREDENTIAL_DEFAULT_ITERS;
|
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];
|
char hash_err[512];
|
||||||
if (credentials_hash_file(opts.hash_credentials_file, iters, stdout, hash_err,
|
if (credentials_hash_file(opts.hash_credentials_file, iters, stdout, hash_err,
|
||||||
sizeof(hash_err)) != 0) {
|
sizeof(hash_err)) != 0) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "server_cli.h"
|
#include "server_cli.h"
|
||||||
#include "charset.h"
|
#include "charset.h"
|
||||||
|
#include "credentials.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
@@ -146,8 +147,10 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
|||||||
}
|
}
|
||||||
char* end = NULL;
|
char* end = NULL;
|
||||||
long n = strtol(inline_value, &end, 10);
|
long n = strtol(inline_value, &end, 10);
|
||||||
if (!end || *end != '\0' || n < 0 || n > 10000000L) {
|
if (!end || *end != '\0' || n < (long)CREDENTIAL_MIN_ITERS ||
|
||||||
set_error(err, err_size, "invalid --iterations '%s'", inline_value);
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
opts->hash_iterations = (uint32_t)n;
|
opts->hash_iterations = (uint32_t)n;
|
||||||
|
|||||||
+87
-19
@@ -30,6 +30,16 @@ struct CredentialStore {
|
|||||||
CredentialEntry* entries;
|
CredentialEntry* entries;
|
||||||
int count;
|
int count;
|
||||||
int capacity;
|
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. */
|
/* 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)
|
if (decoded_len > out_sz)
|
||||||
return false;
|
return false;
|
||||||
/* EVP_DecodeBlock writes the full (padded) quantum, so decode into a scratch
|
/* EVP_DecodeBlock writes the full (padded) quantum, so decode into a scratch
|
||||||
* buffer sized for it and copy only the real bytes out. */
|
* buffer sized for it and copy only the real bytes out. The single `done`
|
||||||
uint8_t scratch[192];
|
* 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);
|
int n = EVP_DecodeBlock(scratch, (const unsigned char*)in, (int)len);
|
||||||
if (n < 0 || (size_t)n != padded_len)
|
if (n < 0 || (size_t)n != padded_len)
|
||||||
return false;
|
goto done;
|
||||||
memcpy(out, scratch, decoded_len);
|
memcpy(out, scratch, decoded_len);
|
||||||
credentials_burn((char*)scratch, sizeof(scratch));
|
|
||||||
*out_len = decoded_len;
|
*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) {
|
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]) {
|
uint8_t server_key[CREDENTIAL_KEY_LEN]) {
|
||||||
if (!password || !salt)
|
if (!password || !salt)
|
||||||
return false;
|
return false;
|
||||||
/* The caller (store parser / client clamp) is responsible for the
|
/* Enforce the full [MIN,MAX] policy here so no caller can derive a verifier
|
||||||
* [MIN,MAX] policy; this primitive only refuses a zero/unbounded work
|
* with a work factor outside the validated store range. */
|
||||||
* factor. Tests exercise the known-answer vector at a smaller count. */
|
if (iters < CREDENTIAL_MIN_ITERS || iters > CREDENTIAL_MAX_ITERS)
|
||||||
if (iters == 0 || iters > CREDENTIAL_MAX_ITERS)
|
|
||||||
return false;
|
return false;
|
||||||
size_t password_len = strlen(password);
|
size_t password_len = strlen(password);
|
||||||
if (password_len > CREDENTIAL_MAX_PASSWORD_LEN || password_len > (size_t)INT_MAX)
|
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);
|
computed = hmac_sha256(v->server_key, CREDENTIAL_KEY_LEN, auth_msg, msg_len, server_sig);
|
||||||
if (computed)
|
if (computed)
|
||||||
memcpy(server_sig_out, server_sig, CREDENTIAL_KEY_LEN);
|
memcpy(server_sig_out, server_sig, CREDENTIAL_KEY_LEN);
|
||||||
/* Constant-time compare over the fixed 32-byte keys; a tampered nonce
|
/* Always run the constant-time key compare (even when `found` is false) and
|
||||||
* changes the AuthMessage and so the recovered key. */
|
* fold the accept decision with bitwise AND so no short-circuit reveals
|
||||||
bool accept = computed && v->found &&
|
* whether the user was found. A tampered nonce changes the AuthMessage and
|
||||||
credentials_secure_equal((const char*)recovered, (const char*)v->stored_key,
|
* 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);
|
CREDENTIAL_KEY_LEN);
|
||||||
|
bool accept = computed & v->found & key_match;
|
||||||
credentials_burn((char*)auth_msg, sizeof(auth_msg));
|
credentials_burn((char*)auth_msg, sizeof(auth_msg));
|
||||||
credentials_burn((char*)client_sig, sizeof(client_sig));
|
credentials_burn((char*)client_sig, sizeof(client_sig));
|
||||||
credentials_burn((char*)client_key, sizeof(client_key));
|
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;
|
ok = false;
|
||||||
break;
|
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) {
|
if (find_user(store, user) >= 0) {
|
||||||
set_error(err, err_size, "credential file '%s' line %d: duplicate entry for user '%.*s'",
|
set_error(err, err_size, "credential file '%s' line %d: duplicate entry for user '%.*s'",
|
||||||
path, line_no, (int)strlen(user), user);
|
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);
|
CredentialStore* store = load_store_file(password_file, err, err_size);
|
||||||
if (!store)
|
if (!store)
|
||||||
return NULL;
|
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)
|
if (!early_input_file)
|
||||||
return store;
|
return store;
|
||||||
|
|
||||||
@@ -568,6 +607,18 @@ CredentialStore* credentials_load(const char* password_file, const char* early_i
|
|||||||
credentials_free(store);
|
credentials_free(store);
|
||||||
return NULL;
|
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
|
/* Layer early input over the password file: an identical verifier dedupes, a
|
||||||
* differing verifier for the same user is ambiguous and fails closed. */
|
* differing verifier for the same user is ambiguous and fails closed. */
|
||||||
for (int i = 0; i < early->count; i++) {
|
for (int i = 0; i < early->count; i++) {
|
||||||
@@ -652,14 +703,23 @@ bool credentials_get_verifier(const CredentialStore* store, const char* user,
|
|||||||
if (!out)
|
if (!out)
|
||||||
return false;
|
return false;
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
/* Start from the dummy verifier: a fresh random salt and the default
|
const char* uname = user ? user : "";
|
||||||
* iteration count, so a miss is shaped exactly like a hit. */
|
/* The dummy verifier is shaped exactly like a hit: the store-wide uniform
|
||||||
if (!credentials_random_bytes(out->salt, CREDENTIAL_SALT_LEN))
|
* iteration count (default for an empty store) and fixed dummy keys. */
|
||||||
return false;
|
out->iters = (store && store->count > 0) ? store->iters : CREDENTIAL_DEFAULT_ITERS;
|
||||||
out->iters = CREDENTIAL_DEFAULT_ITERS;
|
|
||||||
memcpy(out->stored_key, k_dummy_stored_key, CREDENTIAL_KEY_LEN);
|
memcpy(out->stored_key, k_dummy_stored_key, CREDENTIAL_KEY_LEN);
|
||||||
memcpy(out->server_key, k_dummy_server_key, CREDENTIAL_KEY_LEN);
|
memcpy(out->server_key, k_dummy_server_key, CREDENTIAL_KEY_LEN);
|
||||||
out->found = false;
|
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)
|
if (!store || !user || n < 0)
|
||||||
return true;
|
return true;
|
||||||
/* Module-list membership: constant-time full scan, no early break, so the
|
/* 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*)stored_key, sizeof(stored_key));
|
||||||
credentials_burn((char*)server_key, sizeof(server_key));
|
credentials_burn((char*)server_key, sizeof(server_key));
|
||||||
credentials_burn((char*)salt, sizeof(salt));
|
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;
|
return false;
|
||||||
|
}
|
||||||
if (written < 0 || (size_t)written >= out_sz) {
|
if (written < 0 || (size_t)written >= out_sz) {
|
||||||
set_error(err, err_size, "output buffer too small for the credential line");
|
set_error(err, err_size, "output buffer too small for the credential line");
|
||||||
|
credentials_burn(out, out_sz);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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];
|
char store_line[CREDENTIAL_MAX_LINE];
|
||||||
if (!credentials_hash_store_line(user, password, iters, store_line, sizeof(store_line), err,
|
if (!credentials_hash_store_line(user, password, iters, store_line, sizeof(store_line), err,
|
||||||
err_size)) {
|
err_size)) {
|
||||||
|
credentials_burn(store_line, sizeof(store_line));
|
||||||
result = -1;
|
result = -1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,11 @@
|
|||||||
typedef struct CredentialStore CredentialStore;
|
typedef struct CredentialStore CredentialStore;
|
||||||
|
|
||||||
/* One resolved verifier. `found` is false for an unknown user or a user not on
|
/* 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
|
* a module's auth list; the remaining fields then hold a deterministic dummy
|
||||||
* default iteration count and fixed dummy keys, so the server can run the same
|
* salt (HMAC of the store-wide dummy key over the username), the store-wide
|
||||||
* challenge/response math with no enumeration/timing oracle. */
|
* 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 {
|
typedef struct {
|
||||||
uint8_t salt[CREDENTIAL_SALT_LEN];
|
uint8_t salt[CREDENTIAL_SALT_LEN];
|
||||||
uint32_t iters;
|
uint32_t iters;
|
||||||
@@ -73,8 +75,10 @@ typedef struct {
|
|||||||
* opened or that fails the strict grammar is a hard error (err filled, NULL
|
* 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
|
* 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
|
* 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
|
* store (every auth-required module then refuses connections). Every entry in
|
||||||
* given, the --early-input file is layered over --password-file: a duplicate
|
* 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
|
* username whose verifier matches is deduplicated; one whose verifier differs
|
||||||
* is an error (the two sources disagree), never a silent pick.
|
* 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
|
/* 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
|
* 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
|
* miss, *out is filled with a dummy verifier (a deterministic per-username salt
|
||||||
* iterations, fixed dummy keys, found=false). Returns false only on invalid
|
* derived from the store's dummy key, the store-wide uniform iteration count,
|
||||||
* arguments/allocation failure. */
|
* 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,
|
bool credentials_get_verifier(const CredentialStore* store, const char* user,
|
||||||
const char* const* module_users, int n, CredentialVerifier* out);
|
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)
|
* K = PBKDF2-HMAC-SHA256(password, salt, iters, 32)
|
||||||
* ClientKey = HMAC-SHA256(K, "Client Key"); StoredKey = SHA256(ClientKey)
|
* ClientKey = HMAC-SHA256(K, "Client Key"); StoredKey = SHA256(ClientKey)
|
||||||
* ServerKey = HMAC-SHA256(K, "Server Key")
|
* 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],
|
bool credentials_compute_keys(const char* password, const uint8_t salt[CREDENTIAL_SALT_LEN],
|
||||||
uint32_t iters, uint8_t client_key[CREDENTIAL_KEY_LEN],
|
uint32_t iters, uint8_t client_key[CREDENTIAL_KEY_LEN],
|
||||||
uint8_t stored_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
|
/* Shared string send/receive implementation. `redact` selects whether the
|
||||||
* payload body is written to the LOG_DEBUG_PROTO debug log: secrets (daemon
|
* payload body is written to the LOG_DEBUG_PROTO debug log: daemon auth material
|
||||||
* auth username/digest) set it so a --verbose log never captures a replayable
|
* (the username and the proof/signature fields) sets it so a --verbose log never
|
||||||
* credential, while every other string keeps its normal debug trace. */
|
* 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) {
|
static bool protocol_send_str_impl(ProtocolSession* session, const char* data, bool redact) {
|
||||||
if (data == NULL)
|
if (data == NULL)
|
||||||
return false;
|
return false;
|
||||||
@@ -602,7 +603,8 @@ char* receive_str(int fd) {
|
|||||||
return protocol_receive_str(legacy_session(fd, -1));
|
return protocol_receive_str(legacy_session(fd, -1));
|
||||||
}
|
}
|
||||||
/* Redacted variants: identical framing, but the string body is never written to
|
/* 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) {
|
bool send_str_redacted(int fd, const char* data) {
|
||||||
return protocol_send_str_redacted(legacy_session(-1, fd), 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);
|
char* protocol_receive_str(ProtocolSession* session);
|
||||||
/* Redacted string variants: identical wire framing to protocol_send_str /
|
/* Redacted string variants: identical wire framing to protocol_send_str /
|
||||||
* protocol_receive_str, but the payload body is replaced by `<redacted>` in the
|
* 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
|
* LOG_DEBUG_PROTO debug log. Used for daemon auth material (the username and
|
||||||
* a --verbose log can never capture a replayable credential. */
|
* 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);
|
bool protocol_send_str_redacted(ProtocolSession* session, const char* data);
|
||||||
char* protocol_receive_str_redacted(ProtocolSession* session);
|
char* protocol_receive_str_redacted(ProtocolSession* session);
|
||||||
bool protocol_send_data(ProtocolSession* session, const Data* data);
|
bool protocol_send_data(ProtocolSession* session, const Data* data);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ static size_t g_frame_len;
|
|||||||
static size_t g_version_len; /* length of the leading version-string frame */
|
static size_t g_version_len; /* length of the leading version-string frame */
|
||||||
static size_t g_usermap_count_off; /* offset of the usermap count int, 0 = unknown */
|
static size_t g_usermap_count_off; /* offset of the usermap count int, 0 = unknown */
|
||||||
static size_t g_auth_off; /* offset of the auth presence int, 0 = unknown */
|
static size_t g_auth_off; /* offset of the auth presence int, 0 = unknown */
|
||||||
|
static bool g_auth_found; /* whether g_auth_off is valid */
|
||||||
static bool g_frame_ready;
|
static bool g_frame_ready;
|
||||||
|
|
||||||
/* Read the canonical frame from the send peer. The producer shuts down its
|
/* Read the canonical frame from the send peer. The producer shuts down its
|
||||||
@@ -180,17 +181,20 @@ out:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Locate the auth username string (a size_t length followed by its bytes);
|
/* Locate the auth username string (a size_t length followed by its bytes);
|
||||||
* the presence int sits one int before the length. */
|
* the presence int sits one int before the length. The username bytes cannot
|
||||||
|
* start before sizeof(size_t)+sizeof(int) without the presence-int offset
|
||||||
|
* underflowing, so begin the scan there. */
|
||||||
const char* auth_name = "alice";
|
const char* auth_name = "alice";
|
||||||
size_t auth_name_len = strlen(auth_name);
|
size_t auth_name_len = strlen(auth_name);
|
||||||
if (g_frame_len >= sizeof(size_t) + auth_name_len + sizeof(int)) {
|
if (g_frame_len >= sizeof(size_t) + auth_name_len + sizeof(int)) {
|
||||||
for (size_t i = sizeof(size_t); i + auth_name_len <= g_frame_len; i++) {
|
for (size_t i = sizeof(size_t) + sizeof(int); i + auth_name_len <= g_frame_len; i++) {
|
||||||
if (memcmp(g_frame + i, auth_name, auth_name_len) != 0)
|
if (memcmp(g_frame + i, auth_name, auth_name_len) != 0)
|
||||||
continue;
|
continue;
|
||||||
size_t found_len = 0;
|
size_t found_len = 0;
|
||||||
memcpy(&found_len, g_frame + i - sizeof(size_t), sizeof(size_t));
|
memcpy(&found_len, g_frame + i - sizeof(size_t), sizeof(size_t));
|
||||||
if (found_len == auth_name_len) {
|
if (found_len == auth_name_len) {
|
||||||
g_auth_off = i - sizeof(size_t) - sizeof(int);
|
g_auth_off = i - sizeof(size_t) - sizeof(int);
|
||||||
|
g_auth_found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,7 +331,7 @@ int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
|||||||
receive_stream(g_frame, g_version_len, data, size);
|
receive_stream(g_frame, g_version_len, data, size);
|
||||||
|
|
||||||
/* Keep the valid frame up to the shortened auth block, fuzz it. */
|
/* Keep the valid frame up to the shortened auth block, fuzz it. */
|
||||||
if (g_auth_off > 0)
|
if (g_auth_found)
|
||||||
receive_stream(g_frame, g_auth_off, data, size);
|
receive_stream(g_frame, g_auth_off, data, size);
|
||||||
|
|
||||||
/* Keep the valid frame up to the P8 tail, fuzz super_mode + copy-as. */
|
/* Keep the valid frame up to the P8 tail, fuzz super_mode + copy-as. */
|
||||||
|
|||||||
+101
-13
@@ -11,17 +11,21 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
/* Known-answer vector, independently recomputed with Python
|
/* Known-answer vector, independently recomputed with Python
|
||||||
* (hashlib.pbkdf2_hmac / hmac / hashlib.sha256). */
|
* (hashlib.pbkdf2_hmac / hmac / hashlib.sha256) at the default work factor. */
|
||||||
#define KAT_PASSWORD "alice-s3cret"
|
#define KAT_PASSWORD "alice-s3cret"
|
||||||
#define KAT_USER "alice"
|
#define KAT_USER "alice"
|
||||||
#define KAT_ITERS 4096u
|
#define KAT_ITERS CREDENTIAL_DEFAULT_ITERS
|
||||||
#define KAT_CLIENT_KEY "80f0e0af43e34e8aeec1738609c5d1eac8646601b4f244cef5a04a9f13563cf8"
|
#define KAT_CLIENT_KEY "845891d65ab3c9807f7ae5c123ab70714cc8b56173fccfce6a758993e858e17c"
|
||||||
#define KAT_STORED_KEY "5b3b489437085a11fe594ab99154da4cb4ebab4ae8c2edf51fbaa9277e9f9099"
|
#define KAT_STORED_KEY "d192f6da1c54bf73768f0a7c713995212d303c46f809de1b2e407fb3ad1c206b"
|
||||||
#define KAT_SERVER_KEY "38f668736210bd4dbcb5193b9a514c5b1047174eff5f5a80ee4c2b1e8b2c76a1"
|
#define KAT_SERVER_KEY "508ad587574f59700c2d0bbec8417d6fe94bf8e39669adbabe7cbbd8700f86ce"
|
||||||
#define KAT_CLIENT_PROOF "c9b0d397b853176b842a751b9af327270ae0c5e286cb77d16e59fa42245270f6"
|
#define KAT_CLIENT_PROOF "e0cb4b894a7438d75cbb3066aa135d10200b76eea78137c5c04059895eed9242"
|
||||||
#define KAT_SERVER_SIG "93c0d94b9ee29798ffd42734a9becb2168bad1f69e492d2ccb042a43db13bffc"
|
#define KAT_SERVER_SIG "f564e00fa6368e78d35b7116c7624d6cb047a950d87e3799e4e6e8c8954b618a"
|
||||||
#define KAT_SALT_B64 "AAECAwQFBgcICQoLDA0ODw=="
|
#define KAT_SALT_B64 "AAECAwQFBgcICQoLDA0ODw=="
|
||||||
#define KAT_NAME_PREFIX "$fastsync$1$pbkdf2-sha256$"
|
#define KAT_NAME_PREFIX "$fastsync$1$pbkdf2-sha256$"
|
||||||
|
/* The exact store line for the KAT user/password at the KAT salt/count. */
|
||||||
|
#define KAT_STORE_LINE \
|
||||||
|
"alice:$fastsync$1$pbkdf2-sha256$600000$AAECAwQFBgcICQoLDA0ODw==$" \
|
||||||
|
"0ZL22hxUv3N2jwp8cTmVIS0wPEb4Cd4bLkB/s60cIGs=$UIrVh1dPWXAMLQu+yEF9b+lL+OOWaa26vny72HAPhs4="
|
||||||
|
|
||||||
static int g_file_counter = 0;
|
static int g_file_counter = 0;
|
||||||
|
|
||||||
@@ -133,6 +137,10 @@ static void test_credentials_compute_keys_kat() {
|
|||||||
unhex(KAT_SERVER_KEY, expect, sizeof(expect));
|
unhex(KAT_SERVER_KEY, expect, sizeof(expect));
|
||||||
EXPECT_TRUE(memcmp(server_key, expect, sizeof(expect)) == 0);
|
EXPECT_TRUE(memcmp(server_key, expect, sizeof(expect)) == 0);
|
||||||
EXPECT_FALSE(credentials_compute_keys(KAT_PASSWORD, salt, 0, client_key, stored_key, server_key));
|
EXPECT_FALSE(credentials_compute_keys(KAT_PASSWORD, salt, 0, client_key, stored_key, server_key));
|
||||||
|
EXPECT_FALSE(credentials_compute_keys(KAT_PASSWORD, salt, CREDENTIAL_MIN_ITERS - 1, client_key,
|
||||||
|
stored_key, server_key));
|
||||||
|
EXPECT_FALSE(credentials_compute_keys(KAT_PASSWORD, salt, CREDENTIAL_MAX_ITERS + 1, client_key,
|
||||||
|
stored_key, server_key));
|
||||||
EXPECT_FALSE(credentials_compute_keys(NULL, salt, KAT_ITERS, client_key, stored_key, server_key));
|
EXPECT_FALSE(credentials_compute_keys(NULL, salt, KAT_ITERS, client_key, stored_key, server_key));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,9 +174,9 @@ static void test_credentials_auth_message_and_proof_kat() {
|
|||||||
uint8_t server_sig[CREDENTIAL_KEY_LEN];
|
uint8_t server_sig[CREDENTIAL_KEY_LEN];
|
||||||
EXPECT_TRUE(credentials_client_proof(client_key, stored_key, server_key, auth_msg, msg_len, proof,
|
EXPECT_TRUE(credentials_client_proof(client_key, stored_key, server_key, auth_msg, msg_len, proof,
|
||||||
server_sig));
|
server_sig));
|
||||||
unhex(KAT_CLIENT_PROOF, expect, sizeof(expect));
|
unhex(KAT_CLIENT_PROOF, expect, CREDENTIAL_KEY_LEN);
|
||||||
EXPECT_TRUE(memcmp(proof, expect, CREDENTIAL_KEY_LEN) == 0);
|
EXPECT_TRUE(memcmp(proof, expect, CREDENTIAL_KEY_LEN) == 0);
|
||||||
unhex(KAT_SERVER_SIG, expect, sizeof(expect));
|
unhex(KAT_SERVER_SIG, expect, CREDENTIAL_KEY_LEN);
|
||||||
EXPECT_TRUE(memcmp(server_sig, expect, CREDENTIAL_KEY_LEN) == 0);
|
EXPECT_TRUE(memcmp(server_sig, expect, CREDENTIAL_KEY_LEN) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +282,35 @@ static void test_credentials_hash_store_line_roundtrip() {
|
|||||||
free(path);
|
free(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The golden store line (KAT user/password/salt/count) parses back to exactly
|
||||||
|
* the KAT verifier keys, pinning the on-disk encoding independently. */
|
||||||
|
static void test_credentials_store_line_golden() {
|
||||||
|
char* path = make_tmp_file(KAT_STORE_LINE "\n");
|
||||||
|
EXPECT_NOT_NULL(path);
|
||||||
|
char err[512];
|
||||||
|
CredentialStore* store = credentials_load(path, NULL, err, sizeof(err));
|
||||||
|
EXPECT_NOT_NULL(store);
|
||||||
|
EXPECT_TRUE(credentials_store_has(store, "alice"));
|
||||||
|
|
||||||
|
CredentialVerifier v;
|
||||||
|
const char* module_users[] = {"alice"};
|
||||||
|
EXPECT_TRUE(credentials_get_verifier(store, "alice", module_users, 1, &v));
|
||||||
|
EXPECT_TRUE(v.found);
|
||||||
|
EXPECT_EQ_INT((int)v.iters, (int)KAT_ITERS);
|
||||||
|
uint8_t expect[CREDENTIAL_KEY_LEN];
|
||||||
|
unhex(KAT_STORED_KEY, expect, CREDENTIAL_KEY_LEN);
|
||||||
|
EXPECT_TRUE(memcmp(v.stored_key, expect, CREDENTIAL_KEY_LEN) == 0);
|
||||||
|
unhex(KAT_SERVER_KEY, expect, CREDENTIAL_KEY_LEN);
|
||||||
|
EXPECT_TRUE(memcmp(v.server_key, expect, CREDENTIAL_KEY_LEN) == 0);
|
||||||
|
uint8_t salt[CREDENTIAL_SALT_LEN];
|
||||||
|
ramp(salt, sizeof(salt), 0x00);
|
||||||
|
EXPECT_TRUE(memcmp(v.salt, salt, sizeof(salt)) == 0);
|
||||||
|
|
||||||
|
credentials_free(store);
|
||||||
|
rm_temp(path);
|
||||||
|
free(path);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_credentials_store_parse_valid() {
|
static void test_credentials_store_parse_valid() {
|
||||||
char line_alice[CREDENTIAL_MAX_LINE];
|
char line_alice[CREDENTIAL_MAX_LINE];
|
||||||
char line_bob[CREDENTIAL_MAX_LINE];
|
char line_bob[CREDENTIAL_MAX_LINE];
|
||||||
@@ -308,14 +345,27 @@ static void test_credentials_store_parse_valid() {
|
|||||||
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
||||||
EXPECT_TRUE(memcmp(v.stored_key, zero, CREDENTIAL_KEY_LEN) == 0);
|
EXPECT_TRUE(memcmp(v.stored_key, zero, CREDENTIAL_KEY_LEN) == 0);
|
||||||
EXPECT_TRUE(memcmp(v.server_key, zero, CREDENTIAL_KEY_LEN) == 0);
|
EXPECT_TRUE(memcmp(v.server_key, zero, CREDENTIAL_KEY_LEN) == 0);
|
||||||
/* Miss salt is fresh random on each call. */
|
/* Deterministic dummy challenge: the same unknown username always yields the
|
||||||
|
* same salt and iteration count, while different usernames differ, so probing
|
||||||
|
* the store twice cannot reveal membership. */
|
||||||
uint8_t salt_a[CREDENTIAL_SALT_LEN];
|
uint8_t salt_a[CREDENTIAL_SALT_LEN];
|
||||||
uint8_t salt_b[CREDENTIAL_SALT_LEN];
|
uint8_t salt_b[CREDENTIAL_SALT_LEN];
|
||||||
|
uint8_t salt_c[CREDENTIAL_SALT_LEN];
|
||||||
|
uint32_t miss_iters_a = 0;
|
||||||
|
uint32_t miss_iters_b = 0;
|
||||||
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
||||||
memcpy(salt_a, v.salt, sizeof(salt_a));
|
memcpy(salt_a, v.salt, sizeof(salt_a));
|
||||||
|
miss_iters_a = v.iters;
|
||||||
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
||||||
memcpy(salt_b, v.salt, sizeof(salt_b));
|
memcpy(salt_b, v.salt, sizeof(salt_b));
|
||||||
EXPECT_TRUE(memcmp(salt_a, salt_b, sizeof(salt_a)) != 0);
|
miss_iters_b = v.iters;
|
||||||
|
EXPECT_TRUE(memcmp(salt_a, salt_b, sizeof(salt_a)) == 0);
|
||||||
|
EXPECT_EQ_INT((int)miss_iters_a, (int)miss_iters_b);
|
||||||
|
/* A miss is answered with the store-wide uniform iteration count. */
|
||||||
|
EXPECT_EQ_INT((int)miss_iters_a, (int)CREDENTIAL_MIN_ITERS);
|
||||||
|
EXPECT_TRUE(credentials_get_verifier(store, "trudy", module_users, 1, &v));
|
||||||
|
memcpy(salt_c, v.salt, sizeof(salt_c));
|
||||||
|
EXPECT_TRUE(memcmp(salt_a, salt_c, sizeof(salt_a)) != 0);
|
||||||
|
|
||||||
credentials_free(store);
|
credentials_free(store);
|
||||||
rm_temp(path);
|
rm_temp(path);
|
||||||
@@ -357,8 +407,8 @@ static void test_credentials_store_parse_rejects_malformed() {
|
|||||||
short_key,
|
short_key,
|
||||||
empty_field,
|
empty_field,
|
||||||
"ali "
|
"ali "
|
||||||
"ce:$fastsync$1$pbkdf2-sha256$600000$AAECAwQFBgcICQoLDA0ODw==$WztIlDcIWhH+"
|
"ce:$fastsync$1$pbkdf2-sha256$600000$AAECAwQFBgcICQoLDA0ODw==$0ZL22hxUv3N2jwp8"
|
||||||
"WUq5kVTaTLTrq0rowu31H7qpJ36fkJk=$OPZoc2IQvU28tRk7mlFMWxBHF07/X1qA7kwrHossdqE=\n",
|
"cTmVIS0wPEb4Cd4bLkB/s60cIGs=$UIrVh1dPWXAMLQu+yEF9b+lL+OOWaa26vny72HAPhs4=\n",
|
||||||
};
|
};
|
||||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||||
char* path = make_tmp_file(cases[i]);
|
char* path = make_tmp_file(cases[i]);
|
||||||
@@ -401,6 +451,25 @@ static void test_credentials_store_duplicate_rejected() {
|
|||||||
free(path);
|
free(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A store must be uniform in its iteration count so a miss can be challenged
|
||||||
|
* with the store-wide count without leaking membership. */
|
||||||
|
static void test_credentials_store_rejects_nonuniform_iters() {
|
||||||
|
char line_a[CREDENTIAL_MAX_LINE];
|
||||||
|
char line_b[CREDENTIAL_MAX_LINE];
|
||||||
|
EXPECT_TRUE(make_store_line("alice", KAT_PASSWORD, CREDENTIAL_MIN_ITERS, line_a, sizeof(line_a)));
|
||||||
|
EXPECT_TRUE(
|
||||||
|
make_store_line("bob", "bob-s3cret", CREDENTIAL_MIN_ITERS * 2, line_b, sizeof(line_b)));
|
||||||
|
char contents[2 * CREDENTIAL_MAX_LINE + 8];
|
||||||
|
snprintf(contents, sizeof(contents), "%s\n%s\n", line_a, line_b);
|
||||||
|
char* path = make_tmp_file(contents);
|
||||||
|
EXPECT_NOT_NULL(path);
|
||||||
|
char err[512];
|
||||||
|
EXPECT_NULL(credentials_load(path, NULL, err, sizeof(err)));
|
||||||
|
EXPECT_TRUE(strstr(err, "uniform") != NULL);
|
||||||
|
rm_temp(path);
|
||||||
|
free(path);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_credentials_store_parse_missing_file() {
|
static void test_credentials_store_parse_missing_file() {
|
||||||
char err[512];
|
char err[512];
|
||||||
const CredentialStore* store =
|
const CredentialStore* store =
|
||||||
@@ -414,6 +483,11 @@ static void test_credentials_store_empty_and_null() {
|
|||||||
CredentialStore* store = credentials_load(NULL, NULL, err, sizeof(err));
|
CredentialStore* store = credentials_load(NULL, NULL, err, sizeof(err));
|
||||||
EXPECT_NOT_NULL(store);
|
EXPECT_NOT_NULL(store);
|
||||||
EXPECT_EQ_INT(credentials_store_size(store), 0);
|
EXPECT_EQ_INT(credentials_store_size(store), 0);
|
||||||
|
/* An empty store answers a miss with the default work factor. */
|
||||||
|
CredentialVerifier v;
|
||||||
|
EXPECT_TRUE(credentials_get_verifier(store, "nobody", NULL, 0, &v));
|
||||||
|
EXPECT_FALSE(v.found);
|
||||||
|
EXPECT_EQ_INT((int)v.iters, (int)CREDENTIAL_DEFAULT_ITERS);
|
||||||
credentials_free(store);
|
credentials_free(store);
|
||||||
|
|
||||||
char* path = make_tmp_file("# nothing here\n; nor here\n");
|
char* path = make_tmp_file("# nothing here\n; nor here\n");
|
||||||
@@ -449,19 +523,24 @@ static void test_credentials_early_input_merge() {
|
|||||||
char alice_file[CREDENTIAL_MAX_LINE + 2];
|
char alice_file[CREDENTIAL_MAX_LINE + 2];
|
||||||
char bob_file[CREDENTIAL_MAX_LINE + 2];
|
char bob_file[CREDENTIAL_MAX_LINE + 2];
|
||||||
char alice_other[CREDENTIAL_MAX_LINE];
|
char alice_other[CREDENTIAL_MAX_LINE];
|
||||||
|
char bob_other_iters[CREDENTIAL_MAX_LINE];
|
||||||
snprintf(alice_file, sizeof(alice_file), "%s\n", alice);
|
snprintf(alice_file, sizeof(alice_file), "%s\n", alice);
|
||||||
snprintf(bob_file, sizeof(bob_file), "%s\n", bob);
|
snprintf(bob_file, sizeof(bob_file), "%s\n", bob);
|
||||||
EXPECT_TRUE(make_store_line("alice", "different-s3cret", CREDENTIAL_MIN_ITERS, alice_other,
|
EXPECT_TRUE(make_store_line("alice", "different-s3cret", CREDENTIAL_MIN_ITERS, alice_other,
|
||||||
sizeof(alice_other)));
|
sizeof(alice_other)));
|
||||||
|
EXPECT_TRUE(make_store_line("carol", "carol-s3cret", CREDENTIAL_MIN_ITERS * 2, bob_other_iters,
|
||||||
|
sizeof(bob_other_iters)));
|
||||||
|
|
||||||
char* pw = make_tmp_file(alice_file);
|
char* pw = make_tmp_file(alice_file);
|
||||||
char* early = make_tmp_file(bob_file);
|
char* early = make_tmp_file(bob_file);
|
||||||
char* early_same = make_tmp_file(alice_file); /* byte-identical verifier dedupes */
|
char* early_same = make_tmp_file(alice_file); /* byte-identical verifier dedupes */
|
||||||
char* early_diff = make_tmp_file(alice_other);
|
char* early_diff = make_tmp_file(alice_other);
|
||||||
|
char* early_iters = make_tmp_file(bob_other_iters);
|
||||||
EXPECT_NOT_NULL(pw);
|
EXPECT_NOT_NULL(pw);
|
||||||
EXPECT_NOT_NULL(early);
|
EXPECT_NOT_NULL(early);
|
||||||
EXPECT_NOT_NULL(early_same);
|
EXPECT_NOT_NULL(early_same);
|
||||||
EXPECT_NOT_NULL(early_diff);
|
EXPECT_NOT_NULL(early_diff);
|
||||||
|
EXPECT_NOT_NULL(early_iters);
|
||||||
char err[512];
|
char err[512];
|
||||||
|
|
||||||
/* A second file adds a new user. */
|
/* A second file adds a new user. */
|
||||||
@@ -483,14 +562,21 @@ static void test_credentials_early_input_merge() {
|
|||||||
EXPECT_NULL(store);
|
EXPECT_NULL(store);
|
||||||
EXPECT_TRUE(err[0] != '\0');
|
EXPECT_TRUE(err[0] != '\0');
|
||||||
|
|
||||||
|
/* A layered store must stay uniform in its iteration count. */
|
||||||
|
store = credentials_load(pw, early_iters, err, sizeof(err));
|
||||||
|
EXPECT_NULL(store);
|
||||||
|
EXPECT_TRUE(strstr(err, "uniform") != NULL);
|
||||||
|
|
||||||
rm_temp(pw);
|
rm_temp(pw);
|
||||||
rm_temp(early);
|
rm_temp(early);
|
||||||
rm_temp(early_same);
|
rm_temp(early_same);
|
||||||
rm_temp(early_diff);
|
rm_temp(early_diff);
|
||||||
|
rm_temp(early_iters);
|
||||||
free(pw);
|
free(pw);
|
||||||
free(early);
|
free(early);
|
||||||
free(early_same);
|
free(early_same);
|
||||||
free(early_diff);
|
free(early_diff);
|
||||||
|
free(early_iters);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_credentials_read_secret_file() {
|
static void test_credentials_read_secret_file() {
|
||||||
@@ -677,10 +763,12 @@ void test_credentials(void) {
|
|||||||
test_credentials_verify_response_kat();
|
test_credentials_verify_response_kat();
|
||||||
test_credentials_username_valid();
|
test_credentials_username_valid();
|
||||||
test_credentials_hash_store_line_roundtrip();
|
test_credentials_hash_store_line_roundtrip();
|
||||||
|
test_credentials_store_line_golden();
|
||||||
test_credentials_store_parse_valid();
|
test_credentials_store_parse_valid();
|
||||||
test_credentials_store_parse_rejects_malformed();
|
test_credentials_store_parse_rejects_malformed();
|
||||||
test_credentials_store_rejects_legacy_hex();
|
test_credentials_store_rejects_legacy_hex();
|
||||||
test_credentials_store_duplicate_rejected();
|
test_credentials_store_duplicate_rejected();
|
||||||
|
test_credentials_store_rejects_nonuniform_iters();
|
||||||
test_credentials_store_parse_missing_file();
|
test_credentials_store_parse_missing_file();
|
||||||
test_credentials_store_empty_and_null();
|
test_credentials_store_empty_and_null();
|
||||||
test_credentials_store_overlong_line_rejected();
|
test_credentials_store_overlong_line_rejected();
|
||||||
|
|||||||
Reference in New Issue
Block a user