v0.1.15 - Rename binaries to nsigner/nsigner_client, integrate client into release pipeline, update role+path authorization model with wizard presets and wildcard path support
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Executable
+821
@@ -0,0 +1,821 @@
|
||||
#!/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
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
stop_server
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# 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_otp_encrypt_decrypt
|
||||
test_call_verb
|
||||
test_error_cases
|
||||
test_auto_discovery
|
||||
|
||||
# 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
|
||||
Reference in New Issue
Block a user