Compare commits

...

2 Commits

39 changed files with 9416 additions and 4225 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
build/
.git/
*.o
/*.a
!resources/
!resources/**

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ build/
*.tar.gz
.gitea_token
resources/
.test_mnemonic

7
.roo/commands/push.md Normal file
View File

@@ -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"

View File

@@ -1,77 +1,81 @@
# 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/key_store.c \
/build/src/socket_name.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

View File

@@ -1,6 +1,6 @@
CC := gcc
CFLAGS := -Wall -Wextra -std=c99 -O2 -Isrc -Isrc/cjson
LDFLAGS :=
CFLAGS := -Wall -Wextra -std=c99 -O2 -Isrc -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,19 +18,11 @@ SOURCES := \
$(SRC_DIR)/dispatcher.c \
$(SRC_DIR)/policy.c \
$(SRC_DIR)/server.c \
$(SRC_DIR)/cjson/cJSON.c
$(SRC_DIR)/key_store.c \
$(SRC_DIR)/socket_name.c
HEADERS :=
HEADERS := \
$(SRC_DIR)/main.h \
$(SRC_DIR)/secure_mem.h \
$(SRC_DIR)/mnemonic.h \
$(SRC_DIR)/role_table.h \
$(SRC_DIR)/selector.h \
$(SRC_DIR)/enforcement.h \
$(SRC_DIR)/dispatcher.h \
$(SRC_DIR)/policy.h \
$(SRC_DIR)/server.h \
$(SRC_DIR)/cjson/cJSON.h
TEST_MNEMONIC_TARGET := $(BUILD_DIR)/test_mnemonic
TEST_ROLE_TARGET := $(BUILD_DIR)/test_role_table
@@ -39,14 +31,18 @@ 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
$(TARGET_DEV): $(SOURCES) $(HEADERS)
dev: lib $(TARGET_DEV)
$(TARGET_DEV): $(SOURCES)
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(SOURCES) -o $(TARGET_DEV) $(LDFLAGS)
@@ -62,7 +58,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,27 +81,30 @@ test-dispatcher: $(TEST_DISPATCHER_TARGET)
test-policy: $(TEST_POLICY_TARGET)
./$(TEST_POLICY_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
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
@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)
$(TEST_ROLE_TARGET): $(TEST_DIR)/test_role_table.c $(SRC_DIR)/role_table.c $(SRC_DIR)/role_table.h
$(TEST_ROLE_TARGET): $(TEST_DIR)/test_role_table.c $(SRC_DIR)/role_table.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_role_table.c $(SRC_DIR)/role_table.c -o $(TEST_ROLE_TARGET) $(LDFLAGS)
$(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c $(SRC_DIR)/selector.h $(SRC_DIR)/role_table.h
$(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c -o $(TEST_SELECTOR_TARGET) $(LDFLAGS)
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/enforcement.h $(SRC_DIR)/role_table.h
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c
@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)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@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)/key_store.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
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c -o $(TEST_POLICY_TARGET) $(LDFLAGS)
@@ -113,5 +112,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
@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)

View File

@@ -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_<word1>_<word2>`, 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 <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":["<event_json>",{"role":"main"}]}'
nsigner client --socket-name nsigner_hairy_dog '{"id":"2","method":"sign_event","params":["<event_json>",{"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":["<event_json>",{"role":"main"}]}'
$ nsigner client --socket-name nsigner_hairy_dog '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
{"id":"2","result":"<signed_event_json>"}
```

View File

@@ -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 <arm64|armv7|x86_64>]"
echo "Usage: $0 [--arch <x86_64|arm64|armv7>]"
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 <arm64|armv7|x86_64>]"
echo "Usage: $0 [--arch <x86_64|arm64|armv7>]"
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"

View File

@@ -84,18 +84,23 @@ update_version_in_header() {
local major="$2"
local minor="$3"
local patch="$4"
local version_file=""
if [[ ! -f "src/main.h" ]]; then
print_error "src/main.h not found"
if [[ -f "src/main.h" ]]; then
version_file="src/main.h"
elif [[ -f "src/main.c" ]]; then
version_file="src/main.c"
else
print_error "Neither src/main.h nor src/main.c found"
exit 1
fi
sed -i "s/#define NSIGNER_VERSION \".*\"/#define NSIGNER_VERSION \"$new_version\"/" src/main.h
sed -i "s/#define NSIGNER_VERSION_MAJOR [0-9]\+/#define NSIGNER_VERSION_MAJOR $major/" src/main.h
sed -i "s/#define NSIGNER_VERSION_MINOR [0-9]\+/#define NSIGNER_VERSION_MINOR $minor/" src/main.h
sed -i "s/#define NSIGNER_VERSION_PATCH [0-9]\+/#define NSIGNER_VERSION_PATCH $patch/" src/main.h
sed -i "s/#define NSIGNER_VERSION \".*\"/#define NSIGNER_VERSION \"$new_version\"/" "$version_file"
sed -i "s/#define NSIGNER_VERSION_MAJOR [0-9]\+/#define NSIGNER_VERSION_MAJOR $major/" "$version_file"
sed -i "s/#define NSIGNER_VERSION_MINOR [0-9]\+/#define NSIGNER_VERSION_MINOR $minor/" "$version_file"
sed -i "s/#define NSIGNER_VERSION_PATCH [0-9]\+/#define NSIGNER_VERSION_PATCH $patch/" "$version_file"
print_success "Updated src/main.h to $new_version"
print_success "Updated $version_file to $new_version"
}
increment_version() {

View File

@@ -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_<word1>_<word2>` (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_<w1>_<w2>`.
- 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_<w1>_<w2>` 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_<bip39_word1>_<bip39_word2>` chosen randomly at each launch.
3. `--socket-name <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)

File diff suppressed because it is too large Load Diff

View File

@@ -1,306 +0,0 @@
/*
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef cJSON__h
#define cJSON__h
#ifdef __cplusplus
extern "C"
{
#endif
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
#define __WINDOWS__
#endif
#ifdef __WINDOWS__
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
For *nix builds that support visibility attribute, you can define similar behavior by
setting default visibility to hidden by adding
-fvisibility=hidden (for gcc)
or
-xldscope=hidden (for sun cc)
to CFLAGS
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
*/
#define CJSON_CDECL __cdecl
#define CJSON_STDCALL __stdcall
/* export symbols by default, this is necessary for copy pasting the C and header file */
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_EXPORT_SYMBOLS
#endif
#if defined(CJSON_HIDE_SYMBOLS)
#define CJSON_PUBLIC(type) type CJSON_STDCALL
#elif defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
#elif defined(CJSON_IMPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
#endif
#else /* !__WINDOWS__ */
#define CJSON_CDECL
#define CJSON_STDCALL
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#endif
/* project version */
#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define CJSON_VERSION_PATCH 18
#include <stddef.h>
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
/* The cJSON structure: */
typedef struct cJSON
{
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
} cJSON;
typedef struct cJSON_Hooks
{
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
void *(CJSON_CDECL *malloc_fn)(size_t sz);
void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;
typedef int cJSON_bool;
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_NESTING_LIMIT
#define CJSON_NESTING_LIMIT 1000
#endif
/* Limits the length of circular references can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_CIRCULAR_LIMIT
#define CJSON_CIRCULAR_LIMIT 10000
#endif
/* returns the version of cJSON as a string */
CJSON_PUBLIC(const char*) cJSON_Version(void);
/* Supply malloc, realloc and free functions to cJSON */
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
/* Render a cJSON entity to text for transfer/storage. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
/* Delete a cJSON entity and all subentities. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
/* Returns the number of items in an array (or object). */
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
/* Get item "string" from object. Case insensitive. */
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
/* Check item type and return its value */
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
/* These functions check the type of an item */
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
/* These calls create a cJSON item of the appropriate type. */
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
/* raw json */
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
/* Create a string where valuestring references a string so
* it will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
/* Create an object/array that only references it's elements so
* they will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
/* These utilities create an Array of count items.
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
/* Append item to the specified array/object. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
* writing to `item->string` */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
/* Remove/Detach items from Arrays/Objects. */
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
/* Update array items. */
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
/* Duplicate a cJSON item */
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
* The item->next and ->prev pointers are always zero on return from Duplicate. */
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
* The input pointer json cannot point to a read-only address area, such as a string constant,
* but should point to a readable and writable address area. */
CJSON_PUBLIC(void) cJSON_Minify(char *json);
/* Helper functions for creating and adding items to an object at the same time.
* They return the added item or NULL on failure. */
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
/* helper for the cJSON_SetNumberValue macro */
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
#define cJSON_SetBoolValue(object, boolValue) ( \
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
cJSON_Invalid\
)
/* Macro for iterating over an array or object */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -1,9 +1,446 @@
#include "dispatcher.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
#include "enforcement.h"
#include "selector.h"
#include "cjson/cJSON.h"
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
@@ -75,13 +512,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 +536,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 +642,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 +678,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;
}
}

