From e459e98beb4e7f713d95ba2f9b2026e0bed686da Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 6 Aug 2026 09:11:46 -0400 Subject: [PATCH] v0.1.19 - Added --register-role CLI flag for non-interactive template-role registration; expanded CLI test suite with template-role distinct-pubkey, path-rejection, ml-dsa-65 and slh-dsa-128s sign/verify tests (45 pass, 0 fail) --- src/main.c | 221 ++++++++++++++++++++++--- tests/test_n_signer_client.sh | 297 ++++++++++++++++++++++++++++++++++ 2 files changed, 494 insertions(+), 24 deletions(-) diff --git a/src/main.c b/src/main.c index 894fd68..5473e46 100644 --- a/src/main.c +++ b/src/main.c @@ -813,8 +813,8 @@ int socket_name_random(char *out, size_t out_len); /* Version information (auto-updated by build/version tooling) */ #define NSIGNER_VERSION_MAJOR 0 #define NSIGNER_VERSION_MINOR 1 -#define NSIGNER_VERSION_PATCH 18 -#define NSIGNER_VERSION "v0.1.18" +#define NSIGNER_VERSION_PATCH 19 +#define NSIGNER_VERSION "v0.1.19" /* NSIGNER_HEADERLESS_DECLS_END */ @@ -1349,7 +1349,8 @@ static void print_usage(const char *program_name) { tui_print("nsigner - single-binary signer program"); tui_print("Usage:"); tui_print(" %s [--socket-name|--name|-n ] [--listen|-l ]", program_name); - tui_print(" [--preapprove|-p ]... [--auth|-a ]"); + tui_print(" [--preapprove|-p ]... [--register-role ]..."); + tui_print(" [--auth|-a ]"); tui_print(" [--mnemonic-stdin|--mnemonic-fd ] [--allow-all|-A]"); tui_print(" [--bridge-source-trusted]"); tui_print(" Run signer server (unix mode has TUI)"); @@ -1365,6 +1366,11 @@ static void print_usage(const char *program_name) { tui_print(" --preapprove, -p SPEC"); tui_print(" Pre-approve a caller for a role (repeatable)"); tui_print(" SPEC: caller=,role= or caller=,nostr_index="); + tui_print(" --register-role SPEC"); + tui_print(" Register a named path-role non-interactively (repeatable)"); + tui_print(" SPEC: ::"); + tui_print(" e.g. nostr_range:secp256k1:m/44'/1237'/*'/0/0"); + tui_print(" Suppresses the default 'main' role in non-TTY mode"); tui_print(" --auth, -a MODE Auth envelope policy per listener: off|optional|required"); tui_print(" --mnemonic-stdin Read mnemonic from stdin (one line) at startup"); tui_print(" --mnemonic-fd N Read mnemonic from inherited fd N (one line) at startup"); @@ -2667,6 +2673,145 @@ static role_purpose_t purpose_from_path(const char *path) { return PURPOSE_NOSTR; /* default */ } +/* + * Parse a --register-role spec of the form: + * :: + * and register it into the role table. The curve field may be empty + * (e.g. "name::path") in which case the curve is auto-detected from the + * path prefix via purpose_from_path + the default curve for that purpose. + * + * The path-template uses the same syntax as the TUI wizard: + * - Wildcard: * or *' (any non-negative integer) + * - Range: 0-99 or 0-99' (inclusive range) + * - Set: 1+3+5 or 1+3-5+10 + * - Fixed: a literal path with no variable segment + * + * Returns 0 on success, -1 on parse error. + */ +static int register_role_from_spec(role_table_t *role_table, const char *spec) { + char buf[512]; + char *name, *curve_str, *path_token; + role_curve_t curve; + role_purpose_t purpose; + char template[ROLE_PATH_MAX]; + int range_lo, range_hi; + int allowed_indices[64]; + int allowed_count = 0; + role_entry_t new_role; + + if (role_table == NULL || spec == NULL) { + return -1; + } + + strncpy(buf, spec, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + /* Split on ':' — name:curve:path-template */ + name = buf; + curve_str = strchr(buf, ':'); + if (curve_str == NULL) { + fprintf(stderr, "--register-role: missing ':' separator in '%s'\n", spec); + return -1; + } + *curve_str = '\0'; + curve_str++; + path_token = strchr(curve_str, ':'); + if (path_token == NULL) { + fprintf(stderr, "--register-role: missing path template in '%s' (expected name:curve:path)\n", spec); + return -1; + } + *path_token = '\0'; + path_token++; + + if (name[0] == '\0') { + fprintf(stderr, "--register-role: empty role name in '%s'\n", spec); + return -1; + } + if (path_token[0] == '\0') { + fprintf(stderr, "--register-role: empty path template in '%s'\n", spec); + return -1; + } + + /* Resolve curve: empty string → auto-detect from path */ + if (curve_str[0] == '\0') { + purpose = purpose_from_path(path_token); + /* Default curve per purpose */ + switch (purpose) { + case PURPOSE_SSH: curve = CURVE_ED25519; break; + case PURPOSE_AGE: curve = CURVE_X25519; break; + case PURPOSE_PQ_SIG: + /* 102003 → ml-dsa-65, 102004 → slh-dsa-128s */ + if (strncmp(path_token, "m/44'/102004'", 13) == 0) { + curve = CURVE_SLH_DSA_128S; + } else { + curve = CURVE_ML_DSA_65; + } + break; + case PURPOSE_PQ_KEM: curve = CURVE_ML_KEM_768; break; + default: curve = CURVE_SECP256K1; break; + } + } else { + curve = role_curve_from_str(curve_str); + if (curve == CURVE_UNKNOWN) { + fprintf(stderr, "--register-role: unknown curve '%s' in '%s'\n", curve_str, spec); + return -1; + } + purpose = purpose_from_path(path_token); + } + + /* Parse the path template (wildcard/range/set → %d) */ + if (parse_path_template_for_role(path_token, template, sizeof(template), + &range_lo, &range_hi, + allowed_indices, 64, &allowed_count) != 0) { + fprintf(stderr, "--register-role: invalid path template '%s'\n", path_token); + return -1; + } + + /* Validate purpose+curve combination */ + if (crypto_alg_from_role(curve, purpose) == CRYPTO_ALG_UNKNOWN) { + fprintf(stderr, "--register-role: curve '%s' is not valid for path '%s'\n", + role_curve_to_str(curve), path_token); + return -1; + } + + /* Register the role (no approval required in non-interactive mode) */ + memset(&new_role, 0, sizeof(new_role)); + strncpy(new_role.name, name, sizeof(new_role.name) - 1); + new_role.name[sizeof(new_role.name) - 1] = '\0'; + strncpy(new_role.purpose_str, role_purpose_to_str(purpose), sizeof(new_role.purpose_str) - 1); + new_role.purpose_str[sizeof(new_role.purpose_str) - 1] = '\0'; + strncpy(new_role.curve_str, role_curve_to_str(curve), sizeof(new_role.curve_str) - 1); + new_role.curve_str[sizeof(new_role.curve_str) - 1] = '\0'; + new_role.purpose = purpose; + new_role.curve = curve; + new_role.selector_type = SELECTOR_ROLE_PATH; + strncpy(new_role.role_path, template, sizeof(new_role.role_path) - 1); + new_role.role_path[sizeof(new_role.role_path) - 1] = '\0'; + new_role.nostr_index = -1; + new_role.path_range_lo = range_lo; + new_role.path_range_hi = range_hi; + new_role.path_default_index = -1; + new_role.requires_approval = 0; /* non-interactive: no TUI approval */ + if (allowed_count > 0) { + int copy_n = allowed_count; + if (copy_n > (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0]))) { + copy_n = (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0])); + } + memcpy(new_role.path_allowed_indices, allowed_indices, (size_t)copy_n * sizeof(int)); + new_role.path_allowed_count = copy_n; + } else { + new_role.path_allowed_count = 0; + } + new_role.derived = 0; + + if (role_table_add(role_table, &new_role) != 0) { + fprintf(stderr, "--register-role: failed to register role '%s' (table full or duplicate name)\n", name); + return -1; + } + + return 0; +} + static int prompt_load_mnemonic_tui(mnemonic_state_t *mnemonic) { char phrase[MNEMONIC_MAX_LEN]; @@ -3405,6 +3550,8 @@ int main(int argc, char *argv[]) { int argi = 1; const char *preapprove_specs[POLICY_MAX_ENTRIES]; int preapprove_count = 0; + const char *register_role_specs[ROLE_TABLE_MAX_ENTRIES]; + int register_role_count = 0; int auth_mode = NSIGNER_AUTH_OFF; int auth_skew_seconds = AUTH_DEFAULT_SKEW_SECONDS; int allow_all = 0; @@ -3483,6 +3630,19 @@ int main(int argc, char *argv[]) { argi += 2; continue; } + if (strcmp(argv[argi], "--register-role") == 0) { + if (argi + 1 >= argc) { + fprintf(stderr, "Missing value for %s\n", argv[argi]); + return 1; + } + if (register_role_count >= ROLE_TABLE_MAX_ENTRIES) { + fprintf(stderr, "Too many --register-role entries (max %d)\n", ROLE_TABLE_MAX_ENTRIES); + return 1; + } + register_role_specs[register_role_count++] = argv[argi + 1]; + argi += 2; + continue; + } if (strcmp(argv[argi], "--mnemonic-stdin") == 0) { if (mnemonic_source.kind != MNEMONIC_SOURCE_TUI) { fprintf(stderr, "nsigner: --mnemonic-stdin and --mnemonic-fd are mutually exclusive\n"); @@ -3626,27 +3786,40 @@ int main(int argc, char *argv[]) { return 1; } } else { - /* Non-interactive mode: create a default "main" role */ - role_entry_t role; - memset(&role, 0, sizeof(role)); - strncpy(role.name, "main", sizeof(role.name) - 1); - strncpy(role.purpose_str, "nostr", sizeof(role.purpose_str) - 1); - strncpy(role.curve_str, "secp256k1", sizeof(role.curve_str) - 1); - role.purpose = role_purpose_from_str(role.purpose_str); - role.curve = role_curve_from_str(role.curve_str); - role.selector_type = SELECTOR_ROLE_PATH; - strncpy(role.role_path, "m/44'/1237'/0'/0/0", sizeof(role.role_path) - 1); - role.role_path[sizeof(role.role_path) - 1] = '\0'; - role.nostr_index = -1; - role.path_range_lo = -1; - role.path_range_hi = -1; - role.path_default_index = -1; - role.requires_approval = 1; - role.derived = 0; - if (role_table_add(&role_table, &role) != 0) { - fprintf(stderr, "Failed to initialize default role\n"); - mnemonic_unload(&mnemonic); - return 1; + /* Non-interactive mode. If --register-role specs were provided, + * register those roles (suppressing the default "main" role). + * Otherwise, create a default "main" role with the standard + * NIP-06 path. */ + if (register_role_count > 0) { + for (int i = 0; i < register_role_count; ++i) { + if (register_role_from_spec(&role_table, register_role_specs[i]) != 0) { + fprintf(stderr, "Failed to register role from --register-role spec\n"); + mnemonic_unload(&mnemonic); + return 1; + } + } + } else { + role_entry_t role; + memset(&role, 0, sizeof(role)); + strncpy(role.name, "main", sizeof(role.name) - 1); + strncpy(role.purpose_str, "nostr", sizeof(role.purpose_str) - 1); + strncpy(role.curve_str, "secp256k1", sizeof(role.curve_str) - 1); + role.purpose = role_purpose_from_str(role.purpose_str); + role.curve = role_curve_from_str(role.curve_str); + role.selector_type = SELECTOR_ROLE_PATH; + strncpy(role.role_path, "m/44'/1237'/0'/0/0", sizeof(role.role_path) - 1); + role.role_path[sizeof(role.role_path) - 1] = '\0'; + role.nostr_index = -1; + role.path_range_lo = -1; + role.path_range_hi = -1; + role.path_default_index = -1; + role.requires_approval = 1; + role.derived = 0; + if (role_table_add(&role_table, &role) != 0) { + fprintf(stderr, "Failed to initialize default role\n"); + mnemonic_unload(&mnemonic); + return 1; + } } } diff --git a/tests/test_n_signer_client.sh b/tests/test_n_signer_client.sh index 205f176..528bd23 100755 --- a/tests/test_n_signer_client.sh +++ b/tests/test_n_signer_client.sh @@ -206,8 +206,55 @@ stop_server() { fi } +# Second server with --register-role (template roles). Uses a separate socket. +REGROLE_SOCKET_NAME="" +REGROLE_SERVER_PID="" + +start_server_regrole() { + REGROLE_SOCKET_NAME="nsigner_test_regrole_$$" + echo "Starting nsigner server with --register-role (socket: @$REGROLE_SOCKET_NAME)..." + + export NSIGNER_TEST_NONINTERACTIVE_PROMPT=allow + echo "$MNEMONIC" | "$SERVER" \ + --socket-name "$REGROLE_SOCKET_NAME" \ + --allow-all \ + --listen unix \ + --mnemonic-stdin \ + --register-role "nostr_range:secp256k1:m/44'/1237'/*'/0/0" \ + --register-role "ssh_range:ed25519:m/44'/102001'/*'/0'/0'" \ + --register-role "ml_dsa_range:ml-dsa-65:m/44'/102003'/*'/0'/0'" \ + --register-role "slh_dsa_range:slh-dsa-128s:m/44'/102004'/*'/0'/0'" \ + >/dev/null 2>&1 & + REGROLE_SERVER_PID=$! + + local max_attempts=50 + local attempt=0 + while [ $attempt -lt $max_attempts ]; do + if grep -q "$REGROLE_SOCKET_NAME" /proc/net/unix 2>/dev/null; then + echo "Regrole server ready (PID $REGROLE_SERVER_PID, socket @$REGROLE_SOCKET_NAME)" + return 0 + fi + sleep 0.1 + attempt=$((attempt + 1)) + done + echo "ERROR: regrole server did not become ready" + kill "$REGROLE_SERVER_PID" 2>/dev/null + REGROLE_SERVER_PID="" + return 1 +} + +stop_server_regrole() { + if [ -n "$REGROLE_SERVER_PID" ]; then + echo "Stopping regrole server (PID $REGROLE_SERVER_PID)..." + kill "$REGROLE_SERVER_PID" 2>/dev/null + wait "$REGROLE_SERVER_PID" 2>/dev/null || true + REGROLE_SERVER_PID="" + fi +} + cleanup() { stop_server + stop_server_regrole } # --------------------------------------------------------------------------- @@ -589,6 +636,243 @@ test_ml_kem_roundtrip() { fi } +# --------------------------------------------------------------------------- +# Template-role tests (require the regrole server with --register-role) +# --------------------------------------------------------------------------- +test_template_role_distinct_pubkeys() { + echo "" + echo "=== Template-role: distinct pubkeys per account index ===" + + # This test catches the caching bug where a template role returned the + # same pubkey for all concrete paths after the first derivation. + local name="nostr_range index 0 returns 64 hex" + local pk0 + pk0=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + if printf '%s\n' "$pk0" | grep -qE '^[0-9a-f]{64}$'; then + pass "$name" + else + fail "$name" "got: $pk0" + return + fi + + local pk1 pk5 + pk1=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/1'/0/0" get-public-key 2>/dev/null) + pk5=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/5'/0/0" get-public-key 2>/dev/null) + + if [ "$pk0" != "$pk1" ]; then + pass "nostr_range index 0 != index 1 (distinct keys)" + else + fail "nostr_range index 0 != index 1 (distinct keys)" "both: $pk0" + fi + + if [ "$pk0" != "$pk5" ] && [ "$pk1" != "$pk5" ]; then + pass "nostr_range index 5 distinct from 0 and 1" + else + fail "nostr_range index 5 distinct from 0 and 1" "pk0=$pk0 pk1=$pk1 pk5=$pk5" + fi + + # Re-request index 0 to confirm it's stable (not affected by caching) + local pk0_again + pk0_again=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) + if [ "$pk0" = "$pk0_again" ]; then + pass "nostr_range index 0 stable on re-request" + else + fail "nostr_range index 0 stable on re-request" "first=$pk0 second=$pk0_again" + fi +} + +test_template_role_path_rejection() { + echo "" + echo "=== Template-role: path validation rejection ===" + + # Hardened tail should be rejected (role template has unhardened 0/0) + local name="reject hardened tail (0'/0') with path_not_allowed" + local output rc + set +e + output=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0'/0'" get-public-key 2>&1) + rc=$? + set -e + if [ $rc -ne 0 ] && printf '%s\n' "$output" | grep -q 'path_not_allowed'; then + pass "$name" + else + fail "$name" "rc=$rc output=$output" + fi + + # Wrong change segment should be rejected + name="reject wrong change segment (1) with path_not_allowed" + set +e + output=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/1/0" get-public-key 2>&1) + rc=$? + set -e + if [ $rc -ne 0 ] && printf '%s\n' "$output" | grep -q 'path_not_allowed'; then + pass "$name" + else + fail "$name" "rc=$rc output=$output" + fi + + # Unknown role should be rejected + name="reject unknown role with unknown_role" + set +e + output=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role bogus --path "m/44'/1237'/0'/0/0" get-public-key 2>&1) + rc=$? + set -e + if [ $rc -ne 0 ] && printf '%s\n' "$output" | grep -q 'unknown_role'; then + pass "$name" + else + fail "$name" "rc=$rc output=$output" + fi +} + +test_template_role_sign_event() { + echo "" + echo "=== Template-role: sign-event with distinct indices ===" + + local pk0 signed_pubkey + pk0=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) + + local name="sign-event via nostr_range index 0" + local output + output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" sign-event 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + signed_pubkey=$(json_get "pubkey" "$output") + if [ "$signed_pubkey" = "$pk0" ]; then + pass "$name pubkey matches get-public-key" + else + fail "$name pubkey matches get-public-key" "expected $pk0, got $signed_pubkey" + fi + + # Sign with index 1 and confirm different pubkey + local pk1 signed_pubkey1 + pk1=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/1'/0/0" get-public-key 2>/dev/null) + output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/1'/0/0" sign-event 2>/dev/null) || { + fail "sign-event via nostr_range index 1" "exit code $?" + return + } + signed_pubkey1=$(json_get "pubkey" "$output") + if [ "$signed_pubkey1" = "$pk1" ] && [ "$signed_pubkey1" != "$signed_pubkey" ]; then + pass "sign-event nostr_range index 1 has correct and distinct pubkey" + else + fail "sign-event nostr_range index 1 has correct and distinct pubkey" "pk1=$pk1 signed=$signed_pubkey1 prev=$signed_pubkey" + fi +} + +test_ml_dsa_65_roundtrip() { + echo "" + echo "=== ML-DSA-65 sign/verify round-trip ===" + + local name="get-public-key --algorithm ml-dsa-65 --index 0" + local output + output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 get-public-key 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + if printf '%s\n' "$output" | grep -q '"algorithm":"ml-dsa-65"' && \ + printf '%s\n' "$output" | grep -q '"public_key"'; then + pass "$name" + else + fail "$name" "got: $output" + return + fi + + name="sign --algorithm ml-dsa-65 --index 0 68656c6c6f" + local sig_raw sig + sig_raw=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 sign "68656c6c6f" 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + # Sign output is JSON: {"algorithm":"...","key_id":"...","signature":""} + sig=$(echo "$sig_raw" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('signature',''))" 2>/dev/null) + if [ -z "$sig" ]; then + # Fallback: maybe raw hex + sig="$sig_raw" + fi + # ML-DSA-65 signatures are 3309 bytes = 6618 hex chars + if printf '%s\n' "$sig" | grep -qE '^[0-9a-f]{6618}$'; then + pass "$name (sig is 6618 hex chars)" + else + fail "$name (sig is 6618 hex chars)" "got length ${#sig}" + return + fi + + name="verify --algorithm ml-dsa-65 --index 0 68656c6c6f (valid)" + output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 verify "68656c6c6f" "$sig" 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + if printf '%s\n' "$output" | grep -qi 'valid\|true\|verified'; then + pass "$name" + else + fail "$name" "got: $output" + fi + + name="verify --algorithm ml-dsa-65 --index 0 68656c6c6f (invalid sig)" + local wrong_sig + wrong_sig=$(printf '%06618s' 0 | tr ' ' '0') + set +e + output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 verify "68656c6c6f" "$wrong_sig" 2>/dev/null) + local rc=$? + set -e + if [ $rc -ne 0 ] || printf '%s\n' "$output" | grep -qi 'invalid\|false\|not'; then + pass "$name" + else + fail "$name" "rc=$rc output=$output" + fi +} + +test_slh_dsa_128s_roundtrip() { + echo "" + echo "=== SLH-DSA-128s sign/verify round-trip ===" + + local name="get-public-key --algorithm slh-dsa-128s --index 0" + local output + output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm slh-dsa-128s --index 0 get-public-key 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + if printf '%s\n' "$output" | grep -q '"algorithm":"slh-dsa-128s"' && \ + printf '%s\n' "$output" | grep -q '"public_key"'; then + pass "$name" + else + fail "$name" "got: $output" + return + fi + + name="sign --algorithm slh-dsa-128s --index 0 68656c6c6f" + local sig_raw sig + sig_raw=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm slh-dsa-128s --index 0 sign "68656c6c6f" 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + # Sign output is JSON: {"algorithm":"...","key_id":"...","signature":""} + sig=$(echo "$sig_raw" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('signature',''))" 2>/dev/null) + if [ -z "$sig" ]; then + sig="$sig_raw" + fi + # SLH-DSA-128s signatures are 7856 bytes = 15712 hex chars + if printf '%s\n' "$sig" | grep -qE '^[0-9a-f]{15712}$'; then + pass "$name (sig is 15712 hex chars)" + else + fail "$name (sig is 15712 hex chars)" "got length ${#sig}" + return + fi + + name="verify --algorithm slh-dsa-128s --index 0 68656c6c6f (valid)" + output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm slh-dsa-128s --index 0 verify "68656c6c6f" "$sig" 2>/dev/null) || { + fail "$name" "exit code $?" + return + } + if printf '%s\n' "$output" | grep -qi 'valid\|true\|verified'; then + pass "$name" + else + fail "$name" "got: $output" + fi +} + test_otp_encrypt_decrypt() { echo "" echo "=== OTP encrypt/decrypt ===" @@ -790,6 +1074,12 @@ start_server || { exit 1 } +# Start a second server with --register-role (template roles) +start_server_regrole || { + echo "FATAL: could not start nsigner regrole server" + exit 1 +} + # Run tests test_get_info test_get_public_key_nostr @@ -799,11 +1089,18 @@ test_nip04_roundtrip test_nip44_roundtrip test_algorithm_verbs test_ml_kem_roundtrip +test_ml_dsa_65_roundtrip +test_slh_dsa_128s_roundtrip test_otp_encrypt_decrypt test_call_verb test_error_cases test_auto_discovery +# Template-role tests (require regrole server) +test_template_role_distinct_pubkeys +test_template_role_path_rejection +test_template_role_sign_event + # Summary echo "" echo "============================================"