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:
2026-09-12 17:19:33 +02:00
parent 1ba6372017
commit 8c94ec9886
18 changed files with 1828 additions and 477 deletions
+14 -1
View File
@@ -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);