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.
694 lines
26 KiB
C
694 lines
26 KiB
C
#include "test_credentials.h"
|
|
#include "credentials.h"
|
|
#include "test_utils.h"
|
|
#include "utils.h"
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
/* Known-answer vector, independently recomputed with Python
|
|
* (hashlib.pbkdf2_hmac / hmac / hashlib.sha256). */
|
|
#define KAT_PASSWORD "alice-s3cret"
|
|
#define KAT_USER "alice"
|
|
#define KAT_ITERS 4096u
|
|
#define KAT_CLIENT_KEY "80f0e0af43e34e8aeec1738609c5d1eac8646601b4f244cef5a04a9f13563cf8"
|
|
#define KAT_STORED_KEY "5b3b489437085a11fe594ab99154da4cb4ebab4ae8c2edf51fbaa9277e9f9099"
|
|
#define KAT_SERVER_KEY "38f668736210bd4dbcb5193b9a514c5b1047174eff5f5a80ee4c2b1e8b2c76a1"
|
|
#define KAT_CLIENT_PROOF "c9b0d397b853176b842a751b9af327270ae0c5e286cb77d16e59fa42245270f6"
|
|
#define KAT_SERVER_SIG "93c0d94b9ee29798ffd42734a9becb2168bad1f69e492d2ccb042a43db13bffc"
|
|
#define KAT_SALT_B64 "AAECAwQFBgcICQoLDA0ODw=="
|
|
#define KAT_NAME_PREFIX "$fastsync$1$pbkdf2-sha256$"
|
|
|
|
static int g_file_counter = 0;
|
|
|
|
static int hex_nibble(char c) {
|
|
if (c >= '0' && c <= '9')
|
|
return c - '0';
|
|
if (c >= 'a' && c <= 'f')
|
|
return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F')
|
|
return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
static void unhex(const char* hex, uint8_t* out, size_t out_len) {
|
|
for (size_t i = 0; i < out_len; i++)
|
|
out[i] = (uint8_t)((hex_nibble(hex[2 * i]) << 4) | hex_nibble(hex[2 * i + 1]));
|
|
}
|
|
|
|
/* Fill deterministic nonces: out[i] = first + i. */
|
|
static void ramp(uint8_t* out, size_t len, uint8_t first) {
|
|
for (size_t i = 0; i < len; i++)
|
|
out[i] = (uint8_t)(first + i);
|
|
}
|
|
|
|
static char* make_tmp_file(const char* contents) {
|
|
char path[256];
|
|
snprintf(path, sizeof(path), "/tmp/fs_cred_test_%d_%d", (int)getpid(), g_file_counter++);
|
|
FILE* fp = fopen(path, "w");
|
|
if (!fp)
|
|
return NULL;
|
|
size_t n = strlen(contents);
|
|
if (n > 0 && fwrite(contents, 1, n, fp) != n) {
|
|
fclose(fp);
|
|
unlink(path);
|
|
return NULL;
|
|
}
|
|
fclose(fp);
|
|
/* Credential/password files are owner-only; the reader rejects group/other
|
|
* permission bits, so create temp files 0600 like the real ones. */
|
|
chmod(path, 0600);
|
|
return str_dup(path);
|
|
}
|
|
|
|
static void rm_temp(const char* path) {
|
|
if (path)
|
|
unlink(path);
|
|
}
|
|
|
|
/* Build a valid new-format line for user/password at iters. */
|
|
static bool make_store_line(const char* user, const char* password, uint32_t iters, char* out,
|
|
size_t out_sz) {
|
|
char err[256];
|
|
return credentials_hash_store_line(user, password, iters, out, out_sz, err, sizeof(err));
|
|
}
|
|
|
|
static void test_credentials_secure_equal() {
|
|
EXPECT_TRUE(credentials_secure_equal("abc", "abc", 3));
|
|
EXPECT_TRUE(credentials_secure_equal("", "", 0));
|
|
EXPECT_FALSE(credentials_secure_equal("abc", "abd", 3));
|
|
EXPECT_TRUE(credentials_secure_equal("abc", "ab", 2));
|
|
EXPECT_FALSE(credentials_secure_equal("ab", "ac", 2));
|
|
}
|
|
|
|
static void test_credentials_b64() {
|
|
uint8_t salt[CREDENTIAL_SALT_LEN];
|
|
ramp(salt, sizeof(salt), 0x00);
|
|
char encoded[25];
|
|
EXPECT_TRUE(credentials_b64_encode(salt, sizeof(salt), encoded, sizeof(encoded)));
|
|
EXPECT_EQ_STR(encoded, KAT_SALT_B64);
|
|
|
|
uint8_t decoded[CREDENTIAL_SALT_LEN];
|
|
size_t decoded_len = 0;
|
|
EXPECT_TRUE(credentials_b64_decode(encoded, decoded, sizeof(decoded), &decoded_len));
|
|
EXPECT_EQ_INT((int)decoded_len, CREDENTIAL_SALT_LEN);
|
|
EXPECT_TRUE(memcmp(decoded, salt, sizeof(salt)) == 0);
|
|
|
|
/* Malformed input is refused: bad length, bad alphabet, missing buffer. */
|
|
EXPECT_FALSE(credentials_b64_decode("abc", decoded, sizeof(decoded), &decoded_len));
|
|
EXPECT_FALSE(credentials_b64_decode("!!!!", decoded, sizeof(decoded), &decoded_len));
|
|
EXPECT_FALSE(credentials_b64_decode("", decoded, sizeof(decoded), &decoded_len));
|
|
EXPECT_FALSE(credentials_b64_decode(KAT_SALT_B64, decoded, 4, &decoded_len));
|
|
EXPECT_FALSE(credentials_b64_decode(NULL, decoded, sizeof(decoded), &decoded_len));
|
|
EXPECT_FALSE(credentials_b64_encode(NULL, 3, encoded, sizeof(encoded)));
|
|
EXPECT_FALSE(credentials_b64_encode(salt, sizeof(salt), encoded, 3));
|
|
}
|
|
|
|
static void test_credentials_random_bytes() {
|
|
uint8_t a[CREDENTIAL_NONCE_LEN];
|
|
uint8_t b[CREDENTIAL_NONCE_LEN];
|
|
EXPECT_TRUE(credentials_random_bytes(a, sizeof(a)));
|
|
EXPECT_TRUE(credentials_random_bytes(b, sizeof(b)));
|
|
EXPECT_TRUE(memcmp(a, b, sizeof(a)) != 0);
|
|
EXPECT_FALSE(credentials_random_bytes(NULL, 4));
|
|
}
|
|
|
|
static void test_credentials_compute_keys_kat() {
|
|
uint8_t salt[CREDENTIAL_SALT_LEN];
|
|
ramp(salt, sizeof(salt), 0x00);
|
|
uint8_t client_key[CREDENTIAL_KEY_LEN];
|
|
uint8_t stored_key[CREDENTIAL_KEY_LEN];
|
|
uint8_t server_key[CREDENTIAL_KEY_LEN];
|
|
EXPECT_TRUE(
|
|
credentials_compute_keys(KAT_PASSWORD, salt, KAT_ITERS, client_key, stored_key, server_key));
|
|
uint8_t expect[CREDENTIAL_KEY_LEN];
|
|
unhex(KAT_CLIENT_KEY, expect, sizeof(expect));
|
|
EXPECT_TRUE(memcmp(client_key, expect, sizeof(expect)) == 0);
|
|
unhex(KAT_STORED_KEY, expect, sizeof(expect));
|
|
EXPECT_TRUE(memcmp(stored_key, expect, sizeof(expect)) == 0);
|
|
unhex(KAT_SERVER_KEY, expect, sizeof(expect));
|
|
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(NULL, salt, KAT_ITERS, client_key, stored_key, server_key));
|
|
}
|
|
|
|
static void test_credentials_auth_message_and_proof_kat() {
|
|
uint8_t snonce[CREDENTIAL_NONCE_LEN];
|
|
uint8_t cnonce[CREDENTIAL_NONCE_LEN];
|
|
ramp(snonce, sizeof(snonce), 0xa0);
|
|
ramp(cnonce, sizeof(cnonce), 0x10);
|
|
|
|
uint8_t auth_msg[CREDENTIAL_AUTH_MESSAGE_MAX];
|
|
size_t msg_len = 0;
|
|
EXPECT_TRUE(credentials_build_auth_message(KAT_USER, snonce, cnonce, auth_msg, sizeof(auth_msg),
|
|
&msg_len));
|
|
const char* expect_msg =
|
|
"4661737453796e632d417574682d763100000005616c69636500000020a0a1a2a3a4a5a6a7a8a9aaabac"
|
|
"adaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf00000020101112131415161718191a1b1c1d1e1f2021"
|
|
"22232425262728292a2b2c2d2e2f";
|
|
uint8_t expect[CREDENTIAL_AUTH_MESSAGE_MAX];
|
|
size_t expect_len = strlen(expect_msg) / 2;
|
|
unhex(expect_msg, expect, expect_len);
|
|
EXPECT_EQ_INT((int)msg_len, (int)expect_len);
|
|
EXPECT_TRUE(memcmp(auth_msg, expect, expect_len) == 0);
|
|
|
|
uint8_t client_key[CREDENTIAL_KEY_LEN];
|
|
uint8_t stored_key[CREDENTIAL_KEY_LEN];
|
|
uint8_t server_key[CREDENTIAL_KEY_LEN];
|
|
unhex(KAT_CLIENT_KEY, client_key, sizeof(client_key));
|
|
unhex(KAT_STORED_KEY, stored_key, sizeof(stored_key));
|
|
unhex(KAT_SERVER_KEY, server_key, sizeof(server_key));
|
|
uint8_t proof[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,
|
|
server_sig));
|
|
unhex(KAT_CLIENT_PROOF, expect, sizeof(expect));
|
|
EXPECT_TRUE(memcmp(proof, expect, CREDENTIAL_KEY_LEN) == 0);
|
|
unhex(KAT_SERVER_SIG, expect, sizeof(expect));
|
|
EXPECT_TRUE(memcmp(server_sig, expect, CREDENTIAL_KEY_LEN) == 0);
|
|
}
|
|
|
|
static CredentialVerifier kat_verifier(void) {
|
|
CredentialVerifier v;
|
|
memset(&v, 0, sizeof(v));
|
|
ramp(v.salt, sizeof(v.salt), 0x00);
|
|
v.iters = KAT_ITERS;
|
|
unhex(KAT_STORED_KEY, v.stored_key, sizeof(v.stored_key));
|
|
unhex(KAT_SERVER_KEY, v.server_key, sizeof(v.server_key));
|
|
v.found = true;
|
|
return v;
|
|
}
|
|
|
|
static void test_credentials_verify_response_kat() {
|
|
CredentialVerifier v = kat_verifier();
|
|
uint8_t snonce[CREDENTIAL_NONCE_LEN];
|
|
uint8_t cnonce[CREDENTIAL_NONCE_LEN];
|
|
ramp(snonce, sizeof(snonce), 0xa0);
|
|
ramp(cnonce, sizeof(cnonce), 0x10);
|
|
uint8_t proof[CREDENTIAL_KEY_LEN];
|
|
uint8_t expect_sig[CREDENTIAL_KEY_LEN];
|
|
unhex(KAT_CLIENT_PROOF, proof, sizeof(proof));
|
|
unhex(KAT_SERVER_SIG, expect_sig, sizeof(expect_sig));
|
|
|
|
uint8_t server_sig[CREDENTIAL_KEY_LEN];
|
|
EXPECT_TRUE(credentials_verify_response(&v, KAT_USER, snonce, cnonce, proof, server_sig));
|
|
EXPECT_TRUE(memcmp(server_sig, expect_sig, CREDENTIAL_KEY_LEN) == 0);
|
|
|
|
/* Tampered proof refused. */
|
|
uint8_t bad[CREDENTIAL_KEY_LEN];
|
|
memcpy(bad, proof, sizeof(bad));
|
|
bad[0] ^= 0x01;
|
|
EXPECT_FALSE(credentials_verify_response(&v, KAT_USER, snonce, cnonce, bad, server_sig));
|
|
|
|
/* Unit replay: the same proof bound to a different client nonce is refused. */
|
|
uint8_t other[CREDENTIAL_NONCE_LEN];
|
|
memcpy(other, cnonce, sizeof(other));
|
|
other[0] ^= 0x01;
|
|
EXPECT_FALSE(credentials_verify_response(&v, KAT_USER, snonce, other, proof, server_sig));
|
|
/* Different server nonce too. */
|
|
uint8_t other_server[CREDENTIAL_NONCE_LEN];
|
|
memcpy(other_server, snonce, sizeof(other_server));
|
|
other_server[0] ^= 0x01;
|
|
EXPECT_FALSE(credentials_verify_response(&v, KAT_USER, other_server, cnonce, proof, server_sig));
|
|
|
|
/* Wrong user changes the AuthMessage and fails. */
|
|
EXPECT_FALSE(credentials_verify_response(&v, "bob", snonce, cnonce, proof, server_sig));
|
|
|
|
/* found=false never accepts. */
|
|
v.found = false;
|
|
EXPECT_FALSE(credentials_verify_response(&v, KAT_USER, snonce, cnonce, proof, server_sig));
|
|
|
|
/* NULL arguments fail closed. */
|
|
v.found = true;
|
|
EXPECT_FALSE(credentials_verify_response(NULL, KAT_USER, snonce, cnonce, proof, server_sig));
|
|
EXPECT_FALSE(credentials_verify_response(&v, NULL, snonce, cnonce, proof, server_sig));
|
|
EXPECT_FALSE(credentials_verify_response(&v, KAT_USER, NULL, cnonce, proof, server_sig));
|
|
EXPECT_FALSE(credentials_verify_response(&v, KAT_USER, snonce, cnonce, NULL, server_sig));
|
|
}
|
|
|
|
static void test_credentials_username_valid() {
|
|
EXPECT_TRUE(credentials_username_valid("alice"));
|
|
EXPECT_TRUE(credentials_username_valid("a"));
|
|
EXPECT_FALSE(credentials_username_valid(NULL));
|
|
EXPECT_FALSE(credentials_username_valid(""));
|
|
EXPECT_FALSE(credentials_username_valid("bad user"));
|
|
EXPECT_FALSE(credentials_username_valid("tab\there"));
|
|
EXPECT_FALSE(credentials_username_valid("nul\nhere"));
|
|
}
|
|
|
|
/* A generated line round-trips through the store parser and verifies with the
|
|
* same password. */
|
|
static void test_credentials_hash_store_line_roundtrip() {
|
|
char line[CREDENTIAL_MAX_LINE];
|
|
EXPECT_TRUE(make_store_line("alice", KAT_PASSWORD, CREDENTIAL_MIN_ITERS, line, sizeof(line)));
|
|
EXPECT_TRUE(strncmp(line, "alice:", 6) == 0);
|
|
EXPECT_TRUE(strstr(line, KAT_NAME_PREFIX) != NULL);
|
|
|
|
char* path = make_tmp_file(line);
|
|
EXPECT_NOT_NULL(path);
|
|
char err[512];
|
|
CredentialStore* store = credentials_load(path, NULL, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
EXPECT_EQ_INT(credentials_store_size(store), 1);
|
|
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)CREDENTIAL_MIN_ITERS);
|
|
uint8_t client_key[CREDENTIAL_KEY_LEN];
|
|
uint8_t stored_key[CREDENTIAL_KEY_LEN];
|
|
uint8_t server_key[CREDENTIAL_KEY_LEN];
|
|
EXPECT_TRUE(
|
|
credentials_compute_keys(KAT_PASSWORD, v.salt, v.iters, client_key, stored_key, server_key));
|
|
EXPECT_TRUE(memcmp(stored_key, v.stored_key, CREDENTIAL_KEY_LEN) == 0);
|
|
EXPECT_TRUE(memcmp(server_key, v.server_key, CREDENTIAL_KEY_LEN) == 0);
|
|
|
|
credentials_free(store);
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_store_parse_valid() {
|
|
char line_alice[CREDENTIAL_MAX_LINE];
|
|
char line_bob[CREDENTIAL_MAX_LINE];
|
|
EXPECT_TRUE(
|
|
make_store_line("alice", KAT_PASSWORD, CREDENTIAL_MIN_ITERS, line_alice, sizeof(line_alice)));
|
|
EXPECT_TRUE(
|
|
make_store_line("bob", "bob-s3cret", CREDENTIAL_MIN_ITERS, line_bob, sizeof(line_bob)));
|
|
char contents[2 * CREDENTIAL_MAX_LINE + 64];
|
|
snprintf(contents, sizeof(contents), "# server credential store\n; comment\n\n%s\n%s\n",
|
|
line_alice, line_bob);
|
|
char* path = make_tmp_file(contents);
|
|
EXPECT_NOT_NULL(path);
|
|
char err[512];
|
|
CredentialStore* store = credentials_load(path, NULL, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
EXPECT_EQ_INT(credentials_store_size(store), 2);
|
|
EXPECT_TRUE(credentials_store_has(store, "alice"));
|
|
EXPECT_TRUE(credentials_store_has(store, "bob"));
|
|
EXPECT_FALSE(credentials_store_has(store, "mallory"));
|
|
|
|
/* Unknown user and off-list user both yield a not-found dummy. */
|
|
const char* module_users[] = {"alice"};
|
|
CredentialVerifier v;
|
|
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
|
EXPECT_FALSE(v.found);
|
|
EXPECT_TRUE(credentials_get_verifier(store, "bob", module_users, 1, &v));
|
|
EXPECT_FALSE(v.found);
|
|
EXPECT_TRUE(credentials_get_verifier(store, "alice", module_users, 1, &v));
|
|
EXPECT_TRUE(v.found);
|
|
/* The dummy keys are fixed (all zero) so they can never authenticate. */
|
|
const uint8_t zero[CREDENTIAL_KEY_LEN] = {0};
|
|
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.server_key, zero, CREDENTIAL_KEY_LEN) == 0);
|
|
/* Miss salt is fresh random on each call. */
|
|
uint8_t salt_a[CREDENTIAL_SALT_LEN];
|
|
uint8_t salt_b[CREDENTIAL_SALT_LEN];
|
|
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
|
memcpy(salt_a, v.salt, sizeof(salt_a));
|
|
EXPECT_TRUE(credentials_get_verifier(store, "mallory", module_users, 1, &v));
|
|
memcpy(salt_b, v.salt, sizeof(salt_b));
|
|
EXPECT_TRUE(memcmp(salt_a, salt_b, sizeof(salt_a)) != 0);
|
|
|
|
credentials_free(store);
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_store_parse_rejects_malformed() {
|
|
/* A valid salt (16 bytes -> 24 b64 chars) / keys (32 bytes -> 44 chars). */
|
|
uint8_t sixteen[CREDENTIAL_SALT_LEN] = {0};
|
|
uint8_t thirtytwo[CREDENTIAL_KEY_LEN] = {0};
|
|
char salt_b64[25];
|
|
char key_b64[45];
|
|
credentials_b64_encode(sixteen, sizeof(sixteen), salt_b64, sizeof(salt_b64));
|
|
credentials_b64_encode(thirtytwo, sizeof(thirtytwo), key_b64, sizeof(key_b64));
|
|
|
|
char below_min[CREDENTIAL_MAX_LINE];
|
|
char above_max[CREDENTIAL_MAX_LINE];
|
|
char short_salt[CREDENTIAL_MAX_LINE];
|
|
char short_key[CREDENTIAL_MAX_LINE];
|
|
char empty_field[CREDENTIAL_MAX_LINE];
|
|
snprintf(below_min, sizeof(below_min), "alice:$fastsync$1$pbkdf2-sha256$99$%s$%s$%s\n", salt_b64,
|
|
key_b64, key_b64);
|
|
snprintf(above_max, sizeof(above_max), "alice:$fastsync$1$pbkdf2-sha256$99999999$%s$%s$%s\n",
|
|
salt_b64, key_b64, key_b64);
|
|
snprintf(short_salt, sizeof(short_salt), "alice:$fastsync$1$pbkdf2-sha256$600000$AAAA$%s$%s\n",
|
|
key_b64, key_b64);
|
|
snprintf(short_key, sizeof(short_key), "alice:$fastsync$1$pbkdf2-sha256$600000$%s$AAAA$%s\n",
|
|
salt_b64, key_b64);
|
|
snprintf(empty_field, sizeof(empty_field), "alice:$fastsync$1$pbkdf2-sha256$600000$%s$%s$\n",
|
|
salt_b64, key_b64);
|
|
|
|
const char* cases[] = {
|
|
"alice\n",
|
|
":anything\n",
|
|
"alice:not-a-verifier\n",
|
|
below_min,
|
|
above_max,
|
|
short_salt,
|
|
short_key,
|
|
empty_field,
|
|
"ali "
|
|
"ce:$fastsync$1$pbkdf2-sha256$600000$AAECAwQFBgcICQoLDA0ODw==$WztIlDcIWhH+"
|
|
"WUq5kVTaTLTrq0rowu31H7qpJ36fkJk=$OPZoc2IQvU28tRk7mlFMWxBHF07/X1qA7kwrHossdqE=\n",
|
|
};
|
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
char* path = make_tmp_file(cases[i]);
|
|
EXPECT_NOT_NULL(path);
|
|
char err[512];
|
|
const CredentialStore* store = credentials_load(path, NULL, err, sizeof(err));
|
|
EXPECT_NULL(store);
|
|
EXPECT_TRUE(err[0] != '\0');
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
}
|
|
|
|
static void test_credentials_store_rejects_legacy_hex() {
|
|
const char* secret = "9b90e524e94995ee4aeae2ee3c428a53405d1e8db147f44facc46797d0caf4c3";
|
|
char contents[CREDENTIAL_MAX_LINE];
|
|
snprintf(contents, sizeof(contents), "alice:%s\n", secret);
|
|
char* path = make_tmp_file(contents);
|
|
EXPECT_NOT_NULL(path);
|
|
char err[512];
|
|
const CredentialStore* store = credentials_load(path, NULL, err, sizeof(err));
|
|
EXPECT_NULL(store);
|
|
EXPECT_TRUE(strstr(err, "legacy") != NULL);
|
|
EXPECT_TRUE(strstr(err, "alice") != NULL);
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_store_duplicate_rejected() {
|
|
char line[CREDENTIAL_MAX_LINE];
|
|
EXPECT_TRUE(make_store_line("alice", KAT_PASSWORD, CREDENTIAL_MIN_ITERS, line, sizeof(line)));
|
|
char contents[2 * CREDENTIAL_MAX_LINE + 8];
|
|
snprintf(contents, sizeof(contents), "%s\n%s\n", line, line);
|
|
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, "duplicate") != NULL);
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_store_parse_missing_file() {
|
|
char err[512];
|
|
const CredentialStore* store =
|
|
credentials_load("/nonexistent/cred-file-xyz", NULL, err, sizeof(err));
|
|
EXPECT_NULL(store);
|
|
EXPECT_TRUE(strstr(err, "cannot open") != NULL);
|
|
}
|
|
|
|
static void test_credentials_store_empty_and_null() {
|
|
char err[512];
|
|
CredentialStore* store = credentials_load(NULL, NULL, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
EXPECT_EQ_INT(credentials_store_size(store), 0);
|
|
credentials_free(store);
|
|
|
|
char* path = make_tmp_file("# nothing here\n; nor here\n");
|
|
EXPECT_NOT_NULL(path);
|
|
store = credentials_load(path, NULL, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
EXPECT_EQ_INT(credentials_store_size(store), 0);
|
|
credentials_free(store);
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_store_overlong_line_rejected() {
|
|
char big[CREDENTIAL_MAX_LINE + 80];
|
|
int n = snprintf(big, sizeof(big), "alice:%s", KAT_NAME_PREFIX);
|
|
memset(big + n, 'a', sizeof(big) - (size_t)n - 1);
|
|
big[sizeof(big) - 2] = '\n';
|
|
big[sizeof(big) - 1] = '\0';
|
|
char* path = make_tmp_file(big);
|
|
EXPECT_NOT_NULL(path);
|
|
char err[512];
|
|
const CredentialStore* store = credentials_load(path, NULL, err, sizeof(err));
|
|
EXPECT_NULL(store);
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_early_input_merge() {
|
|
char alice[CREDENTIAL_MAX_LINE];
|
|
char bob[CREDENTIAL_MAX_LINE];
|
|
EXPECT_TRUE(make_store_line("alice", KAT_PASSWORD, CREDENTIAL_MIN_ITERS, alice, sizeof(alice)));
|
|
EXPECT_TRUE(make_store_line("bob", "bob-s3cret", CREDENTIAL_MIN_ITERS, bob, sizeof(bob)));
|
|
char alice_file[CREDENTIAL_MAX_LINE + 2];
|
|
char bob_file[CREDENTIAL_MAX_LINE + 2];
|
|
char alice_other[CREDENTIAL_MAX_LINE];
|
|
snprintf(alice_file, sizeof(alice_file), "%s\n", alice);
|
|
snprintf(bob_file, sizeof(bob_file), "%s\n", bob);
|
|
EXPECT_TRUE(make_store_line("alice", "different-s3cret", CREDENTIAL_MIN_ITERS, alice_other,
|
|
sizeof(alice_other)));
|
|
|
|
char* pw = make_tmp_file(alice_file);
|
|
char* early = make_tmp_file(bob_file);
|
|
char* early_same = make_tmp_file(alice_file); /* byte-identical verifier dedupes */
|
|
char* early_diff = make_tmp_file(alice_other);
|
|
EXPECT_NOT_NULL(pw);
|
|
EXPECT_NOT_NULL(early);
|
|
EXPECT_NOT_NULL(early_same);
|
|
EXPECT_NOT_NULL(early_diff);
|
|
char err[512];
|
|
|
|
/* A second file adds a new user. */
|
|
CredentialStore* store = credentials_load(pw, early, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
EXPECT_EQ_INT(credentials_store_size(store), 2);
|
|
EXPECT_TRUE(credentials_store_has(store, "alice"));
|
|
EXPECT_TRUE(credentials_store_has(store, "bob"));
|
|
credentials_free(store);
|
|
|
|
/* The same user with the SAME verifier dedupes. */
|
|
store = credentials_load(pw, early_same, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
EXPECT_EQ_INT(credentials_store_size(store), 1);
|
|
credentials_free(store);
|
|
|
|
/* The same user with a DIFFERENT verifier fails closed (ambiguous). */
|
|
store = credentials_load(pw, early_diff, err, sizeof(err));
|
|
EXPECT_NULL(store);
|
|
EXPECT_TRUE(err[0] != '\0');
|
|
|
|
rm_temp(pw);
|
|
rm_temp(early);
|
|
rm_temp(early_same);
|
|
rm_temp(early_diff);
|
|
free(pw);
|
|
free(early);
|
|
free(early_same);
|
|
free(early_diff);
|
|
}
|
|
|
|
static void test_credentials_read_secret_file() {
|
|
char err[512];
|
|
char* user = NULL;
|
|
char* password = NULL;
|
|
|
|
char* path = make_tmp_file("# password file\n"
|
|
"\n"
|
|
"alice:correct horse battery staple\n"
|
|
"ignored:second line\n");
|
|
EXPECT_NOT_NULL(path);
|
|
EXPECT_EQ_INT(credentials_read_secret_file(path, &user, &password, err, sizeof(err)), 0);
|
|
EXPECT_EQ_STR(user, "alice");
|
|
EXPECT_EQ_STR(password, "correct horse battery staple");
|
|
free(user);
|
|
free(password);
|
|
user = password = NULL;
|
|
rm_temp(path);
|
|
free(path);
|
|
|
|
path = make_tmp_file(" bob : s3cret \r\n");
|
|
EXPECT_NOT_NULL(path);
|
|
EXPECT_EQ_INT(credentials_read_secret_file(path, &user, &password, err, sizeof(err)), 0);
|
|
EXPECT_EQ_STR(user, "bob");
|
|
EXPECT_EQ_STR(password, " s3cret ");
|
|
free(user);
|
|
free(password);
|
|
user = password = NULL;
|
|
rm_temp(path);
|
|
free(path);
|
|
|
|
path = make_tmp_file("carol: \n");
|
|
EXPECT_NOT_NULL(path);
|
|
EXPECT_EQ_INT(credentials_read_secret_file(path, &user, &password, err, sizeof(err)), 0);
|
|
EXPECT_EQ_STR(user, "carol");
|
|
EXPECT_EQ_STR(password, " ");
|
|
free(user);
|
|
free(password);
|
|
user = password = NULL;
|
|
rm_temp(path);
|
|
free(path);
|
|
|
|
path = make_tmp_file("");
|
|
EXPECT_NOT_NULL(path);
|
|
EXPECT_EQ_INT(credentials_read_secret_file(path, &user, &password, err, sizeof(err)), -1);
|
|
EXPECT_NULL(user);
|
|
EXPECT_NULL(password);
|
|
EXPECT_TRUE(strstr(err, "no 'user:password'") != NULL);
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_read_secret_file_bad() {
|
|
char err[512];
|
|
const char* cases[] = {
|
|
"alicepassword\n",
|
|
":password\n",
|
|
"alice:\n",
|
|
"alice:\r\n",
|
|
};
|
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
char* path = make_tmp_file(cases[i]);
|
|
EXPECT_NOT_NULL(path);
|
|
char* user = (char*)1;
|
|
char* password = (char*)1;
|
|
EXPECT_EQ_INT(credentials_read_secret_file(path, &user, &password, err, sizeof(err)), -1);
|
|
EXPECT_NULL(user);
|
|
EXPECT_NULL(password);
|
|
EXPECT_TRUE(err[0] != '\0');
|
|
rm_temp(path);
|
|
free(path);
|
|
}
|
|
|
|
char* missing = "/nonexistent/password-file-xyz";
|
|
EXPECT_EQ_INT(credentials_read_secret_file(missing, NULL, NULL, err, sizeof(err)), -1);
|
|
}
|
|
|
|
static void test_credentials_hash_file() {
|
|
char* plaintext = make_tmp_file("# comment\n\n alice :" KAT_PASSWORD "\nbob:bob-s3cret\n");
|
|
EXPECT_NOT_NULL(plaintext);
|
|
FILE* out = tmpfile();
|
|
EXPECT_NOT_NULL(out);
|
|
char err[512];
|
|
EXPECT_EQ_INT(credentials_hash_file(plaintext, CREDENTIAL_MIN_ITERS, out, err, sizeof(err)), 0);
|
|
rewind(out);
|
|
|
|
char line1[CREDENTIAL_MAX_LINE];
|
|
char line2[CREDENTIAL_MAX_LINE];
|
|
EXPECT_NOT_NULL(fgets(line1, sizeof(line1), out));
|
|
EXPECT_NOT_NULL(fgets(line2, sizeof(line2), out));
|
|
EXPECT_NULL(fgets(err, sizeof(err), out)); /* exactly two entries */
|
|
size_t n1 = strlen(line1);
|
|
if (n1 > 0 && line1[n1 - 1] == '\n')
|
|
line1[--n1] = '\0';
|
|
size_t n2 = strlen(line2);
|
|
if (n2 > 0 && line2[n2 - 1] == '\n')
|
|
line2[--n2] = '\0';
|
|
EXPECT_TRUE(strncmp(line1, "alice:", 6) == 0);
|
|
EXPECT_TRUE(strncmp(line2, "bob:", 4) == 0);
|
|
EXPECT_TRUE(strstr(line1, KAT_NAME_PREFIX) != NULL);
|
|
fclose(out);
|
|
|
|
/* The generated lines load as a valid store. */
|
|
char contents[2 * CREDENTIAL_MAX_LINE + 8];
|
|
snprintf(contents, sizeof(contents), "%s\n%s\n", line1, line2);
|
|
char* store_path = make_tmp_file(contents);
|
|
EXPECT_NOT_NULL(store_path);
|
|
CredentialStore* store = credentials_load(store_path, NULL, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
EXPECT_EQ_INT(credentials_store_size(store), 2);
|
|
credentials_free(store);
|
|
rm_temp(store_path);
|
|
free(store_path);
|
|
rm_temp(plaintext);
|
|
free(plaintext);
|
|
|
|
/* An invalid iteration count is refused up front. */
|
|
char* p2 = make_tmp_file("alice:pw\n");
|
|
EXPECT_NOT_NULL(p2);
|
|
FILE* out2 = tmpfile();
|
|
EXPECT_NOT_NULL(out2);
|
|
EXPECT_EQ_INT(credentials_hash_file(p2, 10, out2, err, sizeof(err)), -1);
|
|
EXPECT_TRUE(err[0] != '\0');
|
|
fclose(out2);
|
|
rm_temp(p2);
|
|
free(p2);
|
|
}
|
|
|
|
static void test_credentials_rejects_group_or_other_accessible() {
|
|
char err[512];
|
|
char line[CREDENTIAL_MAX_LINE];
|
|
EXPECT_TRUE(make_store_line("alice", KAT_PASSWORD, CREDENTIAL_MIN_ITERS, line, sizeof(line)));
|
|
char contents[CREDENTIAL_MAX_LINE + 2];
|
|
snprintf(contents, sizeof(contents), "%s\n", line);
|
|
char* path = make_tmp_file(contents);
|
|
EXPECT_NOT_NULL(path);
|
|
|
|
EXPECT_EQ_INT(chmod(path, 0600), 0);
|
|
CredentialStore* store = credentials_load(path, NULL, err, sizeof(err));
|
|
EXPECT_NOT_NULL(store);
|
|
credentials_free(store);
|
|
|
|
EXPECT_EQ_INT(chmod(path, 0640), 0);
|
|
EXPECT_NULL(credentials_load(path, NULL, err, sizeof(err)));
|
|
EXPECT_TRUE(strstr(err, "owner-only") != NULL);
|
|
EXPECT_EQ_INT(chmod(path, 0604), 0);
|
|
EXPECT_NULL(credentials_load(path, NULL, err, sizeof(err)));
|
|
|
|
EXPECT_EQ_INT(chmod(path, 0644), 0);
|
|
char* user = NULL;
|
|
char* password = NULL;
|
|
EXPECT_EQ_INT(credentials_read_secret_file(path, &user, &password, err, sizeof(err)), -1);
|
|
EXPECT_NULL(user);
|
|
EXPECT_NULL(password);
|
|
EXPECT_TRUE(strstr(err, "owner-only") != NULL);
|
|
|
|
char* pw = make_tmp_file("bob:bob-s3cret\n");
|
|
EXPECT_NOT_NULL(pw);
|
|
EXPECT_EQ_INT(chmod(path, 0644), 0);
|
|
EXPECT_NULL(credentials_load(pw, path, err, sizeof(err)));
|
|
|
|
rm_temp(pw);
|
|
rm_temp(path);
|
|
free(pw);
|
|
free(path);
|
|
}
|
|
|
|
static void test_credentials_burn() {
|
|
char secret[32];
|
|
memcpy(secret, "supersecretvalue", 17);
|
|
credentials_burn(secret, 16);
|
|
for (int i = 0; i < 16; i++)
|
|
EXPECT_EQ_INT(secret[i], 0);
|
|
credentials_burn(NULL, 0);
|
|
}
|
|
|
|
void test_credentials(void) {
|
|
test_credentials_secure_equal();
|
|
test_credentials_b64();
|
|
test_credentials_random_bytes();
|
|
test_credentials_compute_keys_kat();
|
|
test_credentials_auth_message_and_proof_kat();
|
|
test_credentials_verify_response_kat();
|
|
test_credentials_username_valid();
|
|
test_credentials_hash_store_line_roundtrip();
|
|
test_credentials_store_parse_valid();
|
|
test_credentials_store_parse_rejects_malformed();
|
|
test_credentials_store_rejects_legacy_hex();
|
|
test_credentials_store_duplicate_rejected();
|
|
test_credentials_store_parse_missing_file();
|
|
test_credentials_store_empty_and_null();
|
|
test_credentials_store_overlong_line_rejected();
|
|
test_credentials_early_input_merge();
|
|
test_credentials_read_secret_file();
|
|
test_credentials_read_secret_file_bad();
|
|
test_credentials_hash_file();
|
|
test_credentials_rejects_group_or_other_accessible();
|
|
test_credentials_burn();
|
|
}
|