Compare commits

...

5 Commits

52 changed files with 11271 additions and 4600 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ build/
.gitea_token
resources/
.test_mnemonic
Trash/

View File

@@ -48,13 +48,13 @@ RUN cd /tmp && \
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
./build.sh --nips=1,4,6,19,44
# Copy source files
COPY src/ /build/src/
# Build nsigner as a fully static binary
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
RUN gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wall -Wextra -std=c99 \
-I/build/nostr_core_lib \
-I/build/nostr_core_lib/nostr_core \
-I/build/nostr_core_lib/cjson \
@@ -67,14 +67,15 @@ RUN gcc -static -O2 -Wall -Wextra -std=c99 \
/build/src/dispatcher.c \
/build/src/policy.c \
/build/src/server.c \
/build/src/crypto.c \
/build/src/transport_frame.c \
/build/src/key_store.c \
/build/src/socket_name.c \
/build/src/bip39_english.c \
/build/nostr_core_lib/libnostr_core_x64.a \
-o /build/nsigner_static \
$(pkg-config --static --libs libcurl openssl) \
-lsecp256k1 -lsqlite3 -lz -lpthread -lm
RUN strip /build/nsigner_static || true
RUN file /build/nsigner_static && \
(ldd /build/nsigner_static 2>&1 || true)

View File

@@ -1,6 +1,6 @@
CC := gcc
CFLAGS := -Wall -Wextra -std=c99 -O2 -Isrc -Isrc/cjson -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson
LDFLAGS := resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -Isrc -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson
LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
SRC_DIR := src
BUILD_DIR := build
@@ -18,23 +18,12 @@ SOURCES := \
$(SRC_DIR)/dispatcher.c \
$(SRC_DIR)/policy.c \
$(SRC_DIR)/server.c \
$(SRC_DIR)/crypto.c \
$(SRC_DIR)/socket_name.c \
$(SRC_DIR)/bip39_english.c
$(SRC_DIR)/transport_frame.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)/crypto.h \
$(SRC_DIR)/socket_name.h \
$(SRC_DIR)/cjson/cJSON.h
TEST_MNEMONIC_TARGET := $(BUILD_DIR)/test_mnemonic
TEST_ROLE_TARGET := $(BUILD_DIR)/test_role_table
@@ -50,11 +39,11 @@ TEST_SOCKET_NAME_TARGET := $(BUILD_DIR)/test_socket_name
all: dev
lib:
cd resources/nostr_core_lib && ./build.sh
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,19,44
dev: lib $(TARGET_DEV)
$(TARGET_DEV): $(SOURCES) $(HEADERS)
$(TARGET_DEV): $(SOURCES)
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(SOURCES) -o $(TARGET_DEV) $(LDFLAGS)
@@ -96,27 +85,27 @@ test-policy: $(TEST_POLICY_TARGET)
test-socket-name: $(TEST_SOCKET_NAME_TARGET)
./$(TEST_SOCKET_NAME_TARGET)
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/mnemonic.h $(SRC_DIR)/secure_mem.h
$(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)/crypto.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/dispatcher.h $(SRC_DIR)/crypto.h $(SRC_DIR)/selector.h $(SRC_DIR)/enforcement.h $(SRC_DIR)/role_table.h $(SRC_DIR)/mnemonic.h $(SRC_DIR)/secure_mem.h $(SRC_DIR)/cjson/cJSON.h
$(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)/crypto.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
$(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)
@@ -124,7 +113,7 @@ $(TEST_INTEGRATION_TARGET): $(TEST_DIR)/test_integration.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_integration.c -o $(TEST_INTEGRATION_TARGET) $(LDFLAGS)
$(TEST_SOCKET_NAME_TARGET): $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c $(SRC_DIR)/socket_name.h
$(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)

View File