View File

@@ -1,42 +0,0 @@
#ifndef NSIGNER_DISPATCHER_H
#define NSIGNER_DISPATCHER_H
#include "role_table.h"
#include "mnemonic.h"
/* Dispatcher context — holds references to shared state */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
#endif /* NSIGNER_DISPATCHER_H */

View File

@@ -1,4 +1,444 @@
#include "enforcement.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <string.h>

View File

@@ -1,36 +0,0 @@
#ifndef NSIGNER_ENFORCEMENT_H
#define NSIGNER_ENFORCEMENT_H
#include "role_table.h"
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
#endif /* NSIGNER_ENFORCEMENT_H */

628
src/key_store.c Normal file
View File

@@ -0,0 +1,628 @@
#define _GNU_SOURCE
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <nostr_core/nostr_common.h>
#include <nostr_core/nip001.h>
#include <nostr_core/nip006.h>
#include <nostr_core/nip019.h>
#include <nostr_core/utils.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define NSIGNER_ENCRYPT_OUTPUT_MAX 65536
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;
}
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;
}

View File

@@ -1,11 +1,448 @@
#define _GNU_SOURCE
#include "main.h"
#include "dispatcher.h"
#include "mnemonic.h"
#include "policy.h"
#include "role_table.h"
#include "server.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 3
#define NSIGNER_VERSION "v0.0.3"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <nostr_core/nostr_common.h>
#include <arpa/inet.h>
#include <errno.h>
@@ -20,8 +457,7 @@
#include <termios.h>
#include <unistd.h>
#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 +612,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 '<json>' Send JSON-RPC request\n", program_name);
printf(" %s client - Read JSON-RPC request from stdin\n", program_name);
printf(" %s [--socket-name <name>] Run signer server + built-in TUI\n", program_name);
printf(" %s [--socket-name <name>] client '<json>' Send JSON-RPC request\n", program_name);
printf(" %s [--socket-name <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 '<json-rpc-request>' | -\n");
if (argc < 1) {
fprintf(stderr, "Usage: nsigner [--socket-name <name>] client '<json-rpc-request>' | -\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 +786,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 +796,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 +866,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 +876,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 +936,43 @@ int main(int argc, char *argv[]) {
return 1;
}
dispatcher_init(&dispatcher, &role_table, &mnemonic);
memset(&key_store, 0, sizeof(key_store));
if (nostr_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");
nostr_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);
nostr_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);
nostr_cleanup();
mnemonic_unload(&mnemonic);
return 1;
}
@@ -362,7 +983,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 +1014,8 @@ int main(int argc, char *argv[]) {
}
server_stop(&server);
crypto_wipe(&key_store);
nostr_cleanup();
mnemonic_unload(&mnemonic);
printf("Shutdown. All secrets wiped.\n");
return 0;

