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)
This commit is contained in:
+197
-24
@@ -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 <name>] [--listen|-l <unix|stdio|qrexec|tcp:HOST:PORT>]", program_name);
|
||||
tui_print(" [--preapprove|-p <SPEC>]... [--auth|-a <off|optional|required>]");
|
||||
tui_print(" [--preapprove|-p <SPEC>]... [--register-role <SPEC>]...");
|
||||
tui_print(" [--auth|-a <off|optional|required>]");
|
||||
tui_print(" [--mnemonic-stdin|--mnemonic-fd <N>] [--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=<id>,role=<name> or caller=<id>,nostr_index=<n>");
|
||||
tui_print(" --register-role SPEC");
|
||||
tui_print(" Register a named path-role non-interactively (repeatable)");
|
||||
tui_print(" SPEC: <name>:<curve>:<path-template>");
|
||||
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:
|
||||
* <name>:<curve>:<path-template>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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":"<hex>"}
|
||||
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":"<hex>"}
|
||||
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 "============================================"
|
||||
|
||||
Reference in New Issue
Block a user