v0.0.57 - Migrated to unified nostr_ prefixed verb names; removed legacy verb aliases (sign_data, ssh_sign, verify_signature, kem_encapsulate, kem_decapsulate, otp_encrypt, otp_decrypt); split get_public_key into algorithm-based get_public_key and role-based nostr_get_public_key; OTP now selected via algorithm:otp instead of curve:otp; consolidated API docs from api.md into README.md

This commit is contained in:
Laan Tungir
2026-07-20 17:29:45 -04:00
parent 96ab9741ef
commit b3421c3e40
51 changed files with 1105 additions and 2282 deletions

View File

@@ -182,7 +182,7 @@ examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPL
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS)
$(TEST_MNEMONIC_INPUT_TARGET): $(TEST_DIR)/test_mnemonic_input.c
@mkdir -p $(BUILD_DIR)
@@ -202,7 +202,7 @@ $(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcemen
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/miner.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
@mkdir -p $(BUILD_DIR)
@@ -226,7 +226,7 @@ $(TEST_QREXEC_AUTH_TARGET): $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envel
$(TEST_MINE_EVENT_TARGET): $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_mine_event.c $(SRC_DIR)/miner.c $(SRC_DIR)/key_store.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_MINE_EVENT_TARGET) $(LDFLAGS)
$(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/role_table.c
@mkdir -p $(BUILD_DIR)
@@ -234,23 +234,23 @@ $(TEST_PQ_CRYPTO_TARGET): $(TEST_DIR)/test_pq_crypto.c $(SRC_DIR)/pq_crypto.c $(
$(TEST_ED25519_X25519_TARGET): $(TEST_DIR)/test_ed25519_x25519.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
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_ed25519_x25519.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 -o $(TEST_ED25519_X25519_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_ed25519_x25519.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)/otp_pad.c libotppad/libotppad.c -o $(TEST_ED25519_X25519_TARGET) $(LDFLAGS)
$(TEST_ML_DSA_65_TARGET): $(TEST_DIR)/test_ml_dsa_65.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
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_dsa_65.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 -o $(TEST_ML_DSA_65_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_dsa_65.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)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_DSA_65_TARGET) $(LDFLAGS)
$(TEST_SLH_DSA_128S_TARGET): $(TEST_DIR)/test_slh_dsa_128s.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
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_slh_dsa_128s.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 -o $(TEST_SLH_DSA_128S_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_slh_dsa_128s.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)/otp_pad.c libotppad/libotppad.c -o $(TEST_SLH_DSA_128S_TARGET) $(LDFLAGS)
$(TEST_ML_KEM_768_TARGET): $(TEST_DIR)/test_ml_kem_768.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
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_kem_768.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 -o $(TEST_ML_KEM_768_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_ml_kem_768.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)/otp_pad.c libotppad/libotppad.c -o $(TEST_ML_KEM_768_TARGET) $(LDFLAGS)
$(TEST_PUBKEY_FORMAT_TARGET): $(TEST_DIR)/test_pubkey_format.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
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_pubkey_format.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 -o $(TEST_PUBKEY_FORMAT_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_pubkey_format.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)/otp_pad.c libotppad/libotppad.c -o $(TEST_PUBKEY_FORMAT_TARGET) $(LDFLAGS)
$(TEST_ALGORITHM_API_TARGET): $(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
@mkdir -p $(BUILD_DIR)

843
README.md

File diff suppressed because it is too large Load Diff

387
api.md
View File

@@ -1,382 +1,15 @@
# n_signer API
`n_signer` exposes a JSON-RPC 2.0-style request/response protocol. Every request is a single JSON object; every response is a single JSON object. This document is the complete, authoritative description of the API.
The complete, authoritative API reference is now in [`README.md`](README.md) §4 (API).
For the security model, transports, and operational behavior, see [`README.md`](README.md). For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).
It covers:
---
- **§4.1 Request format** — JSON-RPC 2.0-style request shape.
- **§4.2 Response format** — success/error shapes and the full error-code table.
- **§4.3 Verbs** — the verb table (positional params + options), the `scheme` option for secp256k1, and the enforcement matrix.
- **§4.4 Algorithms** — the algorithm table (secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp), derivation paths, key sizes, and the OTP one-time-pad model.
- **§4.5 Examples** — worked request/response examples for every verb.
- **§4.6 Role-based selectors** — `nostr_index` / `role` / `role_path` for the `nostr_*` verbs.
- **§4.7 Pre-approval** — `--preapprove` syntax for algorithm-based and Nostr verbs.
## 1. API format
### 1.1 Request
```json
{ "id": "<string>", "method": "<verb>", "params": [ <arg0>, <arg1>, ..., { <options> } ] }
```
- `id` — caller-supplied string echoed verbatim in the response. Used to match requests to responses.
- `method` — the verb name (see [§2](#2-verbs)).
- `params` — a JSON array. Positional arguments come first; the **last array element** is conventionally an options object. The options object is optional for most verbs.
### 1.2 Response (success)
```json
{ "id": "<string>", "result": <value> }
```
`result` is a JSON string. For structured verbs the string is itself a serialized JSON object — clients should `JSON.parse` it.
### 1.3 Response (error)
```json
{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } }
```
### 1.4 Error codes
| Code | Message | Meaning |
|-------|-------------------------------|--------------------------------------------------------------------|
| -32700| `parse_error` | Request was not valid JSON. |
| -32600| `invalid_request` | Missing `id`, `method`, or `params`, or `params` is not an array. |
| -32601| `method_not_found` | Unknown verb, or verb not valid for the selected algorithm. |
| -32602| `invalid_params` | Malformed arguments (bad hex, wrong length, missing field, etc.). |
| 1001 | `ambiguous_role_selector` | More than one role selector was supplied. |
| 1002 | `unknown_role` | No role matched the selector. |
| 1003 | `no_default_role` | No selector given and no `main` role exists. |
| 1004 | `purpose_mismatch` | Role's purpose is not valid for this verb. |
| 1005 | `curve_mismatch` | Role's curve is not valid for this verb. |
| 1006 | `mnemonic_not_loaded` | No mnemonic is loaded in the signer. |
| 1007 | `no_termination_condition` | `nostr_mine_event` called without `difficulty` or `timeout_sec`. |
| 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. |
---
## 2. Verbs
All verbs take their arguments as positional `params` and their options in a trailing options object. Most verbs select a key via the `algorithm` + `index` options (see [§3](#3-algorithms)). The `nostr_*` verbs select a secp256k1 NIP-06 key via `nostr_index` and implement Nostr-protocol-specific serialization on top of the raw crypto.
| Verb | Algorithms | Positional params | Options |
|-------------------------|-----------------------------------------------|----------------------------------|----------------------------------|
| `get_public_key` | all key-deriving algorithms | — | `algorithm`, `index` |
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>` | `algorithm`, `index`, `scheme`* |
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>`, `<signature_hex>` | `algorithm`, `index`, `scheme`* |
| `encapsulate` | ml-kem-768 | `<peer_pubkey_hex>` | `algorithm` |
| `decapsulate` | ml-kem-768 | `<ciphertext_hex>` | `algorithm`, `index` |
| `derive_shared_secret` | x25519 | `<peer_pubkey_hex>` | `algorithm`, `index` |
| `encrypt` | otp | `<plaintext_base64>` | `algorithm`, `encoding` |
| `decrypt` | otp | `<ciphertext>` | `algorithm`, `encoding` |
| `nostr_get_public_key` | secp256k1 (NIP-06) | — | `nostr_index`, `format` |
| `nostr_sign_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index` |
| `nostr_mine_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` |
| `nostr_nip04_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
| `nostr_nip04_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
| `nostr_nip44_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
| `nostr_nip44_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
\* `scheme` is secp256k1-only: `"schnorr"` (default, BIP-340) or `"ecdsa"`.
### 2.1 Enforcement matrix
| Verb | Valid algorithms |
|----------------------------|-----------------------------------------------|
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
| `encapsulate` / `decapsulate` | ml-kem-768 |
| `derive_shared_secret` | x25519 |
| `encrypt` / `decrypt` | otp |
| `get_public_key` | all key-deriving algorithms |
| `nostr_*` | secp256k1 (Nostr protocol) |
Any unlisted `(verb, algorithm)` pair is rejected with `algorithm_not_supported_for_verb` (1010).
---
## 3. Algorithms
All keys derive deterministically from the loaded BIP-39 mnemonic. The caller selects an algorithm by name and a derivation `index` (an integer `<n>` substituted into the algorithm's derivation path). OTP is the exception — it does not derive a key, it consumes a bound one-time pad (see [§3.3](#33-otp)).
### 3.1 Algorithm table
| Algorithm | Key type | FIPS standard | Derivation path | Key sizes (priv / pub, bytes) |
|-----------------|-----------------|---------------|---------------------------------------|-------------------------------|
| `secp256k1` | Signature | — | `m/44'/1237'/<n>'/0/0` (NIP-06) | 32 / 32 |
| `ed25519` | Signature | — | `m/44'/102001'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
| `x25519` | Key agreement | — | `m/44'/102002'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
| `ml-dsa-65` | PQ signature | FIPS 204 | `m/44'/102003'/<n>'/0/0'` → DRBG | 4032 / 1952 |
| `slh-dsa-128s` | PQ signature | FIPS 205 | `m/44'/102004'/<n>'/0/0'` → DRBG | 64 / 32 |
| `ml-kem-768` | PQ KEM | FIPS 203 | `m/44'/102005'/<n>'/0/0'` → DRBG | 2400 / 1184 |
| `otp` | One-time pad | — | (no key — bound USB pad) | n/a |
### 3.2 Key derivation
- **secp256k1** uses standard BIP-32/NIP-06 derivation. The 32-byte path output is the private key scalar.
- **ed25519 / x25519** use SLIP-0010 HMAC-SHA512 derivation (all-hardened paths, as required by SLIP-0010 for ed25519). The 32-byte output is the private key.
- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. Same mnemonic, same index, same key pair every time.
- **otp** does not derive a key. A pad is bound at signer startup (`--otp-pad-dir` + `--otp-pad`); the pad offset advances monotonically across requests.
### 3.3 OTP
The `otp` algorithm is a stream-style one-time pad, not a key-derivation scheme. It is selected like any other algorithm via `{"algorithm":"otp"}` and works with the `encrypt` / `decrypt` verbs. One pad per session; the pad offset advances monotonically across requests and is reported in every response.
---
## 4. Examples
### 4.1 `get_public_key`
```json
{ "id": "1", "method": "get_public_key", "params": [ { "algorithm": "ml-dsa-65", "index": 0 } ] }
```
Response:
```json
{ "id": "1", "result": "{\"algorithm\":\"ml-dsa-65\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}" }
```
`key_id` is the first 16 hex characters of the public key — a short display identifier.
### 4.2 `sign`
```json
{ "id": "2", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "ed25519", "index": 0 } ] }
```
Response:
```json
{ "id": "2", "result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\",\"key_id\":\"<16 hex>\"}" }
```
The first positional argument is the message as hex. For `secp256k1` the `scheme` option selects `"schnorr"` (default, BIP-340) or `"ecdsa"`:
```json
{ "id": "3", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "secp256k1", "index": 0, "scheme": "ecdsa" } ] }
```
### 4.3 `verify`
```json
{ "id": "4", "method": "verify", "params": [ "<message_hex>", "<signature_hex>", { "algorithm": "ed25519", "index": 0 } ] }
```
Response:
```json
{ "id": "4", "result": "{\"valid\":true,\"algorithm\":\"ed25519\"}" }
```
The signer derives its own public key from `(algorithm, index)` and verifies against it. To verify an arbitrary third-party key, use a client-side library.
### 4.4 `encapsulate` (ML-KEM-768)
```json
{ "id": "5", "method": "encapsulate", "params": [ "<peer_pubkey_hex>", { "algorithm": "ml-kem-768" } ] }
```
Response:
```json
{ "id": "5", "result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
```
`peer_pubkey_hex` is the recipient's ML-KEM-768 public key (1184 bytes → 2368 hex chars). Send the returned `ciphertext` to the recipient; both sides end up with the same `shared_secret`.
### 4.5 `decapsulate` (ML-KEM-768)
```json
{ "id": "6", "method": "decapsulate", "params": [ "<ciphertext_hex>", { "algorithm": "ml-kem-768", "index": 0 } ] }
```
Response:
```json
{ "id": "6", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
```
### 4.6 `derive_shared_secret` (X25519)
```json
{ "id": "7", "method": "derive_shared_secret", "params": [ "<peer_pubkey_hex>", { "algorithm": "x25519", "index": 0 } ] }
```
Response:
```json
{ "id": "7", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"x25519\"}" }
```
`peer_pubkey_hex` is the peer's 32-byte X25519 public key (64 hex chars). Feed the returned `shared_secret` into your own symmetric cipher (e.g. AES-GCM, ChaCha20-Poly1305).
### 4.7 `encrypt` / `decrypt` (OTP)
```json
{ "id": "8", "method": "encrypt", "params": [ "<plaintext_base64>", { "algorithm": "otp", "encoding": "ascii" } ] }
{ "id": "9", "method": "decrypt", "params": [ "<ciphertext>", { "algorithm": "otp", "encoding": "ascii" } ] }
```
`encoding` is `"ascii"` (ASCII-armored, default) or `"binary"` (base64-encoded raw `.otp` blob). If omitted on `decrypt`, auto-detection by magic bytes is used.
`encrypt` response:
```json
{
"id": "8",
"result": "{\"ciphertext\":\"<ascii-armor-or-base64-blob>\",\"encoding\":\"ascii\",\"pad_chksum\":\"<64 hex>\",\"pad_offset_before\":288,\"pad_offset_after\":416}"
}
```
`decrypt` response:
```json
{ "id": "9", "result": "{\"plaintext\":\"<base64>\",\"pad_chksum\":\"<64 hex>\"}" }
```
If no pad is bound at startup, the error is `-32601` `otp_pad_not_bound`.
### 4.8 `nostr_get_public_key`
```json
{ "id": "10", "method": "nostr_get_public_key", "params": [ { "nostr_index": 0 } ] }
```
Response (default): a plain 64-hex-char secp256k1 public key string.
Response with `{"format":"structured"}` in options: `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`.
### 4.9 `nostr_sign_event`
Serializes the event to canonical form (`[0, pubkey, created_at, kind, tags, content]`), SHA-256 hashes it to produce the event `id`, signs the hash with BIP-340 Schnorr, and returns the complete signed event.
```json
{ "id": "11", "method": "nostr_sign_event", "params": [ "<event_json>", { "nostr_index": 0 } ] }
```
`<event_json>` is the unsigned event object:
```json
{ "pubkey": "...", "created_at": 1234567890, "kind": 1, "tags": [], "content": "hello" }
```
Response: the signed event JSON string, with `id` and `sig` populated.
### 4.10 `nostr_mine_event`
Mines NIP-13 proof-of-work (adds a `nonce` tag) and signs the event in one step. Mining runs in a detached thread so the signer stays responsive.
```json
{
"id": "12",
"method": "nostr_mine_event",
"params": [ "<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 } ]
}
```
| Option | Required | Default | Meaning |
|---------------|-------------------------------|---------|---------------------------------------------------------------|
| `difficulty` | one of `difficulty`/`timeout` | 0 | Target leading zero bits. Stops early if reached. |
| `timeout_sec` | one of `difficulty`/`timeout` | 600 | Time budget in seconds. Always returns the best event found. |
| `threads` | no | 1 | Mining threads (clamped to 1..32). |
At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when **either** condition is met. Timeout is never an error — the best event found is always returned.
Response:
```json
{
"id": "12",
"result": "{\"event\":\"<signed event JSON with nonce tag>\",\"achieved_difficulty\":18,\"target_difficulty\":20,\"target_reached\":false,\"elapsed_sec\":30,\"attempts\":4523456}"
}
```
Errors:
- `1007` `no_termination_condition` — neither `difficulty` nor `timeout_sec` given.
- `1008` `mining_failed` — internal mining error.
### 4.11 `nostr_nip04_encrypt` / `nostr_nip04_decrypt`
NIP-04 encryption (deprecated in Nostr but still widely used): ECDH + AES-256-CBC, base64 payload.
```json
{ "id": "13", "method": "nostr_nip04_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
{ "id": "14", "method": "nostr_nip04_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
```
`encrypt` returns the NIP-04 ciphertext string; `decrypt` returns the plaintext string.
### 4.12 `nostr_nip44_encrypt` / `nostr_nip44_decrypt`
NIP-44 encryption (current Nostr standard): ECDH + HKDF + ChaCha20-Poly1305 + specific payload format.
```json
{ "id": "15", "method": "nostr_nip44_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
{ "id": "16", "method": "nostr_nip44_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
```
`encrypt` returns the NIP-44 ciphertext string; `decrypt` returns the plaintext string.
---
## 5. Transports
The API is transport-independent. The same JSON request works over every transport; only the framing differs.
| Transport | `--listen` flag | Framing | Caller identity |
|-----------|--------------------------------|------------------------------------------|----------------------------|
| Unix socket (abstract) | `unix` (default on desktop) | Length-prefixed framed JSON | `SO_PEERCRED``uid:<n>` |
| stdio | `stdio` | One framed request/response over stdin/stdout | inherited uid |
| qrexec | `qrexec` | Same as stdio; caller from `QREXEC_REMOTE_DOMAIN` | `qubes:<vm>` |
| FIPS/TCP | `tcp:[host]:port` | Length-prefixed framed JSON | (transport-defined) |
| HTTP | `http:host:port` | Standard HTTP POST, JSON body, no custom framing. CORS enabled. | (transport-defined) |
### 5.1 HTTP examples
Start the signer:
```bash
nsigner --listen http:127.0.0.1:11111 --allow-all
```
Get a public key:
```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
```
Sign a Nostr event:
```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
```
OTP encrypt:
```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"algorithm":"otp","encoding":"ascii"}]}'
```
### 5.2 Unix socket examples (framed mode)
```bash
# get_public_key
nsigner --socket-name nsigner client \
'{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
# Sign a Nostr event
nsigner --socket-name nsigner client \
'{"id":"2","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
# ed25519 sign
nsigner --socket-name nsigner client \
'{"id":"3","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}'
```
---
## 6. Pre-approval
Pre-approval entries skip the interactive prompt for matching requests. They are configured at startup with `--preapprove`.
### 6.1 Algorithm-based
```bash
nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign,verify
nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate
```
### 6.2 Nostr
```bash
nsigner --preapprove caller=uid:1000,nostr_index=0,verb=nostr_sign_event,nostr_get_public_key
```
A `*` wildcard matches any caller, role, or verb. Index ranges use `min-max` syntax. Unmatched requests fall through to the default policy (prompt for same-uid, deny for others).
For the security model, transports, and operational behavior, see [`README.md`](README.md) §1§3 and §5§8. For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).

View File

@@ -22,8 +22,8 @@ for the full integration contract.
|---|---|
| `nsigner_client_t` (stack) | `nsigner_client_t*` (heap) or `nostr_signer_t*` |
| `nsigner_client_init` / `connect_unix` / `close` | `nsigner_transport_open_unix` + `nsigner_client_new` / `nsigner_client_free` |
| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "get_public_key", ...)` |
| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "sign_event", ...)` |
| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "nostr_get_public_key", ...)` |
| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "nostr_sign_event", ...)` |
| `nsigner_client_set_auth` | `nsigner_client_set_auth` or `nostr_signer_nsigner_set_auth` |
| `nsigner_client_request` / `request_raw` | `nsigner_client_call` (returns parsed cJSON result) |
@@ -42,51 +42,46 @@ n_signer supports six algorithms: `secp256k1` (Nostr), `ed25519` (SSH),
`x25519` (age/ECDH), `ml-dsa-65` (PQ signatures, FIPS 204), `slh-dsa-128s`
(PQ hash-based signatures, FIPS 205), and `ml-kem-768` (PQ KEM, FIPS 203).
Use `nsigner_client_call(client, "<verb>", params, &result)` with these verbs:
| Verb | Algorithms | Description |
|---|---|---|
| `get_public_key` | all | Returns the role's public key (see format below) |
| `sign_event` | secp256k1 | Sign a Nostr event (existing) |
| `nip44_encrypt` / `nip44_decrypt` | secp256k1 | NIP-44 (existing) |
| `nip04_encrypt` / `nip04_decrypt` | secp256k1 | NIP-04 (existing) |
| `mine_event` | secp256k1 | NIP-13 PoW mining + sign (existing) |
| `sign_data` | ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes (hex) |
| `verify_signature` | ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature against the role's pubkey |
| `ssh_sign` | ed25519 | Sign an SSH authentication challenge |
| `kem_encapsulate` | ml-kem-768 | Encapsulate with a peer's ML-KEM public key |
| `kem_decapsulate` | ml-kem-768 | Decapsulate a ciphertext with the role's ML-KEM private key |
### Algorithm-based API (new)
In addition to the role-based verbs above, the signer supports algorithm-based verbs where the caller specifies `algorithm` and `index` directly, without needing a role:
The API has two verb families (see [`README.md`](../README.md#4-api) §4 for the full spec):
**Algorithm-based verbs** — the caller specifies `algorithm` and `index` in the
options object. No role table entry is needed.
| Verb | Algorithms | Description |
|---|---|---|
| `get_public_key` | all key-deriving algorithms | Returns the derived public key (structured) |
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Sign arbitrary bytes (hex) |
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | Verify a signature |
| `encapsulate` | ml-kem-768 | KEM encapsulation with peer's public key |
| `decapsulate` | ml-kem-768 | KEM decapsulation with derived private key |
| `derive_shared_secret` | x25519 | ECDH key agreement |
| `get_public_key` (with `algorithm`) | all | Get public key for a derived key |
| `encrypt` / `decrypt` | otp | One-time pad encrypt/decrypt (`algorithm:"otp"`) |
**Nostr protocol verbs** — select a secp256k1 NIP-06 key via `nostr_index` (or
`role`/`role_path`). These are role-based.
| Verb | Description |
|---|---|
| `nostr_get_public_key` | Returns the role's secp256k1 public key |
| `nostr_sign_event` | Sign a Nostr event |
| `nostr_mine_event` | NIP-13 PoW mining + sign |
| `nostr_nip44_encrypt` / `nostr_nip44_decrypt` | NIP-44 encrypt/decrypt |
| `nostr_nip04_encrypt` / `nostr_nip04_decrypt` | NIP-04 encrypt/decrypt |
Example: `nsigner_client_call(client, "sign", "[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]", &result)`
For secp256k1, the optional `scheme` parameter selects `"schnorr"` (default) or `"ecdsa"`.
Old verbs (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`) also accept the `algorithm` parameter and map to the new verbs. Without `algorithm`, they use the role-based path (backward compatible).
### `get_public_key` response format
- **secp256k1 (backward compatible):** `result` is a plain hex string
(`cJSON_IsString(result)` is true, 64 hex chars).
- **secp256k1 with `{"format":"structured"}` option:** `result` is a JSON
string containing `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`.
- **All other algorithms:** `result` is always a JSON string containing
`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}`.
The algorithm-based `get_public_key` always returns a structured JSON string:
`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}`.
The role-based `nostr_get_public_key` returns a plain 64-hex-char secp256k1
public key by default, or the structured form with `{"format":"structured"}`.
Clients should parse the `result` string with `cJSON_Parse` to extract the
`algorithm`, `public_key`, and `key_id` fields for non-secp256k1 algorithms.
`algorithm`, `public_key`, and `key_id` fields when the result is a JSON object.
### Key sizes

View File

@@ -3,12 +3,12 @@
* via Qubes qrexec and performing all three core operations:
*
* 1. get_public_key — retrieve a Nostr public key by nostr_index
* 2. sign_event — sign a Nostr event (kind 1 text note)
* 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back)
* 2. nostr_sign_event — sign a Nostr event (kind 1 text note)
* 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
*
* Note: mine_event (NIP-13 PoW) is also available via the JSON-RPC interface.
* See demo_javascript.js and demo_python.py for mine_event usage examples.
* The high-level nostr_signer API does not yet wrap mine_event.
* Note: nostr_mine_event (NIP-13 PoW) is also available via the JSON-RPC interface.
* See demo_javascript.js and demo_python.py for nostr_mine_event usage examples.
* The high-level nostr_signer API does not yet wrap nostr_mine_event.
*
* This uses the high-level nostr_signer API from nostr_core_lib:
* - nostr_signer_nsigner_qrexec() — qrexec transport (no network)
@@ -132,7 +132,7 @@ static int demo_sign_event(nostr_signer_t *signer, const char *pubkey_hex) {
char *signed_json = NULL;
int rc;
printf("\n=== Demo 2: sign_event (kind 1 text note) ===\n");
printf("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===\n");
/* Build an unsigned Nostr event (kind 1 text note) */
unsigned_event = cJSON_CreateObject();
@@ -163,7 +163,7 @@ static int demo_sign_event(nostr_signer_t *signer, const char *pubkey_hex) {
/* Sign it */
rc = nostr_signer_sign_event(signer, unsigned_event, &signed_event);
if (rc != NOSTR_SUCCESS) {
print_error("sign_event", rc);
print_error("nostr_nostr_sign_event", rc);
cJSON_Delete(unsigned_event);
return rc;
}
@@ -204,14 +204,14 @@ static int demo_nip44(nostr_signer_t *signer, const char *pubkey_hex) {
char *decrypted = NULL;
int rc;
printf("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===\n");
printf("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===\n");
printf(" plaintext: \"%s\"\n", plaintext);
printf(" peer pubkey: %s (self)\n", pubkey_hex);
/* Encrypt */
rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &ciphertext);
if (rc != NOSTR_SUCCESS) {
print_error("nip44_encrypt", rc);
print_error("nostr_nostr_nip44_encrypt", rc);
return rc;
}
@@ -220,7 +220,7 @@ static int demo_nip44(nostr_signer_t *signer, const char *pubkey_hex) {
/* Decrypt (using our own pubkey as the sender) */
rc = nostr_signer_nip44_decrypt(signer, pubkey_hex, ciphertext, &decrypted);
if (rc != NOSTR_SUCCESS) {
print_error("nip44_decrypt", rc);
print_error("nostr_nostr_nip44_decrypt", rc);
free(ciphertext);
return rc;
}

View File

@@ -4,8 +4,8 @@
* running n_signer via Qubes qrexec and performing all three core operations:
*
* 1. get_public_key — retrieve a Nostr public key by nostr_index
* 2. sign_event — sign a Nostr event (kind 1 text note)
* 3. nip44_encrypt — encrypt a message to a peer (and decrypt it back)
* 2. nostr_sign_event — sign a Nostr event (kind 1 text note)
* 3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
*
* Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed —
* identity comes from QREXEC_REMOTE_DOMAIN on the server side.
@@ -120,7 +120,7 @@ async function demoGetPublicKey(targetQube, nostrIndex) {
* Demo 2: Sign a Nostr event (kind 1 text note).
*/
async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) {
console.log("\n=== Demo 2: sign_event (kind 1 text note) ===");
console.log("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===");
const unsignedEvent = {
kind: 1,
@@ -135,12 +135,12 @@ async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) {
const response = await callNsigner(targetQube, {
id: "2",
method: "sign_event",
method: "nostr_nostr_sign_event",
params: [JSON.stringify(unsignedEvent), { nostr_index: nostrIndex }],
});
if (response.error) {
throw new Error(`sign_event failed: ${JSON.stringify(response.error)}`);
throw new Error(`nostr_sign_event failed: ${JSON.stringify(response.error)}`);
}
const signedEvent = JSON.parse(response.result);
@@ -159,19 +159,19 @@ async function demoSignEvent(targetQube, nostrIndex, pubkeyHex) {
async function demoNip44(targetQube, nostrIndex, pubkeyHex) {
const plaintext = "Secret message from n_signer JavaScript demo!";
console.log("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===");
console.log("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===");
console.log(` plaintext: "${plaintext}"`);
console.log(` peer pubkey: ${pubkeyHex} (self)`);
// Encrypt
const encResponse = await callNsigner(targetQube, {
id: "3",
method: "nip44_encrypt",
method: "nostr_nostr_nip44_encrypt",
params: [pubkeyHex, plaintext, { nostr_index: nostrIndex }],
});
if (encResponse.error) {
throw new Error(`nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`);
throw new Error(`nostr_nip44_encrypt failed: ${JSON.stringify(encResponse.error)}`);
}
const ciphertext = encResponse.result;
@@ -180,12 +180,12 @@ async function demoNip44(targetQube, nostrIndex, pubkeyHex) {
// Decrypt
const decResponse = await callNsigner(targetQube, {
id: "4",
method: "nip44_decrypt",
method: "nostr_nostr_nip44_decrypt",
params: [pubkeyHex, ciphertext, { nostr_index: nostrIndex }],
});
if (decResponse.error) {
throw new Error(`nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`);
throw new Error(`nostr_nip44_decrypt failed: ${JSON.stringify(decResponse.error)}`);
}
const decrypted = decResponse.result;
@@ -199,7 +199,7 @@ async function demoNip44(targetQube, nostrIndex, pubkeyHex) {
}
async function demoMineEvent(targetQube, nostrIndex) {
console.log("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---");
console.log("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---");
const event = {
kind: 1,
@@ -210,7 +210,7 @@ async function demoMineEvent(targetQube, nostrIndex) {
console.log(" Mining with difficulty=4, threads=4, timeout_sec=30...");
const response = await callNsigner(targetQube, {
id: "5",
method: "mine_event",
method: "nostr_nostr_mine_event",
params: [JSON.stringify(event), {
difficulty: 4,
threads: 4,
@@ -220,7 +220,7 @@ async function demoMineEvent(targetQube, nostrIndex) {
});
if (response.error) {
throw new Error(`mine_event failed: ${JSON.stringify(response.error)}`);
throw new Error(`nostr_mine_event failed: ${JSON.stringify(response.error)}`);
}
const result = JSON.parse(response.result);

View File

@@ -4,8 +4,8 @@ demo_python.py — comprehensive Python demo for connecting to a running n_signe
via Qubes qrexec and performing all three core operations:
1. get_public_key — retrieve a Nostr public key by nostr_index
2. sign_event — sign a Nostr event (kind 1 text note)
3. nip44_encrypt — encrypt a message to a peer (and decrypt it back)
2. nostr_sign_event — sign a Nostr event (kind 1 text note)
3. nostr_nip44_encrypt — encrypt a message to a peer (and decrypt it back)
Uses qrexec-client-vm (Qubes OS inter-qube IPC). No auth envelope needed —
identity comes from QREXEC_REMOTE_DOMAIN on the server side.
@@ -148,7 +148,7 @@ def demo_get_public_key(target_qube, nostr_index):
def demo_sign_event(target_qube, nostr_index, pubkey_hex):
"""Demo 2: Sign a Nostr event (kind 1 text note)."""
print("\n=== Demo 2: sign_event (kind 1 text note) ===")
print("\n=== Demo 2: nostr_sign_event (kind 1 text note) ===")
unsigned_event = {
"kind": 1,
@@ -165,13 +165,13 @@ def demo_sign_event(target_qube, nostr_index, pubkey_hex):
target_qube,
{
"id": "2",
"method": "sign_event",
"method": "nostr_nostr_sign_event",
"params": [json.dumps(unsigned_event, separators=(",", ":")), {"nostr_index": nostr_index}],
},
)
if "error" in response:
raise RuntimeError(f"sign_event failed: {json.dumps(response['error'])}")
raise RuntimeError(f"nostr_sign_event failed: {json.dumps(response['error'])}")
signed_event = json.loads(response["result"])
print(" Signed event:")
@@ -185,7 +185,7 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex):
"""Demo 3: NIP-44 encrypt and decrypt."""
plaintext = "Secret message from n_signer Python demo!"
print("\n=== Demo 3: nip44_encrypt / nip44_decrypt ===")
print("\n=== Demo 3: nostr_nip44_encrypt / nostr_nip44_decrypt ===")
print(f' plaintext: "{plaintext}"')
print(f" peer pubkey: {pubkey_hex} (self)")
@@ -194,13 +194,13 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex):
target_qube,
{
"id": "3",
"method": "nip44_encrypt",
"method": "nostr_nostr_nip44_encrypt",
"params": [pubkey_hex, plaintext, {"nostr_index": nostr_index}],
},
)
if "error" in enc_response:
raise RuntimeError(f"nip44_encrypt failed: {json.dumps(enc_response['error'])}")
raise RuntimeError(f"nostr_nip44_encrypt failed: {json.dumps(enc_response['error'])}")
ciphertext = enc_response["result"]
print(f" ciphertext: {ciphertext}")
@@ -210,13 +210,13 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex):
target_qube,
{
"id": "4",
"method": "nip44_decrypt",
"method": "nostr_nostr_nip44_decrypt",
"params": [pubkey_hex, ciphertext, {"nostr_index": nostr_index}],
},
)
if "error" in dec_response:
raise RuntimeError(f"nip44_decrypt failed: {json.dumps(dec_response['error'])}")
raise RuntimeError(f"nostr_nip44_decrypt failed: {json.dumps(dec_response['error'])}")
decrypted = dec_response["result"]
print(f' decrypted: "{decrypted}"')
@@ -227,8 +227,8 @@ def demo_nip44(target_qube, nostr_index, pubkey_hex):
raise RuntimeError("Round-trip FAILED: plaintext does not match decrypted")
def demo_mine_event(target_qube, nostr_index):
print("\n--- Demo 4: mine_event (NIP-13 Proof-of-Work) ---")
def demo_nostr_mine_event(target_qube, nostr_index):
print("\n--- Demo 4: nostr_mine_event (NIP-13 Proof-of-Work) ---")
event = {"kind": 1, "content": "Hello Nostr with PoW!", "tags": []}
@@ -237,7 +237,7 @@ def demo_mine_event(target_qube, nostr_index):
target_qube,
{
"id": "5",
"method": "mine_event",
"method": "nostr_nostr_mine_event",
"params": [json.dumps(event), {
"difficulty": 4,
"threads": 4,
@@ -248,7 +248,7 @@ def demo_mine_event(target_qube, nostr_index):
)
if "error" in response:
raise RuntimeError(f"mine_event failed: {json.dumps(response['error'])}")
raise RuntimeError(f"nostr_mine_event failed: {json.dumps(response['error'])}")
result = json.loads(response["result"])
print(f" achieved_difficulty: {result['achieved_difficulty']}")
@@ -282,7 +282,7 @@ def main():
pubkey_hex = demo_get_public_key(target_qube, nostr_index)
demo_sign_event(target_qube, nostr_index, pubkey_hex)
demo_nip44(target_qube, nostr_index, pubkey_hex)
demo_mine_event(target_qube, nostr_index)
demo_nostr_mine_event(target_qube, nostr_index)
print("\n=== Summary ===")
print("All demos completed successfully.")

View File

@@ -140,11 +140,11 @@ def main() -> int:
req = {
"jsonrpc": "2.0",
"id": "2",
"method": "sign_event",
"method": "nostr_sign_event",
"params": params,
}
if not no_auth:
req["auth"] = build_auth_envelope("sign_event", params, caller_priv)
req["auth"] = build_auth_envelope("nostr_sign_event", params, caller_priv)
body = json.dumps(req, separators=(",", ":")).encode("utf-8")
frame = struct.pack(">I", len(body)) + body

View File

@@ -460,7 +460,7 @@
};
const params = [unsignedEvent, getIndexOptions()];
const resp = await rpcCall("sign_event", params, "web-sign-kind1");
const resp = await rpcCall("nostr_sign_event", params, "web-sign-kind1");
signOutEl.textContent = pretty(resp?.result ?? resp);
} catch (e) {
signOutEl.textContent = String(e);
@@ -472,7 +472,7 @@
const peer = requirePeerHex(nip04PeerEl.value);
const msg = String(nip04MsgEl.value || "");
const params = [peer, msg, getIndexOptions()];
const resp = await rpcCall("nip04_encrypt", params, "web-nip04-enc");
const resp = await rpcCall("nostr_nip04_encrypt", params, "web-nip04-enc");
nip04OutEl.textContent = pretty(resp?.result ?? resp);
if (resp && typeof resp.result === "string") {
nip04DecPeerEl.value = peer;
@@ -488,7 +488,7 @@
const peer = requirePeerHex(nip04DecPeerEl.value);
const ciphertext = String(nip04CipherEl.value || "");
const params = [peer, ciphertext, getIndexOptions()];
const resp = await rpcCall("nip04_decrypt", params, "web-nip04-dec");
const resp = await rpcCall("nostr_nip04_decrypt", params, "web-nip04-dec");
nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt");
} catch (e) {
nip04DecOutEl.textContent = String(e);
@@ -500,7 +500,7 @@
const peer = requirePeerHex(nip44PeerEl.value);
const msg = String(nip44MsgEl.value || "");
const params = [peer, msg, getIndexOptions()];
const resp = await rpcCall("nip44_encrypt", params, "web-nip44-enc");
const resp = await rpcCall("nostr_nip44_encrypt", params, "web-nip44-enc");
nip44OutEl.textContent = pretty(resp?.result ?? resp);
if (resp && typeof resp.result === "string") {
nip44DecPeerEl.value = peer;
@@ -516,7 +516,7 @@
const peer = requirePeerHex(nip44DecPeerEl.value);
const ciphertext = String(nip44CipherEl.value || "");
const params = [peer, ciphertext, getIndexOptions()];
const resp = await rpcCall("nip44_decrypt", params, "web-nip44-dec");
const resp = await rpcCall("nostr_nip44_decrypt", params, "web-nip44-dec");
nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt");
} catch (e) {
nip44DecOutEl.textContent = String(e);

View File

@@ -95,7 +95,7 @@ static int query_pubkey(const char *host, int port, int nostr_index,
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) {
fprintf(stderr, "request failed for index %d: %s\n", nostr_index,
nsigner_client_last_error(client));
params = NULL; /* nsigner_client_call took ownership even on failure */

View File

@@ -59,7 +59,7 @@ int main(int argc, char **argv) {
goto cleanup;
}
if (nsigner_client_call(client, "get_public_key", params, &result) != NOSTR_SUCCESS) {
if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) {
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
goto cleanup;
}

View File

@@ -167,7 +167,7 @@ def cmd_get_public_key(args):
def cmd_sign_event(args):
event = json.loads(args.event)
print(json.dumps(rpc(args, "sign_event", {"event": event}), indent=2))
print(json.dumps(rpc(args, "nostr_sign_event", {"event": event}), indent=2))
def build_parser():

View File

@@ -141,8 +141,8 @@ def main():
pt_b64 = base64.b64encode(plaintext.encode()).decode()
resp = run_one_request({
"id": "2",
"method": "otp_encrypt",
"params": [pt_b64, {"encoding": "ascii"}],
"method": "encrypt",
"params": [pt_b64, {"algorithm": "otp", "encoding": "ascii"}],
})
if resp is None or "result" not in resp:
print("ERROR: otp_encrypt failed")
@@ -191,7 +191,7 @@ def main():
}
resp = run_one_request({
"id": "3",
"method": "sign_event",
"method": "nostr_sign_event",
"params": [json.dumps(event_for_signing), {"role": "main"}],
})
if resp is None or "result" not in resp:

View File

@@ -49,7 +49,8 @@ static int get_structured_pubkey(nsigner_client_t *client, const char *role,
cJSON_Delete(params);
return -1;
}
cJSON_AddStringToObject(opts, "role", role);
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
cJSON_AddNumberToObject(opts, "index", 0);
cJSON_AddItemToArray(params, opts);
opts = NULL;
@@ -99,11 +100,12 @@ static int kem_encapsulate(nsigner_client_t *client, const char *role,
cJSON_Delete(params);
return -1;
}
cJSON_AddStringToObject(opts, "role", role);
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
cJSON_AddNumberToObject(opts, "index", 0);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "kem_encapsulate", params, &result) != NOSTR_SUCCESS) {
if (nsigner_client_call(client, "encapsulate", params, &result) != NOSTR_SUCCESS) {
cJSON_Delete(params);
return -1;
}
@@ -147,11 +149,12 @@ static char *kem_decapsulate(nsigner_client_t *client, const char *role,
cJSON_Delete(params);
return NULL;
}
cJSON_AddStringToObject(opts, "role", role);
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
cJSON_AddNumberToObject(opts, "index", 0);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "kem_decapsulate", params, &result) != NOSTR_SUCCESS) {
if (nsigner_client_call(client, "decapsulate", params, &result) != NOSTR_SUCCESS) {
cJSON_Delete(params);
return NULL;
}

View File

@@ -46,7 +46,8 @@ static int get_structured_pubkey(nsigner_client_t *client, const char *role,
cJSON_Delete(params);
return -1;
}
cJSON_AddStringToObject(opts, "role", role);
cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65");
cJSON_AddNumberToObject(opts, "index", 0);
cJSON_AddItemToArray(params, opts);
opts = NULL;
@@ -89,11 +90,12 @@ static char *sign_data(nsigner_client_t *client, const char *role,
cJSON_Delete(params);
return NULL;
}
cJSON_AddStringToObject(opts, "role", role);
cJSON_AddStringToObject(opts, "algorithm", "ml-dsa-65");
cJSON_AddNumberToObject(opts, "index", 0);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "sign_data", params, &result) != NOSTR_SUCCESS) {
if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) {
cJSON_Delete(params);
return NULL;
}

View File

@@ -73,7 +73,7 @@ int main(int argc, char **argv) {
cJSON_AddItemToArray(params, opts);
opts = NULL; /* owned by params now */
if (nsigner_client_call(client, "sign_event", params, &result) != NOSTR_SUCCESS) {
if (nsigner_client_call(client, "nostr_sign_event", params, &result) != NOSTR_SUCCESS) {
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(client));
goto cleanup;
}

View File

@@ -47,7 +47,8 @@ static int get_structured_pubkey(nsigner_client_t *client, const char *role,
cJSON_Delete(params);
return -1;
}
cJSON_AddStringToObject(opts, "role", role);
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
cJSON_AddNumberToObject(opts, "index", 0);
cJSON_AddItemToArray(params, opts);
opts = NULL;
@@ -97,11 +98,12 @@ static char *ssh_sign(nsigner_client_t *client, const char *role,
cJSON_Delete(params);
return NULL;
}
cJSON_AddStringToObject(opts, "role", role);
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
cJSON_AddNumberToObject(opts, "index", 0);
cJSON_AddItemToArray(params, opts);
opts = NULL;
if (nsigner_client_call(client, "ssh_sign", params, &result) != NOSTR_SUCCESS) {
if (nsigner_client_call(client, "sign", params, &result) != NOSTR_SUCCESS) {
cJSON_Delete(params);
return NULL;
}

View File

@@ -1,4 +1,6 @@
# Plan: Legacy Verb Aliases (Migration Path)
# Plan: Legacy Verb Aliases (Migration Path) — COMPLETED
> **Status: Done.** All steps below have been executed. The legacy verb names are gone from the wire protocol, the implementation, the tests, the clients, and the documentation. `make dev` builds clean and `make test` passes (all 10 test binaries, 230+ assertions).
## Context

View File

@@ -204,38 +204,25 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
/* Algorithm-based verb defines (algorithm is a parameter, not implicit).
* Callers must supply `algorithm` explicitly — no implicit defaults. */
#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_GET_PUBLIC_KEY "get_public_key"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* Nostr protocol verbs (secp256k1 NIP-06, role-based selector). */
#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"
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#define VERB_OTP_ENCRYPT "otp_encrypt"
#define VERB_OTP_DECRYPT "otp_decrypt"
/* OTP verbs (one-time pad; selected via algorithm:"otp"). */
#define VERB_ENCRYPT "encrypt"
#define VERB_DECRYPT "decrypt"
@@ -244,7 +231,7 @@ const char *selector_strerror(int err);
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -825,7 +812,8 @@ void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_
* to the role-based path).
*/
/* Check whether a method name is an algorithm-based verb (new or alias). */
/* Check whether a method name is an algorithm-based verb.
* These verbs take an explicit `algorithm` option and bypass the role table. */
static int is_algorithm_verb(const char *method) {
if (method == NULL) return 0;
return (strcmp(method, VERB_SIGN) == 0 ||
@@ -833,24 +821,7 @@ static int is_algorithm_verb(const char *method) {
strcmp(method, VERB_ENCAPSULATE) == 0 ||
strcmp(method, VERB_DECAPSULATE) == 0 ||
strcmp(method, VERB_DERIVE_SHARED) == 0 ||
/* aliases */
strcmp(method, VERB_SIGN_DATA) == 0 ||
strcmp(method, VERB_SSH_SIGN) == 0 ||
strcmp(method, VERB_VERIFY_SIG) == 0 ||
strcmp(method, VERB_KEM_ENCAPS) == 0 ||
strcmp(method, VERB_KEM_DECAPS) == 0);
}
/* Map an alias verb to its canonical algorithm-based verb name.
* Returns the canonical verb, or the original if not an alias. */
static const char *canonical_alg_verb(const char *method) {
if (method == NULL) return method;
if (strcmp(method, VERB_SIGN_DATA) == 0) return VERB_SIGN;
if (strcmp(method, VERB_SSH_SIGN) == 0) return VERB_SIGN;
if (strcmp(method, VERB_VERIFY_SIG) == 0) return VERB_VERIFY;
if (strcmp(method, VERB_KEM_ENCAPS) == 0) return VERB_ENCAPSULATE;
if (strcmp(method, VERB_KEM_DECAPS) == 0) return VERB_DECAPSULATE;
return method;
strcmp(method, VERB_GET_PUBLIC_KEY) == 0);
}
/* Parse the algorithm string from the options object.
@@ -909,7 +880,6 @@ static char *build_alg_result_json(const char *alg_name, const char *key_id,
static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
const char *method, cJSON *params_item,
cJSON *options_item) {
const char *canonical;
crypto_alg_t alg;
int index;
int enforce_rc;
@@ -917,25 +887,14 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
const alg_key_entry_t *key_entry;
const crypto_alg_sizes_t *sz;
if (!is_algorithm_verb(method) && strcmp(method, VERB_GET_PUBLIC_KEY) != 0) {
if (!is_algorithm_verb(method)) {
return NULL; /* not an algorithm-based verb */
}
canonical = canonical_alg_verb(method);
/* Parse algorithm and index from options. */
/* Parse algorithm and index from options. Callers must supply
* `algorithm` explicitly — no implicit defaults. */
alg = parse_algorithm_option(options_item);
/* For aliases that imply a specific algorithm, default if not given. */
if (alg == CRYPTO_ALG_UNKNOWN) {
if (strcmp(method, VERB_SSH_SIGN) == 0) {
alg = CRYPTO_ALG_ED25519;
} else if (strcmp(method, VERB_KEM_ENCAPS) == 0 ||
strcmp(method, VERB_KEM_DECAPS) == 0) {
alg = CRYPTO_ALG_ML_KEM_768;
}
}
if (alg == CRYPTO_ALG_UNKNOWN) {
return make_error_response(id_str, -32602, "missing_or_invalid_algorithm");
}
@@ -943,7 +902,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
index = parse_index_option(options_item);
/* Enforcement: check verb+algorithm validity. */
enforce_rc = enforce_verb_algorithm(canonical, alg);
enforce_rc = enforce_verb_algorithm(method, alg);
if (enforce_rc != ENFORCE_OK) {
if (enforce_rc == ENFORCE_ERR_ALGORITHM) {
return make_error_response(id_str, 1010, "algorithm_not_supported_for_verb");
@@ -968,7 +927,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
}
/* ---- get_public_key (algorithm-based path) ---- */
if (strcmp(canonical, VERB_GET_PUBLIC_KEY) == 0) {
if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0) {
if (alg_key_cache_derive(ctx->alg_key_cache, ctx->mnemonic, alg, index) != 0) {
return make_error_response(id_str, -32602, "key_derivation_failed");
}
@@ -987,7 +946,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
}
/* ---- sign ---- */
if (strcmp(canonical, VERB_SIGN) == 0) {
if (strcmp(method, VERB_SIGN) == 0) {
cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0);
const char *msg_hex;
unsigned char *msg_bytes = NULL;
@@ -1091,7 +1050,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
}
/* ---- verify ---- */
if (strcmp(canonical, VERB_VERIFY) == 0) {
if (strcmp(method, VERB_VERIFY) == 0) {
cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0);
cJSON *sig_hex_item = cJSON_GetArrayItem(params_item, 1);
const char *msg_hex;
@@ -1195,7 +1154,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
}
/* ---- encapsulate (ML-KEM-768) ---- */
if (strcmp(canonical, VERB_ENCAPSULATE) == 0) {
if (strcmp(method, VERB_ENCAPSULATE) == 0) {
cJSON *pub_hex_item = cJSON_GetArrayItem(params_item, 0);
const char *pub_hex;
unsigned char *pub = NULL;
@@ -1256,7 +1215,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
}
/* ---- decapsulate (ML-KEM-768) ---- */
if (strcmp(canonical, VERB_DECAPSULATE) == 0) {
if (strcmp(method, VERB_DECAPSULATE) == 0) {
cJSON *ct_hex_item = cJSON_GetArrayItem(params_item, 0);
const char *ct_hex;
unsigned char *ct = NULL;
@@ -1322,7 +1281,7 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
}
/* ---- derive_shared_secret (x25519) ---- */
if (strcmp(canonical, VERB_DERIVE_SHARED) == 0) {
if (strcmp(method, VERB_DERIVE_SHARED) == 0) {
cJSON *peer_hex_item = cJSON_GetArrayItem(params_item, 0);
const char *peer_hex;
unsigned char *peer_pub = NULL;
@@ -1396,21 +1355,21 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
/* ---- OTP verb handler ----
*
* Handles otp_encrypt and otp_decrypt. These verbs operate on the bound OTP
* pad (one pad per session) and do not use the role table or mnemonic.
* Handles encrypt/decrypt with algorithm:"otp". These verbs operate on the
* bound OTP pad (one pad per session) and do not use the role table or mnemonic.
*
* Request shapes (see plans/otp_nostr_integration.md):
* otp_encrypt: params = [ "<plaintext-base64>", { "encoding": "ascii|binary" } ]
* otp_decrypt: params = [ "<ciphertext>", { "encoding": "ascii|binary" } ]
* Request shapes (see api.md §4.7):
* encrypt: params = [ "<plaintext-base64>", { "algorithm":"otp", "encoding": "ascii|binary" } ]
* decrypt: params = [ "<ciphertext>", { "algorithm":"otp", "encoding": "ascii|binary" } ]
*
* The ciphertext for otp_decrypt is either an ASCII-armored string (encoding
* The ciphertext for decrypt is either an ASCII-armored string (encoding
* "ascii") or a base64-encoded binary .otp blob (encoding "binary"). If
* encoding is omitted, auto-detection by magic bytes is used.
*
* Results are returned as a JSON object string:
* otp_encrypt: {"ciphertext":"<ascii-armor-or-base64-blob>","encoding":"...",
* "pad_chksum":"<64hex>","pad_offset_before":N,"pad_offset_after":M}
* otp_decrypt: {"plaintext":"<base64>","encoding":"..."}
* encrypt: {"ciphertext":"<ascii-armor-or-base64-blob>","encoding":"...",
* "pad_chksum":"<64hex>","pad_offset_before":N,"pad_offset_after":M}
* decrypt: {"plaintext":"<base64>","encoding":"..."}
*/
static char *handle_otp_verb(const char *id_str, const char *method,
cJSON *params_item, cJSON *options_item) {
@@ -1433,7 +1392,7 @@ static char *handle_otp_verb(const char *id_str, const char *method,
return make_error_response(id_str, -32601, "otp_pad_not_bound");
}
if (strcmp(method, VERB_OTP_ENCRYPT) == 0) {
if (strcmp(method, VERB_ENCRYPT) == 0) {
/* Decode base64 plaintext. */
int pt_len = 0;
unsigned char *pt = otppad_base64_decode(payload_item->valuestring, &pt_len);
@@ -1499,7 +1458,7 @@ static char *handle_otp_verb(const char *id_str, const char *method,
return make_error_response(id_str, -32602, "invalid_params");
}
return make_success_response(id_str, out);
} else if (strcmp(method, VERB_OTP_DECRYPT) == 0) {
} else if (strcmp(method, VERB_DECRYPT) == 0) {
unsigned char *pt = NULL;
size_t pt_len = 0;
int rc;
@@ -1573,7 +1532,6 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
char *owned_result = NULL;
ptrdiff_t role_index_diff;
int role_index;
crypto_alg_t alg;
if (ctx == NULL || ctx->role_table == NULL || ctx->mnemonic == NULL || ctx->key_store == NULL || json_request == NULL) {
return make_error_response("null", -32600, "invalid_request");
@@ -1614,88 +1572,39 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
/* ---- Algorithm-based verb routing ----
*
* The new algorithm-based verbs (sign, verify, encapsulate, decapsulate,
* derive_shared_secret) and their aliases (sign_data, ssh_sign,
* verify_signature, kem_encapsulate, kem_decapsulate) are handled here,
* BEFORE the role-based selector resolution. They use the algorithm_key_cache
* for on-demand key derivation and do not need a role table entry.
*
* get_public_key is special: if the options contain an "algorithm" field,
* it takes the algorithm-based path; otherwise it falls through to the
* role-based path (backward compatibility). */
/* New algorithm-based verbs (sign, verify, encapsulate, decapsulate,
* derive_shared_secret) always take the algorithm-based path.
* Old verb aliases (sign_data, ssh_sign, verify_signature, kem_encapsulate,
* kem_decapsulate) only take the algorithm-based path if the "algorithm"
* parameter is present; otherwise they fall through to the role-based
* path for backward compatibility. */
* The algorithm-based verbs (sign, verify, encapsulate, decapsulate,
* derive_shared_secret, get_public_key) are handled here, BEFORE the
* role-based selector resolution. They use the algorithm_key_cache for
* on-demand key derivation and do not need a role table entry. Callers
* must supply `algorithm` explicitly. */
if (is_algorithm_verb(method)) {
int is_new_verb = (strcmp(method, VERB_SIGN) == 0 ||
strcmp(method, VERB_VERIFY) == 0 ||
strcmp(method, VERB_ENCAPSULATE) == 0 ||
strcmp(method, VERB_DECAPSULATE) == 0 ||
strcmp(method, VERB_DERIVE_SHARED) == 0);
int has_algorithm_opt = 0;
if (cJSON_IsObject(options_item)) {
cJSON *alg_check = cJSON_GetObjectItemCaseSensitive(options_item, "algorithm");
if (cJSON_IsString(alg_check) && alg_check->valuestring != NULL) {
has_algorithm_opt = 1;
}
}
if (is_new_verb || has_algorithm_opt) {
char *alg_response = handle_algorithm_verb(ctx, id_str, method,
params_item, options_item);
cJSON_Delete(root);
return alg_response;
}
/* Old alias without algorithm= → fall through to role-based path */
}
if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0 && cJSON_IsObject(options_item)) {
cJSON *alg_check = cJSON_GetObjectItemCaseSensitive(options_item, "algorithm");
if (cJSON_IsString(alg_check) && alg_check->valuestring != NULL) {
/* Algorithm-based get_public_key path. */
char *alg_response = handle_algorithm_verb(ctx, id_str, VERB_GET_PUBLIC_KEY,
params_item, options_item);
cJSON_Delete(root);
return alg_response;
}
char *alg_response = handle_algorithm_verb(ctx, id_str, method,
params_item, options_item);
cJSON_Delete(root);
return alg_response;
}
/* ---- OTP verb routing ----
*
* otp_encrypt and otp_decrypt do not use the role table or the mnemonic;
* they operate on the bound OTP pad (one pad per session, bound at
* startup via --otp-pad-dir / --otp-pad). Route them before role
* encrypt/decrypt with algorithm:"otp" do not use the role table or the
* mnemonic; they operate on the bound OTP pad (one pad per session, bound
* at startup via --otp-pad-dir / --otp-pad). Route them before role
* resolution. */
if (strcmp(method, VERB_OTP_ENCRYPT) == 0 ||
strcmp(method, VERB_OTP_DECRYPT) == 0) {
char *otp_response = handle_otp_verb(id_str, method, params_item, options_item);
cJSON_Delete(root);
return otp_response;
}
/* General encrypt/decrypt verbs: route to OTP if curve is "otp",
* otherwise fall through to the role-based path (for secp256k1 NIP-04/44,
* x25519, ml-kem-768). */
if (strcmp(method, VERB_ENCRYPT) == 0 ||
strcmp(method, VERB_DECRYPT) == 0) {
if (cJSON_IsObject(options_item)) {
cJSON *curve_item = cJSON_GetObjectItemCaseSensitive(options_item, "curve");
if (cJSON_IsString(curve_item) && curve_item->valuestring != NULL &&
strcmp(curve_item->valuestring, "otp") == 0) {
/* Route to OTP handler — map encrypt→otp_encrypt, decrypt→otp_decrypt */
const char *otp_method = (strcmp(method, VERB_ENCRYPT) == 0)
? VERB_OTP_ENCRYPT : VERB_OTP_DECRYPT;
char *otp_response = handle_otp_verb(id_str, otp_method,
cJSON *alg_item = cJSON_GetObjectItemCaseSensitive(options_item, "algorithm");
if (cJSON_IsString(alg_item) && alg_item->valuestring != NULL &&
strcmp(alg_item->valuestring, "otp") == 0) {
char *otp_response = handle_otp_verb(id_str, method,
params_item, options_item);
cJSON_Delete(root);
return otp_response;
}
}
/* For other curves, fall through to role-based path below. */
/* Other algorithms: not valid for encrypt/decrypt in the new API. */
cJSON_Delete(root);
return make_error_response(id_str, 1010, "algorithm_not_supported_for_verb");
}
if (cJSON_IsObject(options_item)) {
@@ -1774,64 +1683,19 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
}
role_index = (int)role_index_diff;
if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0) {
/* get_public_key: returns the role's public key.
if (strcmp(method, VERB_NOSTR_GET_PUBLIC_KEY) == 0) {
/* nostr_get_public_key: returns the role's secp256k1 (NIP-06) public key.
*
* For secp256k1 (backward compatibility): the result is a plain hex
* string (the existing format) unless the client requests the
* structured format via the options field {"format":"structured"}.
*
* For all other algorithms (ed25519, x25519, ML-DSA-65,
* SLH-DSA-128s, ML-KEM-768): the result is a structured JSON object
* By default the result is a plain 64-hex-char secp256k1 public key
* string. If the client requests the structured format via the options
* field {"format":"structured"}, the result is a structured JSON object
* serialized as a string:
* {"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}
* {"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}
*
* The key_id is the first 16 hex characters of the public key — a
* short display identifier (similar to an SSH key fingerprint).
*
* The full (untruncated) public key is read from the key_store via
* crypto_get_pubkey_hex(), because role->pubkey_hex is only
* ROLE_PUBKEY_HEX_MAX (66) bytes — too small for PQ pubkeys.
* short display identifier.
*/
alg = crypto_alg_from_role(role->curve, role->purpose);
if (role->derived && alg != CRYPTO_ALG_SECP256K1 &&
alg != CRYPTO_ALG_UNKNOWN) {
/* Non-secp256k1: always structured. */
cJSON *pk_obj = cJSON_CreateObject();
const char *alg_name;
const char *pub_hex = crypto_get_pubkey_hex(ctx->key_store, role_index);
char key_id[17];
if (pk_obj == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
alg_name = crypto_alg_to_str(alg);
cJSON_AddStringToObject(pk_obj, "algorithm",
(alg_name != NULL) ? alg_name : "unknown");
cJSON_AddStringToObject(pk_obj, "public_key",
(pub_hex != NULL) ? pub_hex : "");
/* key_id = first 16 hex chars of pubkey (or empty if too short). */
if (pub_hex != NULL && strlen(pub_hex) >= 16) {
memcpy(key_id, pub_hex, 16);
key_id[16] = '\0';
} else {
key_id[0] = '\0';
}
cJSON_AddStringToObject(pk_obj, "key_id", key_id);
owned_result = cJSON_PrintUnformatted(pk_obj);
cJSON_Delete(pk_obj);
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (role->derived && alg == CRYPTO_ALG_SECP256K1) {
/* secp256k1: plain hex by default, structured if requested.
* role->pubkey_hex is large enough for secp256k1 (64 hex chars). */
if (role->derived) {
int want_structured = 0;
cJSON *fmt_item = NULL;
@@ -1871,14 +1735,14 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
}
result = owned_result;
} else {
/* Backward-compatible plain hex string. */
/* Plain hex string. */
result = role->pubkey_hex;
}
} else {
/* Not yet derived, or unknown algorithm. */
/* Not yet derived. */
result = "not_yet_derived";
}
} else if (strcmp(method, VERB_SIGN_EVENT) == 0) {
} else if (strcmp(method, VERB_NOSTR_SIGN_EVENT) == 0) {
cJSON *event_item = cJSON_GetArrayItem(params_item, 0);
if (!cJSON_IsString(event_item) || event_item->valuestring == NULL) {
cJSON_Delete(root);
@@ -1891,7 +1755,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_MINE_EVENT) == 0) {
} else if (strcmp(method, VERB_NOSTR_MINE_EVENT) == 0) {
cJSON *event_item = cJSON_GetArrayItem(params_item, 0);
cJSON *diff_item = NULL;
cJSON *threads_item = NULL;
@@ -1945,7 +1809,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, 1008, "mining_failed");
}
result = owned_result;
} else if (strcmp(method, VERB_NIP44_ENCRYPT) == 0) {
} else if (strcmp(method, VERB_NOSTR_NIP44_ENCRYPT) == 0) {
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
@@ -1959,7 +1823,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_NIP44_DECRYPT) == 0) {
} else if (strcmp(method, VERB_NOSTR_NIP44_DECRYPT) == 0) {
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
@@ -1973,7 +1837,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_NIP04_ENCRYPT) == 0) {
} else if (strcmp(method, VERB_NOSTR_NIP04_ENCRYPT) == 0) {
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
@@ -1987,7 +1851,7 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_NIP04_DECRYPT) == 0) {
} else if (strcmp(method, VERB_NOSTR_NIP04_DECRYPT) == 0) {
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
@@ -2001,479 +1865,11 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_ENCRYPT) == 0 ||
strcmp(method, VERB_DECRYPT) == 0) {
/* General encrypt/decrypt verbs (role-based path for non-OTP curves).
* The curve is determined by the role's curve. For secp256k1, the
* nip_version option (4 or 44, default 44) selects NIP-04 or NIP-44.
* For x25519 and ml-kem-768, the key agreement is available but
* direct encrypt/decrypt is not yet implemented. */
const char *curve_str = role->curve_str;
int nip_version = 44; /* default to NIP-44 */
if (cJSON_IsObject(options_item)) {
cJSON *nv = cJSON_GetObjectItemCaseSensitive(options_item, "nip_version");
if (cJSON_IsNumber(nv)) {
nip_version = nv->valueint;
}
}
if (role->curve == CURVE_SECP256K1) {
/* secp256k1: route to NIP-04 or NIP-44 */
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (strcmp(method, VERB_ENCRYPT) == 0) {
if (nip_version == 4) {
owned_result = crypto_nip04_encrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
} else {
owned_result = crypto_nip44_encrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
}
} else { /* decrypt */
if (nip_version == 4) {
owned_result = crypto_nip04_decrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
} else {
owned_result = crypto_nip44_decrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
}
}
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (role->curve == CURVE_X25519) {
/* x25519: ECDH + symmetric encryption — not yet implemented.
* The derive_shared_secret verb is available for key agreement;
* the caller can use the shared secret with their own symmetric
* cipher. Direct encrypt/decrypt will be added in a future release. */
cJSON_Delete(root);
return make_error_response(id_str, -32601,
"encrypt_decrypt_not_implemented_for_x25519");
} else if (role->curve == CURVE_ML_KEM_768) {
/* ml-kem-768: KEM-based hybrid encryption — not yet implemented.
* The encapsulate/decapsulate verbs are available for key agreement;
* direct encrypt/decrypt will be added in a future release. */
cJSON_Delete(root);
return make_error_response(id_str, -32601,
"encrypt_decrypt_not_implemented_for_ml_kem_768");
} else {
cJSON_Delete(root);
return make_error_response(id_str, -32602,
"encrypt_decrypt_not_supported_for_curve");
}
} else if (strcmp(method, VERB_SIGN_DATA) == 0 ||
strcmp(method, VERB_SSH_SIGN) == 0) {
/* sign_data / ssh_sign: params = [message_bytes_hex, options]
* Sign the decoded message bytes with the role's key.
* Returns {"signature":"<hex>","algorithm":"<alg_name>"}.
* Currently only ed25519 is implemented; PQ algorithms return
* not_yet_implemented. */
cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0);
const char *msg_hex;
unsigned char *msg_bytes = NULL;
size_t msg_len = 0;
const unsigned char *priv_key;
const crypto_alg_sizes_t *sz;
unsigned char sig[7856]; /* large enough for ed25519 (64), ML-DSA-65 (3309), SLH-DSA-128s (7856) */
size_t sig_len = 0;
cJSON *result_obj = NULL;
const char *alg_name;
if (!cJSON_IsString(msg_hex_item) || msg_hex_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
msg_hex = msg_hex_item->valuestring;
/* Get the derived key for this role. */
priv_key = crypto_get_private_key(ctx->key_store, role_index);
if (priv_key == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "key_not_derived");
}
/* Determine the algorithm from the role. */
alg = crypto_alg_from_role(role->curve, role->purpose);
sz = crypto_alg_get_sizes(alg);
if (sz == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
/* Only ed25519, ML-DSA-65, and SLH-DSA-128s are implemented for sign_data. */
if (alg != CRYPTO_ALG_ED25519 && alg != CRYPTO_ALG_ML_DSA_65 &&
alg != CRYPTO_ALG_SLH_DSA_128S) {
cJSON_Delete(root);
return make_error_response(id_str, 1009, "not_yet_implemented");
}
/* Decode hex message to bytes. */
msg_len = strlen(msg_hex) / 2;
if (msg_len == 0) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
msg_bytes = (unsigned char *)malloc(msg_len);
if (msg_bytes == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (nostr_hex_to_bytes(msg_hex, msg_bytes, msg_len) != 0) {
free(msg_bytes);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
sig_len = sz->sig_len;
if (alg == CRYPTO_ALG_ML_DSA_65) {
/* ML-DSA-65 signing (deterministic, FIPS 204) */
if (crypto_ml_dsa_65_sign(priv_key, sz->priv_key_len,
msg_bytes, msg_len,
sig, &sig_len) != 0) {
free(msg_bytes);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "signing_failed");
}
} else if (alg == CRYPTO_ALG_SLH_DSA_128S) {
/* SLH-DSA-128s signing (deterministic, FIPS 205) */
if (crypto_slh_dsa_128s_sign(priv_key, sz->priv_key_len,
msg_bytes, msg_len,
sig, &sig_len) != 0) {
free(msg_bytes);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "signing_failed");
}
} else {
/* ed25519 signing */
if (crypto_ed25519_sign(priv_key, sz->priv_key_len,
msg_bytes, msg_len,
sig, &sig_len) != 0) {
free(msg_bytes);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "signing_failed");
}
}
free(msg_bytes);
/* Build result JSON: {"signature":"<hex>","algorithm":"<alg_name>"} */
result_obj = cJSON_CreateObject();
if (result_obj == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
{
char *sig_hex = (char *)malloc(sig_len * 2 + 1);
if (sig_hex == NULL) {
cJSON_Delete(result_obj);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
nostr_bytes_to_hex(sig, sig_len, sig_hex);
cJSON_AddStringToObject(result_obj, "signature", sig_hex);
free(sig_hex);
}
alg_name = crypto_alg_to_str(alg);
cJSON_AddStringToObject(result_obj, "algorithm",
(alg_name != NULL) ? alg_name : "unknown");
owned_result = cJSON_PrintUnformatted(result_obj);
cJSON_Delete(result_obj);
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_VERIFY_SIG) == 0) {
/* verify_signature: params = [message_bytes_hex, signature_hex, options]
* Verify the signature. Returns {"valid":true/false}. */
cJSON *msg_hex_item = cJSON_GetArrayItem(params_item, 0);
cJSON *sig_hex_item = cJSON_GetArrayItem(params_item, 1);
const char *msg_hex;
const char *sig_hex;
unsigned char *msg_bytes = NULL;
unsigned char *sig_bytes = NULL;
size_t msg_len = 0;
size_t sig_len = 0;
const char *pub_hex;
unsigned char *pub = NULL;
size_t pub_hex_len;
const crypto_alg_sizes_t *sz;
int verify_rc;
cJSON *result_obj = NULL;
if (!cJSON_IsString(msg_hex_item) || msg_hex_item->valuestring == NULL ||
!cJSON_IsString(sig_hex_item) || sig_hex_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
msg_hex = msg_hex_item->valuestring;
sig_hex = sig_hex_item->valuestring;
alg = crypto_alg_from_role(role->curve, role->purpose);
sz = crypto_alg_get_sizes(alg);
if (sz == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (alg != CRYPTO_ALG_ED25519 && alg != CRYPTO_ALG_ML_DSA_65 &&
alg != CRYPTO_ALG_SLH_DSA_128S) {
cJSON_Delete(root);
return make_error_response(id_str, 1009, "not_yet_implemented");
}
/* Get the public key hex for this role. */
pub_hex = crypto_get_pubkey_hex(ctx->key_store, role_index);
if (pub_hex == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "key_not_derived");
}
pub_hex_len = strlen(pub_hex);
/* Check pub_hex length matches expected (2 * pub_key_len) */
if (pub_hex_len != sz->pub_key_len * 2) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "key_not_derived");
}
pub = (unsigned char *)malloc(sz->pub_key_len);
if (pub == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (nostr_hex_to_bytes(pub_hex, pub, sz->pub_key_len) != 0) {
free(pub);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
/* Decode message and signature from hex. */
msg_len = strlen(msg_hex) / 2;
sig_len = strlen(sig_hex) / 2;
if (msg_len == 0 || sig_len == 0) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
msg_bytes = (unsigned char *)malloc(msg_len);
sig_bytes = (unsigned char *)malloc(sig_len);
if (msg_bytes == NULL || sig_bytes == NULL) {
free(msg_bytes);
free(sig_bytes);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (nostr_hex_to_bytes(msg_hex, msg_bytes, msg_len) != 0 ||
nostr_hex_to_bytes(sig_hex, sig_bytes, sig_len) != 0) {
free(msg_bytes);
free(sig_bytes);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (alg == CRYPTO_ALG_ML_DSA_65) {
verify_rc = crypto_ml_dsa_65_verify(pub, sz->pub_key_len,
msg_bytes, msg_len,
sig_bytes, sig_len);
} else if (alg == CRYPTO_ALG_SLH_DSA_128S) {
verify_rc = crypto_slh_dsa_128s_verify(pub, sz->pub_key_len,
msg_bytes, msg_len,
sig_bytes, sig_len);
} else {
verify_rc = crypto_ed25519_verify(pub, sz->pub_key_len,
msg_bytes, msg_len,
sig_bytes, sig_len);
}
free(pub);
free(msg_bytes);
free(sig_bytes);
result_obj = cJSON_CreateObject();
if (result_obj == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
cJSON_AddBoolToObject(result_obj, "valid", (verify_rc == 0) ? 1 : 0);
owned_result = cJSON_PrintUnformatted(result_obj);
cJSON_Delete(result_obj);
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_KEM_ENCAPS) == 0) {
/* kem_encapsulate: params = [peer_pubkey_hex, options]
* Encapsulate with the peer's ML-KEM-768 public key.
* Returns {"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"} */
cJSON *pub_hex_item = cJSON_GetArrayItem(params_item, 0);
const char *pub_hex;
unsigned char *pub = NULL;
unsigned char ct[1088];
unsigned char ss[32];
cJSON *result_obj = NULL;
const crypto_alg_sizes_t *sz;
if (!cJSON_IsString(pub_hex_item) || pub_hex_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
pub_hex = pub_hex_item->valuestring;
alg = crypto_alg_from_role(role->curve, role->purpose);
sz = crypto_alg_get_sizes(alg);
if (sz == NULL || alg != CRYPTO_ALG_ML_KEM_768) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
/* Decode hex public key (1184 bytes = 2368 hex chars) */
if (strlen(pub_hex) != sz->pub_key_len * 2) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_pubkey_length");
}
pub = (unsigned char *)malloc(sz->pub_key_len);
if (pub == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (nostr_hex_to_bytes(pub_hex, pub, sz->pub_key_len) != 0) {
free(pub);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_pubkey_hex");
}
if (crypto_ml_kem_768_encaps(pub, sz->pub_key_len, ct, ss) != 0) {
free(pub);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "encaps_failed");
}
free(pub);
/* Build result JSON */
result_obj = cJSON_CreateObject();
if (result_obj == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
{
char *ct_hex = (char *)malloc(sz->ciphertext_len * 2 + 1);
char *ss_hex = (char *)malloc(sz->shared_secret_len * 2 + 1);
if (ct_hex == NULL || ss_hex == NULL) {
free(ct_hex); free(ss_hex);
cJSON_Delete(result_obj);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
nostr_bytes_to_hex(ct, sz->ciphertext_len, ct_hex);
nostr_bytes_to_hex(ss, sz->shared_secret_len, ss_hex);
cJSON_AddStringToObject(result_obj, "ciphertext", ct_hex);
cJSON_AddStringToObject(result_obj, "shared_secret", ss_hex);
free(ct_hex); free(ss_hex);
}
cJSON_AddStringToObject(result_obj, "algorithm", "ml-kem-768");
owned_result = cJSON_PrintUnformatted(result_obj);
cJSON_Delete(result_obj);
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (strcmp(method, VERB_KEM_DECAPS) == 0) {
/* kem_decapsulate: params = [ciphertext_hex, options]
* Decapsulate using the role's ML-KEM-768 private key.
* Returns {"shared_secret":"<hex>","algorithm":"ml-kem-768"} */
cJSON *ct_hex_item = cJSON_GetArrayItem(params_item, 0);
const char *ct_hex;
unsigned char *ct = NULL;
unsigned char ss[32];
const unsigned char *priv_key;
const crypto_alg_sizes_t *sz;
cJSON *result_obj = NULL;
if (!cJSON_IsString(ct_hex_item) || ct_hex_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
ct_hex = ct_hex_item->valuestring;
alg = crypto_alg_from_role(role->curve, role->purpose);
sz = crypto_alg_get_sizes(alg);
if (sz == NULL || alg != CRYPTO_ALG_ML_KEM_768) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
/* Get the role's private key */
priv_key = crypto_get_private_key(ctx->key_store, role_index);
if (priv_key == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "key_not_derived");
}
/* Decode hex ciphertext (1088 bytes = 2176 hex chars) */
if (strlen(ct_hex) != sz->ciphertext_len * 2) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_ciphertext_length");
}
ct = (unsigned char *)malloc(sz->ciphertext_len);
if (ct == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (nostr_hex_to_bytes(ct_hex, ct, sz->ciphertext_len) != 0) {
free(ct);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_ciphertext_hex");
}
if (crypto_ml_kem_768_decaps(priv_key, sz->priv_key_len,
ct, sz->ciphertext_len, ss) != 0) {
free(ct);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "decaps_failed");
}
free(ct);
/* Build result JSON */
result_obj = cJSON_CreateObject();
if (result_obj == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
{
char *ss_hex = (char *)malloc(sz->shared_secret_len * 2 + 1);
if (ss_hex == NULL) {
cJSON_Delete(result_obj);
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
nostr_bytes_to_hex(ss, sz->shared_secret_len, ss_hex);
cJSON_AddStringToObject(result_obj, "shared_secret", ss_hex);
free(ss_hex);
}
cJSON_AddStringToObject(result_obj, "algorithm", "ml-kem-768");
owned_result = cJSON_PrintUnformatted(result_obj);
cJSON_Delete(result_obj);
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else {
/* All remaining verbs (sign, verify, encapsulate, decapsulate,
* derive_shared_secret, get_public_key, encrypt, decrypt) are
* algorithm-based and handled before role resolution. Anything
* reaching here is unrecognized. */
cJSON_Delete(root);
return make_error_response(id_str, -32601, "method_not_found");
}

View File

@@ -202,36 +202,25 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
/* Algorithm-based verb defines (algorithm is a parameter, not implicit).
* Callers must supply `algorithm` explicitly — no implicit defaults. */
#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_GET_PUBLIC_KEY "get_public_key"
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
/* Nostr protocol verbs (secp256k1 NIP-06, role-based selector). */
#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"
/* New algorithm-based verb defines (algorithm is a parameter, not implicit) */
#define VERB_SIGN "sign"
#define VERB_VERIFY "verify"
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
/* OTP verbs (one-time pad; selected via algorithm:"otp"). */
#define VERB_ENCRYPT "encrypt"
#define VERB_DECRYPT "decrypt"
@@ -240,7 +229,7 @@ const char *selector_strerror(int err);
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -712,69 +701,17 @@ int socket_name_random(char *out, size_t out_len);
#include <string.h>
/* Check whether a verb is a Nostr protocol verb (role-based, secp256k1 NIP-06). */
static int is_nostr_verb(const char *verb) {
if (verb == NULL) {
return 0;
}
/* get_public_key is a universal verb — allowed for all algorithms.
* It is handled separately in enforce_verb_role() and excluded here. */
return (strcmp(verb, VERB_SIGN_EVENT) == 0) ||
(strcmp(verb, VERB_NIP44_ENCRYPT) == 0) ||
(strcmp(verb, VERB_NIP44_DECRYPT) == 0) ||
(strcmp(verb, VERB_NIP04_ENCRYPT) == 0) ||
(strcmp(verb, VERB_NIP04_DECRYPT) == 0) ||
(strcmp(verb, VERB_MINE_EVENT) == 0);
}
static int is_sign_data_verb(const char *verb) {
if (verb == NULL) {
return 0;
}
return (strcmp(verb, VERB_SIGN_DATA) == 0) ||
(strcmp(verb, VERB_VERIFY_SIG) == 0);
}
static int is_kem_verb(const char *verb) {
if (verb == NULL) {
return 0;
}
return (strcmp(verb, VERB_KEM_ENCAPS) == 0) ||
(strcmp(verb, VERB_KEM_DECAPS) == 0);
}
/* Check whether a (purpose, curve) pair is allowed for sign_data / verify_signature.
* Returns ENFORCE_OK, ENFORCE_ERR_PURPOSE, or ENFORCE_ERR_CURVE. */
static int enforce_sign_data_role(const role_entry_t *role) {
switch (role->purpose) {
case PURPOSE_SSH:
if (role->curve == CURVE_ED25519) {
return ENFORCE_OK;
}
return ENFORCE_ERR_CURVE;
case PURPOSE_PQ_SIG:
if (role->curve == CURVE_ML_DSA_65 ||
role->curve == CURVE_SLH_DSA_128S) {
return ENFORCE_OK;
}
return ENFORCE_ERR_CURVE;
default:
return ENFORCE_ERR_PURPOSE;
}
}
/* Check whether a (purpose, curve) pair is allowed for kem_encapsulate/decapsulate.
* Returns ENFORCE_OK, ENFORCE_ERR_PURPOSE, or ENFORCE_ERR_CURVE. */
static int enforce_kem_role(const role_entry_t *role) {
if (role->purpose != PURPOSE_PQ_KEM) {
return ENFORCE_ERR_PURPOSE;
}
if (role->curve != CURVE_ML_KEM_768) {
return ENFORCE_ERR_CURVE;
}
return ENFORCE_OK;
return (strcmp(verb, VERB_NOSTR_SIGN_EVENT) == 0) ||
(strcmp(verb, VERB_NOSTR_MINE_EVENT) == 0) ||
(strcmp(verb, VERB_NOSTR_NIP44_ENCRYPT) == 0) ||
(strcmp(verb, VERB_NOSTR_NIP44_DECRYPT) == 0) ||
(strcmp(verb, VERB_NOSTR_NIP04_ENCRYPT) == 0) ||
(strcmp(verb, VERB_NOSTR_NIP04_DECRYPT) == 0);
}
int enforce_verb_role(const char *verb, const role_entry_t *role) {
@@ -782,14 +719,15 @@ int enforce_verb_role(const char *verb, const role_entry_t *role) {
return ENFORCE_ERR_UNKNOWN_VERB;
}
/* get_public_key is a universal verb — allowed for any role whose
* (curve, purpose) maps to a known algorithm via crypto_alg_from_role().
* This lets clients retrieve public keys for all 6 algorithms. */
if (strcmp(verb, VERB_GET_PUBLIC_KEY) == 0) {
crypto_alg_t alg = crypto_alg_from_role(role->curve, role->purpose);
if (alg == CRYPTO_ALG_UNKNOWN) {
/* nostr_get_public_key: returns the role's secp256k1 (NIP-06) public key.
* Requires PURPOSE_NOSTR + CURVE_SECP256K1. */
if (strcmp(verb, VERB_NOSTR_GET_PUBLIC_KEY) == 0) {
if (role->purpose != PURPOSE_NOSTR) {
return ENFORCE_ERR_PURPOSE;
}
if (role->curve != CURVE_SECP256K1) {
return ENFORCE_ERR_CURVE;
}
return ENFORCE_OK;
}
@@ -804,56 +742,25 @@ int enforce_verb_role(const char *verb, const role_entry_t *role) {
return ENFORCE_OK;
}
/* ssh_sign: PURPOSE_SSH + CURVE_ED25519 only. */
if (strcmp(verb, VERB_SSH_SIGN) == 0) {
if (role->purpose != PURPOSE_SSH) {
return ENFORCE_ERR_PURPOSE;
}
if (role->curve != CURVE_ED25519) {
return ENFORCE_ERR_CURVE;
}
return ENFORCE_OK;
}
/* sign_data / verify_signature: SSH+ed25519 or PQ-SIG algorithms. */
if (is_sign_data_verb(verb)) {
return enforce_sign_data_role(role);
}
/* kem_encapsulate / kem_decapsulate: PQ-KEM + ML-KEM-768 only. */
if (is_kem_verb(verb)) {
return enforce_kem_role(role);
}
/* encrypt / decrypt: allowed for secp256k1 (NIP-04/44), x25519 (age),
* and ml-kem-768 (pq-kem). OTP is handled before role resolution. */
if (strcmp(verb, VERB_ENCRYPT) == 0 || strcmp(verb, VERB_DECRYPT) == 0) {
if (role->curve == CURVE_SECP256K1 && role->purpose == PURPOSE_NOSTR) {
return ENFORCE_OK;
}
if (role->curve == CURVE_X25519 && role->purpose == PURPOSE_AGE) {
return ENFORCE_OK;
}
if (role->curve == CURVE_ML_KEM_768 && role->purpose == PURPOSE_PQ_KEM) {
return ENFORCE_OK;
}
return ENFORCE_ERR_CURVE;
}
/* All other verbs (sign, verify, encapsulate, decapsulate,
* derive_shared_secret, get_public_key, encrypt, decrypt) are
* algorithm-based and handled by enforce_verb_algorithm() before
* role resolution. Anything reaching here is unrecognized. */
return ENFORCE_ERR_UNKNOWN_VERB;
}
/* ---- Algorithm-based enforcement (new) ----
/* ---- Algorithm-based enforcement ----
*
* enforce_verb_algorithm() checks whether a verb is valid for a given
* algorithm, WITHOUT consulting the role table or purpose field. This is
* the enforcement path for the new algorithm-based verbs (sign, verify,
* the enforcement path for the algorithm-based verbs (sign, verify,
* encapsulate, decapsulate, derive_shared_secret, get_public_key).
*
* The Nostr-specific verbs (sign_event, nip44_*, nip04_*, mine_event) are
* always secp256k1 — that's a Nostr protocol requirement, not a policy
* choice. They are accepted here only when alg == CRYPTO_ALG_SECP256K1
* so that algorithm-based policy entries can approve them too.
* The Nostr-specific verbs (nostr_sign_event, nostr_nip44_*, nostr_nip04_*,
* nostr_mine_event) are always secp256k1 — that's a Nostr protocol
* requirement, not a policy choice. They are accepted here only when
* alg == CRYPTO_ALG_SECP256K1 so that algorithm-based policy entries can
* approve them too.
*/
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg) {
if (verb == NULL) {
@@ -896,16 +803,13 @@ int enforce_verb_algorithm(const char *verb, crypto_alg_t alg) {
}
/* Nostr verbs: always secp256k1 (protocol requirement). */
if (is_nostr_verb(verb) || strcmp(verb, VERB_MINE_EVENT) == 0) {
if (is_nostr_verb(verb)) {
if (alg == CRYPTO_ALG_SECP256K1) {
return ENFORCE_OK;
}
return ENFORCE_ERR_ALGORITHM;
}
/* Old verb aliases map to the new verbs and are handled by the
* dispatcher before reaching enforcement. If they reach here,
* treat them as unknown (the dispatcher should have remapped). */
return ENFORCE_ERR_UNKNOWN_VERB;
}

View File

@@ -212,18 +212,14 @@ const char *selector_strerror(int err);
#define VERB_DERIVE_SHARED "derive_shared_secret"
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#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"
/*
* Check whether `verb` is allowed to execute against `role`.

View File

@@ -226,19 +226,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -759,8 +760,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 0
#define NSIGNER_VERSION_PATCH 56
#define NSIGNER_VERSION "v0.0.56"
#define NSIGNER_VERSION_PATCH 57
#define NSIGNER_VERSION "v0.0.57"
/* NSIGNER_HEADERLESS_DECLS_END */

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -3,7 +3,7 @@
*
* One pad per session (by design — see plans/otp_nostr_integration.md).
* The pad is bound at startup via otp_pad_bind() and accessed by the
* otp_encrypt / otp_decrypt dispatcher verbs via otp_pad_get_state().
* otp encrypt / otp decrypt dispatcher verbs via otp_pad_get_state().
*
* The pad file is opened read-only and kept open for the lifetime of the
* process. The per-pad .state file (offset counter) is read and written

View File

@@ -1,8 +1,8 @@
/*
* otp_pad.h — OTP pad state for n_signer (one pad per session).
*
* Bound at startup via otp_pad_bind(); accessed by the otp_encrypt /
* otp_decrypt dispatcher verbs. See plans/otp_nostr_integration.md.
* Bound at startup via otp_pad_bind(); accessed by the otp encrypt /
* otp decrypt dispatcher verbs. See plans/otp_nostr_integration.md.
*/
#ifndef NSIGNER_OTP_PAD_H
#define NSIGNER_OTP_PAD_H

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -233,19 +233,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -223,19 +223,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -222,19 +222,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -226,19 +226,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -1760,7 +1761,7 @@ int server_get_caller(int fd, caller_identity_t *out) {
return 0;
}
/* ---- Async mine_event support ---- */
/* ---- Async nostr_mine_event support ---- */
typedef struct {
int client_fd;
@@ -2132,12 +2133,12 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
if (pchk == POLICY_ALLOW && !derivation_error) {
/*
* mine_event is a long-running operation (potentially seconds to minutes).
* nostr_mine_event is a long-running operation (potentially seconds to minutes).
* Spawn a detached thread so the server stays responsive. The thread
* owns the client_fd and request, and sends the response when mining
* completes. We skip the synchronous response path below.
*/
if (strcmp(method, "mine_event") == 0 && client_fd != STDIN_FILENO) {
if (strcmp(method, "nostr_mine_event") == 0 && client_fd != STDIN_FILENO) {
mine_thread_arg_t *marg = malloc(sizeof(mine_thread_arg_t));
if (marg != NULL) {
pthread_t mine_tid;

View File

@@ -222,19 +222,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -78,31 +78,31 @@ else
fi
echo
echo "[2/7] sign_event"
echo "[2/7] nostr_sign_event"
now_ts="$(date +%s)"
event_json='{"kind":1,"content":"hello from tests/test.sh","tags":[],"created_at":__TS__}'
event_json="${event_json/__TS__/${now_ts}}"
escaped_event_json="${event_json//\"/\\\"}"
req_sign="{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"role\":\"main\"}]}"
req_sign="{\"id\":\"2\",\"method\":\"nostr_sign_event\",\"params\":[\"${escaped_event_json}\",{\"role\":\"main\"}]}"
resp_sign="$(run_request "$req_sign")"
echo "request : $req_sign"
echo "response: $resp_sign"
if printf '%s' "$resp_sign" | grep -q '"result"'; then
echo "[ok] sign_event returned result"
echo "[ok] nostr_sign_event returned result"
else
echo "[warn] sign_event did not return result"
echo "[warn] nostr_sign_event did not return result"
fi
echo
echo "[3/7] sign_event with nostr_index=0"
req_sign_idx="{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"nostr_index\":0}]}"
echo "[3/7] nostr_sign_event with nostr_index=0"
req_sign_idx="{\"id\":\"3\",\"method\":\"nostr_sign_event\",\"params\":[\"${escaped_event_json}\",{\"nostr_index\":0}]}"
resp_sign_idx="$(run_request "$req_sign_idx")"
echo "request : $req_sign_idx"
echo "response: $resp_sign_idx"
echo
echo "[4/7] role selector error case (unknown role)"
req_bad_role='{"id":"4","method":"sign_event","params":["{}",{"role":"does_not_exist"}]}'
req_bad_role='{"id":"4","method":"nostr_nostr_sign_event","params":["{}",{"role":"does_not_exist"}]}'
resp_bad_role="$(run_request "$req_bad_role")"
echo "request : $req_bad_role"
echo "response: $resp_bad_role"
@@ -115,13 +115,13 @@ fi
echo
echo "[5/7] NIP-04 self round-trip"
if [[ -n "$pubkey" ]]; then
req_nip04_enc="{\"id\":\"5\",\"method\":\"nip04_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip04_from_test_sh\"]}"
req_nip04_enc="{\"id\":\"5\",\"method\":\"nostr_nostr_nip04_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip04_from_test_sh\"]}"
resp_nip04_enc="$(run_request "$req_nip04_enc")"
echo "request : $req_nip04_enc"
echo "response: $resp_nip04_enc"
cipher_nip04="$(printf '%s' "$resp_nip04_enc" | extract_result_string)"
if [[ -n "$cipher_nip04" ]]; then
req_nip04_dec="{\"id\":\"6\",\"method\":\"nip04_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip04}\"]}"
req_nip04_dec="{\"id\":\"6\",\"method\":\"nostr_nostr_nip04_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip04}\"]}"
resp_nip04_dec="$(run_request "$req_nip04_dec")"
echo "request : $req_nip04_dec"
echo "response: $resp_nip04_dec"
@@ -140,13 +140,13 @@ fi
echo
echo "[6/7] NIP-44 self round-trip"
if [[ -n "$pubkey" ]]; then
req_nip44_enc="{\"id\":\"7\",\"method\":\"nip44_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip44_from_test_sh\"]}"
req_nip44_enc="{\"id\":\"7\",\"method\":\"nostr_nostr_nip44_encrypt\",\"params\":[\"${pubkey}\",\"hello_nip44_from_test_sh\"]}"
resp_nip44_enc="$(run_request "$req_nip44_enc")"
echo "request : $req_nip44_enc"
echo "response: $resp_nip44_enc"
cipher_nip44="$(printf '%s' "$resp_nip44_enc" | extract_result_string)"
if [[ -n "$cipher_nip44" ]]; then
req_nip44_dec="{\"id\":\"8\",\"method\":\"nip44_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip44}\"]}"
req_nip44_dec="{\"id\":\"8\",\"method\":\"nostr_nostr_nip44_decrypt\",\"params\":[\"${pubkey}\",\"${cipher_nip44}\"]}"
resp_nip44_dec="$(run_request "$req_nip44_dec")"
echo "request : $req_nip44_dec"
echo "response: $resp_nip44_dec"

View File

@@ -74,18 +74,14 @@ role_curve_t role_curve_from_str(const char *s);
#define VERB_ENCAPSULATE "encapsulate"
#define VERB_DECAPSULATE "decapsulate"
#define VERB_DERIVE_SHARED "derive_shared_secret"
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
@@ -369,10 +365,10 @@ int main(void) {
enforce_verb_algorithm(VERB_DERIVE_SHARED, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
check("enforce get_public_key + any alg -> OK",
enforce_verb_algorithm(VERB_GET_PUBLIC_KEY, CRYPTO_ALG_ED25519) == ENFORCE_OK);
check("enforce sign_event + secp256k1 -> OK",
enforce_verb_algorithm(VERB_SIGN_EVENT, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
check("enforce sign_event + ed25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_SIGN_EVENT, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
check("enforce nostr_sign_event + secp256k1 -> OK",
enforce_verb_algorithm(VERB_NOSTR_SIGN_EVENT, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
check("enforce nostr_sign_event + ed25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_NOSTR_SIGN_EVENT, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
/* ---- get_public_key with algorithm parameter ---- */
resp = dispatcher_handle_request(&g_dispatcher,
@@ -588,51 +584,51 @@ int main(void) {
resp_has(resp, "algorithm_not_supported_for_verb") || resp_has(resp, "\"code\":1010"));
free(resp);
/* ---- Old verb alias: sign_data with algorithm=ed25519 ---- */
/* ---- Verb: sign with algorithm=ed25519 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"alias1\",\"method\":\"sign_data\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("sign_data alias with algorithm=ed25519 returns result",
"{\"id\":\"alias1\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("sign with algorithm=ed25519 returns result",
resp_has(resp, "\"result\""));
check("sign_data alias has signature",
check("sign has signature",
resp_has(resp, "signature"));
free(resp);
/* ---- Old verb alias: ssh_sign with algorithm=ed25519 ---- */
/* ---- Verb: sign (ssh) with algorithm=ed25519 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"alias2\",\"method\":\"ssh_sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("ssh_sign alias with algorithm=ed25519 returns result",
"{\"id\":\"alias2\",\"method\":\"sign\",\"params\":[\"68656c6c6f\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
check("sign with algorithm=ed25519 returns result",
resp_has(resp, "\"result\""));
free(resp);
/* ---- Old verb alias: kem_encapsulate with algorithm=ml-kem-768 ---- */
/* ---- Verb: encapsulate with algorithm=ml-kem-768 ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"gpk5\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-kem-768\",\"index\":1}]}");
pub_hex = extract_result_field(resp, "public_key");
free(resp);
if (pub_hex) {
snprintf(request, sizeof(request),
"{\"id\":\"alias3\",\"method\":\"kem_encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\"}]}",
"{\"id\":\"alias3\",\"method\":\"encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\"}]}",
pub_hex);
resp = dispatcher_handle_request(&g_dispatcher, request);
check("kem_encapsulate alias with algorithm=ml-kem-768 returns result",
check("encapsulate with algorithm=ml-kem-768 returns result",
resp_has(resp, "\"result\""));
check("kem_encapsulate alias has ciphertext",
check("encapsulate has ciphertext",
resp_has(resp, "ciphertext"));
free(resp);
free(pub_hex);
}
/* ---- Backward compat: sign_event with role still works ---- */
/* ---- nostr_sign_event with role still works ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"bc1\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}");
check("sign_event with role=main (backward compat) returns signed event",
"{\"id\":\"bc1\",\"method\":\"nostr_sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}");
check("nostr_sign_event with role=main returns signed event",
resp_has(resp, "pubkey") && resp_has(resp, "sig"));
free(resp);
/* ---- Backward compat: get_public_key with role still works ---- */
/* ---- nostr_get_public_key with role still works ---- */
resp = dispatcher_handle_request(&g_dispatcher,
"{\"id\":\"bc2\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\"}]}");
check("get_public_key with role=main (backward compat) returns hex",
"{\"id\":\"bc2\",\"method\":\"nostr_get_public_key\",\"params\":[{\"role\":\"main\"}]}");
check("nostr_get_public_key with role=main returns hex",
resp_has(resp, "\"result\""));
free(resp);

View File

@@ -110,7 +110,7 @@ int main(void) {
check_condition("nostr_crypto_init", nostr_crypto_init() == 0);
auth_nonce_cache_init(&cache);
req = make_request_json(privkey, "req-1", "sign_event", (int)time(NULL));
req = make_request_json(privkey, "req-1", "nostr_sign_event", (int)time(NULL));
check_condition("build valid request", req != NULL);
if (req != NULL) {
check_condition("valid auth envelope accepted",
@@ -150,7 +150,7 @@ int main(void) {
check_condition("parse second request", root != NULL);
method_item = (root != NULL) ? cJSON_GetObjectItemCaseSensitive(root, "method") : NULL;
if (method_item != NULL) {
cJSON_SetValuestring(method_item, "sign_event");
cJSON_SetValuestring(method_item, "nostr_sign_event");
}
tampered = (root != NULL) ? cJSON_PrintUnformatted(root) : NULL;
cJSON_Delete(root);
@@ -172,7 +172,7 @@ int main(void) {
free(req);
}
req = make_request_json(privkey, "req-3", "sign_event", (int)time(NULL));
req = make_request_json(privkey, "req-3", "nostr_sign_event", (int)time(NULL));
check_condition("build replay request", req != NULL);
if (req != NULL) {
int first_ok = auth_envelope_verify_request(req,
@@ -198,7 +198,7 @@ int main(void) {
free(req);
}
req = make_request_json(privkey, "req-4", "sign_event", (int)time(NULL) - 1000);
req = make_request_json(privkey, "req-4", "nostr_sign_event", (int)time(NULL) - 1000);
check_condition("build stale request", req != NULL);
if (req != NULL) {
check_condition("stale request rejected",

View File

@@ -223,19 +223,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -813,51 +814,51 @@ int main(void) {
}
}
/* 1. Valid get_public_key no selector -> default main, real pubkey */
/* 1. Valid nostr_get_public_key no selector -> default main, real pubkey */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}");
check_condition("get_public_key default role returns derived hex",
"{\"id\":\"1\",\"method\":\"nostr_get_public_key\",\"params\":[\"\"]}");
check_condition("nostr_get_public_key default role returns derived hex",
response_has(resp, "\"id\":\"1\"") && !response_has(resp, "not_yet_derived") && response_has(resp, "\"result\":\""));
free(resp);
/* 2. sign_event with role main */
/* 2. nostr_sign_event with role main */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}");
check_condition("sign_event with role=main returns signed event",
"{\"id\":\"2\",\"method\":\"nostr_sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}");
check_condition("nostr_sign_event with role=main returns signed event",
response_has(resp, "\"id\":\"2\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at"));
free(resp);
/* 3. sign_event with nostr_index 0 */
/* 3. nostr_sign_event with nostr_index 0 */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello2\\\",\\\"tags\\\":[]}\",{\"nostr_index\":0}]}");
check_condition("sign_event with nostr_index=0 returns signed event",
"{\"id\":\"3\",\"method\":\"nostr_sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello2\\\",\\\"tags\\\":[]}\",{\"nostr_index\":0}]}");
check_condition("nostr_sign_event with nostr_index=0 returns signed event",
response_has(resp, "\"id\":\"3\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at"));
free(resp);
/* 4. ambiguous selector role + nostr_index */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"4\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"main\",\"nostr_index\":0}]}");
"{\"id\":\"4\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\",{\"role\":\"main\",\"nostr_index\":0}]}");
check_condition("ambiguous selector returns 1001",
response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1001"));
free(resp);
/* 5. unknown role */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"5\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"nonexistent\"}]}");
"{\"id\":\"5\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\",{\"role\":\"nonexistent\"}]}");
check_condition("unknown role returns 1002 with unknown_role",
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"code\":1002") && response_has(resp, "unknown_role"));
free(resp);
/* 6. purpose mismatch: sign_event against ssh role */
/* 6. purpose mismatch: nostr_sign_event against ssh role */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"6\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"ssh_key\"}]}");
"{\"id\":\"6\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\",{\"role\":\"ssh_key\"}]}");
check_condition("purpose mismatch returns 1004",
response_has(resp, "\"id\":\"6\"") && response_has(resp, "\"code\":1004"));
free(resp);
/* 7. invalid JSON */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"7\",\"method\":\"sign_event\",\"params\":[\"{}\"]");
"{\"id\":\"7\",\"method\":\"nostr_sign_event\",\"params\":[\"{}\"]");
check_condition("invalid JSON returns -32700",
response_has(resp, "\"code\":-32700"));
free(resp);
@@ -872,7 +873,7 @@ int main(void) {
/* 9. mnemonic not loaded */
mnemonic_unload(&mnemonic);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"9\",\"method\":\"get_public_key\",\"params\":[\"\"]}");
"{\"id\":\"9\",\"method\":\"nostr_get_public_key\",\"params\":[\"\"]}");
check_condition("mnemonic not loaded returns 1006",
response_has(resp, "\"id\":\"9\"") && response_has(resp, "\"code\":1006"));
free(resp);

View File

@@ -9,11 +9,11 @@
* - x25519 ECDH roundtrip: two sides derive matching shared secret
* - x25519 determinism: same seed -> same keypair
* - SLIP-0010 derivation: deterministic seed from mnemonic+path
* - Integration: derive ed25519 key via crypto_derive_one, sign_data via
* - Integration: derive ed25519 key via crypto_derive_one, sign via
* dispatcher, verify signature
* - Enforcement: sign_data allowed on ssh+ed25519, rejected on nostr+secp256k1
* - Enforcement: ssh_sign allowed on ssh+ed25519
* - Enforcement: kem_encapsulate/decapsulate rejected on ssh/ed25519
* - Enforcement: sign allowed on ssh+ed25519, rejected on nostr+secp256k1
* - Enforcement: sign allowed on ssh+ed25519
* - Enforcement: encapsulate/decapsulate rejected on ssh/ed25519
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
@@ -150,19 +150,21 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define ENFORCE_ERR_ALGORITHM -4
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#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_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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
@@ -692,7 +694,7 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: sign_data via dispatcher ---- */
/* ---- Integration: sign via dispatcher ---- */
{
role_table_t table;
role_entry_t ssh_role;
@@ -717,27 +719,27 @@ int main(void) {
dispatcher_init(&dispatcher, &table, &mnemonic_state, &key_store, &g_alg_key_cache);
/* sign_data request */
/* sign request */
snprintf(request, sizeof(request),
"{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}",
"{\"id\":\"test1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("sign_data via dispatcher returns result",
check_condition("sign via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("sign_data result contains signature",
check_condition("sign result contains signature",
resp != NULL && response_has(resp, "signature"));
check_condition("sign_data result contains algorithm ed25519",
check_condition("sign result contains algorithm ed25519",
resp != NULL && response_has(resp, "ed25519"));
free(resp);
/* ssh_sign request */
/* sign request */
snprintf(request, sizeof(request),
"{\"id\":\"test2\",\"method\":\"ssh_sign\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}",
"{\"id\":\"test2\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("ssh_sign via dispatcher returns result",
check_condition("sign via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("ssh_sign result contains signature",
check_condition("sign result contains signature",
resp != NULL && response_has(resp, "signature"));
free(resp);
@@ -745,38 +747,39 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: sign_data on ssh+ed25519 ---- */
/* ---- Enforcement: sign on ssh+ed25519 ---- */
{
role_entry_t ssh_ed = make_ssh_ed25519_entry("ssh", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
role_entry_t pq_kem = make_pq_kem_entry("kem", 0);
check_condition("enforce sign_data + ssh/ed25519 -> OK",
enforce_verb_role(VERB_SIGN_DATA, &ssh_ed) == ENFORCE_OK);
/* sign/verify are algorithm-based now: checked via enforce_verb_algorithm(). */
check_condition("enforce sign + ed25519 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ED25519) == ENFORCE_OK);
check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce sign + secp256k1 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
check_condition("enforce ssh_sign + ssh/ed25519 -> OK",
enforce_verb_role(VERB_SSH_SIGN, &ssh_ed) == ENFORCE_OK);
check_condition("enforce verify + ed25519 -> OK",
enforce_verb_algorithm(VERB_VERIFY, CRYPTO_ALG_ED25519) == ENFORCE_OK);
check_condition("enforce ssh_sign + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SSH_SIGN, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce sign + ml-kem-768 -> ALGORITHM err",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_KEM_768) == ENFORCE_ERR_ALGORITHM);
check_condition("enforce sign_event + ssh/ed25519 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE);
check_condition("enforce nostr_sign_event + ssh/ed25519 -> PURPOSE err",
enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE);
check_condition("enforce kem_encapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_ENCAPS, &pq_kem) == ENFORCE_OK);
check_condition("enforce encapsulate + ml-kem-768 -> OK",
enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK);
check_condition("enforce kem_decapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_DECAPS, &pq_kem) == ENFORCE_OK);
check_condition("enforce decapsulate + ml-kem-768 -> OK",
enforce_verb_algorithm(VERB_DECAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK);
check_condition("enforce kem_encapsulate + ssh/ed25519 -> PURPOSE err",
enforce_verb_role(VERB_KEM_ENCAPS, &ssh_ed) == ENFORCE_ERR_PURPOSE);
check_condition("enforce encapsulate + ed25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
}
/* ---- Dispatcher: kem_encapsulate with invalid pubkey length ---- */
/* ---- Dispatcher: encapsulate with invalid pubkey length ---- */
{
role_table_t table;
role_entry_t kem_role;
@@ -784,7 +787,7 @@ int main(void) {
key_store_t key_store;
dispatcher_ctx_t dispatcher;
char *resp;
const char *request = "{\"id\":\"k1\",\"method\":\"kem_encapsulate\",\"params\":[\"00\",{\"role\":\"kem_main\"}]}";
const char *request = "{\"id\":\"k1\",\"method\":\"encapsulate\",\"params\":[\"00\",{\"algorithm\":\"ml-kem-768\",\"index\":0}]}";
role_table_init(&table);
kem_role = make_pq_kem_entry("kem_main", 0);
@@ -798,11 +801,11 @@ int main(void) {
alg_key_cache_init(&g_alg_key_cache);
dispatcher_init(&dispatcher, &table, &mnemonic_state, &key_store, &g_alg_key_cache);
/* kem_encapsulate is now implemented (Phase 5). With a too-short
/* encapsulate is now implemented (Phase 5). With a too-short
* pubkey hex ("00" = 1 byte, but ML-KEM-768 needs 1184 bytes),
* the dispatcher should return an invalid_pubkey_length error. */
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("kem_encapsulate with short pubkey returns error",
check_condition("encapsulate with short pubkey returns error",
resp != NULL && response_has(resp, "\"error\""));
free(resp);
@@ -810,7 +813,7 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Dispatcher: verify_signature roundtrip ---- */
/* ---- Dispatcher: verify roundtrip ---- */
{
role_table_t table;
role_entry_t ssh_role;
@@ -839,10 +842,10 @@ int main(void) {
/* Sign */
snprintf(sign_req, sizeof(sign_req),
"{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"ssh_main\"}]}",
"{\"id\":\"s1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}",
msg_hex);
sign_resp = dispatcher_handle_request(&dispatcher, sign_req);
check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL);
check_condition("verify roundtrip: sign succeeds", sign_resp != NULL);
/* Extract signature from result */
if (sign_resp != NULL) {
@@ -867,28 +870,28 @@ int main(void) {
/* Verify */
if (sig_str != NULL) {
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"ssh_main\"}]}",
"{\"id\":\"v1\",\"method\":\"verify\",\"params\":[\"%s\",\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}",
msg_hex, sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature returns valid:true",
check_condition("verify returns valid:true",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "true"));
free(verify_resp);
/* Verify with wrong message */
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"ssh_main\"}]}",
"{\"id\":\"v2\",\"method\":\"verify\",\"params\":[\"00ff\",\"%s\",{\"algorithm\":\"ed25519\",\"index\":0}]}",
sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature wrong msg returns valid:false",
check_condition("verify wrong msg returns valid:false",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "false"));
free(verify_resp);
free((void *)sig_str);
} else {
check_condition("verify_signature returns valid:true", 0);
check_condition("verify_signature wrong msg returns valid:false", 0);
check_condition("verify returns valid:true", 0);
check_condition("verify wrong msg returns valid:false", 0);
}
free(sign_resp);

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -739,43 +740,43 @@ int main(void) {
role_entry_t age_x = make_role("age", "x25519");
check_condition(
"sign_event + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_SIGN_EVENT, &nostr_secp) == ENFORCE_OK
"nostr_sign_event + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"get_public_key + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_GET_PUBLIC_KEY, &nostr_secp) == ENFORCE_OK
"nostr_get_public_key + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NOSTR_GET_PUBLIC_KEY, &nostr_secp) == ENFORCE_OK
);
check_condition(
"nip44_encrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NIP44_ENCRYPT, &nostr_secp) == ENFORCE_OK
"nostr_nip44_encrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NOSTR_NIP44_ENCRYPT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"nip44_decrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NIP44_DECRYPT, &nostr_secp) == ENFORCE_OK
"nostr_nip44_decrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NOSTR_NIP44_DECRYPT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"nip04_encrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NIP04_ENCRYPT, &nostr_secp) == ENFORCE_OK
"nostr_nip04_encrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NOSTR_NIP04_ENCRYPT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"sign_event + bitcoin/secp256k1 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_SIGN_EVENT, &bitcoin_secp) == ENFORCE_ERR_PURPOSE
"nostr_sign_event + bitcoin/secp256k1 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &bitcoin_secp) == ENFORCE_ERR_PURPOSE
);
check_condition(
"sign_event + nostr/ed25519 -> ENFORCE_ERR_CURVE",
enforce_verb_role(VERB_SIGN_EVENT, &nostr_ed) == ENFORCE_ERR_CURVE
"nostr_sign_event + nostr/ed25519 -> ENFORCE_ERR_CURVE",
enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &nostr_ed) == ENFORCE_ERR_CURVE
);
check_condition(
"sign_event + ssh/ed25519 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE
"nostr_sign_event + ssh/ed25519 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE
);
check_condition(
@@ -784,8 +785,8 @@ int main(void) {
);
check_condition(
"nip44_encrypt + age/x25519 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_NIP44_ENCRYPT, &age_x) == ENFORCE_ERR_PURPOSE
"nostr_nip44_encrypt + age/x25519 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_NOSTR_NIP44_ENCRYPT, &age_x) == ENFORCE_ERR_PURPOSE
);
printf("%d/10 tests passed\n", g_passes);

View File

@@ -222,19 +222,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -1001,13 +1002,13 @@ int main(void) {
nsigner_client_free(c);
c = NULL;
/* sign_event (fresh connection) */
/* nostr_sign_event (fresh connection) */
t = nsigner_transport_open_unix(SOCKET_NAME_A, 5000);
if (t != NULL) {
c = nsigner_client_new(t);
if (c == NULL) {
t->close(t);
check_condition("connect signer socket for sign_event", 0);
check_condition("connect signer socket for nostr_sign_event", 0);
} else {
params = cJSON_CreateArray();
if (params != NULL) {
@@ -1018,10 +1019,10 @@ int main(void) {
cJSON_AddItemToArray(params, opts);
opts = NULL;
}
if (nsigner_client_call(c, "sign_event", params, &se_result) == NOSTR_SUCCESS) {
check_condition("sign_event has result", se_result != NULL);
if (nsigner_client_call(c, "nostr_sign_event", params, &se_result) == NOSTR_SUCCESS) {
check_condition("nostr_sign_event has result", se_result != NULL);
} else {
check_condition("sign_event request roundtrip", 0);
check_condition("nostr_sign_event request roundtrip", 0);
}
params = NULL; /* nsigner_client_call took ownership */
cJSON_Delete(se_result);
@@ -1029,7 +1030,7 @@ int main(void) {
}
}
} else {
check_condition("connect signer socket for sign_event", 0);
check_condition("connect signer socket for nostr_sign_event", 0);
}
cJSON_Delete(params);
@@ -1052,7 +1053,7 @@ int main(void) {
if (p) { p += 10; strncpy(pub_a, p, 64); pub_a[64]='\0'; }
check_condition("single-instance public key request", pub_a[0] != '\0');
snprintf(req, sizeof(req), "{\"id\":\"5\",\"method\":\"nip04_encrypt\",\"params\":[\"%s\",\"hello_nip04\"]}", pub_a);
snprintf(req, sizeof(req), "{\"id\":\"5\",\"method\":\"nostr_nip04_encrypt\",\"params\":[\"%s\",\"hello_nip04\"]}", pub_a);
free(resp_a); resp_a = NULL;
if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) {
p = strstr(resp_a, "\"result\":\"");
@@ -1062,7 +1063,7 @@ int main(void) {
while (p[i] && p[i] != '\"' && i < sizeof(cipher)-1) { cipher[i]=p[i]; i++; }
cipher[i]='\0';
}
snprintf(req, sizeof(req), "{\"id\":\"6\",\"method\":\"nip04_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher);
snprintf(req, sizeof(req), "{\"id\":\"6\",\"method\":\"nostr_nip04_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher);
free(resp_a); resp_a = NULL;
if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) {
check_condition("nip04 round-trip plaintext recovered", strstr(resp_a, "hello_nip04") != NULL);
@@ -1074,7 +1075,7 @@ int main(void) {
}
memset(cipher, 0, sizeof(cipher));
snprintf(req, sizeof(req), "{\"id\":\"7\",\"method\":\"nip44_encrypt\",\"params\":[\"%s\",\"hello_nip44\"]}", pub_a);
snprintf(req, sizeof(req), "{\"id\":\"7\",\"method\":\"nostr_nip44_encrypt\",\"params\":[\"%s\",\"hello_nip44\"]}", pub_a);
free(resp_a); resp_a = NULL;
if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) {
p = strstr(resp_a, "\"result\":\"");
@@ -1084,7 +1085,7 @@ int main(void) {
while (p[i] && p[i] != '\"' && i < sizeof(cipher)-1) { cipher[i]=p[i]; i++; }
cipher[i]='\0';
}
snprintf(req, sizeof(req), "{\"id\":\"8\",\"method\":\"nip44_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher);
snprintf(req, sizeof(req), "{\"id\":\"8\",\"method\":\"nostr_nip44_decrypt\",\"params\":[\"%s\",\"%s\"]}", pub_a, cipher);
free(resp_a); resp_a = NULL;
if (request_roundtrip_to(SOCKET_NAME_A, req, &resp_a) == 0) {
check_condition("nip44 round-trip plaintext recovered", strstr(resp_a, "hello_nip44") != NULL);

View File

@@ -1,4 +1,4 @@
/* Test for mine_event verb (NIP-13 Proof-of-Work) */
/* Test for nostr_mine_event verb (NIP-13 Proof-of-Work) */
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
@@ -126,18 +126,14 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
@@ -497,7 +493,7 @@ static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
/* Parse a dispatcher response and extract the result object (for mine_event).
/* Parse a dispatcher response and extract the result object (for nostr_mine_event).
* Returns a newly-allocated cJSON result object, or NULL on failure.
* Caller must delete the returned object. */
static cJSON *extract_result_object(const char *response) {
@@ -580,67 +576,67 @@ int main(void) {
derived = crypto_derive_all(&key_store, &table, &mnemonic);
check_condition("crypto_derive_all derives at least one key", derived >= 1);
/* 1. mine_event with low difficulty (2) and short timeout (5 sec) — should reach target */
/* 1. nostr_mine_event with low difficulty (2) and short timeout (5 sec) — should reach target */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"1\",\"method\":\"mine_event\",\"params\":["
"{\"id\":\"1\",\"method\":\"nostr_mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello pow\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":2,\"threads\":2,\"timeout_sec\":5}]}");
check_condition("mine_event low difficulty returns result object",
check_condition("nostr_mine_event low difficulty returns result object",
response_has(resp, "\"id\":\"1\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
check_condition("mine_event low difficulty returns signed event with nonce tag",
check_condition("nostr_mine_event low difficulty returns signed event with nonce tag",
response_has(resp, "nonce") && response_has(resp, "sig") && response_has(resp, "pubkey"));
check_condition("mine_event low difficulty target_reached is true",
check_condition("nostr_mine_event low difficulty target_reached is true",
check_target_reached(resp, 1));
free(resp);
/* 2. mine_event with high difficulty (30) and short timeout (2 sec) — should NOT reach target */
/* 2. nostr_mine_event with high difficulty (30) and short timeout (2 sec) — should NOT reach target */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"2\",\"method\":\"mine_event\",\"params\":["
"{\"id\":\"2\",\"method\":\"nostr_mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"hard pow\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":30,\"threads\":4,\"timeout_sec\":2}]}");
check_condition("mine_event high difficulty returns result object",
check_condition("nostr_mine_event high difficulty returns result object",
response_has(resp, "\"id\":\"2\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
check_condition("mine_event high difficulty target_reached is false",
check_condition("nostr_mine_event high difficulty target_reached is false",
check_target_reached(resp, 0));
check_condition("mine_event high difficulty still returns a signed event",
check_condition("nostr_mine_event high difficulty still returns a signed event",
response_has(resp, "nonce") && response_has(resp, "sig"));
free(resp);
/* 3. mine_event with only timeout (no difficulty) — should mine for full duration */
/* 3. nostr_mine_event with only timeout (no difficulty) — should mine for full duration */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"3\",\"method\":\"mine_event\",\"params\":["
"{\"id\":\"3\",\"method\":\"nostr_mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"timeout only\\\",\\\"tags\\\":[]}\","
"{\"threads\":2,\"timeout_sec\":2}]}");
check_condition("mine_event timeout-only returns result",
check_condition("nostr_mine_event timeout-only returns result",
response_has(resp, "\"id\":\"3\"") && response_has(resp, "\"result\"") && response_has(resp, "achieved_difficulty"));
check_condition("mine_event timeout-only target_reached is false (no target set)",
check_condition("nostr_mine_event timeout-only target_reached is false (no target set)",
check_target_reached(resp, 0));
free(resp);
/* 4. mine_event with neither difficulty nor timeout — should error */
/* 4. nostr_mine_event with neither difficulty nor timeout — should error */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"4\",\"method\":\"mine_event\",\"params\":["
"{\"id\":\"4\",\"method\":\"nostr_mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"no params\\\",\\\"tags\\\":[]}\","
"{\"threads\":2}]}");
check_condition("mine_event with no termination condition returns 1007",
check_condition("nostr_mine_event with no termination condition returns 1007",
response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1007") && response_has(resp, "no_termination_condition"));
free(resp);
/* 5. mine_event with only difficulty (no timeout) — should use safety timeout and reach low target */
/* 5. nostr_mine_event with only difficulty (no timeout) — should use safety timeout and reach low target */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"5\",\"method\":\"mine_event\",\"params\":["
"{\"id\":\"5\",\"method\":\"nostr_mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"difficulty only\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":1,\"threads\":2}]}");
check_condition("mine_event difficulty-only returns result and reaches target",
check_condition("nostr_mine_event difficulty-only returns result and reaches target",
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"result\"") && check_target_reached(resp, 1));
free(resp);
/* 6. mine_event with invalid event JSON */
/* 6. nostr_mine_event with invalid event JSON */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"6\",\"method\":\"mine_event\",\"params\":["
"{\"id\":\"6\",\"method\":\"nostr_mine_event\",\"params\":["
"\"not valid json\","
"{\"difficulty\":1,\"timeout_sec\":2}]}");
check_condition("mine_event with invalid event returns error",
check_condition("nostr_mine_event with invalid event returns error",
response_has(resp, "\"id\":\"6\"") && (response_has(resp, "\"code\":1008") || response_has(resp, "\"code\":-32602")));
free(resp);
@@ -651,7 +647,7 @@ int main(void) {
int pow_diff;
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"7\",\"method\":\"mine_event\",\"params\":["
"{\"id\":\"7\",\"method\":\"nostr_mine_event\",\"params\":["
"\"{\\\"kind\\\":1,\\\"content\\\":\\\"verify pow\\\",\\\"tags\\\":[]}\","
"{\"difficulty\":4,\"threads\":4,\"timeout_sec\":5}]}");

View File

@@ -8,8 +8,8 @@
* - ML-DSA-65 sign/verify with wrong key: verify fails
* - DRBG determinism: same seed -> same randombytes output
* - Integration: derive ML-DSA-65 key from mnemonic via crypto_derive_one,
* sign data via sign_data verb through dispatcher, verify signature
* - Enforcement: sign_data allowed on pq-sig+ml-dsa-65
* sign data via sign verb through dispatcher, verify signature
* - Enforcement: sign allowed on pq-sig+ml-dsa-65
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
@@ -142,19 +142,21 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define ENFORCE_ERR_ALGORITHM -4
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#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_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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
@@ -682,7 +684,7 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: sign_data via dispatcher ---- */
/* ---- Integration: sign via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
@@ -705,16 +707,16 @@ int main(void) {
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
/* sign_data request */
/* sign request */
snprintf(request, sizeof(request),
"{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"test1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("sign_data via dispatcher returns result",
check_condition("sign via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("sign_data result contains signature",
check_condition("sign result contains signature",
resp != NULL && response_has(resp, "signature"));
check_condition("sign_data result contains algorithm ml-dsa-65",
check_condition("sign result contains algorithm ml-dsa-65",
resp != NULL && response_has(resp, "ml-dsa-65"));
free(resp);
@@ -722,7 +724,7 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: verify_signature roundtrip via dispatcher ---- */
/* ---- Integration: verify roundtrip via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
@@ -749,10 +751,10 @@ int main(void) {
/* Sign */
snprintf(sign_req, sizeof(sign_req),
"{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"s1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}",
msg_hex);
sign_resp = dispatcher_handle_request(&dispatcher, sign_req);
check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL);
check_condition("verify roundtrip: sign succeeds", sign_resp != NULL);
/* Extract signature from result */
if (sign_resp != NULL) {
@@ -777,28 +779,28 @@ int main(void) {
/* Verify */
if (sig_str != NULL) {
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"v1\",\"method\":\"verify\",\"params\":[\"%s\",\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}",
msg_hex, sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature returns valid:true",
check_condition("verify returns valid:true",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "true"));
free(verify_resp);
/* Verify with wrong message */
snprintf(verify_req, sizeof(verify_req),
"{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"v2\",\"method\":\"verify\",\"params\":[\"00ff\",\"%s\",{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}",
sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature wrong msg returns valid:false",
check_condition("verify wrong msg returns valid:false",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "false"));
free(verify_resp);
free((void *)sig_str);
} else {
check_condition("verify_signature returns valid:true", 0);
check_condition("verify_signature wrong msg returns valid:false", 0);
check_condition("verify returns valid:true", 0);
check_condition("verify wrong msg returns valid:false", 0);
}
free(sign_resp);
@@ -806,22 +808,26 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: sign_data on pq-sig+ml-dsa-65 ---- */
/* ---- Enforcement: sign on pq-sig+ml-dsa-65 ---- */
{
role_entry_t pq_sig = make_pq_sig_ml_dsa_65_entry("pq", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
check_condition("enforce sign_data + pq-sig/ml-dsa-65 -> OK",
enforce_verb_role(VERB_SIGN_DATA, &pq_sig) == ENFORCE_OK);
/* sign/verify are algorithm-based now. */
check_condition("enforce sign + ml-dsa-65 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_DSA_65) == ENFORCE_OK);
check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce sign + secp256k1 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
check_condition("enforce verify_signature + pq-sig/ml-dsa-65 -> OK",
enforce_verb_role(VERB_VERIFY_SIG, &pq_sig) == ENFORCE_OK);
check_condition("enforce verify + ml-dsa-65 -> OK",
enforce_verb_algorithm(VERB_VERIFY, CRYPTO_ALG_ML_DSA_65) == ENFORCE_OK);
check_condition("enforce sign_event + pq-sig/ml-dsa-65 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_EVENT, &pq_sig) == ENFORCE_ERR_PURPOSE);
check_condition("enforce sign + ml-kem-768 -> ALGORITHM err",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_KEM_768) == ENFORCE_ERR_ALGORITHM);
check_condition("enforce nostr_sign_event + pq-sig/ml-dsa-65 -> PURPOSE err",
enforce_verb_role(VERB_NOSTR_SIGN_EVENT, &pq_sig) == ENFORCE_ERR_PURPOSE);
}
/* Cleanup */

View File

@@ -8,7 +8,7 @@
* - Decaps with wrong ciphertext: different shared secret (implicit rejection)
* - Integration: derive ML-KEM-768 key from mnemonic, encaps via dispatcher,
* decaps via dispatcher, shared secrets match
* - Enforcement: kem_encapsulate/kem_decapsulate allowed on pq-kem+ml-kem-768
* - Enforcement: encapsulate/decapsulate allowed on pq-kem+ml-kem-768
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
@@ -112,19 +112,21 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define ENFORCE_ERR_ALGORITHM -4
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#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_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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
@@ -521,7 +523,7 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: kem_encapsulate / kem_decapsulate via dispatcher ---- */
/* ---- Integration: encapsulate / decapsulate via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
@@ -552,18 +554,18 @@ int main(void) {
check_condition("dispatcher integration: pubkey available", pub_hex != NULL);
if (pub_hex != NULL && decaps_req != NULL) {
/* kem_encapsulate request */
/* encapsulate request */
snprintf(encaps_req, sizeof(encaps_req),
"{\"id\":\"e1\",\"method\":\"kem_encapsulate\",\"params\":[\"%s\",{\"role\":\"pq_kem\"}]}",
"{\"id\":\"e1\",\"method\":\"encapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\",\"index\":0}]}",
pub_hex);
encaps_resp = dispatcher_handle_request(&dispatcher, encaps_req);
check_condition("kem_encapsulate via dispatcher returns result",
check_condition("encapsulate via dispatcher returns result",
encaps_resp != NULL && response_has(encaps_resp, "\"result\""));
check_condition("kem_encapsulate result contains ciphertext",
check_condition("encapsulate result contains ciphertext",
encaps_resp != NULL && response_has(encaps_resp, "ciphertext"));
check_condition("kem_encapsulate result contains shared_secret",
check_condition("encapsulate result contains shared_secret",
encaps_resp != NULL && response_has(encaps_resp, "shared_secret"));
check_condition("kem_encapsulate result contains algorithm ml-kem-768",
check_condition("encapsulate result contains algorithm ml-kem-768",
encaps_resp != NULL && response_has(encaps_resp, "ml-kem-768"));
/* Extract ciphertext and shared_secret from result */
@@ -584,18 +586,18 @@ int main(void) {
cJSON_Delete(encaps_json);
}
}
check_condition("kem_encapsulate: extracted ciphertext hex", ct_str != NULL);
check_condition("kem_encapsulate: extracted shared_secret hex", ss_encaps_str != NULL);
check_condition("encapsulate: extracted ciphertext hex", ct_str != NULL);
check_condition("encapsulate: extracted shared_secret hex", ss_encaps_str != NULL);
/* kem_decapsulate request */
/* decapsulate request */
if (ct_str != NULL) {
snprintf(decaps_req, 6000,
"{\"id\":\"d1\",\"method\":\"kem_decapsulate\",\"params\":[\"%s\",{\"role\":\"pq_kem\"}]}",
"{\"id\":\"d1\",\"method\":\"decapsulate\",\"params\":[\"%s\",{\"algorithm\":\"ml-kem-768\",\"index\":0}]}",
ct_str);
decaps_resp = dispatcher_handle_request(&dispatcher, decaps_req);
check_condition("kem_decapsulate via dispatcher returns result",
check_condition("decapsulate via dispatcher returns result",
decaps_resp != NULL && response_has(decaps_resp, "\"result\""));
check_condition("kem_decapsulate result contains shared_secret",
check_condition("decapsulate result contains shared_secret",
decaps_resp != NULL && response_has(decaps_resp, "shared_secret"));
/* Extract shared_secret from decaps result */
@@ -626,8 +628,8 @@ int main(void) {
free((void *)ss_decaps_str);
free(decaps_resp);
} else {
check_condition("kem_decapsulate via dispatcher returns result", 0);
check_condition("kem_decapsulate result contains shared_secret", 0);
check_condition("decapsulate via dispatcher returns result", 0);
check_condition("decapsulate result contains shared_secret", 0);
check_condition("dispatcher encaps/decaps shared secrets match", 0);
}
@@ -641,22 +643,23 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: kem_encapsulate/kem_decapsulate on pq-kem+ml-kem-768 ---- */
/* ---- Enforcement: encapsulate/decapsulate on pq-kem+ml-kem-768 ---- */
{
role_entry_t pq_kem = make_pq_kem_ml_kem_768_entry("pqk", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
check_condition("enforce kem_encapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_ENCAPS, &pq_kem) == ENFORCE_OK);
/* encapsulate/decapsulate are algorithm-based now. */
check_condition("enforce encapsulate + ml-kem-768 -> OK",
enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK);
check_condition("enforce kem_encapsulate + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_KEM_ENCAPS, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce encapsulate + secp256k1 -> ALGORITHM err",
enforce_verb_algorithm(VERB_ENCAPSULATE, CRYPTO_ALG_SECP256K1) == ENFORCE_ERR_ALGORITHM);
check_condition("enforce kem_decapsulate + pq-kem/ml-kem-768 -> OK",
enforce_verb_role(VERB_KEM_DECAPS, &pq_kem) == ENFORCE_OK);
check_condition("enforce decapsulate + ml-kem-768 -> OK",
enforce_verb_algorithm(VERB_DECAPSULATE, CRYPTO_ALG_ML_KEM_768) == ENFORCE_OK);
check_condition("enforce kem_decapsulate + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_KEM_DECAPS, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce decapsulate + ed25519 -> ALGORITHM err",
enforce_verb_algorithm(VERB_DECAPSULATE, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
}
/* ---- Cleanup ---- */

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/
@@ -801,10 +802,10 @@ int main(void) {
check_condition("parse_preapprove_spec rejects non-numeric nostr_index", rc == -1);
policy_init_default(&table, uid);
rc = policy_check(&table, same_uid, "sign_event", "main", "nostr", NULL);
rc = policy_check(&table, same_uid, "nostr_sign_event", "main", "nostr", NULL);
check_condition("policy_init_default prompts same uid", rc == POLICY_PROMPT);
rc = policy_check(&table, other_uid, "sign_event", "main", "nostr", NULL);
rc = policy_check(&table, other_uid, "nostr_sign_event", "main", "nostr", NULL);
check_condition("policy_init_default prompts different uid", rc == POLICY_PROMPT);
/* Insert before catch-all: matching caller allowed, non-matching still prompted */
@@ -820,9 +821,9 @@ int main(void) {
rc = policy_table_insert_before_last(&table, &grant);
check_condition("policy_table_insert_before_last succeeds", rc == 0);
}
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("insert_before_last matching caller returns POLICY_ALLOW", rc == POLICY_ALLOW);
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL);
rc = policy_check(&table, "uid:2000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("insert_before_last non-matching caller returns POLICY_PROMPT", rc == POLICY_PROMPT);
/* Preapprove parse + insert: matching caller allowed, non-matching prompted */
@@ -831,70 +832,70 @@ int main(void) {
check_condition("preapprove parse for policy insert succeeds", rc == 0);
rc = policy_table_insert_before_last(&table, &parsed);
check_condition("preapprove insert_before_last succeeds", rc == 0);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("preapprove allows matching caller+role", rc == POLICY_ALLOW);
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL);
rc = policy_check(&table, "uid:2000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("preapprove leaves non-matching caller at POLICY_PROMPT", rc == POLICY_PROMPT);
/* Exact caller, verb, role, purpose + never => allow */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("exact match returns POLICY_ALLOW", rc == POLICY_ALLOW);
/* Wildcard caller + deny => deny */
policy_table_init(&table);
add_single_entry(&table, "*", "sign_event", "main", "nostr", PROMPT_DENY);
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "*", "nostr_sign_event", "main", "nostr", PROMPT_DENY);
rc = policy_check(&table, "uid:2000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("wildcard caller with deny returns POLICY_DENY", rc == POLICY_DENY);
/* Caller no match => POLICY_NO_MATCH */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:9999", "nostr_sign_event", "main", "nostr", NULL);
check_condition("caller mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Verb not in list => no match */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "get_public_key", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("verb mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Role not in list => no match */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "throwaway", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "throwaway", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("role mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Purpose not in list => no match */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "bitcoin", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "bitcoin", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("purpose mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Empty verbs list means all verbs */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", NULL, "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "nip44_encrypt", "main", "nostr", NULL);
rc = policy_check(&table, "uid:1000", "nostr_nip44_encrypt", "main", "nostr", NULL);
check_condition("empty verbs list matches any verb", rc == POLICY_ALLOW);
/* prompt=first_per_boot => POLICY_PROMPT */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_FIRST_PER_BOOT);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_FIRST_PER_BOOT);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("first_per_boot returns POLICY_PROMPT", rc == POLICY_PROMPT);
/* prompt=every_request => POLICY_PROMPT */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_EVERY_REQUEST);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_EVERY_REQUEST);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("every_request returns POLICY_PROMPT", rc == POLICY_PROMPT);
/* First matching entry wins */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_DENY);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", NULL);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_DENY);
add_single_entry(&table, "uid:1000", "nostr_sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", NULL);
check_condition("first matching entry wins", rc == POLICY_DENY);
/* Verify out_source for preapprove, session-grant, and default catch-all */
@@ -931,15 +932,15 @@ int main(void) {
check_condition("source test default catch-all add", rc == 0);
src = POLICY_SOURCE_DEFAULT;
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr", &src);
rc = policy_check(&table, "uid:1000", "nostr_sign_event", "main", "nostr", &src);
check_condition("policy_check reports preapprove source", rc == POLICY_ALLOW && src == POLICY_SOURCE_PREAPPROVE);
src = POLICY_SOURCE_DEFAULT;
rc = policy_check(&table, "uid:1001", "sign_event", "main", "nostr", &src);
rc = policy_check(&table, "uid:1001", "nostr_sign_event", "main", "nostr", &src);
check_condition("policy_check reports session-grant source", rc == POLICY_ALLOW && src == POLICY_SOURCE_SESSION_GRANT);
src = POLICY_SOURCE_PREAPPROVE;
rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr", &src);
rc = policy_check(&table, "uid:9999", "nostr_sign_event", "main", "nostr", &src);
check_condition("policy_check resets source for prompt/default", rc == POLICY_PROMPT && src == POLICY_SOURCE_DEFAULT);
}

View File

@@ -148,12 +148,13 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);

View File

@@ -145,18 +145,14 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
@@ -539,16 +535,23 @@ int main(void) {
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\"}]}");
"{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"secp256k1\",\"index\":0}]}");
check_condition("secp256k1 get_public_key returns a response", resp != NULL);
/* Algorithm-based get_public_key always returns a structured object. */
pr = parse_result(resp, &result_str, &result_obj);
check_condition("secp256k1 get_public_key result is a plain string (not object)",
pr == 1 && result_str != NULL);
check_condition("secp256k1 plain hex result is 64 hex chars",
result_str != NULL && strlen(result_str) == 64);
check_condition("secp256k1 plain hex result has no 'algorithm' field",
!response_has(resp, "algorithm"));
check_condition("secp256k1 get_public_key result is a JSON object",
pr == 2 && result_obj != NULL);
if (result_obj != NULL) {
cJSON *pk_item = cJSON_GetObjectItemCaseSensitive(result_obj, "public_key");
check_condition("secp256k1 structured has public_key (64 hex chars)",
pk_item != NULL && cJSON_IsString(pk_item) &&
strlen(pk_item->valuestring) == 64);
} else {
check_condition("secp256k1 structured has public_key (64 hex chars)", 0);
}
check_condition("secp256k1 structured has algorithm field",
response_has(resp, "algorithm"));
free(result_str);
cJSON_Delete(result_obj);
@@ -584,7 +587,7 @@ int main(void) {
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"2\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"main\",\"format\":\"structured\"}]}");
"{\"id\":\"2\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"secp256k1\",\"index\":0,\"format\":\"structured\"}]}");
check_condition("secp256k1 structured get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
@@ -645,7 +648,7 @@ int main(void) {
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"3\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"ssh_main\"}]}");
"{\"id\":\"3\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ed25519\",\"index\":0}]}");
check_condition("ed25519 get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
@@ -710,7 +713,7 @@ int main(void) {
check_condition("ML-DSA-65 sizes available", sz != NULL);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"4\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"pq_sig\"}]}");
"{\"id\":\"4\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-dsa-65\",\"index\":0}]}");
check_condition("ML-DSA-65 get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);
@@ -775,7 +778,7 @@ int main(void) {
check_condition("ML-KEM-768 sizes available", sz != NULL);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"5\",\"method\":\"get_public_key\",\"params\":[{\"role\":\"kem_main\"}]}");
"{\"id\":\"5\",\"method\":\"get_public_key\",\"params\":[{\"algorithm\":\"ml-kem-768\",\"index\":0}]}");
check_condition("ML-KEM-768 get_public_key returns a response", resp != NULL);
pr = parse_result(resp, &result_str, &result_obj);

View File

@@ -304,7 +304,7 @@ int main(void) {
check_condition("nostr_init for auth signing", nostr_init() == 0);
/* no auth => legacy qubes:personal preapprove should match */
if (run_qrexec_once("{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}", &resp) == 0) {
if (run_qrexec_once("{\"id\":\"1\",\"method\":\"nostr_get_public_key\",\"params\":[\"\"]}", &resp) == 0) {
check_condition("qrexec optional/no-auth still allows legacy qubes caller preapprove",
strstr(resp, "\"result\":") != NULL);
free(resp);
@@ -313,7 +313,7 @@ int main(void) {
check_condition("qrexec optional/no-auth request roundtrip", 0);
}
auth_req = build_auth_request(privkey, "2", "get_public_key");
auth_req = build_auth_request(privkey, "2", "nostr_get_public_key");
check_condition("build qrexec auth request", auth_req != NULL);
if (auth_req != NULL) {
if (run_qrexec_once(auth_req, &resp) == 0) {
@@ -328,7 +328,7 @@ int main(void) {
auth_req = NULL;
}
auth_req = build_auth_request(privkey, "3", "get_public_key");
auth_req = build_auth_request(privkey, "3", "nostr_get_public_key");
check_condition("build second qrexec auth request", auth_req != NULL);
if (auth_req != NULL) {
cJSON *root = cJSON_Parse(auth_req);
@@ -338,7 +338,7 @@ int main(void) {
if (root != NULL) {
method_item = cJSON_GetObjectItemCaseSensitive(root, "method");
if (cJSON_IsString(method_item)) {
cJSON_SetValuestring(method_item, "sign_event");
cJSON_SetValuestring(method_item, "nostr_sign_event");
}
tampered = cJSON_PrintUnformatted(root);
cJSON_Delete(root);

View File

@@ -223,19 +223,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/

View File

@@ -8,8 +8,8 @@
* - SLH-DSA-128s sign/verify with wrong key: verify fails
* - DRBG determinism: same seed -> same randombytes output
* - Integration: derive SLH-DSA-128s key from mnemonic via crypto_derive_one,
* sign data via sign_data verb through dispatcher, verify signature
* - Enforcement: sign_data allowed on pq-sig+slh-dsa-128s
* sign data via sign verb through dispatcher, verify signature
* - Enforcement: sign allowed on pq-sig+slh-dsa-128s
*/
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
@@ -142,19 +142,21 @@ const char *selector_strerror(int err);
#define ENFORCE_ERR_PURPOSE -1
#define ENFORCE_ERR_CURVE -2
#define ENFORCE_ERR_UNKNOWN_VERB -3
#define ENFORCE_ERR_ALGORITHM -4
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#define VERB_MINE_EVENT "mine_event"
#define VERB_SIGN_DATA "sign_data"
#define VERB_VERIFY_SIG "verify_signature"
#define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#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_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"
int enforce_verb_role(const char *verb, const role_entry_t *role);
const char *enforce_strerror(int err);
@@ -682,7 +684,7 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: sign_data via dispatcher ---- */
/* ---- Integration: sign via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
@@ -705,16 +707,16 @@ int main(void) {
dispatcher_init(&dispatcher, &table, &mnemonic_state, &g_key_store, &g_alg_key_cache);
/* sign_data request */
/* sign request */
snprintf(request, sizeof(request),
"{\"id\":\"test1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"test1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}",
msg_hex);
resp = dispatcher_handle_request(&dispatcher, request);
check_condition("sign_data via dispatcher returns result",
check_condition("sign via dispatcher returns result",
resp != NULL && response_has(resp, "\"result\""));
check_condition("sign_data result contains signature",
check_condition("sign result contains signature",
resp != NULL && response_has(resp, "signature"));
check_condition("sign_data result contains algorithm slh-dsa-128s",
check_condition("sign result contains algorithm slh-dsa-128s",
resp != NULL && response_has(resp, "slh-dsa-128s"));
free(resp);
@@ -722,7 +724,7 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Integration: verify_signature roundtrip via dispatcher ---- */
/* ---- Integration: verify roundtrip via dispatcher ---- */
{
role_table_t table;
role_entry_t pq_role;
@@ -752,10 +754,10 @@ int main(void) {
/* Sign */
snprintf(sign_req, sizeof(sign_req),
"{\"id\":\"s1\",\"method\":\"sign_data\",\"params\":[\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"s1\",\"method\":\"sign\",\"params\":[\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}",
msg_hex);
sign_resp = dispatcher_handle_request(&dispatcher, sign_req);
check_condition("verify roundtrip: sign_data succeeds", sign_resp != NULL);
check_condition("verify roundtrip: sign succeeds", sign_resp != NULL);
/* Extract signature from result */
if (sign_resp != NULL) {
@@ -780,28 +782,28 @@ int main(void) {
/* Verify */
if (sig_str != NULL && verify_req != NULL) {
snprintf(verify_req, 20000,
"{\"id\":\"v1\",\"method\":\"verify_signature\",\"params\":[\"%s\",\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"v1\",\"method\":\"verify\",\"params\":[\"%s\",\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}",
msg_hex, sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature returns valid:true",
check_condition("verify returns valid:true",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "true"));
free(verify_resp);
/* Verify with wrong message */
snprintf(verify_req, 20000,
"{\"id\":\"v2\",\"method\":\"verify_signature\",\"params\":[\"00ff\",\"%s\",{\"role\":\"pq_main\"}]}",
"{\"id\":\"v2\",\"method\":\"verify\",\"params\":[\"00ff\",\"%s\",{\"algorithm\":\"slh-dsa-128s\",\"index\":0}]}",
sig_str);
verify_resp = dispatcher_handle_request(&dispatcher, verify_req);
check_condition("verify_signature wrong msg returns valid:false",
check_condition("verify wrong msg returns valid:false",
verify_resp != NULL && response_has(verify_resp, "valid") &&
response_has(verify_resp, "false"));
free(verify_resp);
free((void *)sig_str);
} else {
check_condition("verify_signature returns valid:true", 0);
check_condition("verify_signature wrong msg returns valid:false", 0);
check_condition("verify returns valid:true", 0);
check_condition("verify wrong msg returns valid:false", 0);
}
free(verify_req);
@@ -810,19 +812,23 @@ int main(void) {
mnemonic_unload(&mnemonic_state);
}
/* ---- Enforcement: sign_data on pq-sig+slh-dsa-128s ---- */
/* ---- Enforcement: sign on pq-sig+slh-dsa-128s ---- */
{
role_entry_t pq_sig = make_pq_sig_slh_dsa_128s_entry("pq", 0);
role_entry_t nostr_secp = make_nostr_secp_entry("nostr", 0);
check_condition("enforce sign_data + pq-sig/slh-dsa-128s -> OK",
enforce_verb_role(VERB_SIGN_DATA, &pq_sig) == ENFORCE_OK);
/* sign/verify are algorithm-based now. */
check_condition("enforce sign + slh-dsa-128s -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SLH_DSA_128S) == ENFORCE_OK);
check_condition("enforce sign_data + nostr/secp256k1 -> PURPOSE err",
enforce_verb_role(VERB_SIGN_DATA, &nostr_secp) == ENFORCE_ERR_PURPOSE);
check_condition("enforce sign + secp256k1 -> OK",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
check_condition("enforce verify_signature + pq-sig/slh-dsa-128s -> OK",
enforce_verb_role(VERB_VERIFY_SIG, &pq_sig) == ENFORCE_OK);
check_condition("enforce verify + slh-dsa-128s -> OK",
enforce_verb_algorithm(VERB_VERIFY, CRYPTO_ALG_SLH_DSA_128S) == ENFORCE_OK);
check_condition("enforce sign + ml-kem-768 -> ALGORITHM err",
enforce_verb_algorithm(VERB_SIGN, CRYPTO_ALG_ML_KEM_768) == ENFORCE_ERR_ALGORITHM);
}
/* ---- Cleanup ---- */

View File

@@ -220,19 +220,20 @@ const char *selector_strerror(int err);
/* Known verbs */
#define VERB_SIGN_EVENT "sign_event"
#define VERB_GET_PUBLIC_KEY "get_public_key"
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
#define VERB_NIP44_DECRYPT "nip44_decrypt"
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
#define VERB_NIP04_DECRYPT "nip04_decrypt"
#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_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"
/*
* Check whether `verb` is allowed to execute against `role`.
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
*
* The enforcement rules are:
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
*/