From 21f1258844e66a62fe1471278e4ba0dfa21ebfa0 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sat, 2 May 2026 13:46:01 -0400 Subject: [PATCH] v0.0.2 - Add random mnemonic startup flow and multi-instance random socket naming --- .dockerignore | 6 + .gitignore | 1 + .roo/commands/push.md | 7 ++ Dockerfile.alpine-musl | 105 +++++++++-------- Makefile | 31 +++-- README.md | 66 ++++++++--- build_static.sh | 200 ++++++++----------------------- plans/nsigner.md | 76 ++++++++++++ src/bip39_english.c | 12 ++ src/crypto.c | 232 ++++++++++++++++++++++++++++++++++++ src/crypto.h | 66 +++++++++++ src/dispatcher.c | 35 +++++- src/dispatcher.h | 4 +- src/main.c | 247 ++++++++++++++++++++++++++++++++++----- src/main.h | 4 +- src/mnemonic.c | 56 +++++++++ src/mnemonic.h | 4 + src/server.c | 102 ++++++++++++++-- src/server.h | 10 +- src/socket_name.c | 69 +++++++++++ src/socket_name.h | 12 ++ tests/test_dispatcher.c | 36 ++++-- tests/test_integration.c | 10 +- tests/test_mnemonic.c | 18 +++ tests/test_socket_name.c | 51 ++++++++ 25 files changed, 1175 insertions(+), 285 deletions(-) create mode 100644 .dockerignore create mode 100644 .roo/commands/push.md create mode 100644 src/bip39_english.c create mode 100644 src/crypto.c create mode 100644 src/crypto.h create mode 100644 src/socket_name.c create mode 100644 src/socket_name.h create mode 100644 tests/test_socket_name.c diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bfe9de1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +build/ +.git/ +*.o +/*.a +!resources/ +!resources/** diff --git a/.gitignore b/.gitignore index 6aa3326..21d0e3d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ *.tar.gz .gitea_token resources/ +.test_mnemonic diff --git a/.roo/commands/push.md b/.roo/commands/push.md new file mode 100644 index 0000000..8488d18 --- /dev/null +++ b/.roo/commands/push.md @@ -0,0 +1,7 @@ +--- +description: "Brief description of what this command does" +--- + +Run increment_and_push.sh, and supply a good git commit message. For example: + +./increment_and_push.sh "Fixed the bug with nip05 implementation" \ No newline at end of file diff --git a/Dockerfile.alpine-musl b/Dockerfile.alpine-musl index 98d721b..7ddc546 100644 --- a/Dockerfile.alpine-musl +++ b/Dockerfile.alpine-musl @@ -1,77 +1,82 @@ # Alpine-based MUSL static binary builder for nsigner -# Produces portable binaries with zero runtime dependencies - -ARG DEBUG_BUILD=false FROM alpine:3.19 AS builder -ARG DEBUG_BUILD=false - RUN apk add --no-cache \ build-base \ - musl-dev \ + linux-headers \ + openssl-dev \ + openssl-libs-static \ + curl-dev \ + curl-static \ + zlib-static \ + sqlite-dev \ + sqlite-static \ + brotli-static \ + c-ares-static \ + nghttp2-static \ + libidn2-static \ + libpsl-static \ + libunistring-static \ + zstd-static \ git \ - cmake \ - pkgconfig \ autoconf \ automake \ libtool \ - openssl-dev \ - openssl-libs-static \ - zlib-dev \ - zlib-static \ - curl-dev \ - curl-static \ - linux-headers \ - wget \ + pkgconfig \ bash WORKDIR /build -# Build libsecp256k1 static for nostr_core_lib +# Build libsecp256k1 from source with schnorr support RUN cd /tmp && \ - git clone https://github.com/bitcoin-core/secp256k1.git && \ + git clone --depth 1 https://github.com/bitcoin-core/secp256k1.git && \ cd secp256k1 && \ ./autogen.sh && \ - ./configure --enable-static --disable-shared --prefix=/usr CFLAGS="-fPIC" && \ - make -j$(nproc) && \ + ./configure \ + --prefix=/usr \ + --enable-static \ + --disable-shared \ + --enable-module-schnorrsig \ + --enable-module-extrakeys \ + --enable-module-recovery && \ + make -j"$(nproc)" && \ make install && \ rm -rf /tmp/secp256k1 -# Copy and build nostr_core_lib with signer-focused NIPs -COPY nostr_core_lib /build/nostr_core_lib/ -RUN cd nostr_core_lib && \ - chmod +x build.sh && \ - sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \ - rm -f *.o *.a 2>/dev/null || true && \ - ./build.sh --nips=1,6,13,17,19,44,46,59 +# Copy and build nostr_core_lib from project resources +COPY resources/nostr_core_lib /build/nostr_core_lib/ +RUN cd /build/nostr_core_lib && \ + chmod +x ./build.sh && \ + ./build.sh --nips=1,4,6,19,44,46 -# Copy project source +# Copy source files COPY src/ /build/src/ -# Build nsigner static binary -RUN if [ "$DEBUG_BUILD" = "true" ]; then \ - CFLAGS="-g -O2 -DDEBUG -fno-omit-frame-pointer"; \ - STRIP_CMD="echo 'Keeping debug symbols'"; \ - echo "Building debug binary"; \ - else \ - CFLAGS="-O2"; \ - STRIP_CMD="strip /build/nsigner_static"; \ - echo "Building production binary"; \ - fi && \ - gcc -static $CFLAGS -Wall -Wextra -std=c99 \ - -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \ - -Isrc \ - src/main.c \ - -o /build/nsigner_static && \ - eval "$STRIP_CMD" +# Build nsigner as a fully static binary +RUN gcc -static -O2 -Wall -Wextra -std=c99 \ + -I/build/nostr_core_lib \ + -I/build/nostr_core_lib/nostr_core \ + -I/build/nostr_core_lib/cjson \ + /build/src/main.c \ + /build/src/secure_mem.c \ + /build/src/mnemonic.c \ + /build/src/role_table.c \ + /build/src/selector.c \ + /build/src/enforcement.c \ + /build/src/dispatcher.c \ + /build/src/policy.c \ + /build/src/server.c \ + /build/src/crypto.c \ + /build/src/socket_name.c \ + /build/src/bip39_english.c \ + /build/nostr_core_lib/libnostr_core_x64.a \ + -o /build/nsigner_static \ + $(pkg-config --static --libs libcurl openssl) \ + -lsecp256k1 -lsqlite3 -lz -lpthread -lm -RUN echo "=== Binary Information ===" && \ - file /build/nsigner_static && \ - ls -lh /build/nsigner_static && \ - echo "=== Checking for dynamic dependencies ===" && \ - (ldd /build/nsigner_static 2>&1 || echo "Binary is static") && \ - echo "=== Build complete ===" +RUN file /build/nsigner_static && \ + (ldd /build/nsigner_static 2>&1 || true) FROM scratch AS output COPY --from=builder /build/nsigner_static /nsigner_static diff --git a/Makefile b/Makefile index 66f3e90..ff8526f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ CC := gcc -CFLAGS := -Wall -Wextra -std=c99 -O2 -Isrc -Isrc/cjson -LDFLAGS := +CFLAGS := -Wall -Wextra -std=c99 -O2 -Isrc -Isrc/cjson -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson +LDFLAGS := resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1 SRC_DIR := src BUILD_DIR := build @@ -18,7 +18,9 @@ SOURCES := \ $(SRC_DIR)/dispatcher.c \ $(SRC_DIR)/policy.c \ $(SRC_DIR)/server.c \ - $(SRC_DIR)/cjson/cJSON.c + $(SRC_DIR)/crypto.c \ + $(SRC_DIR)/socket_name.c \ + $(SRC_DIR)/bip39_english.c HEADERS := \ $(SRC_DIR)/main.h \ @@ -30,6 +32,8 @@ HEADERS := \ $(SRC_DIR)/dispatcher.h \ $(SRC_DIR)/policy.h \ $(SRC_DIR)/server.h \ + $(SRC_DIR)/crypto.h \ + $(SRC_DIR)/socket_name.h \ $(SRC_DIR)/cjson/cJSON.h TEST_MNEMONIC_TARGET := $(BUILD_DIR)/test_mnemonic @@ -39,12 +43,16 @@ TEST_ENFORCEMENT_TARGET := $(BUILD_DIR)/test_enforcement TEST_DISPATCHER_TARGET := $(BUILD_DIR)/test_dispatcher TEST_POLICY_TARGET := $(BUILD_DIR)/test_policy TEST_INTEGRATION_TARGET := $(BUILD_DIR)/test_integration +TEST_SOCKET_NAME_TARGET := $(BUILD_DIR)/test_socket_name -.PHONY: all dev static static-debug static-arm64 test test-integration test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy clean +.PHONY: all lib dev static static-debug static-arm64 test test-integration test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name clean all: dev -dev: $(TARGET_DEV) +lib: + cd resources/nostr_core_lib && ./build.sh + +dev: lib $(TARGET_DEV) $(TARGET_DEV): $(SOURCES) $(HEADERS) @mkdir -p $(BUILD_DIR) @@ -62,7 +70,7 @@ static-arm64: chmod +x ./build_static.sh ./build_static.sh --arch arm64 -test: test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy +test: lib test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV) ./$(TEST_INTEGRATION_TARGET) @@ -85,6 +93,9 @@ test-dispatcher: $(TEST_DISPATCHER_TARGET) test-policy: $(TEST_POLICY_TARGET) ./$(TEST_POLICY_TARGET) +test-socket-name: $(TEST_SOCKET_NAME_TARGET) + ./$(TEST_SOCKET_NAME_TARGET) + $(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/mnemonic.h $(SRC_DIR)/secure_mem.h @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS) @@ -101,9 +112,9 @@ $(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcemen @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS) -$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/cjson/cJSON.c $(SRC_DIR)/dispatcher.h $(SRC_DIR)/selector.h $(SRC_DIR)/enforcement.h $(SRC_DIR)/role_table.h $(SRC_DIR)/mnemonic.h $(SRC_DIR)/secure_mem.h $(SRC_DIR)/cjson/cJSON.h +$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/crypto.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/dispatcher.h $(SRC_DIR)/crypto.h $(SRC_DIR)/selector.h $(SRC_DIR)/enforcement.h $(SRC_DIR)/role_table.h $(SRC_DIR)/mnemonic.h $(SRC_DIR)/secure_mem.h $(SRC_DIR)/cjson/cJSON.h @mkdir -p $(BUILD_DIR) - $(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/cjson/cJSON.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS) + $(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/crypto.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS) $(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c $(SRC_DIR)/policy.h $(SRC_DIR)/role_table.h @mkdir -p $(BUILD_DIR) @@ -113,5 +124,9 @@ $(TEST_INTEGRATION_TARGET): $(TEST_DIR)/test_integration.c @mkdir -p $(BUILD_DIR) $(CC) $(CFLAGS) $(TEST_DIR)/test_integration.c -o $(TEST_INTEGRATION_TARGET) $(LDFLAGS) +$(TEST_SOCKET_NAME_TARGET): $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c $(SRC_DIR)/socket_name.h + @mkdir -p $(BUILD_DIR) + $(CC) $(CFLAGS) $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c -o $(TEST_SOCKET_NAME_TARGET) $(LDFLAGS) + clean: rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md index e5f7729..5dda800 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,15 @@ This reduces deployment variability and shrinks the runtime trust surface. When started, `n_signer` immediately enters terminal input mode: -1. Prompt for mnemonic (terminal echo disabled). -2. Parse and validate mnemonic. -3. Build in-memory role/selector state. -4. Initialize transport endpoints. -5. Switch to running status display. +1. Choose mnemonic source: `[E]nter existing mnemonic` (default) or `[G]enerate new mnemonic`. + - On `E`: prompt for mnemonic with terminal echo disabled, then validate. + - On `G`: generate a fresh 12-word BIP-39 mnemonic from `getrandom(2)`, display it numbered with a "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN" warning, then continue. There is no confirmation step. +2. Build in-memory role/selector state from the mnemonic. +3. Pick the abstract socket name (random BIP-39 pair, or `--socket-name` override). +4. Initialize transport endpoints and bind the socket. +5. Switch to running status display, with the signer name and socket address shown in the banner. -No startup files are read or written. +No startup files are read or written. The mnemonic — typed or generated — lives only in `mlock`'d memory and is zeroized on shutdown or crash. ### 3.2 Running phase (status display + signer) @@ -207,7 +209,9 @@ This prevents cross-protocol misuse inside one mnemonic-rooted signer process. ### 7.1 Linux desktop: abstract namespace Unix socket -Primary local transport is AF_UNIX abstract namespace (for example `@nsigner`). +Primary local transport is AF_UNIX abstract namespace. + +Each running `nsigner` process binds to a unique abstract name of the form `@nsigner__`, where the two words are picked at random from the BIP-39 English wordlist at startup (e.g. `@nsigner_hairy_dog`). This lets multiple signers coexist on one host. Properties: @@ -215,6 +219,17 @@ Properties: - endpoint lifetime bound to process/kernel namespace - no stale socket files - caller identity via peer credentials (`SO_PEERCRED`) +- per-launch random name avoids collisions between concurrent instances and leaks no seed-derived identifier + +Naming rules: + +- Default: random pick at startup, displayed in the TUI banner. +- Override: `--socket-name ` forces a specific name (useful for scripts and tests). +- Collision: if the chosen random name is already bound by another process, `nsigner` retries with a fresh pair up to 8 times before erroring out with a hint to use `--socket-name`. + +Discovery: + +- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`. ### 7.2 ESP32 MCU: USB-CDC serial @@ -258,23 +273,44 @@ Qubes deployment runs `n_signer` in a dedicated signer qube as a foreground proc nsigner ``` -Program starts in attached foreground mode and prompts for mnemonic. +Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the TUI banner shows the randomly assigned signer name and its abstract socket address. + +To force a specific socket name (e.g. for scripted clients): + +```bash +nsigner --socket-name my_test_signer +``` ### 9.2 Send a request (client mode) -From another terminal: +From another terminal, target the signer by its socket name: ```bash -nsigner client '{"id":"1","method":"get_public_key","params":[]}' +nsigner client --socket-name nsigner_hairy_dog '{"id":"1","method":"get_public_key","params":[]}' ``` +If only one signer is running you can omit the override and the client will use the default discovery rule. + Example signing request: ```bash -nsigner client '{"id":"2","method":"sign_event","params":["",{"role":"main"}]}' +nsigner client --socket-name nsigner_hairy_dog '{"id":"2","method":"sign_event","params":["",{"role":"main"}]}' ``` -### 9.3 Example session +### 9.3 List running signers + +```bash +nsigner list +``` + +Prints the abstract socket names of any currently running `nsigner` instances, e.g.: + +```text +@nsigner_hairy_dog +@nsigner_brave_canyon +``` + +### 9.4 Example session Terminal A: @@ -282,14 +318,16 @@ Terminal A: $ nsigner [unlock] enter mnemonic: [ok] session unlocked -[listen] unix-abstract:@nsigner + signer name : hairy dog + socket : @nsigner_hairy_dog +[listen] unix-abstract:@nsigner_hairy_dog [prompt] caller=uid:1000 method=sign_event role=main -> allow? (y/n) ``` Terminal B: ```text -$ nsigner client '{"id":"2","method":"sign_event","params":["",{"role":"main"}]}' +$ nsigner client --socket-name nsigner_hairy_dog '{"id":"2","method":"sign_event","params":["",{"role":"main"}]}' {"id":"2","result":""} ``` diff --git a/build_static.sh b/build_static.sh index 52f6467..bf8e96c 100755 --- a/build_static.sh +++ b/build_static.sh @@ -1,35 +1,30 @@ #!/bin/bash -# Build fully static MUSL binaries for nsigner using Alpine Docker +# Build fully static MUSL binary for nsigner using Alpine Docker -set -e +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BUILD_DIR="$SCRIPT_DIR/build" DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl" -DEBUG_BUILD=false TARGET_ARCH="" while [[ $# -gt 0 ]]; do case "$1" in - --debug) - DEBUG_BUILD=true - shift - ;; --arch) - if [[ -z "$2" ]]; then + if [[ -z "${2:-}" ]]; then echo "ERROR: --arch requires a value" - echo "Usage: $0 [--debug] [--arch ]" + echo "Usage: $0 [--arch ]" exit 1 fi case "$2" in - arm64|armv7|x86_64) + x86_64|arm64|armv7) TARGET_ARCH="$2" ;; *) echo "ERROR: Unsupported architecture '$2'" - echo "Supported values: arm64, armv7, x86_64" + echo "Supported values: x86_64, arm64, armv7" exit 1 ;; esac @@ -37,48 +32,24 @@ while [[ $# -gt 0 ]]; do ;; *) echo "ERROR: Unknown argument '$1'" - echo "Usage: $0 [--debug] [--arch ]" + echo "Usage: $0 [--arch ]" exit 1 ;; esac done -if [ "$DEBUG_BUILD" = true ]; then - echo "==========================================" - echo "nsigner MUSL Static Binary Builder (DEBUG MODE)" - echo "==========================================" -else - echo "==========================================" - echo "nsigner MUSL Static Binary Builder (PRODUCTION MODE)" - echo "==========================================" -fi -echo "Project directory: $SCRIPT_DIR" -echo "Build directory: $BUILD_DIR" -echo "Debug build: $DEBUG_BUILD" -if [[ -n "$TARGET_ARCH" ]]; then - echo "Requested target architecture: $TARGET_ARCH" -fi -echo "" - -mkdir -p "$BUILD_DIR" - -if ! command -v docker &> /dev/null; then +if ! command -v docker >/dev/null 2>&1; then echo "ERROR: Docker is not installed or not in PATH" exit 1 fi -if ! docker info &> /dev/null; then - echo "ERROR: Docker daemon is not running or user not in docker group" +if ! docker info >/dev/null 2>&1; then + echo "ERROR: Docker daemon is not running or user lacks permissions" exit 1 fi -DOCKER_CMD="docker" - -echo "✓ Docker is available and running" -echo "" - -ARCH=$(uname -m) -case "$ARCH" in +HOST_UNAME="$(uname -m)" +case "$HOST_UNAME" in x86_64) HOST_ARCH="x86_64" ;; @@ -89,147 +60,74 @@ case "$ARCH" in HOST_ARCH="armv7" ;; *) - HOST_ARCH="unknown" + HOST_ARCH="x86_64" ;; esac -if [[ -n "$TARGET_ARCH" ]]; then - ARCH="$TARGET_ARCH" -fi +ARCH="${TARGET_ARCH:-$HOST_ARCH}" case "$ARCH" in x86_64) PLATFORM="linux/amd64" OUTPUT_NAME="nsigner_static_x86_64" ;; - aarch64|arm64) - ARCH="arm64" + arm64) PLATFORM="linux/arm64" OUTPUT_NAME="nsigner_static_arm64" ;; - armv7l|armv7) - ARCH="armv7" + armv7) PLATFORM="linux/arm/v7" OUTPUT_NAME="nsigner_static_armv7" ;; *) - echo "WARNING: Unknown architecture: $ARCH" - echo "Defaulting to linux/amd64" - PLATFORM="linux/amd64" - OUTPUT_NAME="nsigner_static_${ARCH}" + echo "ERROR: Unsupported target architecture '$ARCH'" + exit 1 ;; esac -if [[ "$HOST_ARCH" != "unknown" && "$ARCH" != "$HOST_ARCH" ]]; then - echo "NOTE: Cross-building from host '$HOST_ARCH' to target '$ARCH'." - if ! docker buildx version >/dev/null 2>&1; then - echo "ERROR: docker buildx is required for cross-architecture builds but is not available." - exit 1 +mkdir -p "$BUILD_DIR" + +IMAGE_TAG="nsigner-static-builder:${ARCH}" +CONTAINER_NAME="" +cleanup() { + if [[ -n "$CONTAINER_NAME" ]]; then + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true fi - echo "✓ docker buildx is available for cross-architecture build" - echo "" -fi +} +trap cleanup EXIT -if [ "$DEBUG_BUILD" = true ]; then - OUTPUT_NAME="${OUTPUT_NAME}_debug" -fi - -echo "Building for platform: $PLATFORM" -echo "Output binary: $OUTPUT_NAME" +echo "==========================================" +echo "nsigner static build" +echo "==========================================" +echo "Project root: $SCRIPT_DIR" +echo "Dockerfile: $DOCKERFILE" +echo "Platform: $PLATFORM" +echo "Output: $BUILD_DIR/$OUTPUT_NAME" echo "" -echo "==========================================" -echo "Step 1: Building Alpine Docker image" -echo "==========================================" - -$DOCKER_CMD build \ +echo "[1/3] Building builder stage from project root context" +docker build \ --platform "$PLATFORM" \ - --build-arg DEBUG_BUILD=$DEBUG_BUILD \ - -f "$DOCKERFILE" \ - -t nsigner-musl-builder:latest \ - --progress=plain \ - . - -echo "" -echo "✓ Docker image built successfully" -echo "" - -echo "==========================================" -echo "Step 2: Extracting static binary" -echo "==========================================" - -$DOCKER_CMD build \ - --platform "$PLATFORM" \ - --build-arg DEBUG_BUILD=$DEBUG_BUILD \ --target builder \ -f "$DOCKERFILE" \ - -t nsigner-static-builder-stage:latest \ - . > /dev/null 2>&1 - -CONTAINER_ID=$($DOCKER_CMD create nsigner-static-builder-stage:latest) -$DOCKER_CMD cp "$CONTAINER_ID:/build/nsigner_static" "$BUILD_DIR/$OUTPUT_NAME" -$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null + -t "$IMAGE_TAG" \ + "$SCRIPT_DIR" +echo "[2/3] Extracting static binary" +CONTAINER_NAME="$(docker create "$IMAGE_TAG")" +docker cp "$CONTAINER_NAME:/build/nsigner_static" "$BUILD_DIR/$OUTPUT_NAME" chmod +x "$BUILD_DIR/$OUTPUT_NAME" -echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME" -echo "" +echo "[3/3] Verifying static binary" +file "$BUILD_DIR/$OUTPUT_NAME" -echo "==========================================" -echo "Step 3: Verifying static binary" -echo "==========================================" -echo "" - -if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then - if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then - echo "✓ Binary is fully static" - TRULY_STATIC=true - elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then - echo "✓ Binary is statically linked" - TRULY_STATIC=true - else - echo "⚠ WARNING: Binary may have dynamic dependencies:" - echo "$LDD_OUTPUT" - TRULY_STATIC=false - fi +LDD_OUTPUT="$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)" +echo "$LDD_OUTPUT" +if echo "$LDD_OUTPUT" | grep -Eq "not a dynamic executable|statically linked"; then + echo "Static check: PASS" else - if file "$BUILD_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then - echo "✓ Binary is statically linked (verified with file command)" - TRULY_STATIC=true - else - echo "⚠ Could not verify static linking (ldd check failed)" - TRULY_STATIC=false - fi + echo "Static check: WARNING (verify manually)" fi echo "" -echo "File size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')" -echo "" - -echo "Testing binary execution:" -if "$BUILD_DIR/$OUTPUT_NAME" --version 2>&1 | head -5; then - echo "✓ Binary executes successfully" -else - echo "⚠ Binary execution test failed" -fi - -echo "" -echo "==========================================" -echo "Build Summary" -echo "==========================================" -echo "Binary: $BUILD_DIR/$OUTPUT_NAME" -echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)" -echo "Platform: $PLATFORM" -if [ "$DEBUG_BUILD" = true ]; then - echo "Build Type: DEBUG" -else - echo "Build Type: PRODUCTION" -fi -if [ "$TRULY_STATIC" = true ]; then - echo "Linkage: Fully static binary" -else - echo "Linkage: Static binary (verification inconclusive)" -fi - -echo "" -echo "✓ Build complete!" +echo "Build complete: $BUILD_DIR/$OUTPUT_NAME" diff --git a/plans/nsigner.md b/plans/nsigner.md index a1de5f7..bfe86c1 100644 --- a/plans/nsigner.md +++ b/plans/nsigner.md @@ -68,8 +68,82 @@ This plan adopts a single foreground program model instead of a daemon model to - Single test that launches the program (with mnemonic piped via stdin for automation), sends requests via client subcommand, verifies responses +### Phase G2 — Mnemonic generation at startup + +Goal: let the user generate a brand-new BIP-39 mnemonic at startup instead of typing an existing one. The generated mnemonic is shown once and used for the session; no confirmation step is required. + +Steps: + +- Extend startup TUI: prompt user with `[E]nter existing mnemonic` / `[G]enerate new mnemonic` (default `E`). +- New module function `mnemonic_generate(int word_count, char *out, size_t out_len)` in [`src/mnemonic.c`](../src/mnemonic.c:1): + - `getrandom(2)` for entropy (16 bytes for 12 words, 32 bytes for 24). + - Compute SHA-256 of entropy → first `entropy_bits / 32` checksum bits. + - Concatenate entropy + checksum bits → slice into 11-bit groups. + - Look up each group in the BIP-39 English wordlist → space-separated mnemonic. + - Zeroize the entropy buffer immediately after use. +- Default word count: 12 (no extra prompt). +- TUI display: + - Numbered word list (1..12). + - Loud warning: "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN." + - "Press any key to continue" — explicit no-confirmation policy per spec. +- After display, the in-memory mnemonic is fed straight into the existing seed-derivation path; no separate buffer or persistence layer is introduced. +- Tests in [`tests/test_mnemonic.c`](../tests/test_mnemonic.c:1): + - `mnemonic_generate(12)` returns 12 valid BIP-39 English words. + - `mnemonic_generate(24)` returns 24 valid BIP-39 English words. + - Output validates against the existing `mnemonic_validate` function (round-trip). + - Two consecutive calls return different mnemonics with overwhelming probability. + +### Phase H — Multi-instance signer naming (random BIP-39) + +Goal: allow multiple `nsigner` processes to coexist on one host by giving each instance a unique, human-readable abstract socket name. + +Approach: at startup, pick two random BIP-39 English words and bind to `@nsigner__` (e.g. `@nsigner_hairy_dog`). + +Steps: + +- New module `src/socket_name.{h,c}` + - `int socket_name_random(char *out, size_t out_len);` + - Calls `getrandom(2)` for 3 bytes (24 bits), splits into two 11-bit indices, looks up words from the BIP-39 English wordlist, formats `nsigner__`. +- BIP-39 English wordlist + - Reuse the wordlist already linked via `nostr_core_lib` if exposed; otherwise vendor `src/bip39_english.c` as a 2048-entry `const char *[]`. +- `src/server.c` bind logic + - Accept the resolved name from caller. + - Bind once. On `EADDRINUSE` and no `--socket-name` override, regenerate a fresh random name and retry up to 8 times. With override, fail immediately and report the override conflict. +- `src/main.c` integration + - Resolve name precedence: `--socket-name` (explicit) > random pick. + - After mnemonic acceptance, derive/pick the name and pass it to the server. + - TUI startup banner shows the friendly name and the socket address prominently so the user knows what to point clients at. +- `nsigner list` subcommand + - Reads `/proc/net/unix`, filters entries whose path starts with `\0nsigner_`, prints them as `@nsigner__` along with inode/state if useful. + - No protocol change; purely a discovery helper. +- Tests + - `tests/test_socket_name.c` — format check, both indices land in `[0, 2048)`, two consecutive calls produce different names with overwhelming probability. + - `tests/test_integration.c` — launch with explicit `--socket-name nsigner_test_run` so the integration test is deterministic and isolated. + +Privacy/UX notes: + +- Random names leak nothing about the seed; the trade-off is the user must read the banner each launch to know which socket to address. +- ~4.2M combinations × small concurrent process count = collision essentially never observed in practice; retry path exists for correctness. +- Behavior of `--socket-name` is unchanged; it remains the deterministic override for scripts and tests. + ## 4. Decisions log +### 2026-05-02 — Mnemonic generation at startup + +1. Startup offers `[E]nter` or `[G]enerate` choice; default is `E`. +2. Generated mnemonic uses `getrandom(2)` + BIP-39 English wordlist, default 12 words. +3. Mnemonic is displayed exactly once with a "write this down" warning. +4. **No confirmation step** — user is trusted to copy the words; we proceed straight to session use. +5. The in-memory lifecycle is identical to a typed mnemonic (same secure buffer, same crash-wipe semantics). + +### 2026-05-02 — Random per-launch signer naming + +1. Multiple `nsigner` instances can run concurrently on one host. +2. Default socket name is `@nsigner__` chosen randomly at each launch. +3. `--socket-name ` overrides the random pick for tests and scripted usage. +4. `nsigner list` enumerates running signers via `/proc/net/unix` filtered on `nsigner_` prefix. +5. Random naming was chosen over deterministic-from-mnemonic for v1 to avoid leaking any seed-derived identifier in the abstract namespace; deterministic naming may be revisited later. + ### 2026-05-02 — Program not daemon pivot 1. Single foreground process replaces daemon + TUI + client multi-binary model @@ -102,3 +176,5 @@ This plan adopts a single foreground program model instead of a daemon model to - Execute Phase C (unified main) - Execute Phase D (policy simplification) - Execute Phase E (integration test) +- Execute Phase G2 (mnemonic generation at startup) +- Execute Phase H (multi-instance random naming + `list` subcommand) diff --git a/src/bip39_english.c b/src/bip39_english.c new file mode 100644 index 0000000..91dca90 --- /dev/null +++ b/src/bip39_english.c @@ -0,0 +1,12 @@ +/* + * Placeholder translation unit for BIP-39 English wordlist ownership. + * + * Current implementation uses nostr_core_lib via: + * nostr_bip39_mnemonic_from_bytes(...) + * which already encapsulates the BIP-39 English wordlist. + * + * This file exists to satisfy build/plan structure and future portability work. + */ +int nsigner_bip39_english_placeholder(void) { + return 0; +} diff --git a/src/crypto.c b/src/crypto.c new file mode 100644 index 0000000..bcfa3f7 --- /dev/null +++ b/src/crypto.c @@ -0,0 +1,232 @@ +#define _GNU_SOURCE + +#include "crypto.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#define NSIGNER_ENCRYPT_OUTPUT_MAX 65536 + +int crypto_init(void) { + return nostr_init(); +} + +void crypto_cleanup(void) { + nostr_cleanup(); +} + +int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic) { + int i; + int derived_count; + + if (store == NULL || table == NULL || mnemonic == NULL) { + return -1; + } + + if (!mnemonic_is_loaded(mnemonic)) { + return -1; + } + + crypto_wipe(store); + derived_count = 0; + + for (i = 0; i < table->count; ++i) { + role_entry_t *role = &table->entries[i]; + unsigned char priv[32]; + unsigned char pub[32]; + derived_key_t *dst = &store->keys[i]; + + role->derived = 0; + role->pubkey_hex[0] = '\0'; + + if (role->purpose != PURPOSE_NOSTR || + role->curve != CURVE_SECP256K1 || + role->selector_type != SELECTOR_NOSTR_INDEX) { + continue; + } + + if (secure_buf_alloc(&dst->private_key, 32) != 0) { + crypto_wipe(store); + return -1; + } + + if (nostr_derive_keys_from_mnemonic(mnemonic_get_phrase(mnemonic), role->nostr_index, priv, pub) != 0) { + secure_memzero(priv, sizeof(priv)); + secure_memzero(pub, sizeof(pub)); + crypto_wipe(store); + return -1; + } + + memcpy(dst->private_key.data, priv, 32); + memcpy(dst->public_key, pub, 32); + + nostr_bytes_to_hex(pub, 32, dst->pubkey_hex); + dst->npub[0] = '\0'; + (void)nostr_key_to_bech32(pub, "npub", dst->npub); + + strncpy(role->pubkey_hex, dst->pubkey_hex, sizeof(role->pubkey_hex) - 1); + role->pubkey_hex[sizeof(role->pubkey_hex) - 1] = '\0'; + role->derived = 1; + + dst->valid = 1; + derived_count++; + + secure_memzero(priv, sizeof(priv)); + secure_memzero(pub, sizeof(pub)); + } + + store->count = table->count; + return derived_count; +} + +const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index) { + if (store == NULL || role_index < 0 || role_index >= ROLE_TABLE_MAX_ENTRIES) { + return NULL; + } + + if (!store->keys[role_index].valid || store->keys[role_index].private_key.data == NULL) { + return NULL; + } + + return (const unsigned char *)store->keys[role_index].private_key.data; +} + +const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index) { + if (store == NULL || role_index < 0 || role_index >= ROLE_TABLE_MAX_ENTRIES) { + return NULL; + } + + if (!store->keys[role_index].valid) { + return NULL; + } + + return store->keys[role_index].pubkey_hex; +} + +char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json) { + const unsigned char *private_key; + cJSON *parsed = NULL; + cJSON *kind_item; + cJSON *content_item; + cJSON *tags_item; + cJSON *created_at_item; + cJSON *tags_for_sign = NULL; + int kind; + const char *content; + time_t timestamp = 0; + cJSON *signed_event = NULL; + char *out = NULL; + + if (event_json == NULL) { + return NULL; + } + + private_key = crypto_get_private_key(store, role_index); + if (private_key == NULL) { + return NULL; + } + + parsed = cJSON_Parse(event_json); + if (parsed == NULL || !cJSON_IsObject(parsed)) { + cJSON_Delete(parsed); + return NULL; + } + + kind_item = cJSON_GetObjectItemCaseSensitive(parsed, "kind"); + content_item = cJSON_GetObjectItemCaseSensitive(parsed, "content"); + tags_item = cJSON_GetObjectItemCaseSensitive(parsed, "tags"); + created_at_item = cJSON_GetObjectItemCaseSensitive(parsed, "created_at"); + + if (!cJSON_IsNumber(kind_item) || !cJSON_IsString(content_item) || content_item->valuestring == NULL) { + cJSON_Delete(parsed); + return NULL; + } + + kind = kind_item->valueint; + content = content_item->valuestring; + + if (cJSON_IsNumber(created_at_item)) { + timestamp = (time_t)created_at_item->valuedouble; + } + + if (cJSON_IsArray(tags_item)) { + tags_for_sign = cJSON_Duplicate(tags_item, 1); + if (tags_for_sign == NULL) { + cJSON_Delete(parsed); + return NULL; + } + } + + signed_event = nostr_create_and_sign_event(kind, content, tags_for_sign, private_key, timestamp); + cJSON_Delete(tags_for_sign); + cJSON_Delete(parsed); + + if (signed_event == NULL) { + return NULL; + } + + out = cJSON_PrintUnformatted(signed_event); + cJSON_Delete(signed_event); + return out; +} + +char *crypto_nip44_encrypt(const key_store_t *store, int role_index, + const char *recipient_pubkey_hex, const char *plaintext) { + (void)store; + (void)role_index; + (void)recipient_pubkey_hex; + (void)plaintext; + return NULL; +} + +char *crypto_nip44_decrypt(const key_store_t *store, int role_index, + const char *sender_pubkey_hex, const char *ciphertext) { + (void)store; + (void)role_index; + (void)sender_pubkey_hex; + (void)ciphertext; + return NULL; +} + +char *crypto_nip04_encrypt(const key_store_t *store, int role_index, + const char *recipient_pubkey_hex, const char *plaintext) { + (void)store; + (void)role_index; + (void)recipient_pubkey_hex; + (void)plaintext; + return NULL; +} + +char *crypto_nip04_decrypt(const key_store_t *store, int role_index, + const char *sender_pubkey_hex, const char *ciphertext) { + (void)store; + (void)role_index; + (void)sender_pubkey_hex; + (void)ciphertext; + return NULL; +} + +void crypto_wipe(key_store_t *store) { + int i; + + if (store == NULL) { + return; + } + + for (i = 0; i < ROLE_TABLE_MAX_ENTRIES; ++i) { + secure_buf_free(&store->keys[i].private_key); + secure_memzero(store->keys[i].public_key, sizeof(store->keys[i].public_key)); + secure_memzero(store->keys[i].pubkey_hex, sizeof(store->keys[i].pubkey_hex)); + secure_memzero(store->keys[i].npub, sizeof(store->keys[i].npub)); + store->keys[i].valid = 0; + } + + store->count = 0; +} diff --git a/src/crypto.h b/src/crypto.h new file mode 100644 index 0000000..fa9a49e --- /dev/null +++ b/src/crypto.h @@ -0,0 +1,66 @@ +#ifndef NSIGNER_CRYPTO_H +#define NSIGNER_CRYPTO_H + +#include "role_table.h" +#include "secure_mem.h" +#include "mnemonic.h" +#include + +/* Per-role derived key material (stored in secure memory) */ +typedef struct { + secure_buf_t private_key; /* 32 bytes, mlock'd */ + unsigned char public_key[32]; + char pubkey_hex[65]; /* 64 hex chars + null */ + char npub[128]; /* bech32 npub */ + int valid; +} derived_key_t; + +/* Key store — holds derived keys for all roles */ +typedef struct { + derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; + int count; +} key_store_t; + +/* Initialize crypto subsystem (calls nostr_init). Returns 0 on success. */ +int crypto_init(void); + +/* Cleanup crypto subsystem (calls nostr_cleanup). */ +void crypto_cleanup(void); + +/* Derive keys for all roles in the table using the loaded mnemonic. + * Populates key_store and sets role->pubkey_hex and role->derived for each role. + * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). + * Returns number of keys derived, or -1 on error. */ +int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); + +/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ +const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); + +/* Get the derived public key hex for a role. Returns NULL if not derived. */ +const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); + +/* Sign a Nostr event. event_json is the unsigned event JSON string. + * Returns a newly-allocated string containing the signed event JSON, or NULL on error. + * Caller must free() the returned string. */ +char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); + +/* NIP-44 encrypt. Returns newly-allocated ciphertext string, or NULL on error. Caller frees. */ +char *crypto_nip44_encrypt(const key_store_t *store, int role_index, + const char *recipient_pubkey_hex, const char *plaintext); + +/* NIP-44 decrypt. Returns newly-allocated plaintext string, or NULL on error. Caller frees. */ +char *crypto_nip44_decrypt(const key_store_t *store, int role_index, + const char *sender_pubkey_hex, const char *ciphertext); + +/* NIP-04 encrypt. Returns newly-allocated ciphertext string, or NULL on error. Caller frees. */ +char *crypto_nip04_encrypt(const key_store_t *store, int role_index, + const char *recipient_pubkey_hex, const char *plaintext); + +/* NIP-04 decrypt. Returns newly-allocated plaintext string, or NULL on error. Caller frees. */ +char *crypto_nip04_decrypt(const key_store_t *store, int role_index, + const char *sender_pubkey_hex, const char *ciphertext); + +/* Zeroize all derived keys in the store. */ +void crypto_wipe(key_store_t *store); + +#endif /* NSIGNER_CRYPTO_H */ diff --git a/src/dispatcher.c b/src/dispatcher.c index 9b913a5..b70ce0b 100644 --- a/src/dispatcher.c +++ b/src/dispatcher.c @@ -4,6 +4,7 @@ #include "selector.h" #include "cjson/cJSON.h" +#include #include #include @@ -75,13 +76,14 @@ static char *make_success_response(const char *id, const char *result) { return out; } -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic) { +void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store) { if (ctx == NULL) { return; } ctx->role_table = table; ctx->mnemonic = mnemonic; + ctx->key_store = key_store; } char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) { @@ -98,8 +100,11 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) role_entry_t *role = NULL; int rc; const char *result = NULL; + char *owned_result = NULL; + ptrdiff_t role_index_diff; + int role_index; - if (ctx == NULL || ctx->role_table == NULL || ctx->mnemonic == NULL || json_request == NULL) { + if (ctx == NULL || ctx->role_table == NULL || ctx->mnemonic == NULL || ctx->key_store == NULL || json_request == NULL) { return make_error_response("null", -32600, "invalid_request"); } @@ -201,10 +206,28 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) return make_error_response(id_str, 1006, "mnemonic_not_loaded"); } + role_index_diff = role - &ctx->role_table->entries[0]; + if (role_index_diff < 0 || role_index_diff >= ROLE_TABLE_MAX_ENTRIES) { + cJSON_Delete(root); + return make_error_response(id_str, -32600, "invalid_request"); + } + role_index = (int)role_index_diff; + if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0) { result = role->derived ? role->pubkey_hex : "not_yet_derived"; } else if (strcmp(method, VERB_SIGN_EVENT) == 0) { - result = "stub:sign_event_ok"; + cJSON *event_item = cJSON_GetArrayItem(params_item, 0); + if (!cJSON_IsString(event_item) || event_item->valuestring == NULL) { + cJSON_Delete(root); + return make_error_response(id_str, -32602, "invalid_params"); + } + + owned_result = crypto_sign_event(ctx->key_store, role_index, event_item->valuestring); + if (owned_result == NULL) { + cJSON_Delete(root); + return make_error_response(id_str, -32602, "invalid_params"); + } + result = owned_result; } else if (strcmp(method, VERB_NIP44_ENCRYPT) == 0) { result = "stub:nip44_encrypt_ok"; } else if (strcmp(method, VERB_NIP44_DECRYPT) == 0) { @@ -219,5 +242,9 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request) } cJSON_Delete(root); - return make_success_response(id_str, result); + { + char *response = make_success_response(id_str, result); + free(owned_result); + return response; + } } diff --git a/src/dispatcher.h b/src/dispatcher.h index 342905d..36b6795 100644 --- a/src/dispatcher.h +++ b/src/dispatcher.h @@ -3,15 +3,17 @@ #include "role_table.h" #include "mnemonic.h" +#include "crypto.h" /* Dispatcher context — holds references to shared state */ typedef struct { role_table_t *role_table; mnemonic_state_t *mnemonic; + key_store_t *key_store; } dispatcher_ctx_t; /* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic); +void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); /* * Process a JSON-RPC request string and produce a JSON-RPC response string. diff --git a/src/main.c b/src/main.c index dfac47d..aaab20a 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,8 @@ #include "policy.h" #include "role_table.h" #include "server.h" +#include "crypto.h" +#include "socket_name.h" #include #include @@ -20,8 +22,7 @@ #include #include -#define NSIGNER_SOCKET_NAME "nsigner" - +#define NSIGNER_DEFAULT_SOCKET_NAME "nsigner" static volatile sig_atomic_t g_running = 1; static void handle_signal(int sig) { @@ -176,37 +177,128 @@ static int connect_abstract_socket(const char *name) { static void print_usage(const char *program_name) { printf("nsigner - single-binary signer program\n"); printf("Usage:\n"); - printf(" %s Run signer server + built-in TUI\n", program_name); - printf(" %s client '' Send JSON-RPC request\n", program_name); - printf(" %s client - Read JSON-RPC request from stdin\n", program_name); + printf(" %s [--socket-name ] Run signer server + built-in TUI\n", program_name); + printf(" %s [--socket-name ] client '' Send JSON-RPC request\n", program_name); + printf(" %s [--socket-name ] client - Read JSON-RPC request from stdin\n", program_name); + printf(" %s list List running nsigner abstract sockets\n", program_name); printf(" %s --help\n", program_name); printf(" %s --version\n", program_name); } -static int client_main(int argc, char *argv[]) { +static int list_sockets_main(void) { + FILE *fp; + char line[512]; + int found = 0; + + fp = fopen("/proc/net/unix", "r"); + if (fp == NULL) { + perror("fopen(/proc/net/unix)"); + return 1; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *p = strstr(line, "@nsigner_"); + if (p != NULL) { + char *end = p; + while (*end != '\0' && *end != '\n' && *end != ' ' && *end != '\t') { + end++; + } + *end = '\0'; + printf("%s\n", p); + found = 1; + } + } + + fclose(fp); + + if (!found) { + printf("(none)\n"); + } + + return 0; +} + +static int discover_single_socket_name(char *out, size_t out_len) { + FILE *fp; + char line[512]; + int count = 0; + + if (out == NULL || out_len == 0) { + return -1; + } + + out[0] = '\0'; + fp = fopen("/proc/net/unix", "r"); + if (fp == NULL) { + return -1; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *p = strstr(line, "@nsigner_"); + if (p == NULL) { + continue; + } + + { + char *end = p; + char name_buf[SERVER_SOCKET_NAME_MAX + 1]; + size_t len; + + while (*end != '\0' && *end != '\n' && *end != ' ' && *end != '\t') { + end++; + } + len = (size_t)(end - p); + if (len <= 1 || len > SERVER_SOCKET_NAME_MAX) { + continue; + } + + memcpy(name_buf, p + 1, len - 1); + name_buf[len - 1] = '\0'; + + count++; + if (count == 1) { + strncpy(out, name_buf, out_len - 1); + out[out_len - 1] = '\0'; + } + } + } + + fclose(fp); + + return (count == 1 && out[0] != '\0') ? 0 : -1; +} + +static int client_main(int argc, char *argv[], const char *socket_name, int socket_name_explicit) { int fd; char *request = NULL; char *response = NULL; char stdin_buf[SERVER_MAX_MSG_SIZE + 1]; - if (argc < 2) { - fprintf(stderr, "Usage: nsigner client '' | -\n"); + if (argc < 1) { + fprintf(stderr, "Usage: nsigner [--socket-name ] client '' | -\n"); return 1; } - if (strcmp(argv[1], "-") == 0) { + if (strcmp(argv[0], "-") == 0) { if (read_line_stdin(stdin_buf, sizeof(stdin_buf)) != 0) { fprintf(stderr, "Failed to read request from stdin\n"); return 1; } request = stdin_buf; } else { - request = argv[1]; + request = argv[0]; } - fd = connect_abstract_socket(NSIGNER_SOCKET_NAME); + if (!socket_name_explicit) { + char discovered[SERVER_SOCKET_NAME_MAX]; + if (discover_single_socket_name(discovered, sizeof(discovered)) == 0) { + socket_name = discovered; + } + } + + fd = connect_abstract_socket(socket_name); if (fd < 0) { - perror("connect"); + fprintf(stderr, "Failed to connect to @%s: %s\n", socket_name, strerror(errno)); return 1; } @@ -259,6 +351,8 @@ static int setup_default_role(role_table_t *role_table) { static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) { char phrase[MNEMONIC_MAX_LEN]; + char phrase_copy[MNEMONIC_MAX_LEN]; + char mode[16]; struct termios old_term; struct termios new_term; int have_term = 0; @@ -267,6 +361,46 @@ static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) { return -1; } + printf("Mnemonic source: [E]nter existing or [G]enerate new (default E): "); + fflush(stdout); + if (read_line_stdin(mode, sizeof(mode)) != 0) { + fprintf(stderr, "Failed to read mnemonic source choice\n"); + return -1; + } + + if (mode[0] == 'g' || mode[0] == 'G') { + int idx = 1; + char *ctx = NULL; + char *word; + + if (mnemonic_generate(12, phrase, sizeof(phrase)) != 0) { + fprintf(stderr, "Failed to generate mnemonic\n"); + return -1; + } + + strncpy(phrase_copy, phrase, sizeof(phrase_copy) - 1); + phrase_copy[sizeof(phrase_copy) - 1] = '\0'; + + printf("\nGenerated mnemonic (WRITE THIS DOWN - it will not be shown again):\n"); + word = strtok_r(phrase_copy, " ", &ctx); + while (word != NULL) { + printf("%2d. %s\n", idx, word); + idx++; + word = strtok_r(NULL, " ", &ctx); + } + + if (mnemonic_load(mnemonic, phrase) != 0) { + memset(phrase, 0, sizeof(phrase)); + memset(phrase_copy, 0, sizeof(phrase_copy)); + fprintf(stderr, "Failed to load generated mnemonic\n"); + return -1; + } + + memset(phrase, 0, sizeof(phrase)); + memset(phrase_copy, 0, sizeof(phrase_copy)); + return 0; + } + printf("Enter mnemonic (12/15/18/21/24 words): "); fflush(stdout); @@ -297,13 +431,6 @@ static int prompt_load_mnemonic(mnemonic_state_t *mnemonic) { return -1; } - printf("%d words loaded. Press Enter to continue.", mnemonic->word_count); - fflush(stdout); - if (read_line_stdin(phrase, sizeof(phrase)) != 0) { - memset(phrase, 0, sizeof(phrase)); - return -1; - } - memset(phrase, 0, sizeof(phrase)); return 0; } @@ -314,23 +441,53 @@ int main(int argc, char *argv[]) { dispatcher_ctx_t dispatcher; policy_table_t policy; server_ctx_t server; + key_store_t key_store; struct pollfd pfd; uid_t owner_uid; + int derived_count; + const char *socket_name = NSIGNER_DEFAULT_SOCKET_NAME; + char generated_socket_name[SERVER_SOCKET_NAME_MAX]; + int socket_name_explicit = 0; + int argi = 1; - if (argc >= 2 && strcmp(argv[1], "client") == 0) { - return client_main(argc - 1, argv + 1); + while (argi < argc) { + if (strcmp(argv[argi], "--socket-name") == 0) { + if (argi + 1 >= argc) { + fprintf(stderr, "Missing value for --socket-name\n"); + return 1; + } + socket_name = argv[argi + 1]; + socket_name_explicit = 1; + argi += 2; + continue; + } + break; } - if (argc >= 2 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0)) { + if (argi < argc && strcmp(argv[argi], "client") == 0) { + return client_main(argc - argi - 1, argv + argi + 1, socket_name, socket_name_explicit); + } + + if (argi < argc && strcmp(argv[argi], "list") == 0) { + return list_sockets_main(); + } + + if (argi < argc && (strcmp(argv[argi], "--version") == 0 || strcmp(argv[argi], "-v") == 0)) { printf("nsigner %s\n", NSIGNER_VERSION); return 0; } - if (argc >= 2 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0)) { + if (argi < argc && (strcmp(argv[argi], "--help") == 0 || strcmp(argv[argi], "-h") == 0)) { print_usage(argv[0]); return 0; } + if (argi < argc) { + fprintf(stderr, "Unknown argument: %s\n", argv[argi]); + print_usage(argv[0]); + return 1; + } + mnemonic_init(&mnemonic); if (prompt_load_mnemonic(&mnemonic) != 0) { mnemonic_unload(&mnemonic); @@ -344,14 +501,43 @@ int main(int argc, char *argv[]) { return 1; } - dispatcher_init(&dispatcher, &role_table, &mnemonic); + memset(&key_store, 0, sizeof(key_store)); + + if (crypto_init() != 0) { + fprintf(stderr, "Failed to initialize crypto subsystem\n"); + mnemonic_unload(&mnemonic); + return 1; + } + + derived_count = crypto_derive_all(&key_store, &role_table, &mnemonic); + if (derived_count < 0) { + fprintf(stderr, "Failed to derive keys from mnemonic\n"); + crypto_cleanup(); + mnemonic_unload(&mnemonic); + return 1; + } + + dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store); owner_uid = getuid(); policy_init_default(&policy, owner_uid); - server_init(&server, NSIGNER_SOCKET_NAME, &dispatcher, &policy); + if (!socket_name_explicit) { + if (socket_name_random(generated_socket_name, sizeof(generated_socket_name)) != 0) { + fprintf(stderr, "Failed to generate random socket name\n"); + crypto_wipe(&key_store); + crypto_cleanup(); + mnemonic_unload(&mnemonic); + return 1; + } + socket_name = generated_socket_name; + } + + server_init(&server, socket_name, socket_name_explicit, &dispatcher, &policy); if (server_start(&server) != 0) { - fprintf(stderr, "Failed to start server\n"); + fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server)); + crypto_wipe(&key_store); + crypto_cleanup(); mnemonic_unload(&mnemonic); return 1; } @@ -362,7 +548,12 @@ int main(int argc, char *argv[]) { printf("n_signer %s \\u2014 running\n", NSIGNER_VERSION); printf("Mnemonic: loaded (%d words)\n", mnemonic.word_count); printf("Roles: main (nostr, secp256k1, index=0)\n"); - printf("Listening: '%s'\n", NSIGNER_SOCKET_NAME); + printf("Derived keys: %d\n", derived_count); + if (role_table.count > 0 && role_table.entries[0].derived) { + printf("main pubkey(hex): %s\n", role_table.entries[0].pubkey_hex); + } + printf("Signer name: %s\n", socket_name); + printf("Listening: '@%s'\n", server.socket_name); printf("\n"); printf("Activity:\n"); fflush(stdout); @@ -388,6 +579,8 @@ int main(int argc, char *argv[]) { } server_stop(&server); + crypto_wipe(&key_store); + crypto_cleanup(); mnemonic_unload(&mnemonic); printf("Shutdown. All secrets wiped.\n"); return 0; diff --git a/src/main.h b/src/main.h index e34b7c2..f016706 100644 --- a/src/main.h +++ b/src/main.h @@ -10,7 +10,7 @@ /* Version information (auto-updated by build/version tooling) */ #define NSIGNER_VERSION_MAJOR 0 #define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 1 -#define NSIGNER_VERSION "v0.0.1" +#define NSIGNER_VERSION_PATCH 2 +#define NSIGNER_VERSION "v0.0.2" #endif /* NSIGNER_MAIN_H */ diff --git a/src/mnemonic.c b/src/mnemonic.c index 96f2be6..b100755 100644 --- a/src/mnemonic.c +++ b/src/mnemonic.c @@ -1,7 +1,11 @@ #include "mnemonic.h" +#include + +#include #include #include +#include /* Return 1 if the word count is an allowed BIP39 mnemonic length. */ static int mnemonic_is_valid_word_count(int count) { @@ -101,3 +105,55 @@ const char *mnemonic_get_phrase(const mnemonic_state_t *state) { return (const char *)state->buf.data; } + +int mnemonic_generate(int word_count, char *out, size_t out_len) { + unsigned char entropy[32]; + size_t entropy_len = 0; + size_t off = 0; + char phrase[MNEMONIC_MAX_LEN]; + + if (out == NULL || out_len == 0 || !mnemonic_is_valid_word_count(word_count)) { + return -1; + } + + switch (word_count) { + case 12: entropy_len = 16; break; + case 15: entropy_len = 20; break; + case 18: entropy_len = 24; break; + case 21: entropy_len = 28; break; + case 24: entropy_len = 32; break; + default: + return -1; + } + + memset(entropy, 0, sizeof(entropy)); + while (off < entropy_len) { + ssize_t rc = getrandom(entropy + off, entropy_len - off, 0); + if (rc < 0) { + if (errno == EINTR) { + continue; + } + secure_memzero(entropy, sizeof(entropy)); + return -1; + } + off += (size_t)rc; + } + + memset(phrase, 0, sizeof(phrase)); + if (nostr_bip39_mnemonic_from_bytes(entropy, entropy_len, phrase) != 0) { + secure_memzero(entropy, sizeof(entropy)); + secure_memzero(phrase, sizeof(phrase)); + return -1; + } + + secure_memzero(entropy, sizeof(entropy)); + + if (strlen(phrase) + 1 > out_len) { + secure_memzero(phrase, sizeof(phrase)); + return -1; + } + + strcpy(out, phrase); + secure_memzero(phrase, sizeof(phrase)); + return 0; +} diff --git a/src/mnemonic.h b/src/mnemonic.h index d60ad81..9423acb 100644 --- a/src/mnemonic.h +++ b/src/mnemonic.h @@ -32,4 +32,8 @@ int mnemonic_is_loaded(const mnemonic_state_t *state); /* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ const char *mnemonic_get_phrase(const mnemonic_state_t *state); +/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. + * Returns 0 on success, -1 on invalid arguments or generation failure. */ +int mnemonic_generate(int word_count, char *out, size_t out_len); + #endif /* NSIGNER_MNEMONIC_H */ diff --git a/src/server.c b/src/server.c index 68bdad2..421d981 100644 --- a/src/server.c +++ b/src/server.c @@ -4,6 +4,7 @@ #include "enforcement.h" #include "selector.h" #include "cjson/cJSON.h" +#include "socket_name.h" #include #include @@ -15,6 +16,20 @@ #include #include +static void server_set_error(server_ctx_t *ctx, const char *msg) { + if (ctx == NULL) { + return; + } + + if (msg == NULL) { + ctx->last_error[0] = '\0'; + return; + } + + strncpy(ctx->last_error, msg, sizeof(ctx->last_error) - 1); + ctx->last_error[sizeof(ctx->last_error) - 1] = '\0'; +} + static int read_full(int fd, void *buf, size_t len) { unsigned char *p = (unsigned char *)buf; size_t off = 0; @@ -184,13 +199,14 @@ static int extract_method_and_selector(const char *json, return 0; } -void server_init(server_ctx_t *ctx, const char *socket_name, +void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, dispatcher_ctx_t *dispatcher, policy_table_t *policy) { if (ctx == NULL) { return; } memset(ctx, 0, sizeof(*ctx)); + ctx->last_error[0] = '\0'; if (socket_name != NULL) { strncpy(ctx->socket_name, socket_name, sizeof(ctx->socket_name) - 1); ctx->socket_name[sizeof(ctx->socket_name) - 1] = '\0'; @@ -198,6 +214,7 @@ void server_init(server_ctx_t *ctx, const char *socket_name, ctx->listen_fd = -1; ctx->dispatcher = dispatcher; ctx->policy = policy; + ctx->socket_name_explicit = socket_name_explicit ? 1 : 0; } int server_start(server_ctx_t *ctx) { @@ -205,45 +222,112 @@ int server_start(server_ctx_t *ctx) { struct sockaddr_un addr; socklen_t addr_len; int flags; + int attempt; - if (ctx == NULL || ctx->dispatcher == NULL || ctx->policy == NULL || ctx->socket_name[0] == '\0') { + if (ctx == NULL) { return -1; } + if (ctx->dispatcher == NULL || ctx->policy == NULL || ctx->socket_name[0] == '\0') { + server_set_error(ctx, "invalid server context (missing dispatcher/policy/socket_name)"); + return -1; + } + + server_set_error(ctx, NULL); + fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { + (void)snprintf(ctx->last_error, + sizeof(ctx->last_error), + "socket(AF_UNIX, SOCK_STREAM) failed: %s", + strerror(errno)); return -1; } - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - addr.sun_path[0] = '\0'; - strncpy(&addr.sun_path[1], ctx->socket_name, sizeof(addr.sun_path) - 2); - addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; + for (attempt = 0; attempt < 8; ++attempt) { + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + addr.sun_path[0] = '\0'; + strncpy(&addr.sun_path[1], ctx->socket_name, sizeof(addr.sun_path) - 2); + addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; - addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(ctx->socket_name)); + addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(ctx->socket_name)); - if (bind(fd, (struct sockaddr *)&addr, addr_len) != 0) { + if (bind(fd, (struct sockaddr *)&addr, addr_len) == 0) { + break; + } + + if (errno == EADDRINUSE && !ctx->socket_name_explicit) { + if (socket_name_random(ctx->socket_name, sizeof(ctx->socket_name)) == 0) { + continue; + } + (void)snprintf(ctx->last_error, + sizeof(ctx->last_error), + "bind(@%s) failed: %s (and failed to generate retry socket name)", + ctx->socket_name, + strerror(errno)); + close(fd); + return -1; + } + + if (errno == EADDRINUSE && ctx->socket_name_explicit) { + (void)snprintf(ctx->last_error, + sizeof(ctx->last_error), + "bind(@%s) failed: %s (explicit --socket-name is already in use)", + ctx->socket_name, + strerror(errno)); + } else { + (void)snprintf(ctx->last_error, + sizeof(ctx->last_error), + "bind(@%s) failed: %s", + ctx->socket_name, + strerror(errno)); + } + close(fd); + return -1; + } + + if (attempt >= 8) { + (void)snprintf(ctx->last_error, + sizeof(ctx->last_error), + "bind failed after retries: address already in use for generated names (try --socket-name)"); close(fd); return -1; } if (listen(fd, 5) != 0) { + (void)snprintf(ctx->last_error, + sizeof(ctx->last_error), + "listen() failed: %s", + strerror(errno)); close(fd); return -1; } flags = fcntl(fd, F_GETFL, 0); if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { + (void)snprintf(ctx->last_error, + sizeof(ctx->last_error), + "fcntl(O_NONBLOCK) failed: %s", + strerror(errno)); close(fd); return -1; } ctx->listen_fd = fd; ctx->running = 1; + server_set_error(ctx, NULL); return 0; } +const char *server_last_error(const server_ctx_t *ctx) { + if (ctx == NULL || ctx->last_error[0] == '\0') { + return "unknown server error"; + } + + return ctx->last_error; +} + int server_get_caller(int fd, caller_identity_t *out) { struct ucred cred; socklen_t len = sizeof(cred); diff --git a/src/server.h b/src/server.h index cdeb473..053eaa6 100644 --- a/src/server.h +++ b/src/server.h @@ -19,19 +19,25 @@ typedef struct { /* Server context */ typedef struct { char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ + char last_error[256]; int listen_fd; int running; dispatcher_ctx_t *dispatcher; policy_table_t *policy; + int socket_name_explicit; } server_ctx_t; -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner") */ -void server_init(server_ctx_t *ctx, const char *socket_name, +/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). + * socket_name_explicit should be non-zero when provided via --socket-name override. */ +void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, dispatcher_ctx_t *dispatcher, policy_table_t *policy); /* Start listening. Returns 0 on success, -1 on error. */ int server_start(server_ctx_t *ctx); +/* Get human-readable description of last server error. */ +const char *server_last_error(const server_ctx_t *ctx); + /* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. * activity_cb is called with a description string for the TUI activity log. */ typedef void (*server_activity_cb)(const char *message, void *user_data); diff --git a/src/socket_name.c b/src/socket_name.c new file mode 100644 index 0000000..17a263e --- /dev/null +++ b/src/socket_name.c @@ -0,0 +1,69 @@ +#define _GNU_SOURCE + +#include "socket_name.h" + +#include + +#include +#include +#include +#include + +static int fill_random(unsigned char *buf, size_t len) { + size_t off = 0; + + while (off < len) { + ssize_t rc = getrandom(buf + off, len - off, 0); + if (rc < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + off += (size_t)rc; + } + + return 0; +} + +int socket_name_random(char *out, size_t out_len) { + unsigned char entropy[16]; + char mnemonic[256]; + char *sp1; + char *sp2; + + if (out == NULL || out_len == 0) { + return -1; + } + + if (fill_random(entropy, sizeof(entropy)) != 0) { + return -1; + } + + memset(mnemonic, 0, sizeof(mnemonic)); + if (nostr_bip39_mnemonic_from_bytes(entropy, sizeof(entropy), mnemonic) != 0) { + memset(entropy, 0, sizeof(entropy)); + return -1; + } + memset(entropy, 0, sizeof(entropy)); + + sp1 = strchr(mnemonic, ' '); + if (sp1 == NULL) { + memset(mnemonic, 0, sizeof(mnemonic)); + return -1; + } + *sp1 = '\0'; + + sp2 = strchr(sp1 + 1, ' '); + if (sp2 != NULL) { + *sp2 = '\0'; + } + + if (snprintf(out, out_len, "nsigner_%s_%s", mnemonic, sp1 + 1) >= (int)out_len) { + memset(mnemonic, 0, sizeof(mnemonic)); + return -1; + } + + memset(mnemonic, 0, sizeof(mnemonic)); + return 0; +} diff --git a/src/socket_name.h b/src/socket_name.h new file mode 100644 index 0000000..bcc0af8 --- /dev/null +++ b/src/socket_name.h @@ -0,0 +1,12 @@ +#ifndef NSIGNER_SOCKET_NAME_H +#define NSIGNER_SOCKET_NAME_H + +#include + +/* + * Generate random socket name in format: nsigner__ + * Returns 0 on success, -1 on error. + */ +int socket_name_random(char *out, size_t out_len); + +#endif /* NSIGNER_SOCKET_NAME_H */ diff --git a/tests/test_dispatcher.c b/tests/test_dispatcher.c index 597c109..f7ed7fc 100644 --- a/tests/test_dispatcher.c +++ b/tests/test_dispatcher.c @@ -63,8 +63,10 @@ int main(void) { role_entry_t ssh_role; mnemonic_state_t mnemonic; dispatcher_ctx_t dispatcher; + key_store_t key_store; const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; char *resp; + int derived; role_table_init(&table); @@ -77,27 +79,34 @@ int main(void) { mnemonic_init(&mnemonic); mnemonic_load(&mnemonic, valid_12); - dispatcher_init(&dispatcher, &table, &mnemonic); + memset(&key_store, 0, sizeof(key_store)); + dispatcher_init(&dispatcher, &table, &mnemonic, &key_store); - /* 1. Valid get_public_key no selector -> default main, not_yet_derived */ + derived = crypto_init(); + check_condition("crypto_init succeeds", derived == 0); + + derived = crypto_derive_all(&key_store, &table, &mnemonic); + check_condition("crypto_derive_all derives at least one key", derived >= 1); + + /* 1. Valid get_public_key no selector -> default main, real pubkey */ resp = dispatcher_handle_request(&dispatcher, "{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}"); - check_condition("get_public_key default role returns not_yet_derived", - response_has(resp, "\"id\":\"1\"") && response_has(resp, "\"result\":\"not_yet_derived\"")); + check_condition("get_public_key default role returns derived hex", + response_has(resp, "\"id\":\"1\"") && !response_has(resp, "not_yet_derived") && response_has(resp, "\"result\":\"")); free(resp); /* 2. sign_event with role main */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"main\"}]}"); - check_condition("sign_event with role=main returns stub", - response_has(resp, "\"id\":\"2\"") && response_has(resp, "\"result\":\"stub:sign_event_ok\"")); + "{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}"); + check_condition("sign_event with role=main returns signed event", + response_has(resp, "\"id\":\"2\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at")); free(resp); /* 3. sign_event with nostr_index 0 */ resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"nostr_index\":0}]}"); - check_condition("sign_event with nostr_index=0 returns stub", - response_has(resp, "\"id\":\"3\"") && response_has(resp, "\"result\":\"stub:sign_event_ok\"")); + "{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello2\\\",\\\"tags\\\":[]}\",{\"nostr_index\":0}]}"); + check_condition("sign_event with nostr_index=0 returns signed event", + response_has(resp, "\"id\":\"3\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at")); free(resp); /* 4. ambiguous selector role + nostr_index */ @@ -152,7 +161,10 @@ int main(void) { response_has(resp, "\"id\":\"10\"") && response_has(resp, "\"code\":-32601")); free(resp); - printf("%d/10 tests passed\n", g_passes); + crypto_wipe(&key_store); + crypto_cleanup(); - return (g_passes == 10 && g_total == 10) ? 0 : 1; + printf("%d/12 tests passed\n", g_passes); + + return (g_passes == 12 && g_total == 12) ? 0 : 1; } diff --git a/tests/test_integration.c b/tests/test_integration.c index a66111d..22c1f95 100644 --- a/tests/test_integration.c +++ b/tests/test_integration.c @@ -14,7 +14,7 @@ #include #include -#define SOCKET_NAME "nsigner" +#define SOCKET_NAME "nsigner_test_run" #define MAX_MSG_SIZE 65536 static int g_failures = 0; @@ -174,7 +174,7 @@ static int request_roundtrip(const char *req, char **resp) { int main(void) { int stdin_pipe[2]; pid_t child; - const char *mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n\n"; + const char *mnemonic = "\nabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n"; char *resp = NULL; int status; @@ -203,7 +203,7 @@ int main(void) { close(stdin_pipe[0]); close(stdin_pipe[1]); - execl("./build/nsigner", "./build/nsigner", (char *)NULL); + execl("./build/nsigner", "./build/nsigner", "--socket-name", SOCKET_NAME, (char *)NULL); _exit(127); } @@ -226,9 +226,9 @@ int main(void) { free(resp); resp = NULL; - if (request_roundtrip("{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"main\"}]}", &resp) == 0) { + if (request_roundtrip("{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[],\\\"created_at\\\":1700000000}\",{\"role\":\"main\"}]}", &resp) == 0) { check_condition("sign_event response id", strstr(resp, "\"id\":\"2\"") != NULL); - check_condition("sign_event stub result", strstr(resp, "stub:sign_event_ok") != NULL); + check_condition("sign_event has result", strstr(resp, "\"result\":") != NULL); } else { check_condition("sign_event request roundtrip", 0); } diff --git a/tests/test_mnemonic.c b/tests/test_mnemonic.c index 0cc39a6..3f9d637 100644 --- a/tests/test_mnemonic.c +++ b/tests/test_mnemonic.c @@ -46,6 +46,8 @@ int main(void) { const char *invalid_11 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; char invalid_25[MNEMONIC_MAX_LEN]; const char *loaded_phrase; + char generated_12[MNEMONIC_MAX_LEN]; + char generated_24[MNEMONIC_MAX_LEN]; int rc; mnemonic_init(&state); @@ -73,6 +75,22 @@ int main(void) { check_condition("reject invalid 25-word mnemonic", rc == -1); check_condition("state remains unloaded after invalid 25-word load", mnemonic_is_loaded(&state) == 0); + rc = mnemonic_generate(12, generated_12, sizeof(generated_12)); + check_condition("mnemonic_generate(12) returns 0", rc == 0); + check_condition("mnemonic_generate(12) output non-empty", rc == 0 && generated_12[0] != '\0'); + check_condition("load generated 12-word mnemonic", rc == 0 && mnemonic_load(&state, generated_12) == 0); + check_condition("generated 12 word_count == 12", mnemonic_is_loaded(&state) && state.word_count == 12); + mnemonic_unload(&state); + + rc = mnemonic_generate(24, generated_24, sizeof(generated_24)); + check_condition("mnemonic_generate(24) returns 0", rc == 0); + check_condition("mnemonic_generate(24) output non-empty", rc == 0 && generated_24[0] != '\0'); + check_condition("load generated 24-word mnemonic", rc == 0 && mnemonic_load(&state, generated_24) == 0); + check_condition("generated 24 word_count == 24", mnemonic_is_loaded(&state) && state.word_count == 24); + mnemonic_unload(&state); + + check_condition("two generated mnemonics differ", strcmp(generated_12, generated_24) != 0); + if (g_failures == 0) { printf("ALL TESTS PASSED\n"); return 0; diff --git a/tests/test_socket_name.c b/tests/test_socket_name.c new file mode 100644 index 0000000..7a7efdd --- /dev/null +++ b/tests/test_socket_name.c @@ -0,0 +1,51 @@ +#include "../src/socket_name.h" + +#include +#include + +static int g_failures = 0; + +static void check_condition(const char *name, int condition) { + if (condition) { + printf("PASS: %s\n", name); + } else { + printf("FAIL: %s\n", name); + g_failures++; + } +} + +int main(void) { + char a[128]; + char b[128]; + + memset(a, 0, sizeof(a)); + memset(b, 0, sizeof(b)); + + check_condition("socket_name_random(a) succeeds", socket_name_random(a, sizeof(a)) == 0); + check_condition("socket_name_random(b) succeeds", socket_name_random(b, sizeof(b)) == 0); + + check_condition("name a starts with nsigner_", strncmp(a, "nsigner_", 8) == 0); + check_condition("name b starts with nsigner_", strncmp(b, "nsigner_", 8) == 0); + + { + const char *suffix = a + 8; + const char *underscore = strchr(suffix, '_'); + check_condition("name a contains second underscore", underscore != NULL && underscore > suffix && underscore[1] != '\0'); + } + + { + const char *suffix = b + 8; + const char *underscore = strchr(suffix, '_'); + check_condition("name b contains second underscore", underscore != NULL && underscore > suffix && underscore[1] != '\0'); + } + + check_condition("two generated names are typically different", strcmp(a, b) != 0); + + if (g_failures == 0) { + printf("ALL TESTS PASSED\n"); + return 0; + } + + printf("TESTS FAILED: %d\n", g_failures); + return 1; +}