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:
WHC
2026-10-28 23:06:51 +08:00
co-authored by Cursor
parent 08ca613a0e
commit de9ee28773
18 changed files with 1101 additions and 8 deletions
+173
View File
@@ -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;
}