1119 lines
38 KiB
Bash
Executable File
1119 lines
38 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# test_n_signer_client.sh — Integration test suite for n_signer_client CLI.
|
|
#
|
|
# Spawns a dedicated nsigner server with a known test mnemonic, runs the
|
|
# full verb surface through build/n_signer_client, and tears down.
|
|
#
|
|
# Usage:
|
|
# make test-n-signer-client
|
|
# # or directly:
|
|
# bash tests/test_n_signer_client.sh
|
|
#
|
|
# Prerequisites:
|
|
# - make dev clients (or at least build/nsigner and build/nsigner_client)
|
|
# - jq (optional, falls back to python3/grep)
|
|
#
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
BUILD_DIR="$PROJECT_DIR/build"
|
|
CLIENT="$BUILD_DIR/nsigner_client"
|
|
SERVER="$BUILD_DIR/nsigner"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
SOCKET_NAME="nsigner_test_client_$$"
|
|
SERVER_PID=""
|
|
PASS_COUNT=0
|
|
FAIL_COUNT=0
|
|
SKIP_COUNT=0
|
|
|
|
# Test event JSON (kind 1, deterministic created_at)
|
|
EVENT_JSON='{"kind":1,"content":"hello from test","tags":[],"created_at":1700000000}'
|
|
|
|
# A known secp256k1 public key for NIP-04/44 tests (64 hex chars).
|
|
# We'll derive this from the signer at runtime, but we also need a peer key.
|
|
# Use a well-known test vector pubkey (any valid 64-hex secp256k1 pubkey).
|
|
# This is the pubkey for "abandon..." mnemonic, role=main path=m/44'/1237'/0'/0/0,
|
|
# but we'll discover it dynamically. For peer operations we can use the same
|
|
# pubkey (encrypt to self).
|
|
PEER_PUBKEY="" # filled at runtime
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool detection
|
|
# ---------------------------------------------------------------------------
|
|
HAS_JQ=0
|
|
HAS_PYTHON=0
|
|
if command -v jq &>/dev/null; then
|
|
HAS_JQ=1
|
|
elif command -v python3 &>/dev/null; then
|
|
HAS_PYTHON=1
|
|
fi
|
|
|
|
json_get() {
|
|
# Usage: json_get <key> <json-string>
|
|
# Returns the string value of <key> from the JSON.
|
|
local key="$1"
|
|
local json="$2"
|
|
if [ "$HAS_JQ" -eq 1 ]; then
|
|
echo "$json" | jq -r ".$key // empty"
|
|
elif [ "$HAS_PYTHON" -eq 1 ]; then
|
|
python3 -c "import sys,json; d=json.loads('$json'); print(d.get('$key',''))"
|
|
else
|
|
# Fallback: grep for "key":"value" pattern
|
|
echo "$json" | grep -o "\"$key\":\"[^\"]*\"" | sed "s/\"$key\":\"//;s/\"//" | head -1
|
|
fi
|
|
}
|
|
|
|
json_has_key() {
|
|
local key="$1"
|
|
local json="$2"
|
|
if [ "$HAS_JQ" -eq 1 ]; then
|
|
echo "$json" | jq -e ". | has(\"$key\")" &>/dev/null
|
|
elif [ "$HAS_PYTHON" -eq 1 ]; then
|
|
python3 -c "import sys,json; d=json.loads('$json'); sys.exit(0 if '$key' in d else 1)"
|
|
else
|
|
echo "$json" | grep -q "\"$key\""
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test harness
|
|
# ---------------------------------------------------------------------------
|
|
print_result() {
|
|
local name="$1"
|
|
local status="$2"
|
|
local detail="${3:-}"
|
|
if [ "$status" = "PASS" ]; then
|
|
echo " PASS $name"
|
|
elif [ "$status" = "SKIP" ]; then
|
|
echo " SKIP $name${detail:+: $detail}"
|
|
else
|
|
echo " FAIL $name${detail:+: $detail}"
|
|
fi
|
|
}
|
|
|
|
pass() {
|
|
local name="$1"
|
|
PASS_COUNT=$((PASS_COUNT + 1))
|
|
print_result "$name" "PASS"
|
|
}
|
|
|
|
fail() {
|
|
local name="$1"
|
|
local detail="${2:-}"
|
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
print_result "$name" "FAIL" "$detail"
|
|
}
|
|
|
|
skip() {
|
|
local name="$1"
|
|
local reason="${2:-}"
|
|
SKIP_COUNT=$((SKIP_COUNT + 1))
|
|
print_result "$name" "SKIP" "$reason"
|
|
}
|
|
|
|
# Run a command, check exit code, and optionally grep stdout.
|
|
# Usage: check_test <test-name> <expected-exit> [<grep-pattern>...]
|
|
check_test() {
|
|
local name="$1"
|
|
local expected_exit="$2"
|
|
shift 2
|
|
local patterns=("$@")
|
|
|
|
# Build the command from remaining args (everything after patterns)
|
|
# We need to be careful: the caller passes the command as the last arguments
|
|
# But we already consumed name and expected_exit. The remaining args are
|
|
# patterns + command. We need to separate them.
|
|
# Actually, let's use a different approach: capture patterns and command separately.
|
|
# We'll use a sentinel approach: patterns end before '--'
|
|
# But that's awkward. Let's just use a simpler helper.
|
|
|
|
# For now, we'll use a simpler inline approach in each test.
|
|
:
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server management
|
|
# ---------------------------------------------------------------------------
|
|
start_server() {
|
|
echo "Starting nsigner server (socket: @$SOCKET_NAME)..."
|
|
|
|
# Build the server if not present
|
|
if [ ! -x "$SERVER" ]; then
|
|
echo "Building nsigner server..."
|
|
(cd "$PROJECT_DIR" && make dev) || {
|
|
echo "ERROR: failed to build nsigner server"
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
# Build the client if not present
|
|
if [ ! -x "$CLIENT" ]; then
|
|
echo "Building n_signer_client..."
|
|
(cd "$PROJECT_DIR" && make clients) || {
|
|
echo "ERROR: failed to build n_signer_client"
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
# Start the server with --mnemonic-stdin and --allow-all
|
|
# Set test env vars so non-interactive prompts auto-allow
|
|
# Note: export is needed so the backgrounded server process inherits it
|
|
export NSIGNER_TEST_NONINTERACTIVE_PROMPT=allow
|
|
echo "$MNEMONIC" | "$SERVER" \
|
|
--socket-name "$SOCKET_NAME" \
|
|
--allow-all \
|
|
--listen unix \
|
|
--mnemonic-stdin &
|
|
SERVER_PID=$!
|
|
|
|
# Wait for the server to be ready by polling /proc/net/unix
|
|
local max_attempts=50
|
|
local attempt=0
|
|
while [ $attempt -lt $max_attempts ]; do
|
|
if grep -q "$SOCKET_NAME" /proc/net/unix 2>/dev/null; then
|
|
echo "Server ready (PID $SERVER_PID, socket @$SOCKET_NAME)"
|
|
return 0
|
|
fi
|
|
sleep 0.1
|
|
attempt=$((attempt + 1))
|
|
done
|
|
|
|
# Fallback: try connecting with the client
|
|
if $CLIENT --socket-name "$SOCKET_NAME" --timeout 2000 get-info &>/dev/null; then
|
|
echo "Server ready (PID $SERVER_PID, socket @$SOCKET_NAME)"
|
|
return 0
|
|
fi
|
|
|
|
echo "ERROR: server did not become ready within ${max_attempts} attempts"
|
|
kill "$SERVER_PID" 2>/dev/null
|
|
SERVER_PID=""
|
|
return 1
|
|
}
|
|
|
|
stop_server() {
|
|
if [ -n "$SERVER_PID" ]; then
|
|
echo "Stopping server (PID $SERVER_PID)..."
|
|
kill "$SERVER_PID" 2>/dev/null
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
SERVER_PID=""
|
|
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
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
test_get_info() {
|
|
echo ""
|
|
echo "=== Basic connectivity ==="
|
|
|
|
# get-info
|
|
local name="get-info returns server metadata"
|
|
local output
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" get-info 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
# The server returns the result as a JSON string with escaped quotes.
|
|
# Grep for field names without surrounding quotes to handle both cases.
|
|
if printf '%s\n' "$output" | grep -q 'name' && \
|
|
printf '%s\n' "$output" | grep -q 'verbs' && \
|
|
printf '%s\n' "$output" | grep -q 'algorithms'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "missing expected fields: $output"
|
|
fi
|
|
}
|
|
|
|
test_get_public_key_nostr() {
|
|
local name="get-public-key (nostr, default role) returns 64 hex chars"
|
|
local output
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
# Should be 64 hex characters
|
|
if printf '%s\n' "$output" | grep -qE '^[0-9a-f]{64}$'; then
|
|
pass "$name"
|
|
PEER_PUBKEY="$output"
|
|
else
|
|
fail "$name" "expected 64 hex chars, got: $output"
|
|
fi
|
|
|
|
name="get-public-key --role main --path \"m/44'/1237'/0'/0/0\" returns 64 hex chars"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -qE '^[0-9a-f]{64}$'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected 64 hex chars, got: $output"
|
|
fi
|
|
|
|
name="get-public-key --role main --path \"m/44'/1237'/0'/0/0\" --format structured returns JSON"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" --format structured get-public-key 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"algorithm"' && \
|
|
printf '%s\n' "$output" | grep -q '"public_key"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected structured JSON, got: $output"
|
|
fi
|
|
}
|
|
|
|
test_sign_event() {
|
|
echo ""
|
|
echo "=== Sign event ==="
|
|
|
|
local name="sign-event from stdin (pipe)"
|
|
local output
|
|
output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" sign-event 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"id"' && \
|
|
printf '%s\n' "$output" | grep -q '"pubkey"' && \
|
|
printf '%s\n' "$output" | grep -q '"sig"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected signed event JSON, got: $output"
|
|
return
|
|
fi
|
|
|
|
# Verify pubkey matches get-public-key output
|
|
local signed_pubkey
|
|
signed_pubkey=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('pubkey',''))" 2>/dev/null)
|
|
local expected_pubkey
|
|
expected_pubkey=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null)
|
|
if [ "$signed_pubkey" = "$expected_pubkey" ]; then
|
|
pass "sign-event pubkey matches get-public-key"
|
|
else
|
|
fail "sign-event pubkey matches get-public-key" "expected $expected_pubkey, got $signed_pubkey"
|
|
fi
|
|
|
|
# Verify sig is 128 hex chars (schnorr signature)
|
|
local sig
|
|
sig=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('sig',''))" 2>/dev/null)
|
|
if echo "$sig" | grep -qE '^[0-9a-f]{128}$'; then
|
|
pass "sign-event sig is 128 hex chars"
|
|
else
|
|
fail "sign-event sig is 128 hex chars" "got length ${#sig}: $sig"
|
|
fi
|
|
|
|
# sign-event from argv
|
|
name="sign-event from argv"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" sign-event "$EVENT_JSON" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"id"' && \
|
|
printf '%s\n' "$output" | grep -q '"pubkey"' && \
|
|
printf '%s\n' "$output" | grep -q '"sig"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected signed event JSON, got: $output"
|
|
fi
|
|
}
|
|
|
|
test_mine_event() {
|
|
echo ""
|
|
echo "=== Mine event ==="
|
|
|
|
local name="mine-event with difficulty 4"
|
|
local output
|
|
# Use a short timeout to avoid hanging
|
|
output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" --difficulty 4 mine-event 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
# The mine-event result wraps the signed event in an "event" field as a JSON string.
|
|
# Check for the wrapper fields and also verify the inner event has id/pubkey/sig.
|
|
if printf '%s\n' "$output" | grep -q '"event"' && \
|
|
printf '%s\n' "$output" | grep -q '"achieved_difficulty"' && \
|
|
printf '%s\n' "$output" | grep -q '"target_reached"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected mined event JSON with event/achieved_difficulty/target_reached, got: $output"
|
|
fi
|
|
}
|
|
|
|
test_nip04_roundtrip() {
|
|
echo ""
|
|
echo "=== NIP-04 encrypt/decrypt round-trip ==="
|
|
|
|
local plaintext="hello_nip04_test"
|
|
local name="nip04-encrypt returns ciphertext"
|
|
local ciphertext
|
|
ciphertext=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip04-encrypt "$PEER_PUBKEY" "$plaintext" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if [ -n "$ciphertext" ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "empty ciphertext"
|
|
return
|
|
fi
|
|
|
|
name="nip04-decrypt recovers plaintext"
|
|
local decrypted
|
|
decrypted=$(echo "$ciphertext" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip04-decrypt "$PEER_PUBKEY" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if [ "$decrypted" = "$plaintext" ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected '$plaintext', got '$decrypted'"
|
|
fi
|
|
}
|
|
|
|
test_nip44_roundtrip() {
|
|
echo ""
|
|
echo "=== NIP-44 encrypt/decrypt round-trip ==="
|
|
|
|
local plaintext="hello_nip44_test"
|
|
local name="nip44-encrypt returns ciphertext"
|
|
local ciphertext
|
|
ciphertext=$($CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip44-encrypt "$PEER_PUBKEY" "$plaintext" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if [ -n "$ciphertext" ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "empty ciphertext"
|
|
return
|
|
fi
|
|
|
|
name="nip44-decrypt recovers plaintext"
|
|
local decrypted
|
|
decrypted=$(echo "$ciphertext" | $CLIENT --socket-name "$SOCKET_NAME" --role main --path "m/44'/1237'/0'/0/0" nip44-decrypt "$PEER_PUBKEY" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if [ "$decrypted" = "$plaintext" ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected '$plaintext', got '$decrypted'"
|
|
fi
|
|
}
|
|
|
|
test_algorithm_verbs() {
|
|
echo ""
|
|
echo "=== Algorithm-based verbs ==="
|
|
|
|
local name="get-public-key --algorithm secp256k1 --index 0"
|
|
local output
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm secp256k1 --index 0 get-public-key 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"algorithm":"secp256k1"' && \
|
|
printf '%s\n' "$output" | grep -q '"public_key"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got: $output"
|
|
fi
|
|
|
|
name="get-public-key --algorithm ed25519 --index 0"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 get-public-key 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"algorithm":"ed25519"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got: $output"
|
|
fi
|
|
|
|
name="sign --algorithm ed25519 --index 0 68656c6c6f"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 sign "68656c6c6f" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"signature"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got: $output"
|
|
return
|
|
fi
|
|
|
|
# Extract the signature for verify test
|
|
local ed_sig
|
|
ed_sig=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('signature',''))" 2>/dev/null)
|
|
|
|
name="verify --algorithm ed25519 --index 0 68656c6c6f (valid)"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 verify "68656c6c6f" "$ed_sig" 2>/dev/null) || {
|
|
local rc=$?
|
|
if [ $rc -eq 1 ]; then
|
|
fail "$name" "signature reported as invalid"
|
|
else
|
|
fail "$name" "exit code $rc"
|
|
fi
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q "valid"; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected 'valid', got: $output"
|
|
fi
|
|
|
|
name="verify --algorithm ed25519 --index 0 68656c6c6f (invalid sig)"
|
|
local wrong_sig="abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01"
|
|
set +e
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 verify "68656c6c6f" "$wrong_sig" 2>/dev/null)
|
|
local rc=$?
|
|
set -e
|
|
if [ $rc -eq 1 ] && printf '%s\n' "$output" | grep -q "invalid"; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected exit 1 + 'invalid', got exit $rc: $output"
|
|
fi
|
|
|
|
name="derive --algorithm secp256k1 --index 0 'test-data'"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm secp256k1 --index 0 derive "test-data" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"digest"'; then
|
|
local digest
|
|
digest=$(printf '%s\n' "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('digest',''))" 2>/dev/null)
|
|
if printf '%s\n' "$digest" | grep -qE '^[0-9a-f]{64}$'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "digest not 64 hex chars: $digest"
|
|
fi
|
|
else
|
|
fail "$name" "no digest field: $output"
|
|
fi
|
|
|
|
name="derive-shared-secret --algorithm x25519 --index 0"
|
|
# Use the nostr pubkey as the peer (it's a valid secp256k1 point, which x25519 can work with)
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm x25519 --index 0 derive-shared-secret "$PEER_PUBKEY" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
# The server returns structured JSON for derive-shared-secret
|
|
if printf '%s\n' "$output" | grep -q '"shared_secret"' && \
|
|
printf '%s\n' "$output" | grep -q '"algorithm"'; then
|
|
local ss
|
|
ss=$(printf '%s\n' "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('shared_secret',''))" 2>/dev/null)
|
|
if printf '%s\n' "$ss" | grep -qE '^[0-9a-f]{64}$'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "shared_secret not 64 hex chars: $ss"
|
|
fi
|
|
else
|
|
fail "$name" "expected structured JSON with shared_secret, got: $output"
|
|
fi
|
|
}
|
|
|
|
test_ml_kem_roundtrip() {
|
|
echo ""
|
|
echo "=== ML-KEM-768 encapsulate/decapsulate round-trip ==="
|
|
|
|
local name="get-public-key --algorithm ml-kem-768 --index 0"
|
|
local output
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-kem-768 --index 0 get-public-key 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"algorithm":"ml-kem-768"' && \
|
|
printf '%s\n' "$output" | grep -q '"public_key"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got: $output"
|
|
return
|
|
fi
|
|
|
|
# Extract the ML-KEM public key
|
|
local mlkem_pubkey
|
|
mlkem_pubkey=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('public_key',''))" 2>/dev/null)
|
|
if [ -z "$mlkem_pubkey" ]; then
|
|
fail "extract ml-kem-768 pubkey" "empty"
|
|
return
|
|
fi
|
|
|
|
name="encapsulate --algorithm ml-kem-768 with peer pubkey"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-kem-768 encapsulate "$mlkem_pubkey" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"ciphertext"' && \
|
|
printf '%s\n' "$output" | grep -q '"shared_secret"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got: $output"
|
|
return
|
|
fi
|
|
|
|
# Extract ciphertext and shared_secret from encapsulate
|
|
local ct enc_ss
|
|
ct=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('ciphertext',''))" 2>/dev/null)
|
|
enc_ss=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('shared_secret',''))" 2>/dev/null)
|
|
|
|
name="decapsulate --algorithm ml-kem-768 --index 0 with ciphertext"
|
|
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-kem-768 --index 0 decapsulate "$ct" 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q '"shared_secret"'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got: $output"
|
|
return
|
|
fi
|
|
|
|
# Verify shared secrets match
|
|
local dec_ss
|
|
dec_ss=$(echo "$output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('shared_secret',''))" 2>/dev/null)
|
|
if [ "$enc_ss" = "$dec_ss" ]; then
|
|
pass "ML-KEM-768 encapsulate/decapsulate shared secrets match"
|
|
else
|
|
fail "ML-KEM-768 encapsulate/decapsulate shared secrets match" "enc=$enc_ss dec=$dec_ss"
|
|
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 ==="
|
|
|
|
local name="encrypt --algorithm otp (base64 plaintext)"
|
|
local plaintext_b64="SGVsbG8gT1RQIQ==" # "Hello OTP!" in base64
|
|
local rc=0
|
|
local stdout_file
|
|
local stderr_file
|
|
|
|
stdout_file=$(mktemp /tmp/otp_stdout_XXXXXX)
|
|
stderr_file=$(mktemp /tmp/otp_stderr_XXXXXX)
|
|
|
|
# Run the command, capturing stdout and stderr separately
|
|
set +e
|
|
"$CLIENT" --socket-name "$SOCKET_NAME" --algorithm otp encrypt "$plaintext_b64" >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
|
|
if [ $rc -ne 0 ]; then
|
|
local stderr_text
|
|
stderr_text=$(cat "$stderr_file")
|
|
rm -f "$stdout_file" "$stderr_file"
|
|
# Check if the error is about missing OTP pad
|
|
if printf '%s\n' "$stderr_text" | grep -qi "otp_pad\|pad_not_bound\|no pad\|not available\|not supported\|not configured"; then
|
|
skip "$name" "OTP pad not available on server"
|
|
return
|
|
fi
|
|
fail "$name" "exit code $rc stderr: $stderr_text"
|
|
return
|
|
fi
|
|
|
|
local ciphertext
|
|
ciphertext=$(cat "$stdout_file")
|
|
rm -f "$stdout_file" "$stderr_file"
|
|
|
|
if [ -n "$ciphertext" ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "empty output"
|
|
return
|
|
fi
|
|
|
|
name="decrypt --algorithm otp"
|
|
stdout_file=$(mktemp /tmp/otp_stdout_XXXXXX)
|
|
stderr_file=$(mktemp /tmp/otp_stderr_XXXXXX)
|
|
|
|
set +e
|
|
"$CLIENT" --socket-name "$SOCKET_NAME" --algorithm otp decrypt "$ciphertext" >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
|
|
local decrypted
|
|
decrypted=$(cat "$stdout_file")
|
|
rm -f "$stdout_file" "$stderr_file"
|
|
|
|
if [ $rc -eq 0 ] && [ -n "$decrypted" ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "exit code $rc output: $decrypted"
|
|
fi
|
|
}
|
|
|
|
test_call_verb() {
|
|
echo ""
|
|
echo "=== Generic call verb ==="
|
|
|
|
local name="call get_info via stdin"
|
|
local output
|
|
output=$(echo '[]' | $CLIENT --socket-name "$SOCKET_NAME" call get_info 2>/dev/null) || {
|
|
fail "$name" "exit code $?"
|
|
return
|
|
}
|
|
if printf '%s\n' "$output" | grep -q 'name' && \
|
|
printf '%s\n' "$output" | grep -q 'verbs'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got: $output"
|
|
fi
|
|
}
|
|
|
|
test_error_cases() {
|
|
echo ""
|
|
echo "=== Error cases ==="
|
|
|
|
local name="No socket found (bogus socket name)"
|
|
local rc=0
|
|
local stderr_file
|
|
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
|
|
set +e
|
|
$CLIENT --socket-name "nonexistent_socket_$$" --timeout 1000 get-info 2>"$stderr_file" >/dev/null
|
|
rc=$?
|
|
set -e
|
|
local stderr_text
|
|
stderr_text=$(cat "$stderr_file")
|
|
rm -f "$stderr_file"
|
|
if [ $rc -ne 0 ] && [ -n "$stderr_text" ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected non-zero exit + stderr, got exit $rc stderr: $stderr_text"
|
|
fi
|
|
|
|
name="--index 5 without --algorithm"
|
|
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
|
|
set +e
|
|
$CLIENT --socket-name "$SOCKET_NAME" --index 5 get-public-key 2>"$stderr_file" >/dev/null
|
|
rc=$?
|
|
set -e
|
|
stderr_text=$(cat "$stderr_file")
|
|
rm -f "$stderr_file"
|
|
if [ $rc -ne 0 ] && printf '%s\n' "$stderr_text" | grep -qi "index.*only valid\|--index"; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected error about --index, got exit $rc: $stderr_text"
|
|
fi
|
|
|
|
name="Unknown verb"
|
|
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
|
|
set +e
|
|
$CLIENT --socket-name "$SOCKET_NAME" nonexistent-verb 2>"$stderr_file" >/dev/null
|
|
rc=$?
|
|
set -e
|
|
stderr_text=$(cat "$stderr_file")
|
|
rm -f "$stderr_file"
|
|
if [ $rc -ne 0 ] && printf '%s\n' "$stderr_text" | grep -qi "unknown verb"; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected unknown verb error, got exit $rc: $stderr_text"
|
|
fi
|
|
|
|
name="verify with malformed signature (exit 2)"
|
|
stderr_file=$(mktemp /tmp/err_stderr_XXXXXX)
|
|
set +e
|
|
$CLIENT --socket-name "$SOCKET_NAME" --algorithm ed25519 --index 0 verify "68656c6c6f" "nothex" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
stderr_text=$(cat "$stderr_file")
|
|
rm -f "$stderr_file"
|
|
# Should be exit 2 (error), not exit 1 (invalid)
|
|
if [ $rc -eq 2 ]; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "expected exit 2 (error), got exit $rc: $stderr_text"
|
|
fi
|
|
}
|
|
|
|
test_auto_discovery() {
|
|
echo ""
|
|
echo "=== Auto-discovery ==="
|
|
|
|
local name="Auto-discover socket (only test signer running)"
|
|
# This is best-effort: if only our test signer is running, it should work.
|
|
# If other signers are running, skip.
|
|
local rc=0
|
|
local stdout_file
|
|
local stderr_file
|
|
stdout_file=$(mktemp /tmp/auto_stdout_XXXXXX)
|
|
stderr_file=$(mktemp /tmp/auto_stderr_XXXXXX)
|
|
|
|
set +e
|
|
$CLIENT get-info >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
|
|
local output
|
|
output=$(cat "$stdout_file")
|
|
local stderr_text
|
|
stderr_text=$(cat "$stderr_file")
|
|
rm -f "$stdout_file" "$stderr_file"
|
|
|
|
if [ $rc -eq 0 ]; then
|
|
if printf '%s\n' "$output" | grep -q 'name'; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "got output but missing 'name' field: $output"
|
|
fi
|
|
else
|
|
if printf '%s\n' "$stderr_text" | grep -qi "multiple"; then
|
|
skip "$name" "multiple signer sockets found"
|
|
else
|
|
skip "$name" "auto-discovery failed: $stderr_text"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
trap cleanup EXIT INT TERM
|
|
|
|
echo "============================================"
|
|
echo " n_signer_client Integration Test Suite"
|
|
echo "============================================"
|
|
echo ""
|
|
|
|
# Start the server
|
|
start_server || {
|
|
echo "FATAL: could not start nsigner 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
|
|
test_sign_event
|
|
test_mine_event
|
|
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 "============================================"
|
|
echo " Results"
|
|
echo "============================================"
|
|
echo " PASS: $PASS_COUNT"
|
|
echo " FAIL: $FAIL_COUNT"
|
|
echo " SKIP: $SKIP_COUNT"
|
|
echo " TOTAL: $((PASS_COUNT + FAIL_COUNT + SKIP_COUNT))"
|
|
echo "============================================"
|
|
|
|
if [ "$FAIL_COUNT" -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|