v0.0.45 - Display qrexec service name (qubes.NsignerRpc) in signer connection info; use human-readable timestamps in activity log; fix static release build by adding miner.c to Dockerfile.alpine-musl
This commit is contained in:
459
tests/test_mine_event.c
Normal file
459
tests/test_mine_event.c
Normal file
@@ -0,0 +1,459 @@
|
||||
/* Test for mine_event verb (NIP-13 Proof-of-Work) */
|
||||
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
#include <cJSON.h>
|
||||
|
||||
/* from secure_mem.h */
|
||||
|
||||
typedef struct {
|
||||
void *data;
|
||||
size_t size;
|
||||
int locked;
|
||||
} secure_buf_t;
|
||||
|
||||
int secure_buf_alloc(secure_buf_t *buf, size_t size);
|
||||
void secure_buf_free(secure_buf_t *buf);
|
||||
void secure_memzero(void *ptr, size_t len);
|
||||
|
||||
/* from mnemonic.h */
|
||||
|
||||
#define MNEMONIC_MAX_LEN 256
|
||||
|
||||
typedef struct {
|
||||
secure_buf_t buf;
|
||||
int loaded;
|
||||
int word_count;
|
||||
} mnemonic_state_t;
|
||||
|
||||
void mnemonic_init(mnemonic_state_t *state);
|
||||
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
|
||||
void mnemonic_unload(mnemonic_state_t *state);
|
||||
int mnemonic_is_loaded(const mnemonic_state_t *state);
|
||||
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
|
||||
int mnemonic_generate(int word_count, char *out, size_t out_len);
|
||||
|
||||
/* from role_table.h */
|
||||
|
||||
#define ROLE_NAME_MAX 64
|
||||
#define ROLE_PATH_MAX 128
|
||||
#define ROLE_PURPOSE_MAX 32
|
||||
#define ROLE_CURVE_MAX 16
|
||||
#define ROLE_PUBKEY_HEX_MAX 66
|
||||
#define ROLE_TABLE_MAX_ENTRIES 256
|
||||
|
||||
typedef enum {
|
||||
PURPOSE_NOSTR = 0,
|
||||
PURPOSE_BITCOIN,
|
||||
PURPOSE_SSH,
|
||||
PURPOSE_AGE,
|
||||
PURPOSE_FIPS,
|
||||
PURPOSE_UNKNOWN
|
||||
} role_purpose_t;
|
||||
|
||||
typedef enum {
|
||||
CURVE_SECP256K1 = 0,
|
||||
CURVE_ED25519,
|
||||
CURVE_X25519,
|
||||
CURVE_UNKNOWN
|
||||
} role_curve_t;
|
||||
|
||||
typedef enum {
|
||||
SELECTOR_NOSTR_INDEX,
|
||||
SELECTOR_ROLE_PATH
|
||||
} role_selector_type_t;
|
||||
|
||||
typedef struct {
|
||||
char name[ROLE_NAME_MAX];
|
||||
char purpose_str[ROLE_PURPOSE_MAX];
|
||||
char curve_str[ROLE_CURVE_MAX];
|
||||
role_purpose_t purpose;
|
||||
role_curve_t curve;
|
||||
role_selector_type_t selector_type;
|
||||
int nostr_index;
|
||||
char role_path[ROLE_PATH_MAX];
|
||||
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
|
||||
int derived;
|
||||
} role_entry_t;
|
||||
|
||||
typedef struct {
|
||||
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
|
||||
int count;
|
||||
} role_table_t;
|
||||
|
||||
void role_table_init(role_table_t *table);
|
||||
int role_table_add(role_table_t *table, const role_entry_t *entry);
|
||||
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
|
||||
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
|
||||
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
|
||||
role_entry_t *role_table_get_default(role_table_t *table);
|
||||
role_purpose_t role_purpose_from_str(const char *s);
|
||||
role_curve_t role_curve_from_str(const char *s);
|
||||
const char *role_purpose_to_str(role_purpose_t p);
|
||||
const char *role_curve_to_str(role_curve_t c);
|
||||
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
|
||||
|
||||
/* from selector.h */
|
||||
|
||||
#define SELECTOR_OK 0
|
||||
#define SELECTOR_ERR_AMBIGUOUS -1
|
||||
#define SELECTOR_ERR_NOT_FOUND -2
|
||||
#define SELECTOR_ERR_NO_DEFAULT -3
|
||||
|
||||
typedef struct {
|
||||
int has_role;
|
||||
char role_name[ROLE_NAME_MAX];
|
||||
int has_nostr_index;
|
||||
int nostr_index;
|
||||
int has_role_path;
|
||||
char role_path[ROLE_PATH_MAX];
|
||||
} selector_request_t;
|
||||
|
||||
void selector_request_init(selector_request_t *req);
|
||||
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
|
||||
const char *selector_strerror(int err);
|
||||
|
||||
/* from enforcement.h */
|
||||
|
||||
#define ENFORCE_OK 0
|
||||
#define ENFORCE_ERR_PURPOSE -1
|
||||
#define ENFORCE_ERR_CURVE -2
|
||||
#define ENFORCE_ERR_UNKNOWN_VERB -3
|
||||
|
||||
#define VERB_SIGN_EVENT "sign_event"
|
||||
#define VERB_GET_PUBLIC_KEY "get_public_key"
|
||||
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
|
||||
#define VERB_NIP44_DECRYPT "nip44_decrypt"
|
||||
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
|
||||
#define VERB_NIP04_DECRYPT "nip04_decrypt"
|
||||
#define VERB_MINE_EVENT "mine_event"
|
||||
|
||||
int enforce_verb_role(const char *verb, const role_entry_t *role);
|
||||
const char *enforce_strerror(int err);
|
||||
|
||||
/* from policy.h */
|
||||
|
||||
#define POLICY_MAX_ENTRIES 32
|
||||
#define POLICY_MAX_VERBS 16
|
||||
#define POLICY_MAX_ROLES 16
|
||||
#define POLICY_MAX_PURPOSES 8
|
||||
#define POLICY_VERB_MAX_LEN 32
|
||||
#define POLICY_CALLER_MAX_LEN 160
|
||||
|
||||
typedef enum {
|
||||
PROMPT_NEVER = 0,
|
||||
PROMPT_FIRST_PER_BOOT,
|
||||
PROMPT_EVERY_REQUEST,
|
||||
PROMPT_DENY
|
||||
} prompt_mode_t;
|
||||
|
||||
typedef enum {
|
||||
POLICY_SOURCE_DEFAULT = 0,
|
||||
POLICY_SOURCE_PREAPPROVE,
|
||||
POLICY_SOURCE_SESSION_GRANT
|
||||
} policy_source_t;
|
||||
|
||||
typedef struct {
|
||||
char caller[POLICY_CALLER_MAX_LEN];
|
||||
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
|
||||
int verb_count;
|
||||
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
|
||||
int role_count;
|
||||
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
||||
int purpose_count;
|
||||
prompt_mode_t prompt;
|
||||
policy_source_t source;
|
||||
} policy_entry_t;
|
||||
|
||||
typedef struct {
|
||||
policy_entry_t entries[POLICY_MAX_ENTRIES];
|
||||
int count;
|
||||
} policy_table_t;
|
||||
|
||||
#define POLICY_ALLOW 0
|
||||
#define POLICY_DENY -1
|
||||
#define POLICY_PROMPT -2
|
||||
#define POLICY_NO_MATCH -3
|
||||
|
||||
void policy_table_init(policy_table_t *table);
|
||||
void policy_init_default(policy_table_t *table, uid_t owner_uid);
|
||||
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
||||
int policy_check(const policy_table_t *table, const char *caller_id,
|
||||
const char *verb, const char *role_name, const char *purpose,
|
||||
policy_source_t *out_source);
|
||||
prompt_mode_t prompt_mode_from_str(const char *s);
|
||||
const char *prompt_mode_to_str(prompt_mode_t m);
|
||||
|
||||
/* from crypto.h */
|
||||
|
||||
typedef struct {
|
||||
secure_buf_t private_key;
|
||||
unsigned char public_key[32];
|
||||
char pubkey_hex[65];
|
||||
char npub[128];
|
||||
int valid;
|
||||
} derived_key_t;
|
||||
|
||||
typedef struct {
|
||||
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
|
||||
int count;
|
||||
} key_store_t;
|
||||
|
||||
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
|
||||
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
|
||||
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
|
||||
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
|
||||
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
|
||||
char *crypto_mine_event(const key_store_t *store, int role_index,
|
||||
const char *event_json, int difficulty,
|
||||
int threads, int timeout_sec);
|
||||
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
|
||||
const char *recipient_pubkey_hex, const char *plaintext);
|
||||
char *crypto_nip44_decrypt(const key_store_t *store, int role_index,
|
||||
const char *sender_pubkey_hex, const char *ciphertext);
|
||||
char *crypto_nip04_encrypt(const key_store_t *store, int role_index,
|
||||
const char *recipient_pubkey_hex, const char *plaintext);
|
||||
char *crypto_nip04_decrypt(const key_store_t *store, int role_index,
|
||||
const char *sender_pubkey_hex, const char *ciphertext);
|
||||
void crypto_wipe(key_store_t *store);
|
||||
|
||||
/* from dispatcher.h */
|
||||
|
||||
typedef struct {
|
||||
role_table_t *role_table;
|
||||
mnemonic_state_t *mnemonic;
|
||||
key_store_t *key_store;
|
||||
} dispatcher_ctx_t;
|
||||
|
||||
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
|
||||
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
#include <nostr_core/nostr_common.h>
|
||||
#include <nostr_core/nip013.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_passes = 0;
|
||||
static int g_total = 0;
|
||||
|
||||
static void check_condition(const char *name, int condition) {
|
||||
g_total++;
|
||||
if (condition) {
|
||||
printf("PASS: %s\n", name);
|
||||
g_passes++;
|
||||
} else {
|
||||
printf("FAIL: %s\n", name);
|
||||
}
|
||||
}
|
||||
|
||||
static int response_has(const char *response, const char *needle) {
|
||||
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
|
||||
}
|
||||
|
||||
/* Parse a dispatcher response and extract the result object (for mine_event).
|
||||
* Returns a newly-allocated cJSON result object, or NULL on failure.
|
||||
* Caller must delete the returned object. */
|
||||
static cJSON *extract_result_object(const char *response) {
|
||||
cJSON *resp_json = NULL;
|
||||
cJSON *result_str = NULL;
|
||||
cJSON *result_obj = NULL;
|
||||
|
||||
if (response == NULL) return NULL;
|
||||
|
||||
resp_json = cJSON_Parse(response);
|
||||
if (!resp_json) return NULL;
|
||||
|
||||
result_str = cJSON_GetObjectItemCaseSensitive(resp_json, "result");
|
||||
if (!result_str || !cJSON_IsString(result_str)) {
|
||||
cJSON_Delete(resp_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
result_obj = cJSON_Parse(result_str->valuestring);
|
||||
cJSON_Delete(resp_json);
|
||||
return result_obj;
|
||||
}
|
||||
|
||||
/* Check if target_reached in the result object matches expected */
|
||||
static int check_target_reached(const char *response, int expected) {
|
||||
cJSON *result_obj = extract_result_object(response);
|
||||
cJSON *tr;
|
||||
int val = -1;
|
||||
|
||||
if (!result_obj) return 0;
|
||||
|
||||
tr = cJSON_GetObjectItemCaseSensitive(result_obj, "target_reached");
|
||||
if (tr && cJSON_IsBool(tr)) {
|
||||
val = cJSON_IsTrue(tr) ? 1 : 0;
|
||||
}
|
||||
|
||||
cJSON_Delete(result_obj);
|
||||
return (val == expected);
|
||||
}
|
||||
|
||||
static role_entry_t make_nostr_entry(const char *name, int idx) {
|
||||
role_entry_t e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
strncpy(e.name, name, 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 = role_purpose_from_str(e.purpose_str);
|
||||
e.curve = role_curve_from_str(e.curve_str);
|
||||
e.selector_type = SELECTOR_NOSTR_INDEX;
|
||||
e.nostr_index = idx;
|
||||
e.derived = 0;
|
||||
return e;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
role_table_t table;
|
||||
role_entry_t main_role;
|
||||
mnemonic_state_t mnemonic;
|
||||
dispatcher_ctx_t dispatcher;
|
||||
key_store_t key_store;
|
||||
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||
char *resp;
|
||||
int derived;
|
||||
|
||||
role_table_init(&table);
|
||||
main_role = make_nostr_entry("main", 0);
|
||||
role_table_add(&table, &main_role);
|
||||
|
||||
mnemonic_init(&mnemonic);
|
||||
mnemonic_load(&mnemonic, valid_12);
|
||||
|
||||
memset(&key_store, 0, sizeof(key_store));
|
||||
dispatcher_init(&dispatcher, &table, &mnemonic, &key_store);
|
||||
|
||||
derived = nostr_init();
|
||||
check_condition("nostr_init succeeds", derived == 0);
|
||||
|
||||
derived = crypto_derive_all(&key_store, &table, &mnemonic);
|
||||
check_condition("crypto_derive_all derives at least one key", derived >= 1);
|
||||
|
||||
/* 1. mine_event with low difficulty (2) and short timeout (5 sec) — should reach target */
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"1\",\"method\":\"mine_event\",\"params\":["
|
||||
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello pow\\\",\\\"tags\\\":[]}\","
|
||||
"{\"difficulty\":2,\"threads\":2,\"timeout_sec\":5}]}");
|
||||
check_condition("mine_event low difficulty returns result object",
|
||||
response_has(resp, "\"id\":\"1\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
|
||||
check_condition("mine_event low difficulty returns signed event with nonce tag",
|
||||
response_has(resp, "nonce") && response_has(resp, "sig") && response_has(resp, "pubkey"));
|
||||
check_condition("mine_event low difficulty target_reached is true",
|
||||
check_target_reached(resp, 1));
|
||||
free(resp);
|
||||
|
||||
/* 2. mine_event with high difficulty (30) and short timeout (2 sec) — should NOT reach target */
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"2\",\"method\":\"mine_event\",\"params\":["
|
||||
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hard pow\\\",\\\"tags\\\":[]}\","
|
||||
"{\"difficulty\":30,\"threads\":4,\"timeout_sec\":2}]}");
|
||||
check_condition("mine_event high difficulty returns result object",
|
||||
response_has(resp, "\"id\":\"2\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
|
||||
check_condition("mine_event high difficulty target_reached is false",
|
||||
check_target_reached(resp, 0));
|
||||
check_condition("mine_event high difficulty still returns a signed event",
|
||||
response_has(resp, "nonce") && response_has(resp, "sig"));
|
||||
free(resp);
|
||||
|
||||
/* 3. mine_event with only timeout (no difficulty) — should mine for full duration */
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"3\",\"method\":\"mine_event\",\"params\":["
|
||||
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"timeout only\\\",\\\"tags\\\":[]}\","
|
||||
"{\"threads\":2,\"timeout_sec\":2}]}");
|
||||
check_condition("mine_event timeout-only returns result",
|
||||
response_has(resp, "\"id\":\"3\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
|
||||
check_condition("mine_event timeout-only target_reached is false (no target set)",
|
||||
check_target_reached(resp, 0));
|
||||
free(resp);
|
||||
|
||||
/* 4. mine_event with neither difficulty nor timeout — should error */
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"4\",\"method\":\"mine_event\",\"params\":["
|
||||
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"no params\\\",\\\"tags\\\":[]}\","
|
||||
"{\"threads\":2}]}");
|
||||
check_condition("mine_event with no termination condition returns 1007",
|
||||
response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1007") && response_has(resp, "no_termination_condition"));
|
||||
free(resp);
|
||||
|
||||
/* 5. mine_event with only difficulty (no timeout) — should use safety timeout and reach low target */
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"5\",\"method\":\"mine_event\",\"params\":["
|
||||
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"difficulty only\\\",\\\"tags\\\":[]}\","
|
||||
"{\"difficulty\":1,\"threads\":2}]}");
|
||||
check_condition("mine_event difficulty-only returns result and reaches target",
|
||||
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"result\"") && check_target_reached(resp, 1));
|
||||
free(resp);
|
||||
|
||||
/* 6. mine_event with invalid event JSON */
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"6\",\"method\":\"mine_event\",\"params\":["
|
||||
"\"not valid json\","
|
||||
"{\"difficulty\":1,\"timeout_sec\":2}]}");
|
||||
check_condition("mine_event with invalid event returns error",
|
||||
response_has(resp, "\"id\":\"6\"") && (response_has(resp, "\"code\":1008") || response_has(resp, "\"code\":-32602")));
|
||||
free(resp);
|
||||
|
||||
/* 7. Verify the mined event's difficulty via nip013 validation */
|
||||
{
|
||||
cJSON *resp_json, *result_str, *result_obj, *event_str, *event_json, *id_item;
|
||||
const char *id_hex;
|
||||
int pow_diff;
|
||||
|
||||
resp = dispatcher_handle_request(&dispatcher,
|
||||
"{\"id\":\"7\",\"method\":\"mine_event\",\"params\":["
|
||||
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"verify pow\\\",\\\"tags\\\":[]}\","
|
||||
"{\"difficulty\":4,\"threads\":4,\"timeout_sec\":5}]}");
|
||||
|
||||
resp_json = cJSON_Parse(resp);
|
||||
if (resp_json) {
|
||||
result_str = cJSON_GetObjectItemCaseSensitive(resp_json, "result");
|
||||
if (result_str && cJSON_IsString(result_str)) {
|
||||
result_obj = cJSON_Parse(result_str->valuestring);
|
||||
if (result_obj) {
|
||||
event_str = cJSON_GetObjectItemCaseSensitive(result_obj, "event");
|
||||
if (event_str && cJSON_IsString(event_str)) {
|
||||
event_json = cJSON_Parse(event_str->valuestring);
|
||||
if (event_json) {
|
||||
id_item = cJSON_GetObjectItemCaseSensitive(event_json, "id");
|
||||
if (id_item && cJSON_IsString(id_item)) {
|
||||
id_hex = cJSON_GetStringValue(id_item);
|
||||
pow_diff = nostr_calculate_pow_difficulty(id_hex);
|
||||
check_condition("mined event has difficulty >= 4",
|
||||
pow_diff >= 4);
|
||||
} else {
|
||||
check_condition("mined event has difficulty >= 4", 0);
|
||||
}
|
||||
cJSON_Delete(event_json);
|
||||
} else {
|
||||
check_condition("mined event has difficulty >= 4", 0);
|
||||
}
|
||||
} else {
|
||||
check_condition("mined event has difficulty >= 4", 0);
|
||||
}
|
||||
cJSON_Delete(result_obj);
|
||||
} else {
|
||||
check_condition("mined event has difficulty >= 4", 0);
|
||||
}
|
||||
} else {
|
||||
check_condition("mined event has difficulty >= 4", 0);
|
||||
}
|
||||
cJSON_Delete(resp_json);
|
||||
} else {
|
||||
check_condition("mined event has difficulty >= 4", 0);
|
||||
}
|
||||
free(resp);
|
||||
}
|
||||
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
|
||||
printf("%d/%d tests passed\n", g_passes, g_total);
|
||||
return (g_passes == g_total) ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user