View File

@@ -1,16 +0,0 @@
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
#ifndef NSIGNER_MAIN_H
#define NSIGNER_MAIN_H
/* 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"
#endif /* NSIGNER_MAIN_H */

View File

@@ -1,7 +1,451 @@
#include "mnemonic.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <nostr_core/utils.h>
#include <errno.h>
#include <stddef.h>
#include <string.h>
#include <sys/random.h>
/* Return 1 if the word count is an allowed BIP39 mnemonic length. */
static int mnemonic_is_valid_word_count(int count) {
@@ -101,3 +545,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;
}

View File

@@ -1,35 +0,0 @@
#ifndef NSIGNER_MNEMONIC_H
#define NSIGNER_MNEMONIC_H
#include "secure_mem.h"
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
#endif /* NSIGNER_MNEMONIC_H */

View File

@@ -1,4 +1,444 @@
#include "policy.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>

View File

@@ -1,68 +0,0 @@
#ifndef NSIGNER_POLICY_H
#define NSIGNER_POLICY_H
#include "role_table.h"
#include <sys/types.h>
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
#endif /* NSIGNER_POLICY_H */

View File

@@ -1,4 +1,444 @@
#include "role_table.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <string.h>

View File

@@ -1,89 +0,0 @@
#ifndef NSIGNER_ROLE_TABLE_H
#define NSIGNER_ROLE_TABLE_H
#include <stddef.h>
#include <stdint.h>
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
#endif /* NSIGNER_ROLE_TABLE_H */