@@ -74,7 +74,7 @@ When started, `n_signer` immediately enters terminal input mode:
- 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).
3. Pick the abstract socket name (random BIP-39 pair, or `--socket-name` / `--name` / `-n` 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.
@@ -108,9 +108,9 @@ Activity (latest first)
Hotkeys
-------
a add role
a toggle auto-approve(prompt) for this session
r refresh
l lock session
l lock/reunlock session
q quit
```
@@ -127,7 +127,7 @@ method: sign_event
selector: role=ops
purpose/curve: nostr/secp256k1
[y] allow once [n] deny [a] always allow for this session
[y] allow once [n] deny [a] always allow this session
```
No response is emitted to caller until the local user decides.
@@ -173,6 +173,13 @@ Request shape is JSON-RPC with NIP-46-style methods and optional trailing select
{ "id": "4", "method": "sign_event", "params": ["<event_json>", { "role_path": "m/84'/0'/0'/0/5", "purpose": "bitcoin", "curve": "secp256k1" }] }
```
Implemented signer verbs in this build:
- `get_public_key`
- `sign_event`
- `nip04_encrypt` / `nip04_decrypt`
- `nip44_encrypt` / `nip44_decrypt`
Selector resolution order:
1. `role`
@@ -224,12 +231,15 @@ Properties:
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`.
- Override: `--socket-name <name>` (alias: `--name <name>` / `-n <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 an explicit name override.
Discovery:
- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`.
- `nsigner --listen stdio` runs one framed JSON-RPC request/response over stdin/stdout.
- `nsigner --listen qrexec` is the same stdio framing mode, but caller identity can be derived from `QREXEC_REMOTE_DOMAIN` (displayed as `qubes:<source-vm>`).
- `nsigner --listen tcp:IPv4:PORT` or `tcp:[IPv6]:PORT` enables TCP listening for non-AF_UNIX clients (for example `tcp:127.0.0.1:8080`, `tcp:[::]:8080`, or `tcp:[fd00::1234]:8080`).
### 7.2 ESP32 MCU: USB-CDC serial
@@ -278,7 +288,25 @@ Program starts in attached foreground mode and prompts for mnemonic. After mnemo
To force a specific socket name (e.g. for scripted clients):
```bash
nsigner --socket-name my_test_signer
nsigner --name my_test_signer
```
Qubes/qrexec service mode (single framed request over stdin/stdout):
```bash
nsigner --listen qrexec
```
Generic stdio transport mode (single framed request over stdin/stdout):
```bash
nsigner --listen stdio
```
TCP transport mode (no TUI; serves requests until terminated):
```bash
nsigner --listen tcp:[::]:8080
```
### 9.2 Send a request (client mode)
@@ -286,7 +314,7 @@ nsigner --socket-name my_test_signer
From another terminal, target the signer by its socket name:
```bash
nsigner client --socket-name nsigner_hairy_dog '{"id":"1","method":"get_public_key","params":[]}'
nsigner --socket-name nsigner_hairy_dog client '{"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.
@@ -294,7 +322,7 @@ If only one signer is running you can omit the override and the client will use
Example signing request:
```bash
nsigner client --socket-name nsigner_hairy_dog '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
nsigner -n nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
```
### 9.3 List running signers
@@ -327,7 +355,7 @@ $ nsigner
Terminal B:
```text
$ nsigner client --socket-name nsigner_hairy_dog '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
$ nsigner --socket-name nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
{"id":"2","result":"<signed_event_json>"}
```
@@ -339,13 +367,13 @@ Build outputs a single executable artifact.
- [`Makefile`](Makefile): local build targets
- [`Dockerfile.alpine-musl`](Dockerfile.alpine-musl): reproducible Alpine musl toolchain
- [`increment_and_push.sh`](increment_and_push.sh): version/tag/release workflow
- [`src/main.h`](src/main.h): version macros
- [`src/main.c`](src/main.c): version macros (`NSIGNER_VERSION*`)
Local dev build:
```bash
make dev
./build/nsigner_dev --version
./build/nsigner --version
```
Static build:
@@ -358,6 +386,9 @@ Static build:
## 11. Document map
- [`README.md`](README.md): authoritative behavior specification for the foreground single-program model
- [`documents/CLIENT_IMPLEMENTATION.md`](documents/CLIENT_IMPLEMENTATION.md): client integration contract and framing behavior
- [`documents/QUBES_OS.md`](documents/QUBES_OS.md): Qubes OS deployment/integration checklist for dedicated signer qubes
- [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md): Tier-1 FIPS deployment runbook using loopback TCP listener
- [`plans/nsigner.md`](plans/nsigner.md): implementation plan and sequencing
- [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md): seed phrase domain/use catalog and caveats
- [`firmware/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration

View File

@@ -117,6 +117,7 @@ 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"
strip "$BUILD_DIR/$OUTPUT_NAME" >/dev/null 2>&1 || true
echo "[3/3] Verifying static binary"
file "$BUILD_DIR/$OUTPUT_NAME"

View File

@@ -0,0 +1,458 @@
# CLIENT_IMPLEMENTATION.md
## 1. Purpose
This document is the client-integration spec for `nsigner`.
It is written for agent/tool authors implementing robust request flows against the local signer process.
---
## 2. Discovery and socket targeting
`nsigner` currently supports two transport families:
- Linux AF_UNIX **abstract namespace** sockets.
- Stdio framed mode (`--listen stdio` and `--listen qrexec`) for one request/response exchange.
For AF_UNIX:
- Socket names are exposed in `/proc/net/unix` with a leading `@`.
- Typical runtime names: `@nsigner_hairy_dog`, `@nsigner_brave_canyon`.
- Clients pass the socket name **without** `@` to CLI flags (example: `nsigner_hairy_dog`).
### 2.1 Discovery rules
Use one of these patterns:
1. Explicit target (recommended): pass `--socket-name` / `--name` / `-n`.
2. Enumerate with `nsigner list` and select one.
3. Auto-discovery only when exactly one signer is running.
### 2.2 Enumerating running signers
```bash
nsigner list
```
Expected output format (one per line):
```text
@nsigner
@nsigner_hairy_dog
@nsigner_brave_canyon
```
Clients should accept both `@nsigner` and `@nsigner_*` names.
### 2.3 Stdio / qrexec mode
In server mode:
- `nsigner --listen stdio`: reads exactly one framed request from stdin and writes one framed response to stdout.
- `nsigner --listen qrexec`: same behavior, but caller identity may be tagged from `QREXEC_REMOTE_DOMAIN` as `qubes:<vm-name>`.
This mode is server-side only in the current CLI (the `client` subcommand still targets AF_UNIX).
---
## 3. Transport framing
Signer requests/responses use a length-prefixed frame format over the socket:
- Prefix: 4-byte unsigned big-endian length `N`
- Payload: `N` bytes UTF-8 JSON text
- One JSON-RPC object per frame
### 3.1 Framing pseudocode
Write:
1. Serialize JSON to bytes
2. Compute `len(payload)`
3. Send `uint32_be(length)` then payload bytes
Read:
1. Read exactly 4 bytes
2. Parse big-endian payload length
3. Read exactly `length` bytes
4. Parse JSON
Do not assume line-delimited JSON.
---
## 4. JSON-RPC contract
### 4.1 Request shape
```json
{ "id": "1", "method": "get_public_key", "params": [] }
```
Methods are NIP-46 style verbs.
### 4.2 Implemented methods
- `get_public_key`
- `sign_event`
- `nip04_encrypt`
- `nip04_decrypt`
- `nip44_encrypt`
- `nip44_decrypt`
### 4.3 Selector options
The last param may include selector options:
- `role`
- `nostr_index`
- `role_path`
Resolution order:
1. `role`
2. `nostr_index`
3. `role_path`
4. default role `main`
Conflicting selector fields must be rejected as `ambiguous_role_selector`.
### 4.4 Selector example
```json
{
"id": "2",
"method": "sign_event",
"params": [
"<event_json>",
{ "role": "main" }
]
}
```
---
## 5. Error handling contract
Representative error names clients must handle:
- `invalid_request`
- `method_not_found`
- `ambiguous_role_selector`
- `unknown_role`
- `purpose_mismatch`
- `curve_mismatch`
- `unauthorized`
- `approval_denied`
- `internal_error`
### 5.1 Recovery guidance
- `invalid_request`: client bug or malformed payload; fix request and retry.
- `method_not_found`: version mismatch; feature-detect and downgrade behavior.
- `ambiguous_role_selector`: send exactly one selector strategy.
- `unknown_role`: selector did not resolve; verify role inventory/config.
- `purpose_mismatch` / `curve_mismatch`: selected key is incompatible with method; pick compatible selector.
- `unauthorized`: caller identity disallowed by policy; do not blind-retry.
- `approval_denied`: user rejected prompt; treat as final unless user initiates retry.
- `internal_error`: bounded retry with backoff; surface diagnostics.
---
## 6. Approval semantics
Approval has two layers:
1. Policy/identity checks (caller and method authorization)
2. User prompt (interactive allow/deny)
Important behavior:
- Passing identity checks does **not** bypass prompts.
- Non-interactive/no-TTY contexts can resolve to deny by policy/test configuration.
- Clients must treat `approval_denied` as a normal, expected outcome.
### 6.1 UX recommendation
If a signing call is denied, return control to the user and let them explicitly retry.
---
## 7. Multi-instance, concurrency, and timeouts
### 7.1 Multi-instance safety
- Multiple signers can coexist using different socket names.
- Always pin requests to a selected signer name once chosen.
- Avoid “discover per request” after initial bind in long-running clients.
### 7.2 Connection strategy
- Keep one connection per in-flight request path, or serialize requests if your client runtime is simple.
- Validate response `id` correlation before completing promise/future.
### 7.3 Timeout guidance
Use separate timeouts:
- connect timeout (short)
- write timeout (short)
- read timeout (longer, because human approval may be required)
For prompt-requiring methods, use a read timeout that accounts for user interaction.
---
## 8. Security expectations for clients
- Treat socket access as sensitive local capability.
- Do not log plaintext secrets, private keys, or full decrypted payloads.
- Redact request params for encrypt/decrypt methods in normal logs.
- Validate method-level expectations before sending (selector + purpose compatibility).
- Use least-privilege execution context for any process that can reach the signer socket.
---
## 9. Reference code snippets
## 9.1 C (frame write/read skeleton)
```c
#include <arpa/inet.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
static int write_all(int fd, const void *buf, size_t len) {
const unsigned char *p = (const unsigned char *)buf;
while (len > 0) {
ssize_t n = write(fd, p, len);
if (n <= 0) return -1;
p += (size_t)n;
len -= (size_t)n;
}
return 0;
}
static int read_all(int fd, void *buf, size_t len) {
unsigned char *p = (unsigned char *)buf;
while (len > 0) {
ssize_t n = read(fd, p, len);
if (n <= 0) return -1;
p += (size_t)n;
len -= (size_t)n;
}
return 0;
}
int send_json_frame(int fd, const char *json) {
uint32_t n = (uint32_t)strlen(json);
uint32_t be = htonl(n);
if (write_all(fd, &be, 4) != 0) return -1;
if (write_all(fd, json, n) != 0) return -1;
return 0;
}
```
## 9.2 Python (Unix abstract socket + frame)
```python
import json
import socket
import struct
def send_rpc(socket_name: str, obj: dict) -> dict:
payload = json.dumps(obj, separators=(",", ":")).encode("utf-8")
frame = struct.pack(">I", len(payload)) + payload
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
s.connect("\0" + socket_name) # abstract namespace
s.sendall(frame)
hdr = recv_exact(s, 4)
ln = struct.unpack(">I", hdr)[0]
body = recv_exact(s, ln)
return json.loads(body.decode("utf-8"))
finally:
s.close()
def recv_exact(s: socket.socket, n: int) -> bytes:
out = bytearray()
while len(out) < n:
chunk = s.recv(n - len(out))
if not chunk:
raise ConnectionError("unexpected EOF")
out.extend(chunk)
return bytes(out)
```
## 9.3 TypeScript (Node.js net + frame)
```ts
import net from "node:net";
export async function sendRpc(socketName: string, request: unknown): Promise<any> {
const payload = Buffer.from(JSON.stringify(request), "utf8");
const header = Buffer.alloc(4);
header.writeUInt32BE(payload.length, 0);
return await new Promise((resolve, reject) => {
const socket = net.createConnection({ path: `\u0000${socketName}` });
let chunks: Buffer[] = [];
let needed = 4;
let mode: "header" | "body" = "header";
socket.on("connect", () => {
socket.write(Buffer.concat([header, payload]));
});
socket.on("data", (data) => {
chunks.push(data);
let buf = Buffer.concat(chunks);
while (buf.length >= needed) {
const part = buf.subarray(0, needed);
buf = buf.subarray(needed);
if (mode === "header") {
needed = part.readUInt32BE(0);
mode = "body";
} else {
socket.end();
resolve(JSON.parse(part.toString("utf8")));
return;
}
}
chunks = [buf];
});
socket.on("error", reject);
socket.on("end", () => {
// no-op; resolution occurs when full body is parsed
});
});
}
```
---
## 10. End-to-end transcripts
## 10.1 Happy path: get public key
Request:
```json
{ "id": "1", "method": "get_public_key", "params": [] }
```
Response:
```json
{ "id": "1", "result": "<hex_pubkey>" }
```
## 10.2 Happy path: sign event with explicit role
Request:
```json
{
"id": "2",
"method": "sign_event",
"params": ["<event_json>", { "role": "main" }]
}
```
Response:
```json
{ "id": "2", "result": "<signed_event_json>" }
```
## 10.3 Error path: unknown role
Request:
```json
{
"id": "3",
"method": "sign_event",
"params": ["<event_json>", { "role": "does_not_exist" }]
}
```
Response:
```json
{ "id": "3", "error": { "code": 1002, "message": "unknown_role" } }
```
## 10.4 Error path: ambiguous selector
Request:
```json
{
"id": "4",
"method": "sign_event",
"params": [
"<event_json>",
{ "role": "main", "nostr_index": 0 }
]
}
```
Response:
```json
{ "id": "4", "error": { "message": "ambiguous_role_selector" } }
```
## 10.5 Encrypt/decrypt round trip (NIP-44)
Encrypt request:
```json
{
"id": "5",
"method": "nip44_encrypt",
"params": ["<peer_pubkey>", "hello", { "role": "main" }]
}
```
Encrypt response:
```json
{ "id": "5", "result": "<nip44_ciphertext>" }
```
Decrypt request:
```json
{
"id": "6",
"method": "nip44_decrypt",
"params": ["<peer_pubkey>", "<nip44_ciphertext>", { "role": "main" }]
}
```
Decrypt response:
```json
{ "id": "6", "result": "hello" }
```
---
## 11. Compatibility notes
- If you are writing an autonomous agent client, pin to explicit socket name and explicit role selector.
- Keep method support feature-detected (`method_not_found` fallback).
- Treat approval as asynchronous human gating even for local calls.
- Track and surface signer name, request id, and method for auditability.

View File

@@ -0,0 +1,177 @@
# FIPS_DEPLOYMENT.md
## 1. Scope
This runbook covers a practical Tier-1 deployment of `nsigner` over a TCP listener, with connectivity provided by FIPS as the network substrate.
Tier-1 objective:
- Keep `nsigner` transport simple (`--listen tcp:IPv4:PORT` or `--listen tcp:[IPv6]:PORT`).
- Use FIPS to carry traffic between peers.
- Do not add FIPS runtime dependencies into `nsigner`.
Out of scope in this document:
- Mandatory transport-level TLS/authentication hardening (still planned for a later phase).
- Automatic caller->npub enrichment from FIPS session metadata.
---
## 2. Architecture
Two cooperating layers:
1. **Signer process layer** (`nsigner`)
- Listens on operator-selected TCP endpoint (IPv4 or IPv6).
- Uses existing 4-byte big-endian framed JSON-RPC protocol.
- Keeps existing policy/prompt behavior.
2. **Network substrate layer** (FIPS)
- Establishes peer connectivity between nodes/qubes.
- Carries application traffic to the configured signer TCP endpoint.
Conceptually:
`client app -> local FIPS endpoint -> FIPS mesh -> remote FIPS endpoint -> signer TCP endpoint -> nsigner`
---
## 3. Prerequisites
On signer host/qube:
- Built `nsigner` binary.
- FIPS installed and running.
- Local firewall policy that keeps signer listener local-only.
On caller host/qube:
- FIPS installed and peered with signer host/qube.
- A client implementation that speaks `nsigner` framed JSON-RPC (see `documents/CLIENT_IMPLEMENTATION.md`).
Operational assumptions:
- Operator controls both endpoints.
- Manual verification of peer identity is performed in FIPS tooling before enabling signer traffic.
---
## 4. Start signer in Tier-1 TCP mode
Run `nsigner` in TCP listen mode:
```bash
./build/nsigner --listen tcp:[::]:8080
```
Or bind to a specific FIPS ULA address:
```bash
./build/nsigner --listen tcp:[fd00::1234]:8080
```
Behavior notes:
- Bind target is operator-controlled; pick the narrowest reachable address that satisfies your topology.
- No TUI hotkey loop is required in TCP mode; process serves requests until terminated.
- Caller identity is shown as a TCP endpoint descriptor in activity/prompt context.
---
## 5. FIPS substrate wiring pattern
Because FIPS deployment topologies vary, use this generic pattern:
1. Bind `nsigner` on a deliberate signer endpoint (`[::]:PORT` for broad reach, or specific `fd..` for tighter scope).
2. Configure FIPS service/forwarding so remote authenticated peer traffic is delivered to that signer endpoint.
3. On caller side, direct client traffic to the local FIPS ingress endpoint for that remote service.
Validation checklist:
- FIPS session is established between caller and signer nodes.
- Transport path from caller -> configured signer endpoint succeeds.
- `nsigner` receives framed request and returns framed response.
---
## 6. Minimal validation flow
### 6.1 Liveness check
From caller side, send a framed `get_public_key` request through the FIPS-backed endpoint.
Request JSON:
```json
{"id":"1","method":"get_public_key","params":[]}
```
Expected response:
```json
{"id":"1","result":"<hex_pubkey>"}
```
### 6.2 Signing check
Send `sign_event` with explicit role selector:
```json
{
"id": "2",
"method": "sign_event",
"params": ["<event_json>", {"role":"main"}]
}
```
Expected result: signed event JSON in `result`.
### 6.3 Negative check (policy)
Trigger a request path that requires prompt/denial and confirm client handles policy denial as a normal result path.
---
## 7. Security guardrails
- Prefer binding to a specific FIPS IPv6 ULA (`fd..`) rather than wildcard (`[::]`) when possible.
- Do not expose signer port directly on LAN/WAN.
- Keep FIPS peer allowlist tight; avoid broad trust domains.
- Treat FIPS connectivity as transport, not authorization bypass.
- Preserve interactive approval where required by policy.
---
## 8. Troubleshooting
### 8.1 `invalid tcp listen target`
Cause:
- `--listen` argument does not match `tcp:HOST:PORT` or `tcp:[IPv6]:PORT`.
Fix:
- Use valid numeric port and valid IPv4/IPv6 literal host.
### 8.2 Framing parse failures (`parse_error`)
Cause:
- Client sent line-delimited/raw JSON instead of framed JSON.
Fix:
- Send 4-byte big-endian length prefix followed by exact UTF-8 JSON payload bytes.
### 8.3 FIPS path up, signer path down
Cause:
- FIPS session exists but forwarding/service mapping to signer loopback endpoint is missing.
Fix:
- Verify substrate service routing config and local endpoint mapping.
---
## 9. Next hardening steps (post Tier-1)
- Add automated two-node validation script for operator smoke checks.
- Add optional identity enrichment from FIPS session metadata (`peer_npub`).
- Introduce remote TCP mode only with mandatory TLS + authenticated caller key flow.

179
documents/QUBES_OS.md Normal file
View File

@@ -0,0 +1,179 @@
# QUBES_OS.md
## 1. Goal
Run `n_signer` inside a dedicated Qubes OS qube (for example `vault`-like behavior), and let caller qubes access signing via qrexec with explicit policy control.
This doc outlines what must be implemented/packaged for a reliable Qubes deployment path.
---
## 2. Current status (where we are now)
Implemented in current codebase:
- `nsigner` supports `--listen qrexec` and `--listen stdio`.
- Framing is transport-agnostic and shared via length-prefixed JSON (`4-byte big-endian length + payload`).
- In qrexec/stdio mode, server handles one framed request-response exchange.
- Caller identity extraction supports `QREXEC_REMOTE_DOMAIN`, surfaced as `qubes:<source-vm>` when available.
Still missing for complete Qubes integration:
- qrexec service file + wrapper script artifacts.
- dom0 qrexec policy artifacts with sane defaults.
- install/uninstall guidance and verification flow for real Qubes deployment.
- packaging path (`packaging/qubes/`) and docs wired into README map.
---
## 3. Architecture in Qubes
### 3.1 Components
- **Signer qube** (target): runs `nsigner` service entrypoint.
- **Caller qube(s)**: apps/tools invoking qrexec service.
- **dom0 policy**: controls which caller qubes may invoke signer service.
### 3.2 Request path
1. Caller qube invokes qrexec service (e.g. `qubes.NsignerRpc`).
2. qrexec starts service command inside signer qube.
3. Service command runs `nsigner --listen qrexec`.
4. Caller sends framed JSON-RPC request over qrexec stdio channel.
5. `nsigner` returns framed JSON-RPC response.
### 3.3 Trust and identity
- Source qube identity comes from `QREXEC_REMOTE_DOMAIN`.
- `n_signer` maps caller as `qubes:<source-vm>` where available.
- qrexec policy in dom0 remains first enforcement boundary.
- `n_signer` policy/approval remains second boundary.
---
## 4. Required implementation tasks
## 4.1 Service entrypoint artifacts ✅ Implemented
Implemented repo artifacts:
- `packaging/qubes/rpc/qubes.NsignerRpc`
- `packaging/qubes/install-service.sh`
`qubes.NsignerRpc` runs:
- `exec /usr/local/bin/nsigner --listen qrexec`
Install inside the signer qube:
```bash
sudo sh packaging/qubes/install-service.sh
```
This installs the qrexec service to `/etc/qubes-rpc/qubes.NsignerRpc` with executable permissions.
## 4.2 dom0 policy artifacts ✅ Implemented
Implemented repo artifacts:
- `packaging/qubes/policy.d/40-nsigner.policy`
- `packaging/qubes/install-policy.sh`
Policy defaults now use explicit `ask` plus deny catch-all:
- `qubes.NsignerRpc * @anyvm @tag:nsigner-signer ask default_target=nsigner-vault`
- `qubes.NsignerRpc * @anyvm @anyvm deny`
Install in dom0:
```bash
sudo sh packaging/qubes/install-policy.sh
```
This installs `/etc/qubes/policy.d/40-nsigner.policy` and prints signer-tag guidance.
## 4.3 Policy model inside n_signer for qubes callers ✅ Implemented
Current code reads caller as `qubes:<vm>` and qrexec default behavior is hardened.
In qrexec mode, default prompt behavior is now:
- `PROMPT_EVERY_REQUEST`
This replaces the previous permissive `PROMPT_NEVER` temporary setting.
## 4.4 Client helper examples ✅ Implemented
Added:
- `documents/qubes_client_examples.md`
Includes:
- shell helper example invoking `qrexec-client-vm` with framed request/response handling
- Python helper example implementing frame encode/decode over qrexec stdio channel
- reference to `documents/CLIENT_IMPLEMENTATION.md` for full protocol details
---
## 5. Operational runbook
## 5.1 Setup signer qube
- install `nsigner` binary at `/usr/local/bin/nsigner`
- run `sudo sh packaging/qubes/install-service.sh`
- verify `/etc/qubes-rpc/qubes.NsignerRpc` exists and is executable
## 5.2 Setup dom0 policy
- run `sudo sh packaging/qubes/install-policy.sh`
- tag signer qube (example): `qvm-tags nsigner-vault add nsigner-signer`
- reload qrexec policy per Qubes procedure/version
## 5.3 Verification
- from caller qube, invoke test request (`get_public_key`)
- confirm signer qube receives request
- confirm activity log displays `qubes:<source-vm>` caller prefix
- validate deny behavior from unauthorized qube
## 5.4 Failure checks
- malformed frame -> parse error response
- missing policy -> deny path
- missing `QREXEC_REMOTE_DOMAIN` -> fallback identity path
---
## 6. Security requirements
- Never run signer service in disposable qube if mnemonic persistence is expected.
- Prefer dedicated minimal template for signer qube.
- Keep qrexec policy narrowly scoped (explicit source + target).
- Require user approval for sensitive methods unless explicitly intended otherwise.
- Log caller identity and method (without secret payload logging).
---
## 7. Documentation tasks
Update these after packaging lands:
- `README.md`
- add Qubes deployment subsection under transport/usage
- add `documents/QUBES_OS.md` and moved `documents/CLIENT_IMPLEMENTATION.md` in document map
- `plans/nsigner.md`
- mark T1 done with packaging status clearly separated
---
## 8. Definition of done (Qubes)
Qubes integration is considered complete when:
1. qrexec service artifact exists and is installable.
2. dom0 policy artifact exists with secure default pattern.
3. End-to-end call from allowed caller qube succeeds.
4. Call from unauthorized qube is denied.
5. Caller displayed as `qubes:<vm>` in activity.
6. README + docs include full setup and troubleshooting.

View File

@@ -0,0 +1,93 @@
# qubes_client_examples.md
This document shows minimal caller-qube examples for invoking `nsigner --listen qrexec` through Qubes qrexec.
For complete protocol details (framing, JSON-RPC, error handling), see `documents/CLIENT_IMPLEMENTATION.md`.
---
## 1) Shell example (`qrexec-client-vm` + framed JSON)
This sends one `get_public_key` request and decodes one framed response.
```bash
#!/bin/sh
set -eu
TARGET_QUBE="nsigner-vault"
SERVICE="qubes.NsignerRpc"
REQ='{"id":"1","method":"get_public_key","params":[]}'
python3 - "$TARGET_QUBE" "$SERVICE" "$REQ" <<'PY'
import json
import struct
import subprocess
import sys
target, service, req_json = sys.argv[1], sys.argv[2], sys.argv[3]
frame = struct.pack(">I", len(req_json.encode("utf-8"))) + req_json.encode("utf-8")
p = subprocess.Popen(
["qrexec-client-vm", target, service],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
out, _ = p.communicate(frame)
if p.returncode != 0:
raise SystemExit(f"qrexec-client-vm failed: {p.returncode}")
if len(out) < 4:
raise SystemExit("short response (missing frame header)")
n = struct.unpack(">I", out[:4])[0]
payload = out[4:4+n]
if len(payload) != n:
raise SystemExit("short response payload")
print(json.dumps(json.loads(payload.decode("utf-8")), indent=2))
PY
```
---
## 2) Python example (explicit frame helpers over qrexec stdio)
```python
#!/usr/bin/env python3
import json
import struct
import subprocess
def frame_encode(obj: dict) -> bytes:
payload = json.dumps(obj, separators=(",", ":")).encode("utf-8")
return struct.pack(">I", len(payload)) + payload
def frame_decode(buf: bytes) -> dict:
if len(buf) < 4:
raise ValueError("missing frame header")
n = struct.unpack(">I", buf[:4])[0]
payload = buf[4:4 + n]
if len(payload) != n:
raise ValueError("short frame payload")
return json.loads(payload.decode("utf-8"))
def call_nsigner_qrexec(target_qube: str, request: dict) -> dict:
proc = subprocess.Popen(
["qrexec-client-vm", target_qube, "qubes.NsignerRpc"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = proc.communicate(frame_encode(request))
if proc.returncode != 0:
raise RuntimeError(f"qrexec failed ({proc.returncode}): {err.decode('utf-8', 'replace')}")
return frame_decode(out)
if __name__ == "__main__":
req = {"id": "1", "method": "get_public_key", "params": []}
resp = call_nsigner_qrexec("nsigner-vault", req)
print(json.dumps(resp, indent=2))
```

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() {

271
install_qube_fips_nsigner.sh Executable file
View File

@@ -0,0 +1,271 @@
#!/usr/bin/env bash
set -euo pipefail
# User-only installer for Qubes AppVM persistence model.
# Nothing is written to /usr, /etc, or other root-owned paths.
#
# Installs into $HOME:
# - nsigner -> ~/.local/bin/nsigner
# - fips/fipsctl/fipstop -> ~/.local/bin/
# - fips config -> ~/.config/fips/fips.yaml
# - user service -> ~/.config/systemd/user/fips.service
#
# Usage:
# bash install_qube_fips_nsigner.sh
# bash install_qube_fips_nsigner.sh --help
#
# Optional env vars:
# NSIGNER_VERSION=v0.0.6
# FIPS_VERSION=v0.2.0-rel
# NSIGNER_GITEA_TOKEN=<token> # if n_signer release assets are private
# NSIGNER_BINARY_URL=<direct url to nsigner_static_x86_64>
NSIGNER_VERSION="${NSIGNER_VERSION:-v0.0.6}"
FIPS_VERSION="${FIPS_VERSION:-v0.2.0-rel}"
NSIGNER_RELEASE_PAGE="https://git.laantungir.net/laantungir/n_signer/releases/tag/${NSIGNER_VERSION}"
NSIGNER_API_TAG_URL="https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases/tags/${NSIGNER_VERSION}"
NSIGNER_GIT_URL="https://git.laantungir.net/laantungir/n_signer.git"
FIPS_RELEASE_PAGE="https://github.com/jmcorgan/fips/releases/tag/${FIPS_VERSION}"
FIPS_ARCHIVE_URL="https://github.com/jmcorgan/fips/archive/refs/tags/${FIPS_VERSION}.tar.gz"
PREFIX_BIN="${HOME}/.local/bin"
PREFIX_CFG="${HOME}/.config"
PREFIX_DATA="${HOME}/.local/share/nsigner-fips-install"
SYSTEMD_USER_DIR="${HOME}/.config/systemd/user"
log() { printf "\033[1;34m[INFO]\033[0m %s\n" "$*"; }
warn() { printf "\033[1;33m[WARN]\033[0m %s\n" "$*"; }
err() { printf "\033[1;31m[ERR ]\033[0m %s\n" "$*"; }
show_help() {
cat <<EOF
Usage: bash install_qube_fips_nsigner.sh [options]
User-only install (Qubes AppVM friendly):
- n_signer ${NSIGNER_VERSION}
- FIPS ${FIPS_VERSION}
- user-level systemd service for FIPS
Options:
-h, --help Show this help and exit
Optional env vars:
NSIGNER_VERSION=v0.0.6
FIPS_VERSION=v0.2.0-rel
NSIGNER_GITEA_TOKEN=<token> # required if n_signer release assets are private
NSIGNER_BINARY_URL=<direct url to nsigner_static_x86_64>
Install paths:
~/.local/bin/nsigner
~/.local/bin/fips
~/.local/bin/fipsctl
~/.local/bin/fipstop
~/.config/fips/fips.yaml
~/.config/systemd/user/fips.service
EOF
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
err "Missing command: $1"
exit 1
}
}
install_build_deps() {
if command -v apt-get >/dev/null 2>&1; then
log "Installing build/runtime dependencies via apt"
sudo apt-get update
sudo apt-get install -y \
ca-certificates curl jq tar gzip git python3 \
build-essential pkg-config libssl-dev zlib1g-dev \
rustc cargo
elif command -v dnf >/dev/null 2>&1; then
log "Installing build/runtime dependencies via dnf"
sudo dnf install -y \
ca-certificates curl jq tar gzip git python3 \
gcc make pkgconf-pkg-config openssl-devel zlib-devel \
rust cargo
else
err "Unsupported distro: need apt-get or dnf to install build dependencies"
exit 1
fi
}
prepare_dirs() {
mkdir -p "${PREFIX_BIN}" "${PREFIX_CFG}/fips" "${PREFIX_DATA}" "${SYSTEMD_USER_DIR}"
}
download_nsigner_asset_url() {
local headers=()
if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then
headers=(-H "Authorization: token ${NSIGNER_GITEA_TOKEN}")
fi
curl -fsSL "${headers[@]}" "${NSIGNER_API_TAG_URL}" \
| jq -r '.assets[]?.browser_download_url // empty' \
| grep -E 'nsigner_static_x86_64$' \
| head -n1 || true
}
install_nsigner() {
log "Installing n_signer ${NSIGNER_VERSION}"
log "Release page: ${NSIGNER_RELEASE_PAGE}"
local asset_url="${NSIGNER_BINARY_URL:-}"
if [[ -z "${asset_url}" ]]; then
asset_url="$(download_nsigner_asset_url)"
fi
if [[ -z "${asset_url}" ]]; then
err "Could not find downloadable n_signer x86_64 release binary for ${NSIGNER_VERSION}."
err "Provide NSIGNER_BINARY_URL or NSIGNER_GITEA_TOKEN so the release asset can be resolved."
exit 1
fi
log "Using n_signer binary URL: ${asset_url}"
if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then
curl -fL -H "Authorization: token ${NSIGNER_GITEA_TOKEN}" -o "${PREFIX_BIN}/nsigner" "${asset_url}"
else
curl -fL -o "${PREFIX_BIN}/nsigner" "${asset_url}"
fi
chmod 0755 "${PREFIX_BIN}/nsigner"
log "Installed ${PREFIX_BIN}/nsigner from release binary"
}
install_fips() {
log "Installing FIPS ${FIPS_VERSION}"
log "Release page: ${FIPS_RELEASE_PAGE}"
rm -rf "${PREFIX_DATA}/fips-src"
mkdir -p "${PREFIX_DATA}/fips-src"
curl -fL -o "${PREFIX_DATA}/fips.tar.gz" "${FIPS_ARCHIVE_URL}"
tar -xzf "${PREFIX_DATA}/fips.tar.gz" -C "${PREFIX_DATA}/fips-src" --strip-components=1
(
cd "${PREFIX_DATA}/fips-src"
cargo build --release
install -m 0755 ./target/release/fips "${PREFIX_BIN}/fips"
install -m 0755 ./target/release/fipsctl "${PREFIX_BIN}/fipsctl"
if [[ -x ./target/release/fipstop ]]; then
install -m 0755 ./target/release/fipstop "${PREFIX_BIN}/fipstop"
fi
)
log "Installed FIPS binaries into ${PREFIX_BIN}"
}
write_fips_user_config() {
local cfg="${PREFIX_CFG}/fips/fips.yaml"
if [[ -f "${cfg}" ]]; then
warn "Keeping existing ${cfg}"
return
fi
cat >"${cfg}" <<'EOF'
# User-local FIPS config for AppVM persistence
# Edit peers to join your mesh.
node:
identity:
persistent: true
# Try userspace tun name; adjust if your environment requires a different setting.
tun:
enabled: true
name: fips0
mtu: 1280
dns:
enabled: true
bind_addr: "127.0.0.1"
port: 5354
transports:
udp:
bind_addr: "0.0.0.0:2121"
tcp:
bind_addr: "0.0.0.0:8443"
peers: []
EOF
log "Wrote ${cfg}"
}
write_fips_user_service() {
local svc="${SYSTEMD_USER_DIR}/fips.service"
cat >"${svc}" <<EOF
[Unit]
Description=FIPS mesh daemon (user)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=${PREFIX_BIN}/fips --config ${PREFIX_CFG}/fips/fips.yaml
Restart=on-failure
RestartSec=2
Environment=RUST_LOG=info
[Install]
WantedBy=default.target
EOF
log "Wrote ${svc}"
}
start_fips_user_service() {
require_cmd systemctl
systemctl --user daemon-reload
systemctl --user enable --now fips
if systemctl --user is-active --quiet fips; then
log "fips user service: active"
else
warn "fips user service is not active"
warn "Check logs with: journalctl --user -u fips -n 200 --no-pager"
fi
}
post_checks() {
export PATH="${PREFIX_BIN}:${PATH}"
log "Running post-install checks"
require_cmd nsigner
require_cmd fips
require_cmd fipsctl
nsigner --version || true
fips --version || true
log "User binaries installed in: ${PREFIX_BIN}"
log "If needed, add to shell PATH: export PATH=\"${PREFIX_BIN}:\$PATH\""
log "Edit peers in: ${PREFIX_CFG}/fips/fips.yaml"
}
main() {
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
show_help
exit 0
fi
install_build_deps
prepare_dirs
install_nsigner
install_fips
write_fips_user_config
write_fips_user_service
start_fips_user_service
post_checks
log "Completed user-only install of n_signer + FIPS"
}
main "$@"

View File

@@ -0,0 +1,17 @@
#!/bin/sh
set -eu
POLICY_SRC="packaging/qubes/policy.d/40-nsigner.policy"
POLICY_DST="/etc/qubes/policy.d/40-nsigner.policy"
if [ ! -f "$POLICY_SRC" ]; then
echo "Missing policy source: $POLICY_SRC" >&2
exit 1
fi
install -m 0644 "$POLICY_SRC" "$POLICY_DST"
echo "Installed qrexec policy to $POLICY_DST"
echo "Tag your signer qube in dom0, for example:"
echo " qvm-tags nsigner-vault add nsigner-signer"
echo "Then reload policy per your Qubes OS version procedures."

View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -eu
SERVICE_SRC="packaging/qubes/rpc/qubes.NsignerRpc"
SERVICE_DST="/etc/qubes-rpc/qubes.NsignerRpc"
if [ ! -f "$SERVICE_SRC" ]; then
echo "Missing service source: $SERVICE_SRC" >&2
exit 1
fi
install -m 0755 "$SERVICE_SRC" "$SERVICE_DST"
echo "Installed qrexec service to $SERVICE_DST"
echo "Executable bit set via install -m 0755."

View File

@@ -0,0 +1,5 @@
# Qubes OS qrexec policy for nsigner
# Syntax: service +argument source target action
# Allow specific qubes to reach the signer qube with user confirmation
qubes.NsignerRpc * @anyvm @tag:nsigner-signer ask default_target=nsigner-vault
qubes.NsignerRpc * @anyvm @anyvm deny

View File

@@ -0,0 +1,2 @@
#!/bin/sh
exec /usr/local/bin/nsigner --listen qrexec

View File

@@ -105,7 +105,7 @@ Steps:
- `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 *[]`.
- Use the wordlist exposed by `nostr_core_lib` (no vendored `bip39_english.c` in this repo).
- `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.
@@ -140,7 +140,7 @@ Privacy/UX notes:
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.
3. `--socket-name <name>` overrides the random pick for tests and scripted usage (aliases: `--name`, `-n`).
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.
@@ -163,18 +163,130 @@ Privacy/UX notes:
## 5. Open questions
- Automated test strategy for interactive TUI (stdin pipe? expect-style? separate test mode flag?)
- ESP32 transport shim interface definition
- NIP-46 relay transport integration timeline
- Role enrollment UX: how many questions at startup vs. "just use main = index 0"?
- Lock-without-exit: needed? Or is quit-and-relaunch sufficient?
- ESP32 transport shim interface definition and frame format details.
- NIP-46 relay transport integration timeline.
- Role enrollment UX depth at startup vs. minimal defaults.
- Optional policy granularity beyond same-uid + interactive prompt (e.g. per-verb/per-role session rules).
## 6. Immediate next work
- Execute Phase A (cleanup)
- Execute Phase B (server module)
- 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)
- Add regression tests for prompt-overlay behavior and running-phase hotkeys.
- Expand integration coverage for NIP-04/NIP-44 edge cases and negative-path errors.
- Document and test static artifact size budgets across targets.
- Define MCU transport adapter contract to prepare desktop/firmware parity.
## 7. Transport expansion roadmap
Goal: keep one signer core, swap transports underneath without touching dispatcher, policy, or role layers. The wire contract in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) (4-byte length-prefixed JSON-RPC) stays identical across every transport; only listener and `caller_identity_t` change.
### 7.0 Prerequisite — transport abstraction (Phase T0)
Before adding any new transport, factor a small adapter contract out of [`src/server.c`](../src/server.c) and [`src/main.c`](../src/main.c).
- New header `src/transport.h` declaring an opaque `nsigner_transport_t` with:
- `accept(listener) -> connection`
- `recv_frame(connection) -> bytes`
- `send_frame(connection, bytes)`
- `peer_identity(connection) -> caller_identity_t`
- `close(connection)` / `shutdown(listener)`
- Generalize `caller_identity_t` to a tagged union of:
- `unix_peer { uid, pid, comm }` (current behavior)
- `qubes { source_qube_name }`
- `tcp_local { addr }`
- `tcp_remote { addr, authenticated_pubkey }`
- `fips { peer_npub }`
- `usb_serial { device_path, asserted_caller }`
- Move `recv_framed` / `send_framed` from `server.c` and `main.c` into a single shared `transport_frame.c` so client and server share one framing implementation.
- Server main loop becomes transport-agnostic (`while accept; recv; dispatch; send`).
- Tests: extend [`tests/test_integration.c`](../tests/test_integration.c) with a transport-loopback fake to validate the abstraction without binding any real socket.
This refactor is purely internal — no observable change.
### 7.1 Phase T1 — Qubes OS qrexec transport
Use Qubes' native inter-qube primitive instead of inventing one.
- Add a qrexec service script (e.g. `qubes.NsignerRpc`) that execs `nsigner` in a "stdio transport" mode where stdin/stdout carry the existing length-prefixed frame protocol.
- New CLI: `nsigner --listen stdio` (and `nsigner --listen qrexec`, behaving identically; `qrexec` value is for documentation/intent).
- Caller identity comes from qrexec environment (`QREXEC_REMOTE_DOMAIN`) and is mapped to `caller_identity_t.kind=qubes`.
- Reference policy file under `packaging/qubes/policy.d/40-nsigner.policy` showing `ask` / `allow` per source qube.
- No new attack surface inside nsigner: dom0 enforces who can even invoke the service.
- Tests: a unit test that injects fake qrexec env vars and a stdio framing harness; an integration script that documents end-to-end install in a Qubes VM (manual, not in CI).
- Docs: add a "Qubes deployment" section to [`README.md`](../README.md) and to [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).
### 7.2 Phase T2 — TCP transport
Smallest IP-based step; on-ramp for non-Linux clients and for FIPS later.
- New CLI: `nsigner --listen tcp:HOST:PORT` (IPv4 literal) or `nsigner --listen tcp:[IPv6]:PORT`.
- Current behavior: accepts operator-selected local/remote bind addresses (including `[::]` and `fd..`), pending later transport hardening.
- Caller identity for TCP: endpoint address/port in caller descriptor. Approval prompt still mandatory.
- `nsigner list` extended to enumerate active TCP listeners (from internal registry; not from `/proc/net/tcp`).
- Same framing as AF_UNIX path; no protocol changes.
- Tests: integration coverage that spawns a child signer with `--listen tcp:127.0.0.1:0` (and one IPv6 case), captures the bound port, runs the same NIP-04/NIP-44/sign_event matrix as AF_UNIX.
- Docs: extend [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) section 2 with `tcp:` discovery rules and section 3 confirming framing parity.
Implementation checklist (Tier-1 delivery):
- [x] Parse `--listen tcp:HOST:PORT` in [`src/main.c`](../src/main.c).
- [x] Parse and validate literal IPv4/IPv6 listen targets in [`src/server.c`](../src/server.c).
- [x] Bind/listen non-blocking TCP sockets and run server loop without TUI dependence.
- [x] Keep existing framed JSON-RPC protocol unchanged via shared [`src/transport_frame.c`](../src/transport_frame.c).
- [ ] Add integration test coverage for `tcp:127.0.0.1:PORT` request flow.
### 7.3 Phase T3 — TCP remote with TLS + caller-pubkey auth
Only after T2 is solid.
- New CLI: `nsigner --listen tcp:0.0.0.0:PORT --allow-remote --tls-cert <pem> --tls-key <pem>`.
- Mandatory: TLS for any non-loopback bind. Refuse to start otherwise.
- Caller authentication: client must sign a per-connection challenge with its declared npub (Schnorr/secp256k1) before any signer verb is dispatched. Identity becomes `tcp_remote { addr, authenticated_pubkey }`.
- Failure modes: `transport_tls_required`, `caller_auth_failed`, `caller_auth_timeout` — all surfaced with new error names in dispatcher and documented in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).
- Approval prompt now displays `caller=npub:abcd…wxyz` instead of `uid:1000`.
- Tests: integration test that exercises happy path, wrong-pubkey, replayed-challenge, expired-challenge.
- Docs: dedicated "Remote TCP deployment" section in `README.md` with strong "do not expose to the public internet without firewalling" warning.
### 7.4 Phase T4 — FIPS substrate integration
FIPS is a *substrate* for an existing TCP listener, not a new transport in nsigner code.
- Deployment topology: nsigner binds a chosen TCP endpoint inside the FIPS network namespace (or on a host where `fips0` is up); peers reach it via `fd00::/8` IPv6 derived from the signer's npub.
- Optional `caller_kind=fips` enrichment: a small sidecar query (`fipsctl show sessions` style) maps the connecting IPv6 address to a peer npub and feeds it into `caller_identity_t.fips { peer_npub }`. If unavailable, fall back to `tcp_remote` identity.
- nsigner does not embed FIPS, does not depend on libfips, and does not require Rust.
- New optional flag: `--peer-id-source fips:/var/run/fips/fips.sock` (path/method TBD per FIPS API).
- Tests: a Docker-compose fixture borrowed from `resources/fips/testing/` that boots two FIPS nodes, runs nsigner on one, runs a Python client (per snippet in [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md)) on the other, and exercises the same verb matrix.
- Docs: new [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md) deep-dive describing identity mapping, npub-as-caller, and operator setup. Cross-link from [`README.md`](../README.md) section 7 (Transport).
Execution tasks for initial FIPS trial:
- [x] Deliver T2 TCP listener as FIPS substrate prerequisite.
- [x] Document signer/caller qube deployment flow in [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md).
- [ ] Add two-node operator validation script (manual) using `fipsctl` + framed JSON-RPC client.
- [ ] Evaluate optional caller identity enrichment from FIPS session metadata.
### 7.5 Phase T5 — USB / serial transport
Two distinct sub-tracks; do not conflate.
- T5a (firmware-side, MCU): ESP32/USB-CDC. Already in the [`firmware/`](../firmware/) track. Same dispatcher; transport adapter is UART read/write loop. `caller_identity_t.kind=usb_serial` with `asserted_caller` because the host claims the identity.
- T5b (host-side optional): `nsigner --listen serial:/dev/ttyACM0,baud=115200`. Useful for desktop signer reachable by a USB-tethered client. Same frame protocol over the serial line. Marks identity as asserted (low trust) and forces approval prompt.
- USB-as-Ethernet (gadget mode, RNDIS/ECM) is **not** a separate transport — it reduces to T2/T3.
- Tests: loopback pty pair (`openpty`) for T5b unit/integration coverage; firmware-side covered in firmware track.
### 7.6 Cross-cutting concerns
Apply once per phase as needed:
- Transport-aware approval prompt: clear visual indication of transport kind and identity (uid vs qube vs npub vs serial-asserted). No silent identity-source confusion.
- Per-transport policy gates: deny-by-default for new identity kinds until operator explicitly enables them in policy.
- Discovery (`nsigner list`) becomes per-transport pluggable (proc/net/unix today, internal registry for tcp, qrexec service announce for qubes, fips peer table for fips).
- Audit logging: include transport kind and identity descriptor in every approval/decision record.
- Error name parity: every new transport introduces only well-named errors (extend the table in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) section 5).
### 7.7 Decision points (open)
- D1: Land T0 (refactor) before any transport, or in parallel with T1?
- D2: Bundle T2 and T3 as one phase, or hard split (loopback-only first, then remote-with-TLS later)?
- D3: T4 FIPS — embed an explicit `caller_kind=fips` path in nsigner now, or treat FIPS as plain TCP and revisit identity enrichment after a working deployment?
- D4: T5b host-side serial — in scope for desktop nsigner, or strictly firmware track?
- D5: Qubes packaging — ship `packaging/qubes/` artifacts in this repo, or document only and let operators wire it up?

View File

@@ -1,12 +0,0 @@
/*
* Placeholder translation unit for BIP-39 English wordlist ownership.
*
* Current implementation uses nostr_core_lib via:
* nostr_bip39_mnemonic_from_bytes(...)
* which already encapsulates the BIP-39 English wordlist.
*
* This file exists to satisfy build/plan structure and future portability work.
*/
int nsigner_bip39_english_placeholder(void) {
return 0;
}

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,232 +0,0 @@
#define _GNU_SOURCE
#include "crypto.h"
#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_init(void) {
return nostr_init();
}
void crypto_cleanup(void) {
nostr_cleanup();
}
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic) {
int i;
int derived_count;
if (store == NULL || table == NULL || mnemonic == NULL) {
return -1;
}
if (!mnemonic_is_loaded(mnemonic)) {
return -1;
}
crypto_wipe(store);
derived_count = 0;
for (i = 0; i < table->count; ++i) {
role_entry_t *role = &table->entries[i];
unsigned char priv[32];
unsigned char pub[32];
derived_key_t *dst = &store->keys[i];
role->derived = 0;
role->pubkey_hex[0] = '\0';
if (role->purpose != PURPOSE_NOSTR ||
role->curve != CURVE_SECP256K1 ||
role->selector_type != SELECTOR_NOSTR_INDEX) {
continue;
}
if (secure_buf_alloc(&dst->private_key, 32) != 0) {
crypto_wipe(store);
return -1;
}
if (nostr_derive_keys_from_mnemonic(mnemonic_get_phrase(mnemonic), role->nostr_index, priv, pub) != 0) {
secure_memzero(priv, sizeof(priv));
secure_memzero(pub, sizeof(pub));
crypto_wipe(store);
return -1;
}
memcpy(dst->private_key.data, priv, 32);
memcpy(dst->public_key, pub, 32);
nostr_bytes_to_hex(pub, 32, dst->pubkey_hex);
dst->npub[0] = '\0';
(void)nostr_key_to_bech32(pub, "npub", dst->npub);
strncpy(role->pubkey_hex, dst->pubkey_hex, sizeof(role->pubkey_hex) - 1);
role->pubkey_hex[sizeof(role->pubkey_hex) - 1] = '\0';
role->derived = 1;
dst->valid = 1;
derived_count++;
secure_memzero(priv, sizeof(priv));
secure_memzero(pub, sizeof(pub));
}
store->count = table->count;
return derived_count;
}
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index) {
if (store == NULL || role_index < 0 || role_index >= ROLE_TABLE_MAX_ENTRIES) {
return NULL;
}
if (!store->keys[role_index].valid || store->keys[role_index].private_key.data == NULL) {
return NULL;
}
return (const unsigned char *)store->keys[role_index].private_key.data;
}
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index) {
if (store == NULL || role_index < 0 || role_index >= ROLE_TABLE_MAX_ENTRIES) {
return NULL;
}
if (!store->keys[role_index].valid) {
return NULL;
}
return store->keys[role_index].pubkey_hex;
}
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json) {
const unsigned char *private_key;
cJSON *parsed = NULL;
cJSON *kind_item;
cJSON *content_item;
cJSON *tags_item;
cJSON *created_at_item;
cJSON *tags_for_sign = NULL;
int kind;
const char *content;
time_t timestamp = 0;
cJSON *signed_event = NULL;
char *out = NULL;
if (event_json == NULL) {
return NULL;
}
private_key = crypto_get_private_key(store, role_index);
if (private_key == NULL) {
return NULL;
}
parsed = cJSON_Parse(event_json);
if (parsed == NULL || !cJSON_IsObject(parsed)) {
cJSON_Delete(parsed);
return NULL;
}
kind_item = cJSON_GetObjectItemCaseSensitive(parsed, "kind");
content_item = cJSON_GetObjectItemCaseSensitive(parsed, "content");
tags_item = cJSON_GetObjectItemCaseSensitive(parsed, "tags");
created_at_item = cJSON_GetObjectItemCaseSensitive(parsed, "created_at");
if (!cJSON_IsNumber(kind_item) || !cJSON_IsString(content_item) || content_item->valuestring == NULL) {
cJSON_Delete(parsed);
return NULL;
}
kind = kind_item->valueint;
content = content_item->valuestring;
if (cJSON_IsNumber(created_at_item)) {
timestamp = (time_t)created_at_item->valuedouble;
}
if (cJSON_IsArray(tags_item)) {
tags_for_sign = cJSON_Duplicate(tags_item, 1);
if (tags_for_sign == NULL) {
cJSON_Delete(parsed);
return NULL;
}
}
signed_event = nostr_create_and_sign_event(kind, content, tags_for_sign, private_key, timestamp);
cJSON_Delete(tags_for_sign);
cJSON_Delete(parsed);
if (signed_event == NULL) {
return NULL;
}
out = cJSON_PrintUnformatted(signed_event);
cJSON_Delete(signed_event);
return out;
}
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext) {
(void)store;
(void)role_index;
(void)recipient_pubkey_hex;
(void)plaintext;
return NULL;
}
char *crypto_nip44_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext) {
(void)store;
(void)role_index;
(void)sender_pubkey_hex;
(void)ciphertext;
return NULL;
}
char *crypto_nip04_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext) {
(void)store;
(void)role_index;
(void)recipient_pubkey_hex;
(void)plaintext;
return NULL;
}
char *crypto_nip04_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext) {
(void)store;
(void)role_index;
(void)sender_pubkey_hex;
(void)ciphertext;
return NULL;
}
void crypto_wipe(key_store_t *store) {
int i;
if (store == NULL) {
return;
}
for (i = 0; i < ROLE_TABLE_MAX_ENTRIES; ++i) {
secure_buf_free(&store->keys[i].private_key);
secure_memzero(store->keys[i].public_key, sizeof(store->keys[i].public_key));
secure_memzero(store->keys[i].pubkey_hex, sizeof(store->keys[i].pubkey_hex));
secure_memzero(store->keys[i].npub, sizeof(store->keys[i].npub));
store->keys[i].valid = 0;
}
store->count = 0;
}

View File

@@ -1,66 +0,0 @@
#ifndef NSIGNER_CRYPTO_H
#define NSIGNER_CRYPTO_H
#include "role_table.h"
#include "secure_mem.h"
#include "mnemonic.h"
#include <cJSON.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;
/* Initialize crypto subsystem (calls nostr_init). Returns 0 on success. */
int crypto_init(void);
/* Cleanup crypto subsystem (calls nostr_cleanup). */
void crypto_cleanup(void);
/* Derive keys for all roles in the table using the loaded mnemonic.
* Populates key_store and sets role->pubkey_hex and role->derived for each role.
* Only derives for roles with purpose=nostr and curve=secp256k1 (for now).
* Returns number of keys derived, or -1 on error. */
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
/* Get the derived private key for a role (by table index). Returns NULL if not derived. */
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
/* Get the derived public key hex for a role. Returns NULL if not derived. */
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
/* Sign a Nostr event. event_json is the unsigned event JSON string.
* Returns a newly-allocated string containing the signed event JSON, or NULL on error.
* Caller must free() the returned string. */
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
/* NIP-44 encrypt. Returns newly-allocated ciphertext string, or NULL on error. Caller frees. */
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext);
/* NIP-44 decrypt. Returns newly-allocated plaintext string, or NULL on error. Caller frees. */
char *crypto_nip44_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext);
/* NIP-04 encrypt. Returns newly-allocated ciphertext string, or NULL on error. Caller frees. */
char *crypto_nip04_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext);
/* NIP-04 decrypt. Returns newly-allocated plaintext string, or NULL on error. Caller frees. */
char *crypto_nip04_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
#endif /* NSIGNER_CRYPTO_H */

View File

@@ -1,8 +1,456 @@
#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);
/* NIP-44 encrypt/decrypt */
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext);
char *crypto_nip44_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext);
/* NIP-04 encrypt/decrypt */
char *crypto_nip04_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext);
char *crypto_nip04_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext);
/* Zeroize all derived keys in the store. */
void crypto_wipe(key_store_t *store);
/* 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 unknown_role
* 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>
@@ -178,7 +626,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, 1001, "ambiguous_role_selector");
}
if (rc == SELECTOR_ERR_NOT_FOUND) {
return make_error_response(id_str, 1002, "role_not_found");
return make_error_response(id_str, 1002, "unknown_role");
}
if (rc == SELECTOR_ERR_NO_DEFAULT) {
return make_error_response(id_str, 1003, "no_default_role");
@@ -229,13 +677,61 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
}
result = owned_result;
} else if (strcmp(method, VERB_NIP44_ENCRYPT) == 0) {
result = "stub:nip44_encrypt_ok";
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
owned_result = crypto_nip44_encrypt(ctx->key_store, role_index, peer_item->valuestring, msg_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_DECRYPT) == 0) {
result = "stub:nip44_decrypt_ok";
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
owned_result = crypto_nip44_decrypt(ctx->key_store, role_index, peer_item->valuestring, msg_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_NIP04_ENCRYPT) == 0) {
result = "stub:nip04_encrypt_ok";
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
owned_result = crypto_nip04_encrypt(ctx->key_store, role_index, peer_item->valuestring, msg_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_NIP04_DECRYPT) == 0) {
result = "stub:nip04_decrypt_ok";
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
owned_result = crypto_nip04_decrypt(ctx->key_store, role_index, peer_item->valuestring, msg_item->valuestring);
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else {
cJSON_Delete(root);
return make_error_response(id_str, -32601, "method_not_found");

View File

@@ -1,44 +0,0 @@
#ifndef NSIGNER_DISPATCHER_H
#define NSIGNER_DISPATCHER_H
#include "role_table.h"
#include "mnemonic.h"
#include "crypto.h"
/* Dispatcher context — holds references to shared state */
typedef struct {
role_table_t *role_table;
mnemonic_state_t *mnemonic;
key_store_t *key_store;
} dispatcher_ctx_t;
/* Initialize dispatcher context */
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, 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);
#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 */

756
src/key_store.c Normal file
View File

@@ -0,0 +1,756 @@
#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/nip004.h>
#include <nostr_core/nip006.h>
#include <nostr_core/nip019.h>
#include <nostr_core/nip044.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;
}
static int parse_hex_pubkey32(const char *hex, unsigned char out[32]) {
if (hex == NULL || out == NULL || strlen(hex) != 64) {
return -1;
}
if (nostr_hex_to_bytes(hex, out, 32) != 0) {
return -1;
}
return 0;
}
char *crypto_nip44_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext) {
const unsigned char *private_key;
unsigned char recipient_pub[32];
char *out;
if (plaintext == NULL || recipient_pubkey_hex == NULL) {
return NULL;
}
private_key = crypto_get_private_key(store, role_index);
if (private_key == NULL || parse_hex_pubkey32(recipient_pubkey_hex, recipient_pub) != 0) {
return NULL;
}
out = (char *)malloc(NSIGNER_ENCRYPT_OUTPUT_MAX);
if (out == NULL) {
return NULL;
}
out[0] = '\0';
if (nostr_nip44_encrypt(private_key, recipient_pub, plaintext, out, NSIGNER_ENCRYPT_OUTPUT_MAX) != 0) {
free(out);
return NULL;
}
return out;
}
char *crypto_nip44_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext) {
const unsigned char *private_key;
unsigned char sender_pub[32];
char *out;
if (ciphertext == NULL || sender_pubkey_hex == NULL) {
return NULL;
}
private_key = crypto_get_private_key(store, role_index);
if (private_key == NULL || parse_hex_pubkey32(sender_pubkey_hex, sender_pub) != 0) {
return NULL;
}
out = (char *)malloc(NSIGNER_ENCRYPT_OUTPUT_MAX);
if (out == NULL) {
return NULL;
}
out[0] = '\0';
if (nostr_nip44_decrypt(private_key, sender_pub, ciphertext, out, NSIGNER_ENCRYPT_OUTPUT_MAX) != 0) {
free(out);
return NULL;
}
return out;
}
char *crypto_nip04_encrypt(const key_store_t *store, int role_index,
const char *recipient_pubkey_hex, const char *plaintext) {
const unsigned char *private_key;
unsigned char recipient_pub[32];
char *out;
if (plaintext == NULL || recipient_pubkey_hex == NULL) {
return NULL;
}
private_key = crypto_get_private_key(store, role_index);
if (private_key == NULL || parse_hex_pubkey32(recipient_pubkey_hex, recipient_pub) != 0) {
return NULL;
}
out = (char *)malloc(NSIGNER_ENCRYPT_OUTPUT_MAX);
if (out == NULL) {
return NULL;
}
out[0] = '\0';
if (nostr_nip04_encrypt(private_key, recipient_pub, plaintext, out, NSIGNER_ENCRYPT_OUTPUT_MAX) != 0) {
free(out);
return NULL;
}
return out;
}
char *crypto_nip04_decrypt(const key_store_t *store, int role_index,
const char *sender_pubkey_hex, const char *ciphertext) {
const unsigned char *private_key;
unsigned char sender_pub[32];
char *out;
if (ciphertext == NULL || sender_pubkey_hex == NULL) {
return NULL;
}
private_key = crypto_get_private_key(store, role_index);
if (private_key == NULL || parse_hex_pubkey32(sender_pubkey_hex, sender_pub) != 0) {
return NULL;
}
out = (char *)malloc(NSIGNER_ENCRYPT_OUTPUT_MAX);
if (out == NULL) {
return NULL;
}
out[0] = '\0';
if (nostr_nip04_decrypt(private_key, sender_pub, ciphertext, out, NSIGNER_ENCRYPT_OUTPUT_MAX) != 0) {
free(out);
return NULL;
}
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;
}

File diff suppressed because it is too large Load Diff

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 2
#define NSIGNER_VERSION "v0.0.2"
#endif /* NSIGNER_MAIN_H */

View File

@@ -1,4 +1,444 @@
#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>

View File

@@ -1,39 +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);
/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out.
* Returns 0 on success, -1 on invalid arguments or generation failure. */
int mnemonic_generate(int word_count, char *out, size_t out_len);
#endif /* NSIGNER_MNEMONIC_H */

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,12 +1,462 @@
#define _GNU_SOURCE
#include "server.h"
#include "enforcement.h"
#include "selector.h"
#include "cjson/cJSON.h"
#include "socket_name.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
#define NSIGNER_LISTEN_UNIX 0
#define NSIGNER_LISTEN_STDIO 1
#define NSIGNER_LISTEN_QREXEC 2
#define NSIGNER_LISTEN_TCP 3
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
int kind;
char caller_id[64]; /* "uid:<n>" or "qubes:<vm>" */
char source_qube[64];
} 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;
int listen_mode;
int stdio_handled;
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,
int listen_mode,
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 */
int transport_send_framed(int fd, const char *payload);
int transport_recv_framed(int fd, char **out_payload, size_t max_size);
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
@@ -16,6 +466,66 @@
#include <sys/un.h>
#include <unistd.h>
static int g_prompt_always_allow = 0;
static int g_noninteractive_prompt_default = -1;
void server_set_prompt_always_allow(int enabled) {
g_prompt_always_allow = enabled ? 1 : 0;
}
void server_set_noninteractive_prompt_default(int decision) {
if (decision == POLICY_ALLOW || decision == POLICY_DENY) {
g_noninteractive_prompt_default = decision;
} else {
g_noninteractive_prompt_default = -1;
}
}
static int prompt_for_policy_decision(const caller_identity_t *caller,
const char *method,
const char *role_name,
const char *purpose) {
int ch;
if (g_prompt_always_allow) {
return POLICY_ALLOW;
}
if (!isatty(STDIN_FILENO)) {
if (g_noninteractive_prompt_default == POLICY_ALLOW ||
g_noninteractive_prompt_default == POLICY_DENY) {
return g_noninteractive_prompt_default;
}
return POLICY_DENY;
}
printf("\nApproval required\n");
printf("caller: %s\n", (caller != NULL) ? caller->caller_id : "unknown");
printf("method: %s\n", (method != NULL) ? method : "unknown");
printf("role: %s\n", (role_name != NULL) ? role_name : "unknown");
printf("purpose: %s\n", (purpose != NULL) ? purpose : "unknown");
printf("[y] allow once [n] deny [a] always allow this session\n> ");
fflush(stdout);
ch = getchar();
while (ch != EOF && ch != '\n') {
int next = getchar();
if (next == EOF || next == '\n') {
break;
}
}
ch = tolower(ch);
if (ch == 'a') {
g_prompt_always_allow = 1;
return POLICY_ALLOW;
}
if (ch == 'y') {
return POLICY_ALLOW;
}
return POLICY_DENY;
}
static void server_set_error(server_ctx_t *ctx, const char *msg) {
if (ctx == NULL) {
return;
@@ -30,98 +540,81 @@ static void server_set_error(server_ctx_t *ctx, const char *msg) {
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;
while (off < len) {
ssize_t n = read(fd, p + off, len - off);
if (n == 0) {
static int parse_tcp_target(const char *target,
int *out_family,
char *out_host,
size_t out_host_sz,
uint16_t *out_port) {
const char *p;
const char *host_start;
const char *host_end;
const char *port_start;
char port_buf[16];
size_t host_len;
size_t port_len;
char *endptr = NULL;
long port_long;
if (target == NULL || out_family == NULL || out_host == NULL || out_port == NULL ||
out_host_sz == 0) {
return -1;
}
if (strncmp(target, "tcp:", 4) != 0) {
return -1;
}
p = target + 4;
if (*p == '[') {
host_start = p + 1;
host_end = strchr(host_start, ']');
if (host_end == NULL || host_end[1] != ':') {
return -1;
}
if (n < 0) {
if (errno == EINTR) {
continue;
}
port_start = host_end + 2;
} else {
host_start = p;
host_end = strrchr(p, ':');
if (host_end == NULL || host_end == host_start) {
return -1;
}
off += (size_t)n;
port_start = host_end + 1;
}
return 0;
}
host_len = (size_t)(host_end - host_start);
if (host_len == 0 || host_len >= out_host_sz) {
return -1;
}
memcpy(out_host, host_start, host_len);
out_host[host_len] = '\0';
static int write_full(int fd, const void *buf, size_t len) {
const unsigned char *p = (const unsigned char *)buf;
size_t off = 0;
port_len = strlen(port_start);
if (port_len == 0 || port_len >= sizeof(port_buf)) {
return -1;
}
memcpy(port_buf, port_start, port_len + 1);
while (off < len) {
ssize_t n = write(fd, p + off, len - off);
if (n < 0) {
if (errno == EINTR) {
continue;
}
errno = 0;
port_long = strtol(port_buf, &endptr, 10);
if (errno != 0 || endptr == port_buf || *endptr != '\0' || port_long < 1 || port_long > 65535) {
return -1;
}
{
struct in6_addr addr6;
struct in_addr addr4;
if (inet_pton(AF_INET6, out_host, &addr6) == 1) {
*out_family = AF_INET6;
} else if (inet_pton(AF_INET, out_host, &addr4) == 1) {
*out_family = AF_INET;
} else {
return -1;
}
off += (size_t)n;
}
return 0;
}
static int recv_framed(int fd, char **out_payload) {
uint32_t be_len;
uint32_t len;
char *payload;
if (out_payload == NULL) {
return -1;
}
*out_payload = NULL;
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
len = ntohl(be_len);
if (len == 0 || len > SERVER_MAX_MSG_SIZE) {
return -1;
}
payload = (char *)malloc((size_t)len + 1U);
if (payload == NULL) {
return -1;
}
if (read_full(fd, payload, len) != 0) {
free(payload);
return -1;
}
payload[len] = '\0';
*out_payload = payload;
return 0;
}
static int send_framed(int fd, const char *payload) {
uint32_t len;
uint32_t be_len;
if (payload == NULL) {
return -1;
}
len = (uint32_t)strlen(payload);
be_len = htonl(len);
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
if (write_full(fd, payload, len) != 0) {
return -1;
}
*out_port = (uint16_t)port_long;
return 0;
}
@@ -200,6 +693,7 @@ static int extract_method_and_selector(const char *json,
}
void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit,
int listen_mode,
dispatcher_ctx_t *dispatcher, policy_table_t *policy) {
if (ctx == NULL) {
return;
@@ -212,6 +706,8 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp
ctx->socket_name[sizeof(ctx->socket_name) - 1] = '\0';
}
ctx->listen_fd = -1;
ctx->listen_mode = listen_mode;
ctx->stdio_handled = 0;
ctx->dispatcher = dispatcher;
ctx->policy = policy;
ctx->socket_name_explicit = socket_name_explicit ? 1 : 0;
@@ -235,6 +731,105 @@ int server_start(server_ctx_t *ctx) {
server_set_error(ctx, NULL);
if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
ctx->listen_fd = STDIN_FILENO;
ctx->running = 1;
ctx->stdio_handled = 0;
server_set_error(ctx, NULL);
return 0;
}
if (ctx->listen_mode == NSIGNER_LISTEN_TCP) {
int family;
uint16_t port;
char host[64];
int one = 1;
int prc = parse_tcp_target(ctx->socket_name, &family, host, sizeof(host), &port);
if (prc != 0) {
(void)snprintf(ctx->last_error,
sizeof(ctx->last_error),
"invalid tcp listen target: %s (expected tcp:IPv4:PORT or tcp:[IPv6]:PORT)",
ctx->socket_name);
return -1;
}
fd = socket(family, SOCK_STREAM, 0);
if (fd < 0) {
(void)snprintf(ctx->last_error,
sizeof(ctx->last_error),
"socket(tcp) failed: %s",
strerror(errno));
return -1;
}
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
if (family == AF_INET) {
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_family = AF_INET;
addr4.sin_port = htons(port);
if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
close(fd);
server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
return -1;
}
if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
(void)snprintf(ctx->last_error,
sizeof(ctx->last_error),
"bind(%s) failed: %s",
ctx->socket_name,
strerror(errno));
close(fd);
return -1;
}
} else {
struct sockaddr_in6 addr6;
memset(&addr6, 0, sizeof(addr6));
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(port);
if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
close(fd);
server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
return -1;
}
if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
(void)snprintf(ctx->last_error,
sizeof(ctx->last_error),
"bind(%s) failed: %s",
ctx->socket_name,
strerror(errno));
close(fd);
return -1;
}
}
if (listen(fd, 16) != 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;
}
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
(void)snprintf(ctx->last_error,
@@ -338,6 +933,56 @@ int server_get_caller(int fd, caller_identity_t *out) {
memset(out, 0, sizeof(*out));
if (fd == STDIN_FILENO) {
const char *src = getenv("QREXEC_REMOTE_DOMAIN");
out->kind = NSIGNER_LISTEN_STDIO;
if (src != NULL && src[0] != '\0') {
out->kind = NSIGNER_LISTEN_QREXEC;
strncpy(out->source_qube, src, sizeof(out->source_qube) - 1);
out->source_qube[sizeof(out->source_qube) - 1] = '\0';
(void)snprintf(out->caller_id, sizeof(out->caller_id), "qubes:%.57s", out->source_qube);
return 0;
}
out->uid = getuid();
out->gid = getgid();
out->pid = getpid();
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
return 0;
}
{
struct sockaddr_storage peer;
socklen_t peer_len = sizeof(peer);
if (getpeername(fd, (struct sockaddr *)&peer, &peer_len) == 0) {
if (peer.ss_family == AF_INET) {
const struct sockaddr_in *in4 = (const struct sockaddr_in *)&peer;
char ip[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &in4->sin_addr, ip, sizeof(ip)) != NULL) {
out->kind = NSIGNER_LISTEN_TCP;
(void)snprintf(out->caller_id,
sizeof(out->caller_id),
"tcp:%s:%u",
ip,
(unsigned int)ntohs(in4->sin_port));
return 0;
}
} else if (peer.ss_family == AF_INET6) {
const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&peer;
char ip6[INET6_ADDRSTRLEN];
if (inet_ntop(AF_INET6, &in6->sin6_addr, ip6, sizeof(ip6)) != NULL) {
out->kind = NSIGNER_LISTEN_TCP;
(void)snprintf(out->caller_id,
sizeof(out->caller_id),
"tcp:[%s]:%u",
ip6,
(unsigned int)ntohs(in6->sin6_port));
return 0;
}
}
}
}
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
return -1;
}
@@ -345,6 +990,7 @@ int server_get_caller(int fd, caller_identity_t *out) {
out->uid = cred.uid;
out->gid = cred.gid;
out->pid = cred.pid;
out->kind = NSIGNER_LISTEN_UNIX;
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
return 0;
}
@@ -367,26 +1013,38 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
return -1;
}
client_fd = accept(ctx->listen_fd, NULL, NULL);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
if (ctx->stdio_handled) {
return 0;
}
return -1;
client_fd = STDIN_FILENO;
ctx->stdio_handled = 1;
} else {
client_fd = accept(ctx->listen_fd, NULL, NULL);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return 0;
}
return -1;
}
}
if (server_get_caller(client_fd, &caller) != 0) {
close(client_fd);
if (client_fd != STDIN_FILENO) {
close(client_fd);
}
return -1;
}
if (recv_framed(client_fd, &request) != 0) {
if (transport_recv_framed(client_fd, &request, SERVER_MAX_MSG_SIZE) != 0) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"parse_error\"}}");
if (response != NULL) {
(void)send_framed(client_fd, response);
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
free(response);
}
close(client_fd);
if (client_fd != STDIN_FILENO) {
close(client_fd);
}
return 1;
}
@@ -404,6 +1062,10 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
}
pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose);
if (pchk == POLICY_PROMPT) {
pchk = prompt_for_policy_decision(&caller, method, role_name, purpose);
}
if (pchk == POLICY_ALLOW) {
verdict = "ALLOWED";
response = dispatcher_handle_request(ctx->dispatcher, request);
@@ -411,18 +1073,18 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}");
}
} else {
verdict = "DENIED";
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2001,\"message\":\"policy_denied\"}}");
}
if (response != NULL) {
(void)send_framed(client_fd, response);
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
}
(void)snprintf(activity,
sizeof(activity),
"uid=%u pid=%d %s(%s) %s",
(unsigned int)caller.uid,
(int)caller.pid,
"%s %s(%s) %s",
caller.caller_id,
method,
role_name,
verdict);
@@ -433,7 +1095,9 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
free(request);
free(response);
close(client_fd);
if (client_fd != STDIN_FILENO) {
close(client_fd);
}
return 1;
}
@@ -443,7 +1107,9 @@ void server_stop(server_ctx_t *ctx) {
}
if (ctx->listen_fd >= 0) {
close(ctx->listen_fd);
if (ctx->listen_mode == NSIGNER_LISTEN_UNIX || ctx->listen_mode == NSIGNER_LISTEN_TCP) {
close(ctx->listen_fd);
}
ctx->listen_fd = -1;
}
ctx->running = 0;

View File

@@ -1,52 +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) */
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);
#endif /* NSIGNER_SERVER_H */

View File

@@ -1,6 +1,446 @@
#define _GNU_SOURCE
#include "socket_name.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>

View File

@@ -1,12 +0,0 @@
#ifndef NSIGNER_SOCKET_NAME_H
#define NSIGNER_SOCKET_NAME_H
#include <stddef.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);
#endif /* NSIGNER_SOCKET_NAME_H */

104
src/transport_frame.c Normal file
View File

@@ -0,0 +1,104 @@
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int transport_read_full(int fd, void *buf, size_t len) {
unsigned char *p = (unsigned char *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = read(fd, p + off, len - off);
if (n == 0) {
return -1;
}
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
off += (size_t)n;
}
return 0;
}
static int transport_write_full(int fd, const void *buf, size_t len) {
const unsigned char *p = (const unsigned char *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = write(fd, p + off, len - off);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
off += (size_t)n;
}
return 0;
}
int transport_send_framed(int fd, const char *payload) {
uint32_t len;
uint32_t be_len;
if (payload == NULL) {
return -1;
}
len = (uint32_t)strlen(payload);
be_len = htonl(len);
if (transport_write_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
if (transport_write_full(fd, payload, len) != 0) {
return -1;
}
return 0;
}
int transport_recv_framed(int fd, char **out_payload, size_t max_size) {
uint32_t be_len;
uint32_t len;
char *payload;
if (out_payload == NULL || max_size == 0) {
return -1;
}
*out_payload = NULL;
if (transport_read_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
len = ntohl(be_len);
if (len == 0 || len > max_size) {
return -1;
}
payload = (char *)malloc((size_t)len + 1U);
if (payload == NULL) {
return -1;
}
if (transport_read_full(fd, payload, len) != 0) {
free(payload);
return -1;
}
payload[len] = '\0';
*out_payload = payload;
return 0;
}

176
tests/test.sh Executable file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env bash
set -euo pipefail
NSIGNER_BIN="${NSIGNER_BIN:-./build/nsigner}"
socket_name=""
if [[ ! -x "$NSIGNER_BIN" ]]; then
echo "[error] nsigner binary not found or not executable at: $NSIGNER_BIN"
echo " Build it first with: make dev"
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--name|--socket-name)
if [[ $# -lt 2 ]]; then
echo "[error] missing value for $1"
exit 1
fi
socket_name="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 [-n|--name|--socket-name <socket_name>]"
exit 0
;;
*)
echo "[error] unknown argument: $1"
echo "Usage: $0 [-n|--name|--socket-name <socket_name>]"
exit 1
;;
esac
done
echo "n_signer interactive smoke test"
echo "Using binary: $NSIGNER_BIN"
echo
if [[ -z "$socket_name" ]]; then
default_socket=""
if [[ -r /proc/net/unix ]]; then
default_socket="$(awk '/@nsigner_/ {sub(/^@/, "", $8); print $8; exit}' /proc/net/unix || true)"
fi
if [[ -n "$default_socket" ]]; then
read -r -p "Signer socket name (without @) [${default_socket}]: " socket_name
socket_name="${socket_name:-$default_socket}"
else
read -r -p "Signer socket name (without @, e.g. nsigner_hairy_dog): " socket_name
fi
fi
if [[ -z "${socket_name}" ]]; then
echo "[error] socket name is required"
exit 1
fi
run_request() {
local req="$1"
"$NSIGNER_BIN" --socket-name "$socket_name" client "$req" || true
}
extract_result_string() {
sed -n 's/.*"result":"\([^"]*\)".*/\1/p'
}
echo
echo "[1/7] get_public_key"
req_get='{"id":"1","method":"get_public_key","params":[]}'
resp_get="$(run_request "$req_get")"
echo "request : $req_get"
echo "response: $resp_get"
pubkey="$(printf '%s' "$resp_get" | sed -n 's/.*"result":"\([0-9a-fA-F]\{64\}\)".*/\1/p')"
if [[ -n "$pubkey" ]]; then
echo "pubkey : $pubkey"
else
echo "[warn] could not parse pubkey from response"
fi
echo
echo "[2/7] sign_event"
now_ts="$(date +%s)"
event_json='{"kind":1,"content":"hello from tests/test.sh","tags":[],"created_at":__TS__}'
event_json="${event_json/__TS__/${now_ts}}"
escaped_event_json="${event_json//\"/\\\"}"
req_sign="{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"role\":\"main\"}]}"
resp_sign="$(run_request "$req_sign")"
echo "request : $req_sign"
echo "response: $resp_sign"
if printf '%s' "$resp_sign" | grep -q '"result"'; then
echo "[ok] sign_event returned result"
else
echo "[warn] sign_event did not return result"
fi
echo
echo "[3/7] sign_event with nostr_index=0"
req_sign_idx="{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"nostr_index\":0}]}"
resp_sign_idx="$(run_request "$req_sign_idx")"
echo "request : $req_sign_idx"
echo "response: $resp_sign_idx"
echo
echo "[4/7] role selector error case (unknown role)"
req_bad_role='{"id":"4","method":"sign_event","params":["{}",{"role":"does_not_exist"}]}'
resp_bad_role="$(run_request "$req_bad_role")"
echo "request : $req_bad_role"
echo "response: $resp_bad_role"
if printf '%s' "$resp_bad_role" | grep -q '"message":"unknown_role"'; then
echo "[ok] unknown role error name is unknown_role"
else
echo "[warn] unknown role error name mismatch"
fi
echo
echo "[5/7] NIP-04 self round-trip"
if [[ -n "$pubkey" ]]; then
req_nip04_enc="{\"id\":\"5\",\"method\":\"nip04_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip04_from_test_sh\"]}"
resp_nip04_enc="$(run_request "$req_nip04_enc")"
echo "request : $req_nip04_enc"
echo "response: $resp_nip04_enc"
cipher_nip04="$(printf '%s' "$resp_nip04_enc" | extract_result_string)"
if [[ -n "$cipher_nip04" ]]; then
req_nip04_dec="{\"id\":\"6\",\"method\":\"nip04_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip04}\"]}"
resp_nip04_dec="$(run_request "$req_nip04_dec")"
echo "request : $req_nip04_dec"
echo "response: $resp_nip04_dec"
if printf '%s' "$resp_nip04_dec" | grep -q '"result":"hello_nip04_from_test_sh"'; then
echo "[ok] nip04 decrypt recovered plaintext"
else
echo "[warn] nip04 decrypt did not recover expected plaintext"
fi
else
echo "[warn] could not parse nip04 ciphertext"
fi
else
echo "[warn] skipping nip04 test (missing pubkey)"
fi
echo
echo "[6/7] NIP-44 self round-trip"
if [[ -n "$pubkey" ]]; then
req_nip44_enc="{\"id\":\"7\",\"method\":\"nip44_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip44_from_test_sh\"]}"
resp_nip44_enc="$(run_request "$req_nip44_enc")"
echo "request : $req_nip44_enc"
echo "response: $resp_nip44_enc"
cipher_nip44="$(printf '%s' "$resp_nip44_enc" | extract_result_string)"
if [[ -n "$cipher_nip44" ]]; then
req_nip44_dec="{\"id\":\"8\",\"method\":\"nip44_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip44}\"]}"
resp_nip44_dec="$(run_request "$req_nip44_dec")"
echo "request : $req_nip44_dec"
echo "response: $resp_nip44_dec"
if printf '%s' "$resp_nip44_dec" | grep -q '"result":"hello_nip44_from_test_sh"'; then
echo "[ok] nip44 decrypt recovered plaintext"
else
echo "[warn] nip44 decrypt did not recover expected plaintext"
fi
else
echo "[warn] could not parse nip44 ciphertext"
fi
else
echo "[warn] skipping nip44 test (missing pubkey)"
fi
echo
echo "[7/7] list currently running signers"
list_resp="$($NSIGNER_BIN list 2>&1 || true)"
echo "$list_resp"
if printf '%s' "$list_resp" | grep -q '^@nsigner'; then
echo "[ok] signer list contains nsigner entries"
else
echo "[warn] no @nsigner entries found"
fi
echo
echo "Smoke test complete."

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>
@@ -82,8 +521,8 @@ int main(void) {
memset(&key_store, 0, sizeof(key_store));
dispatcher_init(&dispatcher, &table, &mnemonic, &key_store);
derived = crypto_init();
check_condition("crypto_init succeeds", derived == 0);
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);
@@ -116,11 +555,11 @@ int main(void) {
response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1001"));
free(resp);
/* 5. role not found */
/* 5. unknown role */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"5\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"nonexistent\"}]}");
check_condition("role not found returns 1002",
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"code\":1002"));
check_condition("unknown role returns 1002 with unknown_role",
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"code\":1002") && response_has(resp, "unknown_role"));
free(resp);
/* 6. purpose mismatch: sign_event against ssh role */
@@ -162,7 +601,7 @@ int main(void) {
free(resp);
crypto_wipe(&key_store);
crypto_cleanup();
nostr_cleanup();
printf("%d/12 tests passed\n", g_passes);

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,8 @@
#include <time.h>
#include <unistd.h>
#define SOCKET_NAME "nsigner_test_run"
#define SOCKET_NAME_A "nsigner_test_run_a"
#define SOCKET_NAME_B "nsigner_test_run_b"
#define MAX_MSG_SIZE 65536
static int g_failures = 0;
@@ -154,8 +597,8 @@ static int recv_framed(int fd, char **out_payload) {
return 0;
}
static int request_roundtrip(const char *req, char **resp) {
int fd = connect_socket_retry(SOCKET_NAME, 5000);
static int request_roundtrip_to(const char *socket_name, const char *req, char **resp) {
int fd = connect_socket_retry(socket_name, 5000);
int rc;
if (fd < 0) {
@@ -174,6 +617,7 @@ static int request_roundtrip(const char *req, char **resp) {
int main(void) {
int stdin_pipe[2];
pid_t child;
pid_t child2;
const char *mnemonic = "\nabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n";
char *resp = NULL;
int status;
@@ -199,25 +643,60 @@ int main(void) {
close(null_fd);
}
(void)setenv("NSIGNER_TEST_FORCE_PROMPT", "1", 1);
(void)setenv("NSIGNER_TEST_NONINTERACTIVE_PROMPT", "allow", 1);
dup2(stdin_pipe[0], STDIN_FILENO);
close(stdin_pipe[0]);
close(stdin_pipe[1]);
execl("./build/nsigner", "./build/nsigner", "--socket-name", SOCKET_NAME, (char *)NULL);
execl("./build/nsigner", "./build/nsigner", "--socket-name", SOCKET_NAME_A, (char *)NULL);
_exit(127);
}
close(stdin_pipe[0]);
if (write_full(stdin_pipe[1], mnemonic, strlen(mnemonic)) == 0) {
check_condition("feed mnemonic to child stdin", 1);
} else {
check_condition("feed mnemonic to child stdin", 0);
}
close(stdin_pipe[1]);
sleep_ms(800);
{
int stdin_pipe2[2];
if (pipe(stdin_pipe2) != 0) {
perror("pipe2");
return 1;
}
child2 = fork();
if (child2 < 0) {
perror("fork2");
return 1;
}
if (child2 == 0) {
int null_fd = open("/dev/null", O_WRONLY);
if (null_fd >= 0) {
dup2(null_fd, STDOUT_FILENO);
dup2(null_fd, STDERR_FILENO);
close(null_fd);
}
(void)setenv("NSIGNER_TEST_FORCE_PROMPT", "1", 1);
(void)setenv("NSIGNER_TEST_HOTKEYS", "a", 1);
dup2(stdin_pipe2[0], STDIN_FILENO);
close(stdin_pipe2[0]);
close(stdin_pipe2[1]);
execl("./build/nsigner", "./build/nsigner", "--socket-name", SOCKET_NAME_B, (char *)NULL);
_exit(127);
}
close(stdin_pipe2[0]);
(void)write_full(stdin_pipe2[1], mnemonic, strlen(mnemonic));
close(stdin_pipe2[1]);
}
if (request_roundtrip("{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}", &resp) == 0) {
sleep_ms(1000);
if (request_roundtrip_to(SOCKET_NAME_A, "{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}", &resp) == 0) {
check_condition("get_public_key response id", strstr(resp, "\"id\":\"1\"") != NULL);
check_condition("get_public_key has result", strstr(resp, "\"result\":") != NULL);
} else {
@@ -226,7 +705,7 @@ int main(void) {
free(resp);
resp = NULL;
if (request_roundtrip("{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[],\\\"created_at\\\":1700000000}\",{\"role\":\"main\"}]}", &resp) == 0) {
if (request_roundtrip_to(SOCKET_NAME_A, "{\"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 has result", strstr(resp, "\"result\":") != NULL);
} else {
@@ -234,6 +713,73 @@ int main(void) {
}
free(resp);
{
char *resp_a = NULL;
char *resp_b = NULL;
char pub_a[65] = {0};
char pub_b[65] = {0};
char cipher[2048] = {0};
char req[4096];
const char *p;
if (request_roundtrip_to(SOCKET_NAME_A, "{\"id\":\"3\",\"method\":\"get_public_key\",\"params\":[\"\"]}", &resp_a) == 0 &&
request_roundtrip_to(SOCKET_NAME_B, "{\"id\":\"4\",\"method\":\"get_public_key\",\"params\":[\"\"]}", &resp_b) == 0) {
p = strstr(resp_a, "\"result\":\"");
if (p) { p += 10; strncpy(pub_a, p, 64); pub_a[64]='\0'; }
p = strstr(resp_b, "\"result\":\"");
if (p) { p += 10; strncpy(pub_b, p, 64); pub_b[64]='\0'; }
check_condition("multi-instance distinct sockets respond", pub_a[0] && pub_b[0]);
snprintf(req, sizeof(req), "{\"id\":\"5\",\"method\":\"nip04_encrypt\",\"params\":[\"%s\",\"hello_nip04\"]}", pub_b);
free(resp_a); resp_a = NULL;
if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) {
p = strstr(resp_a, "\"result\":\"");
if (p) {
size_t i = 0;
p += 10;
while (p[i] && p[i] != '\"' && i < sizeof(cipher)-1) { cipher[i]=p[i]; i++; }
cipher[i]='\0';
}
snprintf(req, sizeof(req), "{\"id\":\"6\",\"method\":\"nip04_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher);
free(resp_b); resp_b = NULL;
if (request_roundtrip_to(SOCKET_NAME_B, req, &resp_b) == 0) {
check_condition("nip04 round-trip plaintext recovered", strstr(resp_b, "hello_nip04") != NULL);
} else {
check_condition("nip04 decrypt request roundtrip", 0);
}
} else {
check_condition("nip04 encrypt request roundtrip", 0);
}
memset(cipher, 0, sizeof(cipher));
snprintf(req, sizeof(req), "{\"id\":\"7\",\"method\":\"nip44_encrypt\",\"params\":[\"%s\",\"hello_nip44\"]}", pub_b);
free(resp_a); resp_a = NULL;
if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) {
p = strstr(resp_a, "\"result\":\"");
if (p) {
size_t i = 0;
p += 10;
while (p[i] && p[i] != '\"' && i < sizeof(cipher)-1) { cipher[i]=p[i]; i++; }
cipher[i]='\0';
}
snprintf(req, sizeof(req), "{\"id\":\"8\",\"method\":\"nip44_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher);
free(resp_b); resp_b = NULL;
if (request_roundtrip_to(SOCKET_NAME_B, req, &resp_b) == 0) {
check_condition("nip44 round-trip plaintext recovered", strstr(resp_b, "hello_nip44") != NULL);
} else {
check_condition("nip44 decrypt request roundtrip", 0);
}
} else {
check_condition("nip44 encrypt request roundtrip", 0);
}
} else {
check_condition("multi-instance public key requests", 0);
}
free(resp_a);
free(resp_b);
}
if (kill(child, SIGTERM) == 0) {
check_condition("send SIGTERM to child", 1);
} else {
@@ -246,6 +792,9 @@ int main(void) {
check_condition("child exited", 0);
}
(void)kill(child2, SIGTERM);
(void)waitpid(child2, &status, 0);
if (g_failures == 0) {
printf("ALL TESTS PASSED\n");
return 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>

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>

View File

@@ -1,4 +1,444 @@
#include "../src/socket_name.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>