Compare commits

...
4 Commits
33 changed files with 2765 additions and 43 deletions
+10 -2
View File
@@ -80,6 +80,7 @@ TEST_SLH_DSA_128S_TARGET := $(BUILD_DIR)/test_slh_dsa_128s
TEST_ML_KEM_768_TARGET := $(BUILD_DIR)/test_ml_kem_768
TEST_PUBKEY_FORMAT_TARGET := $(BUILD_DIR)/test_pubkey_format
TEST_ALGORITHM_API_TARGET := $(BUILD_DIR)/test_algorithm_api
TEST_PATH_WHITELIST_TARGET := $(BUILD_DIR)/test_path_whitelist
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
@@ -89,7 +90,7 @@ EXAMPLE_PQ_KEM_TARGET := $(BUILD_DIR)/example_pq_kem
EXAMPLE_SSH_SIGN_TARGET := $(BUILD_DIR)/example_ssh_sign
DEMO_C99_TARGET := $(BUILD_DIR)/demo_c99
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event test-pq-crypto test-ed25519-x25519 test-ml-dsa-65 test-slh-dsa-128s test-ml-kem-768 test-pubkey-format test-algorithm-api examples test-client clean
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event test-pq-crypto test-ed25519-x25519 test-ml-dsa-65 test-slh-dsa-128s test-ml-kem-768 test-pubkey-format test-algorithm-api test-path-whitelist examples test-client clean
all: dev
@@ -117,7 +118,7 @@ static-arm64:
firmware-feather:
cd firmware/feather_s3_tft && idf.py build
test: lib test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event test-pq-crypto test-ed25519-x25519 test-ml-dsa-65 test-slh-dsa-128s test-ml-kem-768 test-pubkey-format test-client
test: lib test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-mine-event test-pq-crypto test-ed25519-x25519 test-ml-dsa-65 test-slh-dsa-128s test-ml-kem-768 test-pubkey-format test-path-whitelist test-client
test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
./$(TEST_INTEGRATION_TARGET)
@@ -176,6 +177,9 @@ test-pubkey-format: $(TEST_PUBKEY_FORMAT_TARGET)
test-algorithm-api: $(TEST_ALGORITHM_API_TARGET)
./$(TEST_ALGORITHM_API_TARGET)
test-path-whitelist: $(TEST_PATH_WHITELIST_TARGET)
./$(TEST_PATH_WHITELIST_TARGET)
test-client: examples
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(EXAMPLE_PQ_SIGN_TARGET) $(EXAMPLE_PQ_KEM_TARGET) $(EXAMPLE_SSH_SIGN_TARGET) $(DEMO_C99_TARGET)
@@ -256,6 +260,10 @@ $(TEST_ALGORITHM_API_TARGET): $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_cry
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.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)/policy.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ALGORITHM_API_TARGET) $(LDFLAGS)
$(TEST_PATH_WHITELIST_TARGET): $(TEST_DIR)/test_path_whitelist.c $(SRC_DIR)/server.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.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)/policy.c $(SRC_DIR)/auth_envelope.c $(SRC_DIR)/transport_frame.c $(SRC_DIR)/socket_name.c $(SRC_DIR)/http_listener.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_path_whitelist.c $(SRC_DIR)/server.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.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)/policy.c $(SRC_DIR)/auth_envelope.c $(SRC_DIR)/transport_frame.c $(SRC_DIR)/socket_name.c $(SRC_DIR)/http_listener.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_PATH_WHITELIST_TARGET) $(LDFLAGS)
$(EXAMPLE_GET_PUBLIC_KEY_TARGET): $(EXAMPLES_DIR)/get_public_key_client.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_public_key_client.c -o $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(LDFLAGS)
+87 -1
View File
@@ -191,6 +191,10 @@ Error codes:
| 1008 | `mining_failed` | Internal error during proof-of-work mining. |
| 1009 | `not_yet_implemented` | Verb+algorithm combination is reserved but not yet implemented. |
| 1010 | `algorithm_not_supported_for_verb` | The `algorithm` value is not valid for this verb. |
| 2002 | `index_not_allowed` | `nostr_index` not in the index whitelist. |
| 2003 | `path_not_allowed` | `role_path` not in the path whitelist. |
| 2004 | `index_required` | Named path-role has no default index and none given. |
| 2005 | `index_out_of_range` | `index` outside the named role's `[lo,hi]` range. |
### 4.3 Verbs
@@ -484,10 +488,92 @@ The `nostr_*` verbs select a secp256k1 NIP-06 key via the options object. Suppor
|----------------|--------------------------------------------------|
| `nostr_index` | NIP-06 index `n` → path `m/44'/1237'/<n>'/0/0` |
| `role` | Name of a pre-registered role entry |
| `role_path` | Full BIP-44 derivation path (must match a registered role) |
| `role_path` | Full BIP-44 derivation path (must be on the path whitelist or match a registered role) |
| `index` | Optional: index for a named path-role template (see below) |
Selector resolution order: `role``nostr_index``role_path` → default role `main`. Conflicting selectors are rejected with `ambiguous_role_selector` (1001). The role's `(purpose, curve)` must be `(nostr, secp256k1)` — any other combination is rejected with `purpose_mismatch` (1004) or `curve_mismatch` (1005).
#### Named path-roles
In the interactive wizard, you can define **named path-roles** that bind a role name (which acts as an access token for clients) to a derivation path template. The derivation path is hidden from clients — they only know the role name.
```
Wizard:
Define a named path role? [y/N] y
Role name: myrole
Curve:
1) secp256k1 (Nostr, Bitcoin)
2) ed25519 (SSH)
3) x25519 (key agreement, Age)
4) ml-dsa-65 (post-quantum signatures)
5) slh-dsa-128s (post-quantum signatures)
6) ml-kem-768 (post-quantum KEM)
Select [1]: 1
Path template (use N-M for range, A+B+C for set, e.g. m/44'/1237'/1-100/2/0):
(arrow keys to edit, Enter to accept):
m/44'/1237'/0-3/1/0
Default index [0]:
```
Purpose is auto-detected from the path prefix (e.g. `m/44'/1237'` → nostr, `m/44'/102001'` → ssh). The path template is pre-filled with `m/44'/1237'/0'/0/0` and can be edited inline with arrow keys, backspace, and delete.
**Path template syntax:**
- **Range**: `m/44'/1237'/0-3/1/0` — index 0..3, hardened if segment ends with `'` (e.g. `0-3'`)
- **Set**: `m/44'/1237'/1+34+54/1/0` — specific indices 1, 34, 54
- **Fixed path**: `m/44'/1237'/0'/0/0` — no variable segment, single fixed key (no index needed)
- The first segment that is a plain number, range (`N-M`), or set (`A+B+C`) becomes the variable (`%d`). Segments with `'` (like `44'`, `1237'`) are treated as literal hardened constants.
**Default index**: Defaults to 0 if 0 is within the allowed range/set, otherwise the first element. The user can override at the prompt.
When named path-roles are defined, the path whitelist prompt is skipped (the roles themselves define the allowed paths).
Clients then request keys by role name, optionally with an `index` within the allowed range:
```json
{"id":"1","method":"nostr_get_public_key","params":[{"role":"myrole"}]}
```
→ derives `m/44'/1237'/0/1/0` (default index 0).
```json
{"id":"2","method":"nostr_get_public_key","params":[{"role":"myrole","index":2}]}
```
→ derives `m/44'/1237'/2/1/0` (index 2, within range 0-3).
```json
{"id":"3","method":"nostr_get_public_key","params":[{"role":"myrole","index":5}]}
```
`2005 index_out_of_range` (5 is outside 0-3).
```json
{"id":"4","method":"nostr_get_public_key","params":[{"role":"unknown"}]}
```
`1002 unknown_role` (name not registered).
For fixed-path roles (no range/set), clients omit the `index` field:
```json
{"id":"5","method":"nostr_get_public_key","params":[{"role":"fixedrole"}]}
```
→ derives the single fixed path.
#### Path whitelist (`--allow-index`)
The `--allow-index` flag accepts both integer `nostr_index` tokens and path-template tokens. A `role_path` request is auto-registered and derived on demand if it matches a whitelisted template; otherwise it returns `2003 path_not_allowed`.
**Note**: When named path-roles are defined in the wizard, the path whitelist prompt is skipped — the roles themselves define the allowed paths. The `--allow-index` flag can still be used for additional raw `role_path` access.
```
nsigner --allow-index "m/44'/1237'/0-3/1/0"
```
Allowed syntax (comma-separated):
- `all` — no restriction (default)
- `0-3` / `0,1,3` — integer `nostr_index` values (backward compatible)
- `m/44'/1237'/0-3/0/0` — NIP-06 paths X=0..3
- `m/44'/1237'/0-3/1/0` — custom paths X=0..3, change=1
- `m/44'/1237'/1+34+54/1/0` — set of specific indices (1, 34, 54)
- `m/44'/1237'/0-3/0/0,m/44'/1237'/0-3/1/0` — multiple templates
### 4.7 Pre-approval
Pre-approval entries skip the interactive prompt for matching requests. They are configured at startup with `--preapprove`.
+179
View File
@@ -0,0 +1,179 @@
# Plan: `n_signer_client` — Linux CLI for n_signer
## Goal
A standalone Linux command-line client `n_signer_client` that connects to a
running `n_signer` process over its abstract UNIX socket (and optionally the
other framed transports) and exposes the full verb surface over stdin/stdout so
that signed events can be piped directly into `nak publish`.
## Deliverable
- New file: [`client/n_signer_client.c`](../client/n_signer_client.c) — single-file C99 program.
- New Makefile target producing `build/n_signer_client`.
- Updated [`client/README.md`](../client/README.md) with usage and the pipe-to-nak recipe.
The binary links `nostr_core_lib` exactly like the existing examples
[`examples/sign_event_client.c`](../examples/sign_event_client.c) and
[`examples/get_public_key_client.c`](../examples/get_public_key_client.c). It
uses:
- [`nsigner_transport_open_unix`](../resources/nostr_core_lib/nostr_core/nsigner_transport.h) (and optionally `_tcp`, `_serial`, `_qrexec`)
- [`nsigner_client_new`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) / [`nsigner_client_free`](../resources/nostr_core_lib/nostr_core/nsigner_client.h)
- [`nsigner_client_call`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) (takes ownership of `params`)
- [`nsigner_client_set_auth`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) for TCP mode
## CLI shape
```
n_signer_client [global options] <verb> [verb args...]
```
Global options:
| Flag | Default | Meaning |
|---|---|---|
| `--socket-name`, `-n <name>` | auto-discover | Abstract socket name without `@` |
| `--role <name>` | none | Selector `{"role":"<name>"}` (Nostr verbs) |
| `--nostr-index <N>` | none | Selector `{"nostr_index":N}` (mutually exclusive with `--role`) |
| `--algorithm <alg>` | none | Algorithm-based verbs: `secp256k1`/`ed25519`/`x25519`/`ml-dsa-65`/`slh-dsa-128s`/`ml-kem-768`/`otp` |
| `--index <N>` | `0` | Algorithm key index |
| `--scheme <schnorr\|ecdsa>` | `schnorr` | secp256k1 sign/verify scheme |
| `--format <plain\|structured>` | `plain` | `get-public-key` output shape |
| `--timeout <ms>` | `5000` | Transport timeout |
| `--tcp <host:port>` | none | Use TCP transport (requires `--auth-privkey`) |
| `--serial <device>` | none | Use USB CDC-ACM serial transport |
| `--qrexec <qube:service>` | none | Use Qubes qrexec transport |
| `--auth-privkey <32-byte hex>` | none | Auth envelope privkey for TCP |
| `--auth-label <text>` | none | Auth envelope label |
Auto-discovery: when no `--socket-name` and no explicit transport is given,
enumerate via `nsigner_transport_list_unix` and proceed only if exactly one
`nsigner*` socket exists (mirror `discover_single_socket_name` in
[`src/main.c`](../src/main.c)).
## Verb surface (full)
### Nostr verbs (role-based; selector from `--role` / `--nostr-index`)
| Verb | RPC method | stdin/argv | stdout |
|---|---|---|---|
| `get-public-key` | `nostr_get_public_key` | none | pubkey hex (or structured JSON with `--format structured`) |
| `sign-event` | `nostr_sign_event` | event JSON from argv or one stdin line | signed event JSON, one line |
| `mine-event` | `nostr_mine_event` | event JSON from argv or stdin; `--difficulty`, `--threads`, `--timeout-sec` | signed mined event JSON |
| `nip04-encrypt <peer-pubkey>` | `nostr_nip04_encrypt` | plaintext from argv or stdin | ciphertext |
| `nip04-decrypt <peer-pubkey>` | `nostr_nip04_decrypt` | ciphertext from argv or stdin | plaintext |
| `nip44-encrypt <peer-pubkey>` | `nostr_nip44_encrypt` | plaintext from argv or stdin | ciphertext |
| `nip44-decrypt <peer-pubkey>` | `nostr_nip44_decrypt` | ciphertext from argv or stdin | plaintext |
### Algorithm-based verbs (use `--algorithm` and `--index`)
| Verb | RPC method | argv | stdout |
|---|---|---|---|
| `get-public-key` | `get_public_key` | none | structured JSON `{"algorithm":...,"public_key":...,"key_id":...}` |
| `sign <msg-hex>` | `sign` | hex bytes | signature hex |
| `verify <msg-hex> <sig-hex>` | `verify` | hex bytes | `valid` / `invalid` (exit 0/1) |
| `derive <data>` | `derive` | UTF-8 data (argv or stdin) | `{"algorithm":...,"key_id":...,"digest":...}` |
| `encapsulate <peer-pubkey-hex>` | `encapsulate` | hex | `{"ciphertext":...,"shared_secret":...}` |
| `decapsulate <ciphertext-hex>` | `decapsulate` | hex | `{"shared_secret":...}` |
| `derive-shared-secret <peer-pubkey-hex>` | `derive_shared_secret` | hex | shared secret hex |
### Generic escape hatch
| Verb | RPC method | input | stdout |
|---|---|---|---|
| `call <method>` | `<method>` | JSON `params` array from stdin (one line) or argv | raw `result` JSON |
This keeps the client future-proof for any new server verb without a CLI rewrite.
## stdin/stdout contract (pipe-friendly)
- All payload output goes to stdout as a single line, newline-terminated.
- All diagnostics go to stderr.
- Exit code: `0` on success, non-zero on transport/RPC error (use
`nsigner_client_last_error` for the message).
- `sign-event` reads event JSON from argv if present, else reads exactly one
line from stdin. This is the pipe-to-nak path:
```bash
echo '{"kind":1,"content":"hello","tags":[],"created_at":1700000000}' \
| n_signer_client sign-event \
| nak publish
```
- `nip04-encrypt` / `nip44-encrypt` read plaintext from argv or stdin.
- `nip04-decrypt` / `nip44-decrypt` read ciphertext from argv or stdin.
- `sign` / `verify` / `encapsulate` / `decapsulate` / `derive-shared-secret`
take hex from argv (binary payloads, not pipe-friendly text).
- `derive` takes UTF-8 data from argv or stdin.
- `call` reads a JSON `params` array from stdin (one line) or argv.
## Selector handling
- `--role <name>``{"role":"<name>"}` in the options object.
- `--nostr-index <N>``{"nostr_index":N}` (mutually exclusive with `--role`).
- Default: no selector (server picks default role `main`).
- For algorithm verbs, `--algorithm` and `--index` populate the options object
instead; `--scheme` adds `"scheme"` for secp256k1 sign/verify.
## Transport
- Default: UNIX abstract socket via `nsigner_transport_open_unix(name, timeout_ms)`.
- `--tcp host:port``nsigner_transport_open_tcp` (requires `--auth-privkey`
32-byte hex; calls `nsigner_client_set_auth` with `--auth-label`).
- `--serial /dev/ttyACM0``nsigner_transport_open_serial`.
- `--qrexec qube:service``nsigner_transport_open_qrexec`.
- The vtable is uniform so all four transports share the same call path after
construction.
## Build
Add to [`Makefile`](../Makefile):
```make
N_SIGNER_CLIENT_TARGET := $(BUILD_DIR)/n_signer_client
clients: $(N_SIGNER_CLIENT_TARGET)
$(N_SIGNER_CLIENT_TARGET): $(CLIENT_DIR)/n_signer_client.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(CLIENT_DIR)/n_signer_client.c -o $(N_SIGNER_CLIENT_TARGET) $(LDFLAGS)
```
Add `clients` to the `all` aggregate and to the `test-client` target so it is
built alongside the examples.
## Testing
1. Manual smoke test against a running `nsigner`:
- `n_signer_client get-public-key` → 64-hex pubkey.
- `echo '{"kind":1,"content":"hello","tags":[],"created_at":1}' | n_signer_client sign-event` → signed event with `id`, `pubkey`, `sig`.
- Pipe to `nak event` / `nak publish` to verify the signed event is well-formed.
- `n_signer_client --algorithm ed25519 sign 68656c6c6f` → 64-byte sig hex.
2. Optional bash script `tests/test_n_signer_client.sh` that:
- Spawns `nsigner --socket-name nsigner_test --listen unix --mnemonic-stdin` with a fixed test mnemonic.
- Runs each verb and asserts on stdout shape.
- Tears down the server.
## Mermaid flow
```mermaid
flowchart LR
A[stdin or argv event JSON] --> B[n_signer_client sign-event]
B --> C[nsigner_transport_open_unix]
C --> D[nsigner_client_call nostr_sign_event]
D --> E[nsigner @nsigner socket]
E --> F[signed event JSON result]
F --> G[stdout one line]
G --> H[nak publish]
```
## Out of scope
- No TUI, no approval UI — the human attendant lives in the running `nsigner`
process; the client is just a thin wire caller.
- No key storage, no mnemonic handling.
- No HTTP listener client (the `http_listener` is server-side; the client uses
the framed transports).
- No NIP-46 bunker mode (covered separately by
[`plans/nip46_bunker_mode.md`](nip46_bunker_mode.md)).
+406
View File
@@ -0,0 +1,406 @@
# Plan: Named path-roles + path-template whitelist in the wizard
## Goal
Let the user define **named roles bound to a derivation path template** in the
interactive wizard. The client then selects a key by **role name** (not by raw
path), and optionally by an **index within the role's allowed range**. The
derivation path stays hidden on the signer side — the role name acts as an
access token: if the client doesn't know the name, they can't get the key.
Example wizard session:
```
Define a named path role? [y/N] y
Role name: myrole
Purpose [nostr]: nostr
Curve [secp256k1]: secp256k1
Path template: m/44'/1237'/0-3/1/0
Default index: 1 (optional — press Enter to require explicit index)
Role 'myrole' registered: purpose=nostr curve=secp256k1 path=m/44'/1237'/0-3/1/0 (index 0..3, default 1).
Define another? [y/N] n
```
The purpose + curve combination must be valid per `crypto_alg_from_role()`
(see [`src/key_store.c`](src/key_store.c) / [`src/enforcement.c`](src/enforcement.c)).
The wizard validates the combination and re-prompts on invalid input. Valid
combinations:
| Purpose | Curve | Algorithm | Typical path prefix |
|-----------|----------------|----------------|----------------------------|
| nostr | secp256k1 | secp256k1 | m/44'/1237'/... |
| bitcoin | secp256k1 | secp256k1 | m/84'/0'/... / m/86'/... |
| ssh | ed25519 | ed25519 | m/44'/102001'/... |
| age | x25519 | x25519 | m/44'/102002'/... |
| fips | secp256k1 | secp256k1 | (FIPS mode) |
| pq-sig | ml-dsa-65 | ml-dsa-65 | m/44'/102003'/... |
| pq-sig | slh-dsa-128s | slh-dsa-128s | m/44'/102004'/... |
| pq-kem | ml-kem-768 | ml-kem-768 | m/44'/102005'/... |
The curve determines which `derive_*` function runs
([`derive_for_role`](src/key_store.c:1004)). The path template is passed
verbatim to `crypto_derive_seed_from_mnemonic` for all curves except
`secp256k1`+`nostr`, which uses the NIP-06 helper when the path matches the
NIP-06 form and the new `nostr_derive_keys_from_path` helper otherwise.
Client requests:
```json
{"id":"1","method":"nostr_get_public_key","params":[{},{"role":"myrole"}]}
```
→ derives `m/44'/1237'/1/1/0` (default index 1) and returns the pubkey.
```json
{"id":"2","method":"nostr_get_public_key","params":[{},{"role":"myrole","index":2}]}
```
→ derives `m/44'/1237'/2/1/0` (index 2, within allowed range 0-3).
```json
{"id":"3","method":"nostr_get_public_key","params":[{},{"role":"myrole","index":5}]}
```
`2003 index_out_of_range` (5 is outside 0-3).
```json
{"id":"4","method":"nostr_get_public_key","params":[{},{"role":"unknown"}]}
```
`1002 unknown_role` (name not registered).
## Why this design
The user's insight: a **role name is a password**. The client never sees the
derivation path; they only know the role name the operator gave them. This:
1. **Hides the path** from the client — they can't enumerate or guess paths.
2. **Acts as access control** — must know the name to get the key.
3. **Enforces a range** — the server only derives paths within the template's
range, so even a knowing client can't escape to `m/44'/1237'/99/1/0`.
4. **Is backward compatible** — existing `nostr_index` and `role_path`
selectors still work; named path-roles are an additive feature.
## Root cause recap (3 compounding defects this plan fixes)
1. No code path registers `SELECTOR_ROLE_PATH` roles at runtime — only
`SELECTOR_NOSTR_INDEX` roles are created
([`role_table_register_nostr_index`](src/role_table.c:805),
[`setup_default_role`](src/main.c:1708)).
2. [`crypto_derive_all`](src/key_store.c:1054) / [`crypto_derive_one`](src/key_store.c:1102)
explicitly skip roles where `selector_type != SELECTOR_NOSTR_INDEX`.
3. [`derive_secp256k1`](src/key_store.c:699) builds the path from `role->nostr_index`,
ignoring `role->role_path` entirely. The other derive_* functions
(ed25519, x25519, ml_dsa_65, slh_dsa_128s, ml_kem_768) do the same via
`snprintf(..., "m/44'/10200X'/%d'/0'/0'", role->nostr_index)`.
The "auto approve all" setting ([`g_prompt_always_allow`](src/server.c:953)) only
bypasses the approval prompt — it never runs because the 1002 hard selector error
fires first at [`server.c:2074`](src/server.c:2074) /
[`dispatcher.c:1784`](src/dispatcher.c:1784).
## Design
### New: path-template role entry
Extend `role_entry_t` (in `src/role_table.c` and mirrored decls) with two
fields:
```c
/* In role_entry_t, added after role_path[]: */
int path_range_lo; /* for SELECTOR_ROLE_PATH roles: inclusive lower bound
for the %d placeholder in role_path; -1 = no range
(single fixed path) */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index to use when client sends {"role":...}
without "index"; -1 = require explicit index */
```
A path-template role stores its template in `role_path` with a `%d`-style
placeholder segment, e.g. `role_path = "m/44'/1237'/%d/1/0"`,
`path_range_lo = 0`, `path_range_hi = 3`, `path_default_index = 1`.
### Path-template data model for the whitelist
(Kept from the previous plan — the whitelist is the underlying mechanism the
wizard uses to validate, but the user-facing UX is the named-role prompt.)
```c
#define PATH_WHITELIST_MAX_TEMPLATES 16
#define PATH_TEMPLATE_MAX_LEN 128
typedef struct {
char template[PATH_TEMPLATE_MAX_LEN]; /* "m/44'/1237'/%d/1/0" */
int range_lo;
int range_hi;
} path_template_t;
typedef struct {
int active;
int count;
path_template_t templates[PATH_WHITELIST_MAX_TEMPLATES];
} path_whitelist_t;
```
Add `path_whitelist_t path_whitelist;` to `server_ctx_t`.
### Spec syntax (for `--allow-index` CLI flag and raw whitelist input)
Each comma-separated token may be:
- `all` → no restriction
- `0-3` / `0,1,3` → existing integer `nostr_index` syntax (backward compat)
- `m/44'/1237'/0-3/0/0` → path template, range 0..3
- `m/44'/1237'/0-3/1/0` → path template, range 0..3 (the user's case)
- `m/44'/1237'/0-3/0/0,m/44'/1237'/0-3/1/0` → multiple templates
A token containing `/` is a path template; the first segment matching
`^[0-9]+(-[0-9]+)?$` is the range placeholder.
### Named-role wizard syntax (primary UX)
The wizard prompt offers two modes:
1. **Quick mode** (existing): enter a whitelist spec as above. Roles are
auto-registered on demand when a client sends a matching `role_path`.
2. **Named mode** (new): define named roles bound to path templates. The
client uses `{"role":"name"}` (optionally with `"index":N`).
## Implementation steps
### Step 1 — Extend `role_entry_t` with path-range fields
Files: `src/role_table.c` (definition), and every .c with headerless decls
mirroring `role_entry_t` (search for `selector_type` field to find all copies).
Add `path_range_lo`, `path_range_hi`, `path_default_index` after `role_path[]`.
### Step 2 — Add `path_whitelist_t` struct + field to `server_ctx_t`
Files: `src/server.c` (definition + field), `src/main.c` (headerless decls
mirror), and any other .c declaring `server_ctx_t` (search for
`index_whitelist_active`). Add constants `PATH_WHITELIST_MAX_TEMPLATES`,
`PATH_TEMPLATE_MAX_LEN`.
### Step 3 — Implement `server_set_path_whitelist()` parser in `src/server.c`
```c
int server_set_path_whitelist(server_ctx_t *ctx, const char *spec);
```
Unified parser: integer tokens → existing bitmap; path-template tokens →
`path_whitelist.templates[]`. `"all"` clears both. Returns 0 / -1.
Keep `server_set_index_whitelist` as a thin wrapper (backward compat).
### Step 4 — Implement `server_path_whitelist_allows()` in `src/server.c`
```c
int server_path_whitelist_allows(const server_ctx_t *ctx, const char *role_path);
```
Iterate templates, format each candidate with the range, `strcmp`. Return 1/0.
### Step 5 — Add `role_table_register_role_path()` helper in `src/role_table.c`
```c
int role_table_register_role_path(role_table_t *table, const char *path,
role_purpose_t purpose, role_curve_t curve,
int range_lo, int range_hi, int default_index);
```
- `purpose` and `curve` are caller-supplied (from the wizard prompt), not
hardcoded. The caller must validate the combination via
`crypto_alg_from_role(curve, purpose) != CRYPTO_ALG_UNKNOWN` before calling.
- Idempotent via `role_table_find_by_path` (compare template + range).
- Sets `selector_type = SELECTOR_ROLE_PATH`, copies `path` (with `%d`)
into `role_path`, sets `purpose`/`curve`/`purpose_str`/`curve_str` from the
enum + string forms, sets the range fields, `derived = 0`.
- Add the prototype to the headerless-decls block in every .c that includes
role_table decls.
### Step 6 — Make `derive_secp256k1` honor `role_path` in `src/key_store.c`
- When `role->selector_type == SELECTOR_ROLE_PATH`:
- If `role_path` contains `%d`, the caller must have already resolved the
concrete path (see Step 8 — the server formats `role_path` with the
chosen index before calling `crypto_derive_one`). So `derive_secp256k1`
just uses `role->role_path` directly as the full BIP-32 path.
- Call `crypto_derive_seed_from_mnemonic(phrase, role->role_path, seed, 32)`
then derive secp256k1 priv/pub from that seed.
- Add helper `nostr_derive_keys_from_path(const char *mnemonic, const char *path,
unsigned char *priv, unsigned char *pub)` (or inline using the existing
BIP-32 seed→key derivation that `nostr_derive_keys_from_mnemonic` uses).
- When `SELECTOR_NOSTR_INDEX`, keep existing behavior.
- Apply the same `SELECTOR_ROLE_PATH` branch to the other derive_* functions.
### Step 7 — Remove the `SELECTOR_NOSTR_INDEX`-only guards in `src/key_store.c`
- [`crypto_derive_all`](src/key_store.c:1054): allow `SELECTOR_ROLE_PATH`.
- [`crypto_derive_one`](src/key_store.c:1102): allow `SELECTOR_ROLE_PATH`.
### Step 8 — Wire named path-roles + whitelist into `src/server.c` request handling
In the selector-resolution block ([`server.c:2028-2066`](src/server.c:2028)):
**Case A — client sends `{"role":"myrole"}` (named path-role):**
- `selector_resolve` finds the role by name (already works for registered roles).
- If the role is a path-template role (`SELECTOR_ROLE_PATH` with `%d`):
- Read optional `"index"` from the request options.
- If no `index` and `path_default_index >= 0` → use `path_default_index`.
- If no `index` and `path_default_index < 0` → `2004 index_required`.
- Validate `index` is in `[path_range_lo, path_range_hi]` → else `2003 index_out_of_range`.
- Format the concrete path: `snprintf(concrete, ..., role_path, index)`.
- Set `pending_derivation = 1` if the role isn't derived yet, with the
concrete path stored for `crypto_derive_one`.
- If the role is a `nostr_index` role → existing behavior.
**Case B — client sends `{"role_path":"m/44'/1237'/1/1/0"}` (raw path):**
- If `server_path_whitelist_allows(ctx, role_path)` → set
`pending_derivation = 1`, synthesize role name, `purpose=nostr`,
`curve=secp256k1`.
- Else → `2003 path_not_allowed`.
**Case C — client sends `{"nostr_index":N}`:** existing behavior unchanged.
In the `if (pchk == POLICY_ALLOW && pending_derivation)` block
([`server.c:2106`](src/server.c:2106)):
- For named path-roles: the role already exists in the table; just call
`crypto_derive_one` with the concrete path (temporarily set
`role->role_path` to the concrete path, or pass the path via a side channel).
- For raw `role_path`: `role_table_register_role_path` (no `%d`, fixed path)
→ `crypto_derive_one`.
### Step 9 — Add the named-role wizard prompt in `src/main.c`
New function `prompt_named_path_roles(role_table_t *role_table)`:
```
Define a named path role? [y/N] y
Role name: myrole
Purpose [nostr]: nostr
Curve [secp256k1]: secp256k1
Path template (use 0-3 for a range, or a single number): m/44'/1237'/0-3/1/0
Default index [1]: 1
Role 'myrole' registered: purpose=nostr curve=secp256k1 path=m/44'/1237'/0-3/1/0 (index 0..3, default 1).
Define another? [y/N] n
```
- **Purpose** prompt: default `nostr`; accept any of
`nostr|bitcoin|ssh|age|fips|pq-sig|pq-kem`; parse via
`role_purpose_from_str()`.
- **Curve** prompt: default `secp256k1`; accept any of
`secp256k1|ed25519|x25519|ml-dsa-65|slh-dsa-128s|ml-kem-768`; parse via
`role_curve_from_str()`.
- **Validate** the purpose+curve combination:
`crypto_alg_from_role(curve, purpose) != CRYPTO_ALG_UNKNOWN`; re-prompt on
invalid combo (e.g. `nostr`+`ed25519` is invalid).
- Parse the path template: find the range segment, extract `range_lo`/`range_hi`,
store template with `%d`.
- Call `role_table_register_role_path(table, template, purpose, curve,
range_lo, range_hi, default_index)`.
- Loop until user declines.
- Call this after [`setup_default_role`](src/main.c:1708) and before
`crypto_derive_all` (so named roles are pre-derived at startup using their
default index).
Also update [`prompt_index_whitelist()`](src/main.c:2088) to mention that
named path-roles bypass the raw-path whitelist (they're explicitly registered).
### Step 10 — Update `--allow-index` flag + wizard text in `src/main.c`
- Update `--allow-index` help ([`main.c:1109`](src/main.c:1109)) to mention
path templates.
- Update call sites at [`main.c:2902`](src/main.c:2902) /
[`main.c:2945`](src/main.c:2945) / [`main.c:2973`](src/main.c:2973) to call
`server_set_path_whitelist`.
### Step 11 — (Optional) Also handle `role_path` in `src/dispatcher.c`
[`dispatcher.c:1778-1791`](src/dispatcher.c:1778) returns 1002 on
`SELECTOR_ERR_NOT_FOUND`. **Decision**: scope to `server.c` only for now;
stdio/qrexec still returns 1002 for unknown `role_path` (future work). Named
roles registered at startup work everywhere because they're in the role table
before any request arrives.
### Step 12 — Tests
- [`tests/test_role_table.c`](tests/test_role_table.c): test
`role_table_register_role_path` (idempotent, range fields stored).
- [`tests/test_integration.c`](tests/test_integration.c) or new
`tests/test_path_whitelist.c`:
- Parse `m/44'/1237'/0-3/0/0` → assert `server_path_whitelist_allows` returns
1 for `m/44'/1237'/2/0/0` and 0 for `m/44'/1237'/5/0/0`.
- Parse `m/44'/1237'/0-3/1/0` → assert allows `m/44'/1237'/1/1/0` (the user's
exact case), denies `m/44'/1237'/1/0/0`.
- End-to-end (named role): register `myrole` with template
`m/44'/1237'/%d/1/0`, range 0-3, default 1. Send
`{"role":"myrole"}` → assert pubkey for `m/44'/1237'/1/1/0`.
Send `{"role":"myrole","index":2}` → assert pubkey for
`m/44'/1237'/2/1/0`. Send `{"role":"myrole","index":5}` → assert
`2003 index_out_of_range`.
- End-to-end (raw path): start server with
`--allow-index "m/44'/1237'/0-3/1/0"`, send
`{"role_path":"m/44'/1237'/1/1/0"}` → assert valid pubkey.
Send `{"role_path":"m/44'/1237'/1/0/0"}` → assert `2003 path_not_allowed`.
- Backward compat: `--allow-index "0-3"` still works for `nostr_index`.
### Step 13 — Docs
- [`README.md`](README.md) §4.6: document named path-roles, the `"index"`
option, and the `2003`/`2004` error codes.
- [`README.md`](README.md) §3 (wizard): document the named-role prompt.
- [`api.md`](api.md): add error codes `2003 path_not_allowed` /
`2003 index_out_of_range` / `2004 index_required`.
- [`README.md`](README.md) error table: add the new codes.
## New error codes
| Code | Message | Meaning |
|-------|----------------------|------------------------------------------------------|
| 2003 | `path_not_allowed` | `role_path` not on the path whitelist. |
| 2003 | `index_out_of_range` | `index` outside the named role's `[lo,hi]` range. |
| 2004 | `index_required` | Named path-role has no default index and none given. |
(2003 is reused for both path-not-allowed and index-out-of-range since they're
both "whitelist range" violations; the message distinguishes them. If you
prefer distinct codes, use 2005 for `index_out_of_range`.)
## Open questions / decisions
- **Placeholder detection**: first path segment matching `^[0-9]+(-[0-9]+)?$`
is the range. No explicit `X` char needed.
- **Default purpose/curve**: `nostr` / `secp256k1` for now. Inferring from path
prefix is future work.
- **Flag name**: keep `--allow-index` for backward compat; path syntax accepted
by the same flag.
- **Pre-derivation**: named roles with a default index are pre-derived at
startup (in `crypto_derive_all`); roles without a default are derived on
first request.
- **dispatcher.c scope**: stdio/qrexec gets named roles (they're in the table
at startup) but not raw-path auto-registration (future work).
- **Distinct error codes for 2003**: decision pending — reuse 2003 with
different messages, or split into 2003/2005.
## Mermaid: request flow after implementation
```mermaid
flowchart TD
A[Client request] --> B{selector type?}
B -- role name --> C[role_table_find_by_name]
C --> D{found?}
D -- no --> E[1002 unknown_role]
D -- yes --> F{is path-template role?}
F -- no, nostr_index --> G[existing nostr_index path]
F -- yes --> H{index in options?}
H -- yes --> I{index in range lo..hi?}
H -- no --> J{default_index set?}
J -- no --> K[2004 index_required]
J -- yes --> I
I -- no --> L[2003 index_out_of_range]
I -- yes --> M[format concrete path with index]
M --> N[derive + execute verb]
G --> N
B -- role_path --> O[server_path_whitelist_allows]
O -- no --> P[2003 path_not_allowed]
O -- yes --> Q[auto-register + derive]
Q --> N
B -- nostr_index --> R[existing index whitelist check]
R --> N
```
+7
View File
@@ -118,6 +118,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -176,6 +181,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+172 -9
View File
@@ -118,6 +118,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -176,6 +181,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
@@ -673,6 +680,128 @@ int socket_name_random(char *out, size_t out_len);
#define NSIGNER_ENCRYPT_OUTPUT_MAX 65536
/*
* Parse a BIP-44 derivation path string (e.g. "m/44'/1237'/1/1/0") into a
* uint32_t array suitable for nostr_bip32_derive_path(). Hardened segments
* are indicated by a trailing ' (or h). Returns the number of path components
* on success, or -1 on parse error. max_path is the max number of entries
* in the path_out array.
*/
static int parse_bip44_path(const char *path_str, uint32_t *path_out, int max_path) {
char buf[ROLE_PATH_MAX];
char *p;
int count = 0;
if (path_str == NULL || path_out == NULL || max_path <= 0) {
return -1;
}
strncpy(buf, path_str, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
/* Skip leading "m" or "m/" */
p = buf;
if (*p == 'm' || *p == 'M') {
p++;
if (*p == '/') {
p++;
} else if (*p != '\0') {
return -1; /* "m" must be followed by '/' or end */
}
}
while (*p != '\0' && count < max_path) {
char *slash = strchr(p, '/');
char seg[24];
size_t seg_len;
int hardened = 0;
char *endptr = NULL;
long val;
if (slash != NULL) {
seg_len = (size_t)(slash - p);
} else {
seg_len = strlen(p);
}
if (seg_len == 0 || seg_len >= sizeof(seg)) {
return -1;
}
memcpy(seg, p, seg_len);
seg[seg_len] = '\0';
/* Check for hardened marker ' or h at end */
if (seg[seg_len - 1] == '\'' || seg[seg_len - 1] == 'h' || seg[seg_len - 1] == 'H') {
hardened = 1;
seg[seg_len - 1] = '\0';
}
val = strtol(seg, &endptr, 10);
if (*endptr != '\0' || val < 0 || val > 0x7FFFFFFF) {
return -1;
}
path_out[count] = (uint32_t)val;
if (hardened) {
path_out[count] |= 0x80000000u;
}
count++;
p = (slash != NULL) ? slash + 1 : "";
if (*p == '\0') {
break;
}
}
return count;
}
/*
* Derive a secp256k1 key from an explicit BIP-44 path string.
* Uses BIP-32 derivation (nostr_bip32_key_from_seed + nostr_bip32_derive_path).
* priv_out and pub_out must each be at least 32 bytes. Returns 0 on success,
* -1 on failure.
*/
static int derive_secp256k1_from_path(const char *mnemonic, const char *path_str,
unsigned char *priv_out, unsigned char *pub_out) {
unsigned char bip39_seed[64];
nostr_hd_key_t master_key;
nostr_hd_key_t derived_key;
uint32_t path[16];
int path_len;
if (mnemonic == NULL || path_str == NULL || priv_out == NULL || pub_out == NULL) {
return -1;
}
path_len = parse_bip44_path(path_str, path, (int)(sizeof(path) / sizeof(path[0])));
if (path_len <= 0) {
return -1;
}
if (nostr_bip39_mnemonic_to_seed(mnemonic, "", bip39_seed, sizeof(bip39_seed)) != 0) {
return -1;
}
if (nostr_bip32_key_from_seed(bip39_seed, sizeof(bip39_seed), &master_key) != 0) {
secure_memzero(bip39_seed, sizeof(bip39_seed));
return -1;
}
if (nostr_bip32_derive_path(&master_key, path, (size_t)path_len, &derived_key) != 0) {
secure_memzero(bip39_seed, sizeof(bip39_seed));
secure_memzero(&master_key, sizeof(master_key));
return -1;
}
memcpy(priv_out, derived_key.private_key, 32);
memcpy(pub_out, derived_key.public_key + 1, 32); /* x-only (drop compression prefix) */
secure_memzero(bip39_seed, sizeof(bip39_seed));
secure_memzero(&master_key, sizeof(master_key));
secure_memzero(&derived_key, sizeof(derived_key));
return 0;
}
/*
* Derive a secp256k1 (Nostr) key for a role into the variable-length
* derived_key_t. Returns 0 on success, -1 on failure.
@@ -682,6 +811,7 @@ static int derive_secp256k1(derived_key_t *dst, const role_entry_t *role,
unsigned char priv[32];
unsigned char pub[32];
const crypto_alg_sizes_t *sz;
int rc;
sz = crypto_alg_get_sizes(CRYPTO_ALG_SECP256K1);
if (sz == NULL) {
@@ -696,8 +826,14 @@ static int derive_secp256k1(derived_key_t *dst, const role_entry_t *role,
return -1;
}
if (nostr_derive_keys_from_mnemonic(mnemonic_get_phrase(mnemonic),
role->nostr_index, priv, pub) != 0) {
if (role->selector_type == SELECTOR_ROLE_PATH) {
rc = derive_secp256k1_from_path(mnemonic_get_phrase(mnemonic),
role->role_path, priv, pub);
} else {
rc = nostr_derive_keys_from_mnemonic(mnemonic_get_phrase(mnemonic),
role->nostr_index, priv, pub);
}
if (rc != 0) {
secure_memzero(priv, sizeof(priv));
secure_memzero(pub, sizeof(pub));
secure_buf_free(&dst->private_key);
@@ -747,7 +883,12 @@ static int derive_ed25519(derived_key_t *dst, const role_entry_t *role,
return -1;
}
snprintf(path, sizeof(path), "m/44'/102001'/%d'/0'/0'", role->nostr_index);
if (role->selector_type == SELECTOR_ROLE_PATH) {
strncpy(path, role->role_path, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
} else {
snprintf(path, sizeof(path), "m/44'/102001'/%d'/0'/0'", role->nostr_index);
}
if (crypto_derive_seed_from_mnemonic(mnemonic_get_phrase(mnemonic), path,
seed, sizeof(seed)) != 0) {
@@ -805,7 +946,12 @@ static int derive_x25519(derived_key_t *dst, const role_entry_t *role,
return -1;
}
snprintf(path, sizeof(path), "m/44'/102002'/%d'/0'/0'", role->nostr_index);
if (role->selector_type == SELECTOR_ROLE_PATH) {
strncpy(path, role->role_path, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
} else {
snprintf(path, sizeof(path), "m/44'/102002'/%d'/0'/0'", role->nostr_index);
}
if (crypto_derive_seed_from_mnemonic(mnemonic_get_phrase(mnemonic), path,
seed, sizeof(seed)) != 0) {
@@ -861,7 +1007,12 @@ static int derive_ml_dsa_65(derived_key_t *dst, const role_entry_t *role,
return -1;
}
snprintf(path, sizeof(path), "m/44'/102003'/%d'/0'/0'", role->nostr_index);
if (role->selector_type == SELECTOR_ROLE_PATH) {
strncpy(path, role->role_path, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
} else {
snprintf(path, sizeof(path), "m/44'/102003'/%d'/0'/0'", role->nostr_index);
}
if (crypto_derive_seed_from_mnemonic(mnemonic_get_phrase(mnemonic), path,
seed, sizeof(seed)) != 0) {
@@ -914,7 +1065,12 @@ static int derive_slh_dsa_128s(derived_key_t *dst, const role_entry_t *role,
return -1;
}
snprintf(path, sizeof(path), "m/44'/102004'/%d'/0'/0'", role->nostr_index);
if (role->selector_type == SELECTOR_ROLE_PATH) {
strncpy(path, role->role_path, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
} else {
snprintf(path, sizeof(path), "m/44'/102004'/%d'/0'/0'", role->nostr_index);
}
if (crypto_derive_seed_from_mnemonic(mnemonic_get_phrase(mnemonic), path,
seed, sizeof(seed)) != 0) {
@@ -967,7 +1123,12 @@ static int derive_ml_kem_768(derived_key_t *dst, const role_entry_t *role,
return -1;
}
snprintf(path, sizeof(path), "m/44'/102005'/%d'/0'/0'", role->nostr_index);
if (role->selector_type == SELECTOR_ROLE_PATH) {
strncpy(path, role->role_path, sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
} else {
snprintf(path, sizeof(path), "m/44'/102005'/%d'/0'/0'", role->nostr_index);
}
if (crypto_derive_seed_from_mnemonic(mnemonic_get_phrase(mnemonic), path,
seed, sizeof(seed)) != 0) {
@@ -1051,7 +1212,8 @@ int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_st
role->derived = 0;
role->pubkey_hex[0] = '\0';
if (role->selector_type != SELECTOR_NOSTR_INDEX) {
if (role->selector_type != SELECTOR_NOSTR_INDEX &&
role->selector_type != SELECTOR_ROLE_PATH) {
continue;
}
@@ -1099,7 +1261,8 @@ int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_st
dst->alg = CRYPTO_ALG_UNKNOWN;
dst->valid = 0;
if (role->selector_type != SELECTOR_NOSTR_INDEX) {
if (role->selector_type != SELECTOR_NOSTR_INDEX &&
role->selector_type != SELECTOR_ROLE_PATH) {
return -1;
}
+706 -20
View File
@@ -119,6 +119,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -160,7 +165,11 @@ const char *role_curve_to_str(role_curve_t c);
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */
int role_table_register_role_path(role_table_t *table, const char *name, const char *path,
role_purpose_t purpose, role_curve_t curve,
int range_lo, int range_hi, int default_index,
const int *allowed_indices, int allowed_count);
/* from selector.h */
@@ -180,6 +189,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
@@ -674,6 +685,24 @@ typedef struct {
#define INDEX_WHITELIST_MAX 256
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8)
#define PATH_WHITELIST_MAX_TEMPLATES 16
#define PATH_TEMPLATE_MAX_LEN 128
#define PATH_TEMPLATE_MAX_INDICES 64 /* max allowed indices per template (for sets) */
typedef struct {
char template[PATH_TEMPLATE_MAX_LEN]; /* e.g. "m/44'/1237'/%d/1/0" — one %d placeholder */
int range_lo; /* inclusive lower bound (for range form) */
int range_hi; /* inclusive upper bound (== range_lo for single) */
int allowed_indices[PATH_TEMPLATE_MAX_INDICES]; /* explicit set of allowed indices */
int allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} path_template_t;
typedef struct {
int active; /* 1 if any path templates are configured */
int count;
path_template_t templates[PATH_WHITELIST_MAX_TEMPLATES];
} path_whitelist_t;
typedef struct {
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
char last_error[256];
@@ -689,6 +718,7 @@ typedef struct {
int bridge_source_trusted;
int index_whitelist_active;
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE];
path_whitelist_t path_whitelist; /* path-template whitelist for role_path requests */
} server_ctx_t;
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
@@ -725,6 +755,14 @@ void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
/* Set the nostr_index whitelist from a spec string ("all", "1,3,4", "0-3", "0-3,7,9") */
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec);
/* Set the unified whitelist (integer nostr_index + path templates) from a spec string.
* Spec: "all", or comma-separated tokens. Integer tokens ("0-3","1,3,4") set the
* nostr_index bitmap. Path-template tokens ("m/44'/1237'/0-3/1/0") set the path whitelist. */
int server_set_path_whitelist(server_ctx_t *ctx, const char *spec);
/* Check if a role_path is allowed by the path whitelist. Returns 1 if allowed, 0 if not. */
int server_path_whitelist_allows(const server_ctx_t *ctx, const char *role_path);
/* Configure non-interactive prompt fallback: -1 disabled, POLICY_ALLOW, or POLICY_DENY */
void server_set_noninteractive_prompt_default(int decision);
@@ -762,8 +800,8 @@ int socket_name_random(char *out, size_t out_len);
/* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 1
#define NSIGNER_VERSION_PATCH 10
#define NSIGNER_VERSION "v0.1.10"
#define NSIGNER_VERSION_PATCH 14
#define NSIGNER_VERSION "v0.1.14"
/* NSIGNER_HEADERLESS_DECLS_END */
@@ -931,6 +969,204 @@ static int read_line_stdin(char *buf, size_t buf_sz) {
return 0;
}
/*
* Read a line from stdin with inline editing support, using termios raw mode.
* Pre-fills the buffer with `prefill` (if non-NULL), positions the cursor at
* the end, and allows arrow-key navigation, backspace, delete, home, end,
* and regular character insertion. On Enter, returns the edited string in
* `buf`. Returns 0 on success, -1 on error/EOF.
*
* Only works when stdin is a TTY. Falls back to read_line_stdin if not a TTY
* (in which case prefill is ignored).
*/
static int read_line_editable(char *buf, size_t buf_sz, const char *prefill) {
struct termios old_term, new_term;
size_t len = 0; /* current text length */
size_t pos = 0; /* cursor position (0..len) */
int fd = STDIN_FILENO;
int was_raw = 0;
if (buf == NULL || buf_sz == 0) {
return -1;
}
/* If not a TTY, fall back to plain fgets */
if (!isatty(fd)) {
return read_line_stdin(buf, buf_sz);
}
/* Pre-fill */
if (prefill != NULL) {
len = strlen(prefill);
if (len >= buf_sz) len = buf_sz - 1;
memcpy(buf, prefill, len);
buf[len] = '\0';
pos = len;
} else {
buf[0] = '\0';
}
/* Enter raw mode */
if (tcgetattr(fd, &old_term) == 0) {
new_term = old_term;
new_term.c_lflag &= ~(ICANON | ECHO);
new_term.c_cc[VMIN] = 1;
new_term.c_cc[VTIME] = 0;
if (tcsetattr(fd, TCSANOW, &new_term) == 0) {
was_raw = 1;
}
}
/* Draw the initial pre-filled text */
if (len > 0) {
fputs(buf, stdout);
fflush(stdout);
}
for (;;) {
char ch;
ssize_t n = read(fd, &ch, 1);
if (n <= 0) {
if (was_raw) tcsetattr(fd, TCSANOW, &old_term);
return -1;
}
if (ch == '\n' || ch == '\r') {
/* Enter — done */
buf[len] = '\0';
fputc('\n', stdout);
fflush(stdout);
break;
} else if (ch == 0x7f || ch == 0x08) {
/* Backspace (DEL or BS) — delete char before cursor */
if (pos > 0) {
size_t i;
for (i = pos - 1; i < len - 1; i++) {
buf[i] = buf[i + 1];
}
len--;
pos--;
buf[len] = '\0';
/* Redraw: move to start of field, clear line, redraw, reposition */
fputs("\r\033[K", stdout); /* CR + clear to end of line */
fputs(buf, stdout);
if (pos < len) {
/* Move cursor left to pos */
printf("\033[%zuD", len - pos);
}
fflush(stdout);
}
} else if (ch == 0x1b) {
/* Escape sequence — arrow keys, etc. */
char seq[2];
if (read(fd, &seq[0], 1) <= 0) continue;
if (read(fd, &seq[1], 1) <= 0) continue;
if (seq[0] == '[') {
if (seq[1] == 'D') {
/* Left arrow */
if (pos > 0) {
pos--;
fputs("\033[D", stdout);
fflush(stdout);
}
} else if (seq[1] == 'C') {
/* Right arrow */
if (pos < len) {
pos++;
fputs("\033[C", stdout);
fflush(stdout);
}
} else if (seq[1] == 'A' || seq[1] == 'B') {
/* Up/Down — ignore */
} else if (seq[1] == 'H') {
/* Home — move to start */
if (pos > 0) {
printf("\033[%zuD", pos);
pos = 0;
fflush(stdout);
}
} else if (seq[1] == 'F') {
/* End — move to end */
if (pos < len) {
printf("\033[%zuC", len - pos);
pos = len;
fflush(stdout);
}
} else if (seq[1] == '3') {
/* Delete (Delete key = ESC [ 3 ~ ) */
char tilde;
if (read(fd, &tilde, 1) <= 0) continue;
if (tilde == '~' && pos < len) {
size_t i;
for (i = pos; i < len - 1; i++) {
buf[i] = buf[i + 1];
}
len--;
buf[len] = '\0';
fputs("\r\033[K", stdout);
fputs(buf, stdout);
if (pos < len) {
printf("\033[%zuD", len - pos);
}
fflush(stdout);
}
}
}
} else if (ch == 0x01) {
/* Ctrl-A — home */
if (pos > 0) {
printf("\033[%zuD", pos);
pos = 0;
fflush(stdout);
}
} else if (ch == 0x05) {
/* Ctrl-E — end */
if (pos < len) {
printf("\033[%zuC", len - pos);
pos = len;
fflush(stdout);
}
} else if (ch == 0x15) {
/* Ctrl-U — clear entire line */
if (pos > 0) {
fputs("\r\033[K", stdout);
len = 0;
pos = 0;
buf[0] = '\0';
fflush(stdout);
}
} else if ((unsigned char)ch >= 0x20 && (unsigned char)ch < 0x7f) {
/* Regular printable character — insert at cursor */
if (len < buf_sz - 1) {
size_t i;
/* Shift characters right to make room */
for (i = len; i > pos; i--) {
buf[i] = buf[i - 1];
}
buf[pos] = ch;
len++;
buf[len] = '\0';
/* Redraw from cursor position */
fputs("\r\033[K", stdout);
fputs(buf, stdout);
pos++;
if (pos < len) {
printf("\033[%zuD", len - pos);
}
fflush(stdout);
}
}
/* Ignore other control characters */
}
/* Restore terminal */
if (was_raw) {
tcsetattr(fd, TCSANOW, &old_term);
}
return 0;
}
static int read_cmd_output_local(const char *cmd, char **out_buf) {
FILE *fp;
@@ -1106,7 +1342,7 @@ static void print_usage(const char *program_name) {
tui_print(" --mnemonic-fd N Read mnemonic from inherited fd N (one line) at startup");
tui_print(" --allow-all, -A Allow all policy prompts for this server session");
tui_print(" --bridge-source-trusted Accept qrexec_source preamble on unix connections (bridge mode)");
tui_print(" --allow-index SPEC Restrict which nostr_index values this session can access");
tui_print(" --allow-index SPEC Restrict which nostr_index / role_path values this session can access");
tui_print(" SPEC: 'all' (default), '1,3,4', '0-3', or '0-3,7,9'");
tui_print(" --otp-pad-dir DIR Bind an OTP pad directory at startup (one pad per session)");
tui_print(" --otp-pad SPEC Pad chksum (64 hex) or unique prefix; required with --otp-pad-dir");
@@ -1481,8 +1717,44 @@ static void role_table_get_cell(int row, int col, char *out, size_t out_size, vo
case 3:
if (r->selector_type == SELECTOR_NOSTR_INDEX) {
(void)snprintf(out, out_size, "m/44'/1237'/%d'/0/0", r->nostr_index);
} else {
} else if (r->path_range_lo < 0 && r->path_allowed_count == 0) {
/* Fixed path (no %d placeholder) */
(void)snprintf(out, out_size, "%s", r->role_path);
} else {
/* Template path — replace %d with range or set description */
char range_str[64];
char display[ROLE_PATH_MAX + 64];
const char *pct;
const char *tail;
if (r->path_allowed_count > 0) {
/* Set: e.g. "1+34+54" */
int si;
int off = 0;
for (si = 0; si < r->path_allowed_count && off < (int)sizeof(range_str) - 12; ++si) {
off += snprintf(range_str + off, sizeof(range_str) - off,
"%s%d", (si == 0) ? "" : "+", r->path_allowed_indices[si]);
}
range_str[off] = '\0';
} else if (r->path_range_lo == r->path_range_hi) {
/* Single index */
snprintf(range_str, sizeof(range_str), "%d", r->path_range_lo);
} else {
/* Range */
snprintf(range_str, sizeof(range_str), "%d-%d", r->path_range_lo, r->path_range_hi);
}
/* Replace first %d in role_path with range_str */
pct = strstr(r->role_path, "%d");
if (pct != NULL) {
size_t prefix_len = (size_t)(pct - r->role_path);
tail = pct + 2; /* skip "%d" */
snprintf(display, sizeof(display), "%.*s%s%s",
(int)prefix_len, r->role_path, range_str, tail);
} else {
snprintf(display, sizeof(display), "%s", r->role_path);
}
(void)snprintf(out, out_size, "%s", display);
}
break;
default:
@@ -1725,6 +1997,403 @@ static int setup_default_role(role_table_t *role_table) {
return role_table_add(role_table, &role);
}
/*
* Parse a path template token (e.g. "m/44'/1237'/0-3/1/0" or
* "m/44'/1237'/1+34+54/1/0") into a template with %d placeholder and
* allowed indices. Returns 0 on success, -1 on parse error.
*
* On success:
* template_out — the path with %d replacing the numeric/range/set segment
* range_lo/range_hi — set to the min/max of the allowed indices
* allowed_indices_out / allowed_count_out — the explicit set (if set form
* was used); allowed_count_out is 0 for pure range/single form
*/
static int parse_path_template_for_role(const char *token,
char *template_out, size_t template_sz,
int *range_lo, int *range_hi,
int *allowed_indices_out, int max_allowed,
int *allowed_count_out) {
char buf[ROLE_PATH_MAX];
char *p;
int found_range = 0;
if (token == NULL || template_out == NULL || range_lo == NULL || range_hi == NULL ||
allowed_indices_out == NULL || allowed_count_out == NULL) {
return -1;
}
strncpy(buf, token, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
*range_lo = 0;
*range_hi = 0;
*allowed_count_out = 0;
template_out[0] = '\0';
p = buf;
/* Copy up to and including the first '/' */
{
char *first_slash = strchr(p, '/');
if (first_slash == NULL) {
return -1;
}
size_t prefix_len = (size_t)(first_slash - p) + 1;
if (prefix_len >= template_sz) {
return -1;
}
memcpy(template_out, p, prefix_len);
template_out[prefix_len] = '\0';
p = first_slash + 1;
}
while (p != NULL && *p != '\0') {
char *next_slash = strchr(p, '/');
char seg[64];
size_t seg_len;
if (next_slash != NULL) {
seg_len = (size_t)(next_slash - p);
} else {
seg_len = strlen(p);
}
if (seg_len == 0 || seg_len >= sizeof(seg)) {
return -1;
}
memcpy(seg, p, seg_len);
seg[seg_len] = '\0';
if (!found_range) {
/* Check for range/set markers first. Only strip hardened marker
* (' or h) from segments that contain - or + (range/set forms).
* A plain number with ' (like 44') is a literal hardened constant,
* NOT a variable. */
char *plus = strchr(seg, '+');
char *dash = strchr(seg, '-');
int is_range_or_set = (plus != NULL || dash != NULL);
int seg_hardened = 0;
if (is_range_or_set && seg_len > 0 &&
(seg[seg_len - 1] == '\'' || seg[seg_len - 1] == 'h' || seg[seg_len - 1] == 'H')) {
seg_hardened = 1;
seg[seg_len - 1] = '\0';
seg_len--;
}
if (plus != NULL) {
/* Set form: "1+34+54" or "1+3-5+10" */
int set_count = 0;
char *tok = seg;
int set_ok = 1;
while (tok != NULL && *tok != '\0') {
char *next_plus = strchr(tok, '+');
if (next_plus != NULL) *next_plus = '\0';
char *sub_dash = strchr(tok, '-');
if (sub_dash != NULL) {
*sub_dash = '\0';
char *e1 = NULL, *e2 = NULL;
long lo = strtol(tok, &e1, 10);
long hi = strtol(sub_dash + 1, &e2, 10);
if (*e1 != '\0' || *e2 != '\0' || lo < 0 || hi < 0 || lo > hi) {
set_ok = 0; break;
}
for (long vi = lo; vi <= hi && set_count < max_allowed; vi++) {
allowed_indices_out[set_count++] = (int)vi;
}
} else {
char *e = NULL;
long val = strtol(tok, &e, 10);
if (*e != '\0' || val < 0) { set_ok = 0; break; }
if (set_count < max_allowed) {
allowed_indices_out[set_count++] = (int)val;
}
}
tok = (next_plus != NULL) ? next_plus + 1 : NULL;
}
if (set_ok && set_count > 0) {
found_range = 1;
*allowed_count_out = set_count;
*range_lo = allowed_indices_out[0];
*range_hi = allowed_indices_out[set_count - 1];
if (strlen(template_out) + 5 >= template_sz) return -1;
strcat(template_out, "%d");
if (seg_hardened) strcat(template_out, "'");
strcat(template_out, "/");
} else {
if (strlen(template_out) + seg_len + 3 >= template_sz) return -1;
strcat(template_out, seg);
strcat(template_out, "/");
}
} else if (dash != NULL) {
/* Range form: "N-M" */
*dash = '\0';
char *e1 = NULL, *e2 = NULL;
long lo = strtol(seg, &e1, 10);
long hi = strtol(dash + 1, &e2, 10);
if (*e1 != '\0' || *e2 != '\0' || lo < 0 || hi < 0 || lo > hi) {
*dash = '-'; /* restore dash */
if (strlen(template_out) + seg_len + 3 >= template_sz) return -1;
strcat(template_out, seg);
strcat(template_out, "/");
} else {
found_range = 1;
*range_lo = (int)lo;
*range_hi = (int)hi;
if (strlen(template_out) + 5 >= template_sz) return -1;
strcat(template_out, "%d");
if (seg_hardened) strcat(template_out, "'");
strcat(template_out, "/");
}
} else {
/* Single number — only treat as variable if NO hardened marker.
* A segment like "44'" is a literal hardened constant. */
char *e = NULL;
long v = strtol(seg, &e, 10);
if (*e != '\0' || v < 0) {
/* Not a plain number (has ' or other chars) — literal */
if (strlen(template_out) + seg_len + 3 >= template_sz) return -1;
strcat(template_out, seg);
strcat(template_out, "/");
} else {
/* Plain number without ' — this is the variable */
found_range = 1;
*range_lo = (int)v;
*range_hi = (int)v;
if (strlen(template_out) + 5 >= template_sz) return -1;
strcat(template_out, "%d");
strcat(template_out, "/");
}
}
} else {
if (strlen(template_out) + seg_len + 2 >= template_sz) return -1;
strcat(template_out, seg);
strcat(template_out, "/");
}
p = (next_slash != NULL) ? next_slash + 1 : NULL;
}
/* Remove trailing '/' */
{
size_t tlen = strlen(template_out);
if (tlen > 0 && template_out[tlen - 1] == '/') {
template_out[tlen - 1] = '\0';
}
}
if (!found_range) {
/* Fixed path — no variable segment. Treat as a single fixed key. */
*range_lo = -1;
*range_hi = -1;
*allowed_count_out = 0;
}
return 0;
}
/*
* Auto-detect purpose from a derivation path prefix.
* m/44'/1237' → nostr, m/44'/102001' → ssh, etc.
* Falls back to PURPOSE_NOSTR for unrecognized prefixes.
*/
static role_purpose_t purpose_from_path(const char *path) {
if (path == NULL) return PURPOSE_NOSTR;
if (strncmp(path, "m/44'/1237'", 11) == 0) return PURPOSE_NOSTR;
if (strncmp(path, "m/44'/102001'", 13) == 0) return PURPOSE_SSH;
if (strncmp(path, "m/44'/102002'", 13) == 0) return PURPOSE_AGE;
if (strncmp(path, "m/44'/102003'", 13) == 0) return PURPOSE_PQ_SIG;
if (strncmp(path, "m/44'/102004'", 13) == 0) return PURPOSE_PQ_SIG;
if (strncmp(path, "m/44'/102005'", 13) == 0) return PURPOSE_PQ_KEM;
if (strncmp(path, "m/84'", 4) == 0) return PURPOSE_BITCOIN;
if (strncmp(path, "m/86'", 4) == 0) return PURPOSE_BITCOIN;
return PURPOSE_NOSTR; /* default */
}
/*
* Interactive prompt to define named path-roles. Each role binds a name
* (which acts as an access token for clients) to a derivation path template
* with an optional range and default index. The path is hidden from clients.
*
* Only shown when stdin is a TTY and mnemonic was loaded via TUI.
*/
static void prompt_named_path_roles(role_table_t *role_table) {
char input[256];
if (role_table == NULL) {
return;
}
for (;;) {
tui_render_content_screen(NULL, "Named path roles — bind a role name to a derivation path template");
printf("Define a named path role? [y/N] ");
fflush(stdout);
if (read_line_stdin(input, sizeof(input)) != 0) {
return;
}
if (tolower((unsigned char)input[0]) != 'y') {
return;
}
/* Role name */
char role_name[ROLE_NAME_MAX];
printf(" Role name: ");
fflush(stdout);
if (read_line_stdin(role_name, sizeof(role_name)) != 0) return;
/* Trim trailing whitespace */
{
size_t len = strlen(role_name);
while (len > 0 && (role_name[len-1] == '\n' || role_name[len-1] == '\r' ||
role_name[len-1] == ' ' || role_name[len-1] == '\t')) {
role_name[--len] = '\0';
}
}
if (role_name[0] == '\0') {
printf(" Empty role name, skipping.\n");
continue;
}
if (role_table_find_by_name(role_table, role_name) != NULL) {
printf(" Role '%s' already exists, skipping.\n", role_name);
continue;
}
/* Curve — numbered selection */
printf(" Curve:\n");
printf(" 1) secp256k1 (Nostr, Bitcoin)\n");
printf(" 2) ed25519 (SSH)\n");
printf(" 3) x25519 (key agreement, Age)\n");
printf(" 4) ml-dsa-65 (post-quantum signatures)\n");
printf(" 5) slh-dsa-128s (post-quantum signatures)\n");
printf(" 6) ml-kem-768 (post-quantum KEM)\n");
printf(" Select [1]: ");
fflush(stdout);
char curve_choice[16];
if (read_line_stdin(curve_choice, sizeof(curve_choice)) != 0) return;
{
size_t len = strlen(curve_choice);
while (len > 0 && (curve_choice[len-1] == '\n' || curve_choice[len-1] == '\r' ||
curve_choice[len-1] == ' ' || curve_choice[len-1] == '\t')) {
curve_choice[--len] = '\0';
}
}
int cchoice = 1;
if (curve_choice[0] != '\0') {
cchoice = atoi(curve_choice);
if (cchoice < 1 || cchoice > 6) cchoice = 1;
}
role_curve_t curve;
switch (cchoice) {
case 2: curve = CURVE_ED25519; break;
case 3: curve = CURVE_X25519; break;
case 4: curve = CURVE_ML_DSA_65; break;
case 5: curve = CURVE_SLH_DSA_128S; break;
case 6: curve = CURVE_ML_KEM_768; break;
default: curve = CURVE_SECP256K1; break;
}
/* Path template — pre-filled with default, inline editing */
printf(" Path template (use N-M for range, A+B+C for set, e.g. m/44'/1237'/1-100/2/0):\n");
printf(" (arrow keys to edit, Enter to accept):\n ");
fflush(stdout);
char path_token[ROLE_PATH_MAX];
if (read_line_editable(path_token, sizeof(path_token),
"m/44'/1237'/0'/0/0") != 0) return;
{
size_t len = strlen(path_token);
while (len > 0 && (path_token[len-1] == '\n' || path_token[len-1] == '\r' ||
path_token[len-1] == ' ' || path_token[len-1] == '\t')) {
path_token[--len] = '\0';
}
}
if (path_token[0] == '\0') {
strncpy(path_token, "m/44'/1237'/0'/0/0", sizeof(path_token) - 1);
path_token[sizeof(path_token) - 1] = '\0';
}
char template[ROLE_PATH_MAX];
int range_lo, range_hi;
int allowed_indices[64];
int allowed_count = 0;
if (parse_path_template_for_role(path_token, template, sizeof(template),
&range_lo, &range_hi,
allowed_indices, 64, &allowed_count) != 0) {
printf(" Invalid path template: '%s'.\n", path_token);
continue;
}
/* Auto-detect purpose from path prefix (hidden from user) */
role_purpose_t purpose = purpose_from_path(template);
/* Validate purpose+curve combination */
if (crypto_alg_from_role(curve, purpose) == CRYPTO_ALG_UNKNOWN) {
printf(" Curve %s is not valid for path prefix of this template. Try a different curve.\n",
role_curve_to_str(curve));
continue;
}
int default_index = -1;
int is_fixed = (range_lo < 0); /* fixed path, no variable segment */
if (!is_fixed) {
/* Default index — only for templated paths.
* Default to 0 if it's in the allowed range/set, otherwise range_lo. */
int suggested_default;
if (allowed_count > 0) {
/* Set form: check if 0 is in the set */
int j, has_zero = 0;
for (j = 0; j < allowed_count; j++) {
if (allowed_indices[j] == 0) { has_zero = 1; break; }
}
suggested_default = has_zero ? 0 : range_lo;
} else {
/* Range form: 0 is valid if range_lo <= 0 <= range_hi */
suggested_default = (range_lo <= 0 && range_hi >= 0) ? 0 : range_lo;
}
char default_idx_str[16];
printf(" Default index [%d]: ", suggested_default);
fflush(stdout);
if (read_line_stdin(default_idx_str, sizeof(default_idx_str)) != 0) return;
{
size_t len = strlen(default_idx_str);
while (len > 0 && (default_idx_str[len-1] == '\n' || default_idx_str[len-1] == '\r' ||
default_idx_str[len-1] == ' ' || default_idx_str[len-1] == '\t')) {
default_idx_str[--len] = '\0';
}
}
if (default_idx_str[0] == '\0') {
default_index = suggested_default;
} else {
char *endp = NULL;
long di = strtol(default_idx_str, &endp, 10);
if (*endp != '\0' || di < range_lo || di > range_hi) {
printf(" Default index out of range [%d-%d], using %d.\n",
range_lo, range_hi, suggested_default);
default_index = suggested_default;
} else {
default_index = (int)di;
}
}
}
/* Register the role */
if (role_table_register_role_path(role_table, role_name, template,
purpose, curve,
range_lo, range_hi, default_index,
(allowed_count > 0) ? allowed_indices : NULL,
allowed_count) != 0) {
printf(" Failed to register role '%s' (table full?).\n", role_name);
} else if (is_fixed) {
printf(" Role '%s' registered: curve=%s path=%s (fixed).\n",
role_name, role_curve_to_str(curve), template);
} else {
printf(" Role '%s' registered: curve=%s path=%s (default index %d).\n",
role_name, role_curve_to_str(curve), template, default_index);
}
}
}
static int prompt_load_mnemonic_tui(mnemonic_state_t *mnemonic) {
char phrase[MNEMONIC_MAX_LEN];
char phrase_copy[MNEMONIC_MAX_LEN];
@@ -2089,14 +2758,15 @@ static char *prompt_index_whitelist(void) {
char input[256];
for (;;) {
tui_render_content_screen(NULL, "Index whitelist — restrict which nostr_index values this session can access");
printf("Enter allowed indices, or press Enter for 'all' (no restriction):\n\n");
tui_render_content_screen(NULL, "Whitelist — restrict which nostr_index / role_path values this session can access");
printf("Enter allowed indices/paths, or press Enter for 'all' (no restriction):\n\n");
printf(" Examples:\n");
printf(" all (default — allow all indices)\n");
printf(" 0 (only index 0)\n");
printf(" 0,1,3 (specific indices)\n");
printf(" 0-3 (range 0 through 3)\n");
printf(" 0,2-3,7 (mixed list and ranges)\n");
printf(" all (default — allow all)\n");
printf(" 0 (only nostr_index 0)\n");
printf(" 0-3 (nostr_index 0..3)\n");
printf(" m/44'/1237'/0-3/0/0 (NIP-06 paths X=0..3)\n");
printf(" m/44'/1237'/0-3/1/0 (custom paths X=0..3, change=1)\n");
printf(" m/44'/1237'/0-3/0/0,m/44'/1237'/0-3/1/0 (both)\n");
printf("\n Enter = all\n");
printf("> ");
fflush(stdout);
@@ -2123,7 +2793,7 @@ static char *prompt_index_whitelist(void) {
{
server_ctx_t tmp;
memset(&tmp, 0, sizeof(tmp));
if (server_set_index_whitelist(&tmp, input) != 0) {
if (server_set_path_whitelist(&tmp, input) != 0) {
printf("Invalid spec: '%s'. Try again or press Enter for 'all'.\n", input);
continue;
}
@@ -2696,6 +3366,11 @@ int main(int argc, char *argv[]) {
return 1;
}
/* Interactive named path-role definition (only in TUI mode) */
if (mnemonic_source.kind == MNEMONIC_SOURCE_TUI && isatty(STDIN_FILENO)) {
prompt_named_path_roles(&role_table);
}
memset(&key_store, 0, sizeof(key_store));
alg_key_cache_init(&alg_key_cache);
@@ -2801,11 +3476,22 @@ int main(int argc, char *argv[]) {
return 1;
}
/* Index whitelist prompt (only if --allow-index wasn't given on CLI) */
/* Index whitelist prompt (only if --allow-index wasn't given on CLI
* AND no named path-roles were defined — named roles are themselves
* the allowed set, so the separate whitelist is redundant. */
if (allow_index_spec == NULL) {
char *wl_spec = prompt_index_whitelist();
if (wl_spec != NULL) {
allow_index_spec = wl_spec; /* will be freed at program exit */
int has_path_roles = 0;
for (int i = 0; i < role_table.count; i++) {
if (role_table.entries[i].selector_type == SELECTOR_ROLE_PATH) {
has_path_roles = 1;
break;
}
}
if (!has_path_roles) {
char *wl_spec = prompt_index_whitelist();
if (wl_spec != NULL) {
allow_index_spec = wl_spec; /* will be freed at program exit */
}
}
}
@@ -2899,7 +3585,7 @@ int main(int argc, char *argv[]) {
server_set_bridge_source_trusted(&server, 1);
}
if (allow_index_spec != NULL) {
if (server_set_index_whitelist(&server, allow_index_spec) != 0) {
if (server_set_path_whitelist(&server, allow_index_spec) != 0) {
fprintf(stderr, "Invalid --allow-index spec: %s\n", allow_index_spec);
fprintf(stderr, "Expected: 'all', '1,3,4', '0-3', or '0-3,7,9'\n");
crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache);
@@ -2942,7 +3628,7 @@ int main(int argc, char *argv[]) {
&dispatcher,
&policy);
if (allow_index_spec != NULL) {
server_set_index_whitelist(&servers[tcp_server_idx], allow_index_spec);
server_set_path_whitelist(&servers[tcp_server_idx], allow_index_spec);
}
if (server_start(&servers[tcp_server_idx]) != 0) {
fprintf(stderr, "Failed to start FIPS/TCP server on %s: %s\n",
@@ -2970,7 +3656,7 @@ int main(int argc, char *argv[]) {
&dispatcher,
&policy);
if (allow_index_spec != NULL) {
server_set_index_whitelist(&servers[http_server_idx], allow_index_spec);
server_set_path_whitelist(&servers[http_server_idx], allow_index_spec);
}
if (server_start(&servers[http_server_idx]) != 0) {
fprintf(stderr, "Failed to start HTTP server on %s: %s\n",
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+12 -1
View File
@@ -126,6 +126,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -167,7 +172,11 @@ const char *role_curve_to_str(role_curve_t c);
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */
int role_table_register_role_path(role_table_t *table, const char *name, const char *path,
role_purpose_t purpose, role_curve_t curve,
int range_lo, int range_hi, int default_index,
const int *allowed_indices, int allowed_count);
/* from selector.h */
@@ -187,6 +196,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+75
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -177,6 +182,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
@@ -832,6 +839,74 @@ int role_table_register_nostr_index(role_table_t *table, int nostr_index) {
return role_table_add(table, &role);
}
/*
* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path
* template (with optional %d placeholder and range/set). Idempotent: if a role
* with the same path template already exists, returns 0.
*
* `path` may contain a "%d" placeholder (for ranged/set templates) or be a
* fixed path (no placeholder). range_lo/range_hi specify the allowed index
* range for the placeholder; for fixed paths, pass range_lo == range_hi == 0.
* If allowed_indices != NULL and allowed_count > 0, the set form is used
* instead of the range. default_index is the index used when a client sends
* {"role":"name"} without an explicit "index"; -1 means require an explicit
* index.
*/
int role_table_register_role_path(role_table_t *table, const char *name,
const char *path, role_purpose_t purpose,
role_curve_t curve,
int range_lo, int range_hi, int default_index,
const int *allowed_indices, int allowed_count) {
role_entry_t role;
int i;
if (table == NULL || name == NULL || path == NULL) {
return -1;
}
/* Idempotent: check if a role with this path template already exists */
for (i = 0; i < table->count; ++i) {
if (table->entries[i].selector_type == SELECTOR_ROLE_PATH &&
strcmp(table->entries[i].role_path, path) == 0) {
return 0;
}
}
memset(&role, 0, sizeof(role));
strncpy(role.name, name, sizeof(role.name) - 1);
role.name[sizeof(role.name) - 1] = '\0';
strncpy(role.purpose_str, role_purpose_to_str(purpose), sizeof(role.purpose_str) - 1);
role.purpose_str[sizeof(role.purpose_str) - 1] = '\0';
strncpy(role.curve_str, role_curve_to_str(curve), sizeof(role.curve_str) - 1);
role.curve_str[sizeof(role.curve_str) - 1] = '\0';
role.purpose = purpose;
role.curve = curve;
role.selector_type = SELECTOR_ROLE_PATH;
strncpy(role.role_path, path, sizeof(role.role_path) - 1);
role.role_path[sizeof(role.role_path) - 1] = '\0';
role.nostr_index = -1;
role.path_range_lo = range_lo;
role.path_range_hi = range_hi;
role.path_default_index = default_index;
if (allowed_indices != NULL && allowed_count > 0) {
int copy_n = allowed_count;
if (copy_n > (int)(sizeof(role.path_allowed_indices) / sizeof(role.path_allowed_indices[0]))) {
copy_n = (int)(sizeof(role.path_allowed_indices) / sizeof(role.path_allowed_indices[0]));
}
memcpy(role.path_allowed_indices, allowed_indices, (size_t)copy_n * sizeof(int));
role.path_allowed_count = copy_n;
} else {
role.path_allowed_count = 0;
}
role.derived = 0;
return role_table_add(table, &role);
}
role_purpose_t role_purpose_from_str(const char *s) {
if (str_eq(s, "nostr")) {
return PURPOSE_NOSTR;
+7
View File
@@ -118,6 +118,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -176,6 +181,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+502 -4
View File
@@ -119,6 +119,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -160,7 +165,11 @@ const char *role_curve_to_str(role_curve_t c);
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */
int role_table_register_role_path(role_table_t *table, const char *name, const char *path,
role_purpose_t purpose, role_curve_t curve,
int range_lo, int range_hi, int default_index,
const int *allowed_indices, int allowed_count);
/* from selector.h */
@@ -180,6 +189,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
@@ -677,6 +688,24 @@ typedef struct {
#define INDEX_WHITELIST_MAX 256 /* nostr_index range 0-255 */
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) /* 32 bytes */
#define PATH_WHITELIST_MAX_TEMPLATES 16
#define PATH_TEMPLATE_MAX_LEN 128
#define PATH_TEMPLATE_MAX_INDICES 64 /* max allowed indices per template (for sets) */
typedef struct {
char template[PATH_TEMPLATE_MAX_LEN]; /* e.g. "m/44'/1237'/%d/1/0" — one %d placeholder */
int range_lo; /* inclusive lower bound (for range form) */
int range_hi; /* inclusive upper bound (== range_lo for single) */
int allowed_indices[PATH_TEMPLATE_MAX_INDICES]; /* explicit set of allowed indices */
int allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} path_template_t;
typedef struct {
int active; /* 1 if any path templates are configured */
int count;
path_template_t templates[PATH_WHITELIST_MAX_TEMPLATES];
} path_whitelist_t;
typedef struct {
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
char last_error[256];
@@ -692,6 +721,7 @@ typedef struct {
int bridge_source_trusted; /* when set, unix connections send a qrexec_source preamble */
int index_whitelist_active; /* 1 if index whitelist is set (not "all") */
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE]; /* bitmap of allowed nostr_index values */
path_whitelist_t path_whitelist; /* path-template whitelist for role_path requests */
} server_ctx_t;
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
@@ -1301,6 +1331,12 @@ static int extract_method_and_selector(const char *json,
selector_req->has_role_path = 1;
json_copy_string(selector_req->role_path, sizeof(selector_req->role_path), tmp->valuestring, "");
}
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "index");
if (cJSON_IsNumber(tmp)) {
selector_req->has_index = 1;
selector_req->index = tmp->valueint;
}
}
}
@@ -1334,6 +1370,7 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp
ctx->bridge_source_trusted = 0;
ctx->index_whitelist_active = 0;
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
memset(&ctx->path_whitelist, 0, sizeof(ctx->path_whitelist));
if (!g_auth_nonce_cache_inited) {
auth_nonce_cache_init(&g_auth_nonce_cache);
g_auth_nonce_cache_inited = 1;
@@ -1428,6 +1465,313 @@ int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index) {
return whitelist_get_bit(ctx->index_whitelist, nostr_index);
}
/*
* Parse a single path-template token (e.g. "m/44'/1237'/0-3/1/0") into a
* path_template_t. The first path segment matching ^[0-9]+(-[0-9]+)?$ is
* treated as the range placeholder and replaced with "%d" in the stored
* template. Returns 0 on success, -1 on parse error.
*/
static int parse_path_template_token(path_template_t *out, const char *token) {
char buf[PATH_TEMPLATE_MAX_LEN];
char *p;
char *seg;
int found_range = 0;
if (out == NULL || token == NULL) {
return -1;
}
strncpy(buf, token, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
/* buf starts with "m/" — split by '/' and find the first numeric/range segment */
memset(out->template, 0, sizeof(out->template));
out->range_lo = 0;
out->range_hi = 0;
/* Build the output template, replacing the first numeric segment with %d */
p = buf;
seg = strchr(p, '/');
if (seg != NULL) {
/* copy up to and including the first '/' */
size_t prefix_len = (size_t)(seg - p) + 1;
if (prefix_len >= sizeof(out->template)) {
return -1;
}
memcpy(out->template, p, prefix_len);
out->template[prefix_len] = '\0';
p = seg + 1;
} else {
/* no '/' — not a valid path template */
return -1;
}
while (p != NULL && *p != '\0') {
char *next_slash = strchr(p, '/');
size_t seg_len;
char seg_buf[32];
if (next_slash != NULL) {
seg_len = (size_t)(next_slash - p);
} else {
seg_len = strlen(p);
}
if (seg_len >= sizeof(seg_buf)) {
return -1;
}
memcpy(seg_buf, p, seg_len);
seg_buf[seg_len] = '\0';
if (!found_range) {
/* Check if this segment is a number, range "N-M", or set "A+B+C" */
char *plus = strchr(seg_buf, '+');
char *dash = strchr(seg_buf, '-');
if (plus != NULL) {
/* Set form: "1+34+54" or "1+3-5+10" — parse each + separated entry */
int set_count = 0;
char *tok = seg_buf;
int set_ok = 1;
while (tok != NULL && *tok != '\0') {
char *next_plus = strchr(tok, '+');
if (next_plus != NULL) *next_plus = '\0';
/* Each token is either "N" or "N-M" */
char *sub_dash = strchr(tok, '-');
if (sub_dash != NULL) {
*sub_dash = '\0';
char *e1 = NULL, *e2 = NULL;
long lo = strtol(tok, &e1, 10);
long hi = strtol(sub_dash + 1, &e2, 10);
if (*e1 != '\0' || *e2 != '\0' || lo < 0 || hi < 0 || lo > hi) {
set_ok = 0; break;
}
for (long vi = lo; vi <= hi && set_count < PATH_TEMPLATE_MAX_INDICES; vi++) {
out->allowed_indices[set_count++] = (int)vi;
}
} else {
char *e = NULL;
long val = strtol(tok, &e, 10);
if (*e != '\0' || val < 0) { set_ok = 0; break; }
if (set_count < PATH_TEMPLATE_MAX_INDICES) {
out->allowed_indices[set_count++] = (int)val;
}
}
tok = (next_plus != NULL) ? next_plus + 1 : NULL;
}
if (set_ok && set_count > 0) {
found_range = 1;
out->allowed_count = set_count;
out->range_lo = out->allowed_indices[0];
out->range_hi = out->allowed_indices[set_count - 1];
if (strlen(out->template) + 3 >= sizeof(out->template)) return -1;
strcat(out->template, "%d");
strcat(out->template, "/");
} else {
/* not a valid set — treat as literal segment */
if (strlen(out->template) + seg_len + 2 >= sizeof(out->template)) return -1;
strcat(out->template, seg_buf);
strcat(out->template, "/");
}
} else if (dash != NULL) {
/* Range form: "N-M" */
*dash = '\0';
char *endptr1 = NULL, *endptr2 = NULL;
long lo = strtol(seg_buf, &endptr1, 10);
long hi = strtol(dash + 1, &endptr2, 10);
if (*endptr1 != '\0' || *endptr2 != '\0' || lo < 0 || hi < 0 || lo > hi) {
/* not a numeric range — treat as literal segment */
if (strlen(out->template) + seg_len + 2 >= sizeof(out->template)) {
return -1;
}
strcat(out->template, seg_buf);
strcat(out->template, "/");
} else {
found_range = 1;
out->range_lo = (int)lo;
out->range_hi = (int)hi;
if (strlen(out->template) + 3 >= sizeof(out->template)) {
return -1;
}
strcat(out->template, "%d");
strcat(out->template, "/");
}
} else {
/* Single number */
char *endptr = NULL;
long val = strtol(seg_buf, &endptr, 10);
if (*endptr != '\0' || val < 0) {
/* not a number — treat as literal segment */
if (strlen(out->template) + seg_len + 2 >= sizeof(out->template)) {
return -1;
}
strcat(out->template, seg_buf);
strcat(out->template, "/");
} else {
found_range = 1;
out->range_lo = (int)val;
out->range_hi = (int)val;
if (strlen(out->template) + 3 >= sizeof(out->template)) {
return -1;
}
strcat(out->template, "%d");
strcat(out->template, "/");
}
}
} else {
/* literal segment after the range */
if (strlen(out->template) + seg_len + 2 >= sizeof(out->template)) {
return -1;
}
strcat(out->template, seg_buf);
strcat(out->template, "/");
}
p = (next_slash != NULL) ? next_slash + 1 : NULL;
}
/* Remove trailing '/' from template */
{
size_t tlen = strlen(out->template);
if (tlen > 0 && out->template[tlen - 1] == '/') {
out->template[tlen - 1] = '\0';
}
}
if (!found_range) {
return -1; /* a path template must contain a numeric/range segment */
}
return 0;
}
/*
* Unified whitelist parser: accepts both integer nostr_index tokens
* ("0-3", "1,3,4") and path-template tokens ("m/44'/1237'/0-3/1/0").
* "all" clears both whitelists. Returns 0 on success, -1 on parse error.
*/
int server_set_path_whitelist(server_ctx_t *ctx, const char *spec) {
char buf[512];
char *p;
if (ctx == NULL || spec == NULL) {
return -1;
}
/* "all" means no restriction */
if (strcmp(spec, "all") == 0) {
ctx->index_whitelist_active = 0;
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
memset(&ctx->path_whitelist, 0, sizeof(ctx->path_whitelist));
return 0;
}
strncpy(buf, spec, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
/* Reset both whitelists before parsing */
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
ctx->index_whitelist_active = 0;
memset(&ctx->path_whitelist, 0, sizeof(ctx->path_whitelist));
p = buf;
while (p != NULL && *p != '\0') {
char *comma = strchr(p, ',');
if (comma != NULL) {
*comma = '\0';
}
/* Skip empty tokens */
if (*p == '\0') {
p = (comma != NULL) ? comma + 1 : NULL;
continue;
}
/* Is this a path template? (contains '/') */
if (strchr(p, '/') != NULL) {
if (ctx->path_whitelist.count >= PATH_WHITELIST_MAX_TEMPLATES) {
return -1;
}
if (parse_path_template_token(
&ctx->path_whitelist.templates[ctx->path_whitelist.count], p) != 0) {
return -1;
}
ctx->path_whitelist.count++;
ctx->path_whitelist.active = 1;
} else {
/* Integer nostr_index token: "N" or "N-M" */
char *dash = strchr(p, '-');
if (dash != NULL) {
*dash = '\0';
char *endptr1 = NULL, *endptr2 = NULL;
long lo = strtol(p, &endptr1, 10);
long hi = strtol(dash + 1, &endptr2, 10);
if (*endptr1 != '\0' || *endptr2 != '\0' || lo < 0 || hi < 0 ||
lo >= INDEX_WHITELIST_MAX || hi >= INDEX_WHITELIST_MAX || lo > hi) {
return -1;
}
for (long i = lo; i <= hi; i++) {
whitelist_set_bit(ctx->index_whitelist, (int)i);
}
} else {
char *endptr = NULL;
long idx = strtol(p, &endptr, 10);
if (*endptr != '\0' || idx < 0 || idx >= INDEX_WHITELIST_MAX) {
return -1;
}
whitelist_set_bit(ctx->index_whitelist, (int)idx);
}
ctx->index_whitelist_active = 1;
}
p = (comma != NULL) ? comma + 1 : NULL;
}
return 0;
}
/*
* Check if a role_path is allowed by the path whitelist.
* Returns 1 if allowed, 0 if not.
*/
int server_path_whitelist_allows(const server_ctx_t *ctx, const char *role_path) {
int i;
if (ctx == NULL || role_path == NULL) {
return 0;
}
if (!ctx->path_whitelist.active) {
/* No path whitelist configured — deny by default (fail-closed for paths) */
return 0;
}
for (i = 0; i < ctx->path_whitelist.count; i++) {
const path_template_t *tpl = &ctx->path_whitelist.templates[i];
char candidate[PATH_TEMPLATE_MAX_LEN];
if (tpl->allowed_count > 0) {
/* Set form: check each allowed index */
int j;
for (j = 0; j < tpl->allowed_count; j++) {
snprintf(candidate, sizeof(candidate), tpl->template, tpl->allowed_indices[j]);
if (strcmp(candidate, role_path) == 0) {
return 1;
}
}
} else {
/* Range form: iterate lo..hi */
int idx;
for (idx = tpl->range_lo; idx <= tpl->range_hi; idx++) {
snprintf(candidate, sizeof(candidate), tpl->template, idx);
if (strcmp(candidate, role_path) == 0) {
return 1;
}
}
}
}
return 0;
}
int server_start(server_ctx_t *ctx) {
int fd;
struct sockaddr_un addr;
@@ -1809,6 +2153,8 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
int pending_derivation = 0;
int hard_selector_error = 0;
int derivation_error = 0;
char concrete_path[ROLE_PATH_MAX]; /* concrete path for named path-role with index */
concrete_path[0] = '\0';
char activity[256];
const char *verdict = "DENIED";
const char *source_label = "no-match";
@@ -2028,7 +2374,8 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
if (extract_method_and_selector(request, method, sizeof(method), &selector_req) == 0) {
if (ctx->dispatcher->role_table != NULL) {
selector_rc = selector_resolve(&selector_req, ctx->dispatcher->role_table, &role);
if (selector_rc == SELECTOR_OK && role != NULL) {
if (selector_rc == SELECTOR_OK && role != NULL &&
role->selector_type == SELECTOR_NOSTR_INDEX) {
json_copy_string(role_name, sizeof(role_name), role->name, "main");
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(role->purpose), "nostr");
} else if (selector_rc == SELECTOR_ERR_NOT_FOUND && selector_req.has_nostr_index) {
@@ -2039,6 +2386,67 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
(void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index);
}
json_copy_string(purpose, sizeof(purpose), "nostr", "nostr");
} else if (selector_rc == SELECTOR_ERR_NOT_FOUND && selector_req.has_role_path) {
/* role_path not in table — check path whitelist for auto-registration */
if (server_path_whitelist_allows(ctx, selector_req.role_path)) {
pending_derivation = 1;
(void)snprintf(role_name, sizeof(role_name), "path_%s", selector_req.role_path);
json_copy_string(purpose, sizeof(purpose), "nostr", "nostr");
} else {
hard_selector_error = -200; /* path_not_allowed sentinel */
}
} else if (selector_rc == SELECTOR_OK && role != NULL &&
role->selector_type == SELECTOR_ROLE_PATH &&
strstr(role->role_path, "%d") == NULL) {
/* Fixed-path named role — no index needed, derive if not yet done */
json_copy_string(role_name, sizeof(role_name), role->name, "main");
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(role->purpose), "nostr");
if (!role->derived) {
pending_derivation = 1;
}
} else if (selector_rc == SELECTOR_OK && role != NULL &&
role->selector_type == SELECTOR_ROLE_PATH &&
strstr(role->role_path, "%d") != NULL) {
/* Named path-role with template — resolve the concrete path from index */
json_copy_string(role_name, sizeof(role_name), role->name, "main");
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(role->purpose), "nostr");
int chosen_index;
if (selector_req.has_index) {
chosen_index = selector_req.index;
} else if (role->path_default_index >= 0) {
chosen_index = role->path_default_index;
} else {
hard_selector_error = -201; /* index_required sentinel */
chosen_index = -1;
}
if (chosen_index >= 0) {
int index_ok;
if (role->path_allowed_count > 0) {
/* Set form: check if index is in the allowed set */
int j;
index_ok = 0;
for (j = 0; j < role->path_allowed_count; j++) {
if (role->path_allowed_indices[j] == chosen_index) {
index_ok = 1;
break;
}
}
} else {
/* Range form: check lo..hi */
index_ok = (chosen_index >= role->path_range_lo &&
chosen_index <= role->path_range_hi);
}
if (!index_ok) {
hard_selector_error = -202; /* index_out_of_range sentinel */
} else {
/* Format the concrete path and store it for derivation */
snprintf(concrete_path, sizeof(concrete_path),
role->role_path, chosen_index);
if (!role->derived) {
pending_derivation = 1;
}
}
}
} else if (selector_rc == SELECTOR_ERR_AMBIGUOUS ||
selector_rc == SELECTOR_ERR_NOT_FOUND ||
selector_rc == SELECTOR_ERR_NO_DEFAULT) {
@@ -2074,6 +2482,15 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
} else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}");
pchk = POLICY_DENY;
} else if (hard_selector_error == -200) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2003,\"message\":\"path_not_allowed\"}}");
pchk = POLICY_DENY;
} else if (hard_selector_error == -201) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2004,\"message\":\"index_required\"}}");
pchk = POLICY_DENY;
} else if (hard_selector_error == -202) {
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2005,\"message\":\"index_out_of_range\"}}");
pchk = POLICY_DENY;
} else if (hard_selector_error == 0) {
/* Normal path: run policy_check (skip if whitelist already denied) */
pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose, &policy_src);
@@ -2110,8 +2527,89 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
if (ctx->dispatcher == NULL ||
ctx->dispatcher->role_table == NULL ||
ctx->dispatcher->key_store == NULL ||
ctx->dispatcher->mnemonic == NULL ||
role_table_register_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index) != 0) {
ctx->dispatcher->mnemonic == NULL) {
derivation_error = 1;
} else if (selector_req.has_role_path) {
/* Auto-register a fixed-path role from the path whitelist */
char path_role_name[ROLE_NAME_MAX];
(void)snprintf(path_role_name, sizeof(path_role_name), "pathrole_%d",
ctx->dispatcher->role_table->count);
if (role_table_register_role_path(ctx->dispatcher->role_table,
path_role_name,
selector_req.role_path,
PURPOSE_NOSTR, CURVE_SECP256K1,
0, 0, -1, NULL, 0) != 0) {
derivation_error = 1;
} else {
new_role = role_table_find_by_path(ctx->dispatcher->role_table,
selector_req.role_path);
if (new_role == NULL) {
derivation_error = 1;
} else {
role_index = (int)(new_role - &ctx->dispatcher->role_table->entries[0]);
if (role_index < 0 || role_index >= ctx->dispatcher->role_table->count ||
crypto_derive_one(ctx->dispatcher->key_store,
ctx->dispatcher->role_table,
ctx->dispatcher->mnemonic,
role_index) != 0) {
derivation_error = 1;
} else {
json_copy_string(role_name, sizeof(role_name), new_role->name, role_name);
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(new_role->purpose), "nostr");
}
}
}
} else if (role != NULL && role->selector_type == SELECTOR_ROLE_PATH &&
strstr(role->role_path, "%d") == NULL) {
/* Fixed-path named role (found by role name) — derive directly */
new_role = role;
role_index = (int)(new_role - &ctx->dispatcher->role_table->entries[0]);
if (role_index < 0 || role_index >= ctx->dispatcher->role_table->count ||
crypto_derive_one(ctx->dispatcher->key_store,
ctx->dispatcher->role_table,
ctx->dispatcher->mnemonic,
role_index) != 0) {
derivation_error = 1;
} else {
json_copy_string(role_name, sizeof(role_name), new_role->name, role_name);
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(new_role->purpose), "nostr");
}
} else if (concrete_path[0] != '\0') {
/* Named path-role with template — derive the concrete path.
* The role already exists in the table; we temporarily set its
* role_path to the concrete path for derivation, then restore. */
char saved_path[ROLE_PATH_MAX];
new_role = role; /* the role resolved by selector_resolve */
if (new_role == NULL) {
derivation_error = 1;
} else {
role_index = (int)(new_role - &ctx->dispatcher->role_table->entries[0]);
if (role_index < 0 || role_index >= ctx->dispatcher->role_table->count) {
derivation_error = 1;
} else {
/* Swap in the concrete path */
strncpy(saved_path, new_role->role_path, sizeof(saved_path) - 1);
saved_path[sizeof(saved_path) - 1] = '\0';
strncpy(new_role->role_path, concrete_path, sizeof(new_role->role_path) - 1);
new_role->role_path[sizeof(new_role->role_path) - 1] = '\0';
new_role->derived = 0;
new_role->pubkey_hex[0] = '\0';
if (crypto_derive_one(ctx->dispatcher->key_store,
ctx->dispatcher->role_table,
ctx->dispatcher->mnemonic,
role_index) != 0) {
derivation_error = 1;
} else {
json_copy_string(role_name, sizeof(role_name), new_role->name, role_name);
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(new_role->purpose), "nostr");
}
/* Restore the template path (keep derived=1 + pubkey from concrete derivation) */
strncpy(new_role->role_path, saved_path, sizeof(new_role->role_path) - 1);
new_role->role_path[sizeof(new_role->role_path) - 1] = '\0';
}
}
} else if (role_table_register_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index) != 0) {
derivation_error = 1;
} else {
new_role = role_table_find_by_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index);
+7
View File
@@ -118,6 +118,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -176,6 +181,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+5
View File
@@ -55,6 +55,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct { role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; int count; } role_table_t;
void role_table_init(role_table_t *table);
+12 -1
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -157,7 +162,11 @@ const char *role_curve_to_str(role_curve_t c);
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */
int role_table_register_role_path(role_table_t *table, const char *name, const char *path,
role_purpose_t purpose, role_curve_t curve,
int range_lo, int range_hi, int default_index,
const int *allowed_indices, int allowed_count);
/* from selector.h */
@@ -177,6 +186,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+8 -1
View File
@@ -101,6 +101,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
@@ -118,7 +123,7 @@ role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
@@ -136,6 +141,8 @@ typedef struct {
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
void selector_request_init(selector_request_t *req);
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+7
View File
@@ -118,6 +118,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -176,6 +181,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+8 -1
View File
@@ -80,6 +80,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
@@ -97,7 +102,7 @@ role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
@@ -113,6 +118,8 @@ typedef struct {
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
void selector_request_init(selector_request_t *req);
+7
View File
@@ -97,6 +97,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
@@ -128,6 +133,8 @@ typedef struct {
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
void selector_request_init(selector_request_t *req);
+7
View File
@@ -73,6 +73,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
@@ -101,6 +106,8 @@ typedef struct {
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
void selector_request_init(selector_request_t *req);
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+443
View File
@@ -0,0 +1,443 @@
/*
* test_path_whitelist.c tests for the path-template whitelist and
* named path-role functionality.
*
* Covers:
* - server_set_path_whitelist parsing (integer + path-template tokens)
* - server_path_whitelist_allows matching
* - role_table_register_role_path (idempotent, range fields)
* - derive_secp256k1_from_path (BIP-44 path parsing + derivation)
*/
#define _GNU_SOURCE
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* from secure_mem.h */
typedef struct {
void *data;
size_t size;
int locked;
} secure_buf_t;
int secure_buf_alloc(secure_buf_t *buf, size_t size);
void secure_buf_free(secure_buf_t *buf);
void secure_memzero(void *ptr, size_t len);
/* from mnemonic.h */
#define MNEMONIC_MAX_LEN 256
typedef struct {
secure_buf_t buf;
int loaded;
int word_count;
} mnemonic_state_t;
void mnemonic_init(mnemonic_state_t *state);
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
void mnemonic_unload(mnemonic_state_t *state);
int mnemonic_is_loaded(const mnemonic_state_t *state);
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
/* from role_table.h */
#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
#define ROLE_TABLE_MAX_ENTRIES 256
typedef enum {
PURPOSE_NOSTR = 0,
PURPOSE_BITCOIN,
PURPOSE_SSH,
PURPOSE_AGE,
PURPOSE_FIPS,
PURPOSE_PQ_SIG,
PURPOSE_PQ_KEM,
PURPOSE_UNKNOWN
} role_purpose_t;
typedef enum {
CURVE_SECP256K1 = 0,
CURVE_ED25519,
CURVE_X25519,
CURVE_ML_DSA_65,
CURVE_SLH_DSA_128S,
CURVE_ML_KEM_768,
CURVE_UNKNOWN
} role_curve_t;
typedef enum {
SELECTOR_NOSTR_INDEX,
SELECTOR_ROLE_PATH
} role_selector_type_t;
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;
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo;
int path_range_hi;
int path_default_index;
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
int count;
} role_table_t;
void role_table_init(role_table_t *table);
int role_table_add(role_table_t *table, const role_entry_t *entry);
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */
int role_table_register_role_path(role_table_t *table, const char *name, const char *path,
role_purpose_t purpose, role_curve_t curve,
int range_lo, int range_hi, int default_index,
const int *allowed_indices, int allowed_count);
/* from selector.h */
#define SELECTOR_OK 0
#define SELECTOR_ERR_AMBIGUOUS -1
#define SELECTOR_ERR_NOT_FOUND -2
#define SELECTOR_ERR_NO_DEFAULT -3
typedef struct {
int has_role;
char role_name[ROLE_NAME_MAX];
int has_nostr_index;
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index;
int index;
} selector_request_t;
void selector_request_init(selector_request_t *req);
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
/* from enforcement.h */
#define ENFORCE_OK 0
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define ENFORCE_ERR_ALGORITHM -4
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define VERB_DERIVE "derive"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key"
#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event"
#define VERB_NOSTR_MINE_EVENT "nostr_mine_event"
#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt"
#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt"
#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt"
#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt"
#define VERB_ENCRYPT "encrypt"
#define VERB_DECRYPT "decrypt"
int enforce_verb_role(const char *verb, const role_entry_t *role);
/* from pq_crypto.h */
typedef enum {
CRYPTO_ALG_SECP256K1 = 0,
CRYPTO_ALG_ED25519,
CRYPTO_ALG_X25519,
CRYPTO_ALG_ML_DSA_65,
CRYPTO_ALG_SLH_DSA_128S,
CRYPTO_ALG_ML_KEM_768,
CRYPTO_ALG_UNKNOWN
} crypto_alg_t;
typedef struct {
size_t priv_key_len;
size_t pub_key_len;
size_t sig_len;
size_t ciphertext_len;
size_t shared_secret_len;
} crypto_alg_sizes_t;
const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose);
const char *crypto_alg_to_str(crypto_alg_t alg);
crypto_alg_t crypto_alg_from_str(const char *s);
/* from key_store.h */
#define KEY_STORE_MAX_ROLES ROLE_TABLE_MAX_ENTRIES
typedef struct {
secure_buf_t private_key;
secure_buf_t public_key;
char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */
char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */
crypto_alg_t alg;
int valid;
} derived_key_t;
typedef struct {
derived_key_t keys[KEY_STORE_MAX_ROLES];
int count;
} key_store_t;
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
/* nostr init/cleanup */
int nostr_init(void);
void nostr_cleanup(void);
/* from server.h (minimal subset for whitelist tests) */
#define SERVER_SOCKET_NAME_MAX 108
#define INDEX_WHITELIST_MAX 256
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8)
#define PATH_WHITELIST_MAX_TEMPLATES 16
#define PATH_TEMPLATE_MAX_LEN 128
typedef struct {
char template[PATH_TEMPLATE_MAX_LEN];
int range_lo;
int range_hi;
int allowed_indices[64];
int allowed_count;
} path_template_t;
typedef struct {
int active;
int count;
path_template_t templates[PATH_WHITELIST_MAX_TEMPLATES];
} path_whitelist_t;
typedef struct {
char socket_name[SERVER_SOCKET_NAME_MAX];
char last_error[256];
int listen_fd;
int running;
int listen_mode;
int stdio_handled;
void *dispatcher; /* dummy */
void *policy; /* dummy */
int socket_name_explicit;
int auth_mode;
int auth_skew_seconds;
int bridge_source_trusted;
int index_whitelist_active;
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE];
path_whitelist_t path_whitelist;
} server_ctx_t;
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec);
int server_set_path_whitelist(server_ctx_t *ctx, const char *spec);
int server_path_whitelist_allows(const server_ctx_t *ctx, const char *role_path);
/* NSIGNER_HEADERLESS_DECLS_END */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static int tests_run = 0;
static int tests_passed = 0;
static void check(const char *desc, int condition) {
tests_run++;
if (condition) {
tests_passed++;
printf("PASS: %s\n", desc);
} else {
printf("FAIL: %s\n", desc);
}
}
int main(void) {
server_ctx_t ctx;
/* ---- Test 1: server_set_path_whitelist with "all" ---- */
memset(&ctx, 0, sizeof(ctx));
check("set_path_whitelist 'all' returns 0",
server_set_path_whitelist(&ctx, "all") == 0);
check("'all' sets index_whitelist_active=0",
ctx.index_whitelist_active == 0);
check("'all' sets path_whitelist.active=0",
ctx.path_whitelist.active == 0);
/* ---- Test 2: integer-only spec (backward compat) ---- */
memset(&ctx, 0, sizeof(ctx));
check("set_path_whitelist '0-3' returns 0",
server_set_path_whitelist(&ctx, "0-3") == 0);
check("'0-3' sets index_whitelist_active=1",
ctx.index_whitelist_active == 1);
check("'0-3' does not set path_whitelist.active",
ctx.path_whitelist.active == 0);
/* ---- Test 3: path-template spec ---- */
memset(&ctx, 0, sizeof(ctx));
check("set_path_whitelist 'm/44\\'/1237\\'/0-3/1/0' returns 0",
server_set_path_whitelist(&ctx, "m/44'/1237'/0-3/1/0") == 0);
check("path template sets path_whitelist.active=1",
ctx.path_whitelist.active == 1);
check("path template count=1",
ctx.path_whitelist.count == 1);
check("path template range_lo=0",
ctx.path_whitelist.templates[0].range_lo == 0);
check("path template range_hi=3",
ctx.path_whitelist.templates[0].range_hi == 3);
/* ---- Test 4: server_path_whitelist_allows matching ---- */
check("path_whitelist_allows m/44'/1237'/1/1/0 (in range)",
server_path_whitelist_allows(&ctx, "m/44'/1237'/1/1/0") == 1);
check("path_whitelist_allows m/44'/1237'/0/1/0 (in range)",
server_path_whitelist_allows(&ctx, "m/44'/1237'/0/1/0") == 1);
check("path_whitelist_allows m/44'/1237'/3/1/0 (in range)",
server_path_whitelist_allows(&ctx, "m/44'/1237'/3/1/0") == 1);
check("path_whitelist denies m/44'/1237'/4/1/0 (out of range)",
server_path_whitelist_allows(&ctx, "m/44'/1237'/4/1/0") == 0);
check("path_whitelist denies m/44'/1237'/1/0/0 (wrong change)",
server_path_whitelist_allows(&ctx, "m/44'/1237'/1/0/0") == 0);
/* ---- Test 5: multiple path templates ---- */
memset(&ctx, 0, sizeof(ctx));
check("set_path_whitelist with two templates returns 0",
server_set_path_whitelist(&ctx,
"m/44'/1237'/0-3/0/0,m/44'/1237'/0-3/1/0") == 0);
check("two templates: count=2",
ctx.path_whitelist.count == 2);
check("two templates: allows m/44'/1237'/2/0/0",
server_path_whitelist_allows(&ctx, "m/44'/1237'/2/0/0") == 1);
check("two templates: allows m/44'/1237'/2/1/0",
server_path_whitelist_allows(&ctx, "m/44'/1237'/2/1/0") == 1);
check("two templates: denies m/44'/1237'/2/2/0",
server_path_whitelist_allows(&ctx, "m/44'/1237'/2/2/0") == 0);
/* ---- Test 6: no path whitelist configured → deny ---- */
memset(&ctx, 0, sizeof(ctx));
check("no path whitelist denies all paths (fail-closed)",
server_path_whitelist_allows(&ctx, "m/44'/1237'/1/1/0") == 0);
/* ---- Test 7: role_table_register_role_path ---- */
{
role_table_t table;
role_table_init(&table);
check("register_role_path returns 0",
role_table_register_role_path(&table, "myrole",
"m/44'/1237'/%d/1/0",
PURPOSE_NOSTR, CURVE_SECP256K1,
0, 3, 1, NULL, 0) == 0);
role_entry_t *r = role_table_find_by_name(&table, "myrole");
check("registered role found by name", r != NULL);
check("registered role is SELECTOR_ROLE_PATH",
r != NULL && r->selector_type == SELECTOR_ROLE_PATH);
check("registered role path_range_lo=0",
r != NULL && r->path_range_lo == 0);
check("registered role path_range_hi=3",
r != NULL && r->path_range_hi == 3);
check("registered role path_default_index=1",
r != NULL && r->path_default_index == 1);
check("registered role purpose=NOSTR",
r != NULL && r->purpose == PURPOSE_NOSTR);
check("registered role curve=SECP256K1",
r != NULL && r->curve == CURVE_SECP256K1);
/* Idempotent: registering the same path again returns 0, no duplicate */
check("register_role_path idempotent returns 0",
role_table_register_role_path(&table, "other",
"m/44'/1237'/%d/1/0",
PURPOSE_NOSTR, CURVE_SECP256K1,
0, 3, 1, NULL, 0) == 0);
check("idempotent: no duplicate added",
table.count == 1);
}
/* ---- Test 8: end-to-end derivation with role_path ---- */
{
role_table_t table;
key_store_t key_store;
mnemonic_state_t mnemonic;
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
int rc;
role_table_init(&table);
mnemonic_init(&mnemonic);
/* Load mnemonic */
rc = mnemonic_load(&mnemonic, valid_12);
check("mnemonic load succeeds", rc == 0);
/* Register a fixed-path role (no %d) */
rc = role_table_register_role_path(&table, "testpath",
"m/44'/1237'/1/1/0",
PURPOSE_NOSTR, CURVE_SECP256K1,
0, 0, -1, NULL, 0);
check("register fixed-path role returns 0", rc == 0);
/* Derive all keys */
if (nostr_init() != 0) {
check("nostr_init succeeds", 0);
mnemonic_unload(&mnemonic);
printf("\n%d/%d tests passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}
check("nostr_init succeeds", 1);
memset(&key_store, 0, sizeof(key_store));
rc = crypto_derive_all(&key_store, &table, &mnemonic);
check("crypto_derive_all with path role succeeds", rc >= 0);
/* Find the role and check it was derived */
role_entry_t *r = role_table_find_by_name(&table, "testpath");
check("testpath role found", r != NULL);
check("testpath role derived", r != NULL && r->derived == 1);
check("testpath pubkey is 64 hex chars",
r != NULL && strlen(r->pubkey_hex) == 64);
/* Verify the pubkey matches the expected NIP-06 index-1 derivation
* (m/44'/1237'/1'/0/0) this is a sanity check that the path
* derivation produces a valid key. The path m/44'/1237'/1/1/0 is
* different from NIP-06 so the pubkey should differ from index 1. */
{
role_table_t nip06_table;
key_store_t nip06_store;
role_table_init(&nip06_table);
role_table_register_nostr_index(&nip06_table, 1);
memset(&nip06_store, 0, sizeof(nip06_store));
crypto_derive_all(&nip06_store, &nip06_table, &mnemonic);
role_entry_t *nip06_r = role_table_find_by_nostr_index(&nip06_table, 1);
check("NIP-06 index 1 derived",
nip06_r != NULL && nip06_r->derived == 1);
check("path m/44'/1237'/1/1/0 differs from NIP-06 index 1 (m/44'/1237'/1'/0/0)",
r != NULL && nip06_r != NULL &&
strcmp(r->pubkey_hex, nip06_r->pubkey_hex) != 0);
}
mnemonic_unload(&mnemonic);
}
nostr_cleanup();
printf("\n%d/%d tests passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+8 -1
View File
@@ -98,6 +98,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
@@ -115,7 +120,7 @@ role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
@@ -133,6 +138,8 @@ typedef struct {
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
void selector_request_init(selector_request_t *req);
+8 -1
View File
@@ -95,6 +95,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
@@ -112,7 +117,7 @@ role_purpose_t role_purpose_from_str(const char *s);
role_curve_t role_curve_from_str(const char *s);
const char *role_purpose_to_str(role_purpose_t p);
const char *role_curve_to_str(role_curve_t c);
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* from selector.h */
@@ -130,6 +135,8 @@ typedef struct {
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
void selector_request_init(selector_request_t *req);
+12 -1
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -157,7 +162,11 @@ const char *role_curve_to_str(role_curve_t c);
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
/* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */
int role_table_register_role_path(role_table_t *table, const char *name, const char *path,
role_purpose_t purpose, role_curve_t curve,
int range_lo, int range_hi, int default_index,
const int *allowed_indices, int allowed_count);
/* from selector.h */
@@ -177,6 +186,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */
+7
View File
@@ -97,6 +97,11 @@ typedef struct {
char role_path[ROLE_PATH_MAX];
char pubkey_hex[ROLE_PUBKEY_HEX_MAX];
int derived;
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
typedef struct {
@@ -128,6 +133,8 @@ typedef struct {
int nostr_index;
int has_role_path;
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
void selector_request_init(selector_request_t *req);
+7
View File
@@ -116,6 +116,11 @@ typedef struct {
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 */
int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */
int path_range_hi; /* inclusive upper bound; == path_range_lo for single */
int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */
int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */
int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */
} role_entry_t;
/* The role table */
@@ -174,6 +179,8 @@ typedef struct {
int has_role_path; /* 1 if "role_path" field was present */
char role_path[ROLE_PATH_MAX];
int has_index; /* 1 if "index" field was present (for named path-roles) */
int index; /* index value for named path-role template */
} selector_request_t;
/* Initialize a selector request (all fields zeroed/unset) */