View File

@@ -1,6 +1,446 @@
#define _GNU_SOURCE
#include "secure_mem.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <stdlib.h>

View File

@@ -1,25 +0,0 @@
#ifndef NSIGNER_SECURE_MEM_H
#define NSIGNER_SECURE_MEM_H
#include <stddef.h>
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
#endif /* NSIGNER_SECURE_MEM_H */

View File

@@ -1,4 +1,444 @@
#include "selector.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <string.h>

View File

@@ -1,39 +0,0 @@
#ifndef NSIGNER_SELECTOR_H
#define NSIGNER_SELECTOR_H
#include "role_table.h"
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
#endif /* NSIGNER_SELECTOR_H */

View File

@@ -1,9 +1,446 @@
#define _GNU_SOURCE
#include "server.h"
#include "enforcement.h"
#include "selector.h"
#include "cjson/cJSON.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <arpa/inet.h>
#include <errno.h>
@@ -15,6 +452,20 @@
#include <sys/un.h>
#include <unistd.h>
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 +635,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 +650,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 +658,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);

View File

@@ -1,46 +0,0 @@
#ifndef NSIGNER_SERVER_H
#define NSIGNER_SERVER_H
#include "dispatcher.h"
#include "policy.h"
#include <sys/types.h>
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* Server context */
typedef struct {
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
int listen_fd;
int running;
dispatcher_ctx_t *dispatcher;
policy_table_t *policy;
} 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,
dispatcher_ctx_t *dispatcher, policy_table_t *policy);
/* Start listening. Returns 0 on success, -1 on error. */
int server_start(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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
#endif /* NSIGNER_SERVER_H */

509
src/socket_name.c Normal file
View File

@@ -0,0 +1,509 @@
#define _GNU_SOURCE
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <nostr_core/utils.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/random.h>
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;
}

View File

@@ -1,7 +1,446 @@
#include "../src/dispatcher.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
#include "../src/enforcement.h"
#include "../src/role_table.h"
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <nostr_core/nostr_common.h>
#include <stdio.h>
#include <stdlib.h>
@@ -63,8 +502,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 +518,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 = nostr_init();
check_condition("nostr_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 +600,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);
nostr_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;
}

View File

@@ -1,4 +1,444 @@
#include "../src/enforcement.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>

View File

