v0.0.16 - Implement deny-by-default approval model with on-demand nostr_index derivation and --preapprove CLI flag
This commit is contained in:
@@ -73,7 +73,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -149,6 +149,9 @@ const char *role_purpose_to_str(role_purpose_t p);
|
||||
/* Curve enum to string */
|
||||
const char *role_curve_to_str(role_curve_t c);
|
||||
|
||||
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
|
||||
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
|
||||
|
||||
|
||||
/* from selector.h */
|
||||
|
||||
@@ -239,6 +242,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -249,6 +258,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -277,7 +287,8 @@ int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
@@ -310,6 +321,9 @@ typedef struct {
|
||||
* Returns number of keys derived, or -1 on error. */
|
||||
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
|
||||
|
||||
/* Derive key for exactly one role index. Returns 0 on success, -1 on error. */
|
||||
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
|
||||
|
||||
/* Get the derived private key for a role (by table index). Returns NULL if not derived. */
|
||||
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
|
||||
|
||||
@@ -515,6 +529,9 @@ int main(void) {
|
||||
role_table_add(&table, &main_role);
|
||||
role_table_add(&table, &ssh_role);
|
||||
|
||||
check_condition("role_table_register_nostr_index adds index 9",
|
||||
role_table_register_nostr_index(&table, 9) == 0 && role_table_find_by_nostr_index(&table, 9) != NULL);
|
||||
|
||||
mnemonic_init(&mnemonic);
|
||||
mnemonic_load(&mnemonic, valid_12);
|
||||
|
||||
@@ -527,6 +544,19 @@ int main(void) {
|
||||
derived = crypto_derive_all(&key_store, &table, &mnemonic);
|
||||
check_condition("crypto_derive_all derives at least one key", derived >= 1);
|
||||
|
||||
{
|
||||
role_entry_t *r9 = role_table_find_by_nostr_index(&table, 9);
|
||||
int idx9 = (r9 != NULL) ? (int)(r9 - &table.entries[0]) : -1;
|
||||
|
||||
check_condition("new role idx9 exists before derive_one", idx9 >= 0);
|
||||
if (idx9 >= 0) {
|
||||
check_condition("crypto_derive_one derives newly registered role",
|
||||
crypto_derive_one(&key_store, &table, &mnemonic, idx9) == 0 &&
|
||||
table.entries[idx9].derived == 1 &&
|
||||
crypto_get_pubkey_hex(&key_store, idx9) != NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/* 1. Valid get_public_key no selector -> default main, real pubkey */
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}");
|
||||
@@ -603,7 +633,7 @@ int main(void) {
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
|
||||
printf("%d/12 tests passed\n", g_passes);
|
||||
printf("%d/%d tests passed\n", g_passes, g_total);
|
||||
|
||||
return (g_passes == 12 && g_total == 12) ? 0 : 1;
|
||||
return (g_passes == g_total) ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -239,6 +239,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -249,6 +255,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -277,7 +284,8 @@ int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
|
||||
@@ -75,7 +75,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -241,6 +241,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -251,6 +257,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -279,7 +286,8 @@ int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
|
||||
@@ -73,7 +73,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -239,6 +239,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -249,6 +255,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -277,7 +284,8 @@ int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
|
||||
@@ -73,7 +73,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -239,6 +239,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -249,6 +255,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -266,18 +273,25 @@ typedef struct {
|
||||
/* Initialize policy table */
|
||||
void policy_table_init(policy_table_t *table);
|
||||
|
||||
/* Initialize default policy: allow same-uid, deny others */
|
||||
/* Initialize default policy table entries. */
|
||||
void policy_init_default(policy_table_t *table, uid_t owner_uid);
|
||||
|
||||
/* Add a policy entry. Returns 0 on success, -1 if full. */
|
||||
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
|
||||
/* Insert an entry before the final catch-all rule. */
|
||||
int policy_table_insert_before_last(policy_table_t *table, const policy_entry_t *entry);
|
||||
|
||||
/* Parse a --preapprove spec into a policy entry. */
|
||||
int parse_preapprove_spec(const char *spec, policy_entry_t *out_entry, int *out_nostr_index);
|
||||
|
||||
/*
|
||||
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
@@ -486,7 +500,9 @@ static void add_single_entry(policy_table_t *table,
|
||||
|
||||
int main(void) {
|
||||
policy_table_t table;
|
||||
policy_entry_t parsed;
|
||||
int rc;
|
||||
int parsed_nostr_index;
|
||||
uid_t uid = getuid();
|
||||
char same_uid[64];
|
||||
char other_uid[64];
|
||||
@@ -494,74 +510,177 @@ int main(void) {
|
||||
(void)snprintf(same_uid, sizeof(same_uid), "uid:%u", (unsigned int)uid);
|
||||
(void)snprintf(other_uid, sizeof(other_uid), "uid:%u", (unsigned int)(uid + 1U));
|
||||
|
||||
policy_init_default(&table, uid);
|
||||
rc = policy_check(&table, same_uid, "sign_event", "main", "nostr");
|
||||
check_condition("policy_init_default allows same uid", rc == POLICY_ALLOW);
|
||||
rc = parse_preapprove_spec("caller=uid:1000,role=main", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec accepts caller+role", rc == 0);
|
||||
check_condition("parse_preapprove_spec caller set", strcmp(parsed.caller, "uid:1000") == 0);
|
||||
check_condition("parse_preapprove_spec role set", strcmp(parsed.roles[0], "main") == 0);
|
||||
check_condition("parse_preapprove_spec role_count=1", parsed.role_count == 1);
|
||||
check_condition("parse_preapprove_spec prompt=never", parsed.prompt == PROMPT_NEVER);
|
||||
check_condition("parse_preapprove_spec nostr_index unchanged for role", parsed_nostr_index == -1);
|
||||
|
||||
rc = policy_check(&table, other_uid, "sign_event", "main", "nostr");
|
||||
check_condition("policy_init_default denies different uid", rc == POLICY_DENY);
|
||||
rc = parse_preapprove_spec("caller=uid:1000,nostr_index=0", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec accepts caller+nostr_index", rc == 0);
|
||||
check_condition("parse_preapprove_spec nostr_index=0 maps to main", strcmp(parsed.roles[0], "main") == 0);
|
||||
check_condition("parse_preapprove_spec returns parsed nostr_index", parsed_nostr_index == 0);
|
||||
|
||||
rc = parse_preapprove_spec("caller=uid:1000,nostr_index=2", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec maps nonzero nostr_index role", strcmp(parsed.roles[0], "nostr_idx_2") == 0);
|
||||
check_condition("parse_preapprove_spec stores nonzero nostr_index", parsed_nostr_index == 2);
|
||||
|
||||
rc = parse_preapprove_spec("role=main", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec rejects missing caller", rc == -1);
|
||||
rc = parse_preapprove_spec("caller=uid:1000", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec rejects missing role and nostr_index", rc == -1);
|
||||
rc = parse_preapprove_spec("caller=uid:1000,role=main,nostr_index=0", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec rejects both role and nostr_index", rc == -1);
|
||||
rc = parse_preapprove_spec("caller=uid:1000,nostr_index=-1", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec rejects negative nostr_index", rc == -1);
|
||||
rc = parse_preapprove_spec("caller=uid:1000,nostr_index=abc", &parsed, &parsed_nostr_index);
|
||||
check_condition("parse_preapprove_spec rejects non-numeric nostr_index", rc == -1);
|
||||
|
||||
policy_init_default(&table, uid);
|
||||
rc = policy_check(&table, same_uid, "sign_event", "main", "nostr", NULL);
|
||||
check_condition("policy_init_default prompts same uid", rc == POLICY_PROMPT);
|
||||
|
||||
rc = policy_check(&table, other_uid, "sign_event", "main", "nostr", NULL);
|
||||
check_condition("policy_init_default prompts different uid", rc == POLICY_PROMPT);
|
||||
|
||||
/* Insert before catch-all: matching caller allowed, non-matching still prompted */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "*", NULL, NULL, NULL, PROMPT_EVERY_REQUEST);
|
||||
{
|
||||
policy_entry_t grant;
|
||||
memset(&grant, 0, sizeof(grant));
|
||||
strncpy(grant.caller, "uid:1000", sizeof(grant.caller) - 1);
|
||||
strncpy(grant.roles[0], "main", sizeof(grant.roles[0]) - 1);
|
||||
grant.role_count = 1;
|
||||
grant.prompt = PROMPT_NEVER;
|
||||
rc = policy_table_insert_before_last(&table, &grant);
|
||||
check_condition("policy_table_insert_before_last succeeds", rc == 0);
|
||||
}
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("insert_before_last matching caller returns POLICY_ALLOW", rc == POLICY_ALLOW);
|
||||
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("insert_before_last non-matching caller returns POLICY_PROMPT", rc == POLICY_PROMPT);
|
||||
|
||||
/* Preapprove parse + insert: matching caller allowed, non-matching prompted */
|
||||
policy_init_default(&table, uid);
|
||||
rc = parse_preapprove_spec("caller=uid:1000,role=main", &parsed, &parsed_nostr_index);
|
||||
check_condition("preapprove parse for policy insert succeeds", rc == 0);
|
||||
rc = policy_table_insert_before_last(&table, &parsed);
|
||||
check_condition("preapprove insert_before_last succeeds", rc == 0);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("preapprove allows matching caller+role", rc == POLICY_ALLOW);
|
||||
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("preapprove leaves non-matching caller at POLICY_PROMPT", rc == POLICY_PROMPT);
|
||||
|
||||
/* Exact caller, verb, role, purpose + never => allow */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("exact match returns POLICY_ALLOW", rc == POLICY_ALLOW);
|
||||
|
||||
/* Wildcard caller + deny => deny */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "*", "sign_event", "main", "nostr", PROMPT_DENY);
|
||||
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("wildcard caller with deny returns POLICY_DENY", rc == POLICY_DENY);
|
||||
|
||||
/* Caller no match => POLICY_NO_MATCH */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
|
||||
rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("caller mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
|
||||
|
||||
/* Verb not in list => no match */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "get_public_key", "main", "nostr", PROMPT_NEVER);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("verb mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
|
||||
|
||||
/* Role not in list => no match */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "throwaway", "nostr", PROMPT_NEVER);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("role mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
|
||||
|
||||
/* Purpose not in list => no match */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "main", "bitcoin", PROMPT_NEVER);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("purpose mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
|
||||
|
||||
/* Empty verbs list means all verbs */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", NULL, "main", "nostr", PROMPT_NEVER);
|
||||
rc = policy_check(&table, "uid:1000", "nip44_encrypt", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "nip44_encrypt", "main", "nostr", NULL);
|
||||
check_condition("empty verbs list matches any verb", rc == POLICY_ALLOW);
|
||||
|
||||
/* prompt=first_per_boot => POLICY_PROMPT */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_FIRST_PER_BOOT);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("first_per_boot returns POLICY_PROMPT", rc == POLICY_PROMPT);
|
||||
|
||||
/* prompt=every_request => POLICY_PROMPT */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_EVERY_REQUEST);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("every_request returns POLICY_PROMPT", rc == POLICY_PROMPT);
|
||||
|
||||
/* First matching entry wins */
|
||||
policy_table_init(&table);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_DENY);
|
||||
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
|
||||
check_condition("first matching entry wins", rc == POLICY_DENY);
|
||||
|
||||
/* Verify out_source for preapprove, session-grant, and default catch-all */
|
||||
policy_table_init(&table);
|
||||
{
|
||||
policy_entry_t pre;
|
||||
policy_entry_t sess;
|
||||
policy_entry_t prompt_all;
|
||||
policy_source_t src = POLICY_SOURCE_DEFAULT;
|
||||
|
||||
memset(&pre, 0, sizeof(pre));
|
||||
strncpy(pre.caller, "uid:1000", sizeof(pre.caller) - 1);
|
||||
strncpy(pre.roles[0], "main", sizeof(pre.roles[0]) - 1);
|
||||
pre.role_count = 1;
|
||||
pre.prompt = PROMPT_NEVER;
|
||||
pre.source = POLICY_SOURCE_PREAPPROVE;
|
||||
rc = policy_table_add(&table, &pre);
|
||||
check_condition("source test preapprove entry add", rc == 0);
|
||||
|
||||
memset(&sess, 0, sizeof(sess));
|
||||
strncpy(sess.caller, "uid:1001", sizeof(sess.caller) - 1);
|
||||
strncpy(sess.roles[0], "main", sizeof(sess.roles[0]) - 1);
|
||||
sess.role_count = 1;
|
||||
sess.prompt = PROMPT_NEVER;
|
||||
sess.source = POLICY_SOURCE_SESSION_GRANT;
|
||||
rc = policy_table_add(&table, &sess);
|
||||
check_condition("source test session entry add", rc == 0);
|
||||
|
||||
memset(&prompt_all, 0, sizeof(prompt_all));
|
||||
strncpy(prompt_all.caller, "*", sizeof(prompt_all.caller) - 1);
|
||||
prompt_all.prompt = PROMPT_EVERY_REQUEST;
|
||||
prompt_all.source = POLICY_SOURCE_DEFAULT;
|
||||
rc = policy_table_add(&table, &prompt_all);
|
||||
check_condition("source test default catch-all add", rc == 0);
|
||||
|
||||
src = POLICY_SOURCE_DEFAULT;
|
||||
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", &src);
|
||||
check_condition("policy_check reports preapprove source", rc == POLICY_ALLOW && src == POLICY_SOURCE_PREAPPROVE);
|
||||
|
||||
src = POLICY_SOURCE_DEFAULT;
|
||||
rc = policy_check(&table, "uid:1001", "sign_event", "main", "nostr", &src);
|
||||
check_condition("policy_check reports session-grant source", rc == POLICY_ALLOW && src == POLICY_SOURCE_SESSION_GRANT);
|
||||
|
||||
src = POLICY_SOURCE_PREAPPROVE;
|
||||
rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr", &src);
|
||||
check_condition("policy_check resets source for prompt/default", rc == POLICY_PROMPT && src == POLICY_SOURCE_DEFAULT);
|
||||
}
|
||||
|
||||
printf("%d/%d tests passed\n", g_passes, g_total);
|
||||
return (g_passes == g_total) ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -149,6 +149,9 @@ const char *role_purpose_to_str(role_purpose_t p);
|
||||
/* Curve enum to string */
|
||||
const char *role_curve_to_str(role_curve_t c);
|
||||
|
||||
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
|
||||
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
|
||||
|
||||
|
||||
/* from selector.h */
|
||||
|
||||
@@ -239,6 +242,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -249,6 +258,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -277,7 +287,8 @@ int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
@@ -530,40 +541,28 @@ int main(void) {
|
||||
rc = role_table_add(&table, &e_main);
|
||||
check_condition("duplicate name rejection returns -2", rc == -2);
|
||||
|
||||
role_table_init(&table);
|
||||
rc = role_table_register_nostr_index(&table, 7);
|
||||
check_condition("register nostr_index=7 returns 0", rc == 0);
|
||||
found = role_table_find_by_nostr_index(&table, 7);
|
||||
check_condition("register nostr_index=7 creates role", found != NULL);
|
||||
check_condition("registered role name is nostr_idx_7", found != NULL && strcmp(found->name, "nostr_idx_7") == 0);
|
||||
check_condition("registered role purpose nostr", found != NULL && found->purpose == PURPOSE_NOSTR);
|
||||
check_condition("registered role curve secp256k1", found != NULL && found->curve == CURVE_SECP256K1);
|
||||
check_condition("registered role selector type nostr_index", found != NULL && found->selector_type == SELECTOR_NOSTR_INDEX);
|
||||
|
||||
rc = role_table_register_nostr_index(&table, 7);
|
||||
check_condition("register nostr_index=7 idempotent second call", rc == 0);
|
||||
check_condition("register nostr_index=7 idempotent no duplicate", table.count == 1);
|
||||
|
||||
role_table_init(&table);
|
||||
for (i = 0; i < ROLE_TABLE_MAX_ENTRIES; ++i) {
|
||||
role_entry_t e;
|
||||
char name_buf[ROLE_NAME_MAX];
|
||||
|
||||
memset(&e, 0, sizeof(e));
|
||||
snprintf(name_buf, sizeof(name_buf), "role_%d", i);
|
||||
strncpy(e.name, name_buf, sizeof(e.name) - 1);
|
||||
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
|
||||
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
|
||||
e.purpose = PURPOSE_NOSTR;
|
||||
e.curve = CURVE_SECP256K1;
|
||||
e.selector_type = SELECTOR_NOSTR_INDEX;
|
||||
e.nostr_index = i;
|
||||
|
||||
rc = role_table_add(&table, &e);
|
||||
check_condition("fill table role add returns 0", rc == 0);
|
||||
rc = role_table_register_nostr_index(&table, i);
|
||||
check_condition("fill table via register_nostr_index returns 0", rc == 0);
|
||||
}
|
||||
|
||||
{
|
||||
role_entry_t overflow;
|
||||
|
||||
memset(&overflow, 0, sizeof(overflow));
|
||||
strncpy(overflow.name, "overflow", sizeof(overflow.name) - 1);
|
||||
strncpy(overflow.purpose_str, "nostr", sizeof(overflow.purpose_str) - 1);
|
||||
strncpy(overflow.curve_str, "secp256k1", sizeof(overflow.curve_str) - 1);
|
||||
overflow.purpose = PURPOSE_NOSTR;
|
||||
overflow.curve = CURVE_SECP256K1;
|
||||
overflow.selector_type = SELECTOR_NOSTR_INDEX;
|
||||
overflow.nostr_index = 999;
|
||||
|
||||
rc = role_table_add(&table, &overflow);
|
||||
check_condition("table full rejection returns -1", rc == -1);
|
||||
}
|
||||
rc = role_table_register_nostr_index(&table, ROLE_TABLE_MAX_ENTRIES + 7);
|
||||
check_condition("register_nostr_index table full returns -1", rc == -1);
|
||||
|
||||
if (g_failures == 0) {
|
||||
printf("ALL TESTS PASSED\n");
|
||||
|
||||
@@ -73,7 +73,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -239,6 +239,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -249,6 +255,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -277,7 +284,8 @@ int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
|
||||
@@ -73,7 +73,7 @@ int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
||||
#define ROLE_TABLE_MAX_ENTRIES 64
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
/* Purpose enum for fast comparison (string form kept for config/display) */
|
||||
typedef enum {
|
||||
@@ -239,6 +239,12 @@ typedef enum {
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
||||
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
||||
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
||||
} policy_source_t;
|
||||
|
||||
/* A single policy entry */
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
||||
@@ -249,6 +255,7 @@ typedef struct {
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
/* Policy table */
|
||||
@@ -277,7 +284,8 @@ int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
||||
*/
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose);
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
|
||||
/* Parse prompt mode from string */
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
|
||||
Reference in New Issue
Block a user