feat: password Realm, single-chair login, shared parliament context (0.9.12)
Derive Realm keys from a user password (Argon2id, no private key on disk). Login claims the sole chair with handover; peers share parliament context; --new-project clears it. CLI peer/discover helpers and Docker alpine recipe included; headless empty-answer retries raised for unattended runs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+173
@@ -213,6 +213,101 @@ int cst_realm_fingerprint(const uint8_t pk[32], char out[33]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_realm_derive_password(cst_realm_keys_t *out, const char *password,
|
||||
const uint8_t salt[16]) {
|
||||
if (!out || !password || !password[0] || !salt)
|
||||
return -1;
|
||||
crypto_argon2_config cfg = {
|
||||
.algorithm = CRYPTO_ARGON2_ID,
|
||||
.nb_blocks = 8192, /* 8 MiB */
|
||||
.nb_passes = 3,
|
||||
.nb_lanes = 1,
|
||||
};
|
||||
crypto_argon2_inputs in = {
|
||||
.pass = (const uint8_t *)password,
|
||||
.salt = salt,
|
||||
.pass_size = (uint32_t)strlen(password),
|
||||
.salt_size = 16,
|
||||
};
|
||||
void *work = malloc((size_t)cfg.nb_blocks * 1024);
|
||||
if (!work)
|
||||
return -1;
|
||||
uint8_t seed[32];
|
||||
crypto_argon2(seed, 32, work, cfg, in, crypto_argon2_no_extras);
|
||||
free(work);
|
||||
int rc = cst_realm_derive(out, seed);
|
||||
crypto_wipe(seed, sizeof(seed));
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cst_realm_is_password_mode(const char *dir) {
|
||||
if (!dir)
|
||||
return 0;
|
||||
char path[OS_PATH_MAX];
|
||||
if (os_path_join(path, sizeof(path), dir, "password.salt") != 0)
|
||||
return 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return 0;
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int cst_realm_init_password(const char *dir, const char *password) {
|
||||
if (!dir || !password || !password[0])
|
||||
return -1;
|
||||
if (os_mkdir_p(dir, 0700) != 0)
|
||||
return -1;
|
||||
uint8_t salt[16];
|
||||
if (fill_random(salt, sizeof(salt)) != 0)
|
||||
return -1;
|
||||
cst_realm_keys_t k;
|
||||
if (cst_realm_derive_password(&k, password, salt) != 0)
|
||||
return -1;
|
||||
char path[OS_PATH_MAX];
|
||||
if (os_path_join(path, sizeof(path), dir, "password.salt") != 0)
|
||||
return -1;
|
||||
if (write_hex_file(path, salt, 16) != 0)
|
||||
return -1;
|
||||
if (os_path_join(path, sizeof(path), dir, "public.key") != 0)
|
||||
return -1;
|
||||
if (write_hex_file(path, k.pk, 32) != 0)
|
||||
return -1;
|
||||
/* Never persist the seed/private key in password mode. */
|
||||
if (os_path_join(path, sizeof(path), dir, "mode") != 0)
|
||||
return -1;
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f)
|
||||
return -1;
|
||||
fputs("password\n", f);
|
||||
fclose(f);
|
||||
crypto_wipe(k.seed, sizeof(k.seed));
|
||||
crypto_wipe(k.sk, sizeof(k.sk));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_realm_unlock_password(const char *dir, const char *password,
|
||||
cst_realm_keys_t *out) {
|
||||
if (!dir || !password || !out)
|
||||
return -1;
|
||||
char path[OS_PATH_MAX];
|
||||
uint8_t salt[16], pk_file[32];
|
||||
if (os_path_join(path, sizeof(path), dir, "password.salt") != 0 ||
|
||||
read_hex_file(path, salt, 16) != 0)
|
||||
return -1;
|
||||
if (cst_realm_derive_password(out, password, salt) != 0)
|
||||
return -1;
|
||||
if (os_path_join(path, sizeof(path), dir, "public.key") != 0 ||
|
||||
read_hex_file(path, pk_file, 32) != 0 ||
|
||||
memcmp(pk_file, out->pk, 32) != 0) {
|
||||
crypto_wipe(out->seed, sizeof(out->seed));
|
||||
crypto_wipe(out->sk, sizeof(out->sk));
|
||||
memset(out, 0, sizeof(*out));
|
||||
return -1; /* wrong password */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_realm_enroll(const cst_realm_keys_t *realm, const char *instance_id,
|
||||
const char *label, int64_t not_after, cst_realm_cert_t *cert,
|
||||
uint8_t instance_seed[32], uint8_t instance_sk[64]) {
|
||||
@@ -367,3 +462,81 @@ int cst_realm_revoked(const char *dir, const char *instance_id) {
|
||||
fclose(f);
|
||||
return hit;
|
||||
}
|
||||
|
||||
static int instance_dir(const char *instance_id, char *out, size_t n) {
|
||||
char realm[OS_PATH_MAX], base[OS_PATH_MAX];
|
||||
if (cst_realm_dir(realm, sizeof(realm)) != 0)
|
||||
return -1;
|
||||
if (os_path_join(base, sizeof(base), realm, "instances") != 0)
|
||||
return -1;
|
||||
if (os_mkdir_p(base, 0700) != 0)
|
||||
return -1;
|
||||
return os_path_join(out, n, base, instance_id);
|
||||
}
|
||||
|
||||
int cst_realm_instance_save(const char *instance_id, const cst_realm_cert_t *cert,
|
||||
const uint8_t instance_seed[32]) {
|
||||
if (!instance_id || !cert || !instance_seed)
|
||||
return -1;
|
||||
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
|
||||
if (instance_dir(instance_id, dir, sizeof(dir)) != 0 ||
|
||||
os_mkdir_p(dir, 0700) != 0)
|
||||
return -1;
|
||||
char *json = cst_realm_cert_to_json(cert);
|
||||
if (!json)
|
||||
return -1;
|
||||
if (os_path_join(path, sizeof(path), dir, "cert.json") != 0) {
|
||||
free(json);
|
||||
return -1;
|
||||
}
|
||||
FILE *f = fopen(path, "wb");
|
||||
int ok = 0;
|
||||
if (f) {
|
||||
ok = fputs(json, f) >= 0 && fputc('\n', f) >= 0;
|
||||
if (fclose(f) != 0)
|
||||
ok = 0;
|
||||
}
|
||||
free(json);
|
||||
if (!ok)
|
||||
return -1;
|
||||
(void)os_chmod_private(path);
|
||||
if (os_path_join(path, sizeof(path), dir, "seed") != 0)
|
||||
return -1;
|
||||
return write_hex_file(path, instance_seed, 32);
|
||||
}
|
||||
|
||||
int cst_realm_instance_load(const char *instance_id, cst_realm_cert_t *cert,
|
||||
uint8_t instance_sk[64]) {
|
||||
if (!instance_id || !cert || !instance_sk)
|
||||
return -1;
|
||||
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
|
||||
if (instance_dir(instance_id, dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
if (os_path_join(path, sizeof(path), dir, "cert.json") != 0)
|
||||
return -1;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return -1;
|
||||
cst_buf_t b = {0};
|
||||
char chunk[4096];
|
||||
size_t n;
|
||||
while ((n = fread(chunk, 1, sizeof(chunk), f)) && b.len < 256 * 1024)
|
||||
cst_buf_append(&b, chunk, n);
|
||||
fclose(f);
|
||||
int ok = b.buf && cst_realm_cert_from_json(b.buf, cert) == 0;
|
||||
cst_buf_free(&b);
|
||||
if (!ok)
|
||||
return -1;
|
||||
uint8_t seed[32];
|
||||
if (os_path_join(path, sizeof(path), dir, "seed") != 0 ||
|
||||
read_hex_file(path, seed, 32) != 0)
|
||||
return -1;
|
||||
uint8_t seed_copy[32], pk[32];
|
||||
memcpy(seed_copy, seed, 32);
|
||||
crypto_ed25519_key_pair(instance_sk, pk, seed_copy);
|
||||
crypto_wipe(seed_copy, sizeof(seed_copy));
|
||||
crypto_wipe(seed, sizeof(seed));
|
||||
if (memcmp(pk, cert->instance_pk, 32) != 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user