Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
821245ac1d | ||
|
|
7cbefe13ec | ||
|
|
d7fb3787e6 | ||
|
|
f0e90e0ea6 |
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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)).
|
||||
@@ -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
|
||||
```
|
||||
@@ -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) */
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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
@@ -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) */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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
@@ -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) */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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) */
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) */
|
||||
|
||||
Reference in New Issue
Block a user