@@ -1,5 +1,447 @@
#define _GNU_SOURCE
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
@@ -14,7 +456,7 @@
#include <time.h>
#include <unistd.h>
#define SOCKET_NAME "nsigner"
#define SOCKET_NAME "nsigner_test_run"
#define MAX_MSG_SIZE 65536
static int g_failures = 0;
@@ -174,7 +616,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 +645,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 +668,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);
}

View File

@@ -1,4 +1,444 @@
#include "../src/mnemonic.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>
@@ -46,6 +486,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 +515,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;

View File

@@ -1,4 +1,444 @@
#include "../src/policy.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>

View File

@@ -1,4 +1,444 @@
#include "../src/role_table.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>

View File

@@ -1,4 +1,444 @@
#include "../src/selector.h"
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>

491
tests/test_socket_name.c Normal file
View File

@@ -0,0 +1,491 @@
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
/*
* Secure memory buffer — mlock'd, zeroized on free.
* Used for mnemonic phrases, private keys, and any sensitive material.
*/
typedef struct {
void *data; /* pointer to locked allocation */
size_t size; /* usable size in bytes */
int locked; /* 1 if mlock succeeded */
} secure_buf_t;
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
int secure_buf_alloc(secure_buf_t *buf, size_t size);
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
void secure_buf_free(secure_buf_t *buf);
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
#define MNEMONIC_MAX_LEN 256
/*
* Mnemonic state — holds the loaded mnemonic in secure memory.
* Only one mnemonic is active at a time per process.
*/
typedef struct {
secure_buf_t buf; /* secure storage for the mnemonic string */
int loaded; /* 1 if a mnemonic is currently loaded */
int word_count; /* 12, 15, 18, 21, or 24 */
} mnemonic_state_t;
/* Initialize mnemonic state (must be called before use). */
void mnemonic_init(mnemonic_state_t *state);
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
/* Zeroize and unload the mnemonic. Idempotent. */
void mnemonic_unload(mnemonic_state_t *state);
/* Check if a mnemonic is currently loaded. */
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);
/* from role_table.h */
/* Maximum limits */
#define ROLE_NAME_MAX 64
#define ROLE_PATH_MAX 128
#define ROLE_PURPOSE_MAX 32
#define ROLE_CURVE_MAX 16
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
#define ROLE_TABLE_MAX_ENTRIES 64
/* Purpose enum for fast comparison (string form kept for config/display) */
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_UNKNOWN
} role_purpose_t;
/* Curve enum */
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_UNKNOWN
} role_curve_t;
/* Selector type — how this role's key is addressed */
typedef enum {
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
SELECTOR_ROLE_PATH /* uses explicit full path */
} role_selector_type_t;
/* A single role entry */
typedef struct {
char name[ROLE_NAME_MAX];
char purpose_str[ROLE_PURPOSE_MAX];
char curve_str[ROLE_CURVE_MAX];
role_purpose_t purpose;
role_curve_t curve;
role_selector_type_t selector_type;
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
int derived; /* 1 if pubkey_hex has been populated */
} role_entry_t;
/* The role table */
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
/* Initialize an empty role table */
void role_table_init(role_table_t *table);
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
int role_table_add(role_table_t *table, const role_entry_t *entry);
/* Find a role by name. Returns pointer to entry or NULL. */
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
/* Find a role by nostr_index. Returns pointer or NULL. */
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
/* Find a role by role_path. Returns pointer or NULL. */
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
role_entry_t *role_table_get_default(role_table_t *table);
/* Parse purpose string to enum */
role_purpose_t role_purpose_from_str(const char *s);
/* Parse curve string to enum */
role_curve_t role_curve_from_str(const char *s);
/* Purpose enum to string */
const char *role_purpose_to_str(role_purpose_t p);
/* Curve enum to string */
const char *role_curve_to_str(role_curve_t c);
/* from selector.h */
/* Error codes for selector resolution */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
/* Parsed selector from a request's options object */
typedef struct {
int has_role; /* 1 if "role" field was present */
char role_name[ROLE_NAME_MAX];
int has_nostr_index; /* 1 if "nostr_index" field was present */
int nostr_index;
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
void selector_request_init(selector_request_t *req);
/*
* Resolve a selector request against the role table.
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
*/
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/*
* Return a human-readable error string for a selector error code.
*/
const char *selector_strerror(int err);
/* from enforcement.h */
/* Error codes */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
int enforce_verb_role(const char *verb, const role_entry_t *role);
/*
* Return a human-readable error string for an enforcement error code.
*/
const char *enforce_strerror(int err);
/* from policy.h */
#define POLICY_MAX_ENTRIES 32
#define POLICY_MAX_VERBS 16
#define POLICY_MAX_ROLES 16
#define POLICY_MAX_PURPOSES 8
#define POLICY_VERB_MAX_LEN 32
#define POLICY_CALLER_MAX_LEN 64
/* Prompt behavior */
typedef enum {
PROMPT_NEVER = 0,
PROMPT_FIRST_PER_BOOT,
PROMPT_EVERY_REQUEST,
PROMPT_DENY
} prompt_mode_t;
/* A single policy entry */
typedef struct {
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
int verb_count;
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
int role_count;
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
int purpose_count;
prompt_mode_t prompt;
} policy_entry_t;
/* Policy table */
typedef struct {
policy_entry_t entries[POLICY_MAX_ENTRIES];
int count;
} policy_table_t;
/* Policy check result */
#define POLICY_ALLOW 0
#define POLICY_DENY -1
#define POLICY_PROMPT -2 /* would need user confirmation */
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
/* Initialize policy table */
void policy_table_init(policy_table_t *table);
/* Initialize default policy: allow same-uid, deny others */
void policy_init_default(policy_table_t *table, uid_t owner_uid);
/* Add a policy entry. Returns 0 on success, -1 if full. */
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
/*
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
*/
int policy_check(const policy_table_t *table, const char *caller_id,
const char *verb, const char *role_name, const char *purpose);
/* Parse prompt mode from string */
prompt_mode_t prompt_mode_from_str(const char *s);
/* Prompt mode to string */
const char *prompt_mode_to_str(prompt_mode_t m);
/* from crypto.h */
/* 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;
/* 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);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* from dispatcher.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, key_store_t *key_store);
/*
* Process a JSON-RPC request string and produce a JSON-RPC response string.
*
* The caller owns the returned string and must free() it.
* Returns NULL only on catastrophic allocation failure.
*
* Response format on success:
* { "id": "...", "result": "..." }
*
* Response format on error:
* { "id": "...", "error": { "code": <int>, "message": "..." } }
*
* Error codes:
* -32700 Parse error (invalid JSON)
* -32600 Invalid request (missing id/method/params)
* -32601 Method not found (unknown verb after enforcement)
* -32602 Invalid params
* 1001 ambiguous_role_selector
* 1002 role_not_found
* 1003 no_default_role
* 1004 purpose_mismatch
* 1005 curve_mismatch
* 1006 mnemonic_not_loaded
*/
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
/* from server.h */
#define SERVER_SOCKET_NAME_MAX 108
#define SERVER_MAX_MSG_SIZE 65536
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
char caller_id[64]; /* "uid:<n>" */
} caller_identity_t;
/* 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").
* 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);
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data);
/* Stop server and close socket */
void server_stop(server_ctx_t *ctx);
/* Extract caller identity from connected fd */
int server_get_caller(int fd, caller_identity_t *out);
/* from socket_name.h */
/*
* Generate random socket name in format: nsigner_<word1>_<word2>
* Returns 0 on success, -1 on error.
*/
int socket_name_random(char *out, size_t out_len);
/* from main.h */
/*
* nsigner main header - version information
*
* Version macros are auto-updated by increment_and_push.sh.
*/
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 2
#define NSIGNER_VERSION "v0.0.2"
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>
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;
}