fix(a7-auth): harden dummy-key temp creation

- Make the atomic-publish temp name unpredictable by appending 16 random
  hex chars to the pid, so a leftover/planted temp cannot be targeted.
- On EEXIST, unlink the stale temp and retry the O_EXCL create once
  (bounded), so a crash leftover or reused pid cannot silently defeat
  sidecar persistence.
- fchmod the temp fd to 0600 after creation (umask can clear owner bits)
  and treat failure as a create failure, so the published sidecar is
  always exactly 0600.
- Clarify comments: the sidecar requires exact 0600 while the store and
  password files only reject group/other bits.
- Add a unit test that a restrictive umask still yields an exact 0600
  sidecar; clean random-suffixed temps in tests.
This commit is contained in:
2026-09-12 19:29:23 +02:00
parent f0381a6b8e
commit ac3c4c7c72
3 changed files with 129 additions and 39 deletions
+9 -6
View File
@@ -26,14 +26,16 @@
* hard-rejected with an actionable "legacy" error; there is no auto-upgrade.
* Use `fastsync-server --hash-credentials` to generate new-format lines.
*
* Alongside the store, credentials_load maintains an owner-only (0600)
* Alongside the store, credentials_load maintains an exact-mode-0600
* `<store_path>.dummykey` sidecar holding the store-wide random dummy key. It
* is auto-created on first load and MUST be preserved across restarts: it makes
* the dummy challenge for an unknown user stable for the life of the store, so
* a daemon restart cannot be used as a username-enumeration oracle. A sidecar
* that is not an owner-only regular file of exactly 32 bytes fails the load
* (fail closed); if it cannot be created (e.g. a read-only mount) the daemon
* warns and uses a transient per-run key instead.
* that is not an exact-mode-0600 regular file of exactly 32 bytes fails the load
* (fail closed); if it cannot be created (e.g. a read-only mount or a restrictive
* umask the fchmod cannot repair) the daemon warns and uses a transient per-run
* key instead. NOTE: the sidecar requires EXACT 0600, whereas the store /
* password files only reject group/other bits (a deliberate difference).
*
* Client --password-file format: the FIRST meaningful (non-comment, non-blank)
* line is `user:password`, holding the literal password. The client keeps it
@@ -165,8 +167,9 @@ bool credentials_verify_response(const CredentialVerifier* v, const char* user,
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`.
/* Read `user:password` lines from `path` (the same no-group/other-bits 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`. */