Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca18e1e42d | ||
|
|
b3421c3e40 | ||
|
|
96ab9741ef | ||
|
|
2af12868e2 | ||
|
|
a0a5987ffa | ||
|
|
0b0ec5eb1a | ||
|
|
0355744103 | ||
|
|
db274ce487 | ||
|
|
56f37e092d | ||
|
|
a017dc40e0 |
20
Makefile
20
Makefile
@@ -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,27 +234,27 @@ $(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
|
||||
$(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 $(SRC_DIR)/otp_pad.c libotppad/libotppad.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c -o $(TEST_ALGORITHM_API_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_algorithm_api.c $(SRC_DIR)/pq_crypto.c $(SRC_DIR)/pq_drbg.c $(PQCLEAN_SOURCES) $(SRC_DIR)/key_store.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/miner.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/policy.c $(SRC_DIR)/otp_pad.c libotppad/libotppad.c -o $(TEST_ALGORITHM_API_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_GET_PUBLIC_KEY_TARGET): $(EXAMPLES_DIR)/get_public_key_client.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
|
||||
15
api.md
Normal file
15
api.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# n_signer API
|
||||
|
||||
The complete, authoritative API reference is now in [`README.md`](README.md) §4 (API).
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
@@ -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,47 @@ 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 |
|
||||
| `derive` | secp256k1 | `HMAC-SHA256(privkey, data)` — key-derived MAC for opaque identifiers (`index` required) |
|
||||
| `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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -136,6 +136,7 @@ In addition to the role-based verbs above, the signer supports algorithm-based v
|
||||
- `encapsulate` — KEM encapsulation (params: `[peer_pubkey_hex, {algorithm}]`)
|
||||
- `decapsulate` — KEM decapsulation (params: `[ciphertext_hex, {algorithm, index}]`)
|
||||
- `derive_shared_secret` — ECDH key agreement (params: `[peer_pubkey_hex, {algorithm, index}]`)
|
||||
- `derive` — `HMAC-SHA256(privkey, data)` key-derived MAC (params: `[data, {algorithm:"secp256k1", index}]`; `index` required). Returns `{algorithm, key_id, digest}` where `digest` is 64 hex chars. Use for deterministic opaque identifiers (e.g. NIP-33 `d` tags) keyed by the derived private key.
|
||||
- `get_public_key` with `algorithm` parameter — returns structured JSON
|
||||
|
||||
Algorithm names: `secp256k1`, `ed25519`, `ml-dsa-65`, `slh-dsa-128s`, `x25519`, `ml-kem-768`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
538
plans/derive_hmac.md
Normal file
538
plans/derive_hmac.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# Plan: `derive` verb — HMAC-SHA256 from a derived private key
|
||||
|
||||
## Problem
|
||||
|
||||
sovereign_browser stores bookmarks as NIP-51 kind 30003 parameterized-replaceable
|
||||
events, one per folder. The `d` tag must be a **deterministic function of the
|
||||
folder path** so that multiple devices editing the same logical folder produce
|
||||
the same `d` tag and the relay's NIP-33 replaceability keeps only the latest
|
||||
event. A random `d` tag would break cross-device sync (each device would emit a
|
||||
new event for the same folder, with no dedup).
|
||||
|
||||
The browser currently computes the `d` tag locally as:
|
||||
|
||||
```
|
||||
hmac_key = HMAC-SHA256(privkey, "sovereign-browser/bookmarks-folder-id-v1")
|
||||
d = HMAC-SHA256(hmac_key, path) → 64 hex chars
|
||||
```
|
||||
|
||||
This only works when the privkey is in browser memory (login methods
|
||||
`generate` / `import`). When the signer is the **nsigner remote backend**, the
|
||||
privkey is held by n_signer and never exposed to the browser, so
|
||||
[`compute_hmac_key()`](../sovereign_browser/src/bookmarks.c:194) returns -1 and
|
||||
[`path_to_d_tag`](../sovereign_browser/src/bookmarks.c:221) falls back to
|
||||
**plaintext** path d tags — a privacy regression that leaks folder names to
|
||||
relays.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a single new algorithm-based verb to n_signer:
|
||||
|
||||
```
|
||||
derive(data) = HMAC-SHA256(privkey, data) → 64 hex chars
|
||||
```
|
||||
|
||||
- `privkey` is the secp256k1 private key derived on demand from the mnemonic at
|
||||
`(algorithm: "secp256k1", index: N)` via the existing
|
||||
[`alg_key_cache_derive`](src/key_store.c:1624).
|
||||
- `data` is an arbitrary caller-supplied UTF-8 string (or hex-encoded bytes).
|
||||
- Returns the 32-byte HMAC digest as 64 lowercase hex chars.
|
||||
|
||||
This is a **single-step** scheme. The browser will switch from its two-step
|
||||
scheme to single-step by concatenating the label and path into the data string
|
||||
before calling `derive`:
|
||||
|
||||
```
|
||||
d = derive("sovereign-browser/bookmarks-folder-id-v1:" + path)
|
||||
= HMAC-SHA256(privkey, "sovereign-browser/bookmarks-folder-id-v1:" + path)
|
||||
```
|
||||
|
||||
The label prefix provides domain separation (so the same path used by a
|
||||
different application produces a different d tag). This is cryptographically
|
||||
equivalent to the two-step scheme for the privacy properties that matter
|
||||
(determinism, opacity, per-user-ness, no label/path recovery from the hash).
|
||||
|
||||
### Why single-step over two-step
|
||||
|
||||
1. **Generic primitive.** `derive(data) = HMAC(privkey, data)` is a clean
|
||||
building block any caller can use for any purpose — bookmark d tags,
|
||||
per-resource identifiers, per-app secrets, etc. A two-step verb bakes a
|
||||
specific construction into the RPC.
|
||||
2. **No state across calls.** A two-step scheme where the browser calls
|
||||
`derive(LABEL)` then `derive(path)` cannot work because step 2 needs
|
||||
`hmac_key` as the HMAC **key**, not `privkey` — and a privkey-keyed-only
|
||||
primitive always uses `privkey` as the key. Single-step avoids this entirely.
|
||||
3. **Migration cost is acceptable.** The browser already has a migration pattern
|
||||
for legacy d tags (detect on load, re-publish with new d tag, kind-5 delete
|
||||
old). The same pattern handles the two-step → single-step transition.
|
||||
|
||||
### Migration of existing two-step bookmark events
|
||||
|
||||
Existing bookmark events on relays have `d = HMAC-SHA256(HMAC-SHA256(privkey, LABEL), path)`.
|
||||
After the switch, the browser will compute `d = HMAC-SHA256(privkey, LABEL + ":" + path)`,
|
||||
which is a different hash. On next `bookmarks_init`:
|
||||
|
||||
1. Fetch all kind 30003 events as today.
|
||||
2. For each event, decrypt content, read `path`.
|
||||
3. Compute the **new** single-step d tag for that path.
|
||||
4. If the event's `d` tag does **not** match the new d tag (i.e. it's an old
|
||||
two-step tag), re-publish the event with the new d tag and emit a kind-5
|
||||
deletion for the old event id.
|
||||
5. This is the same logic the browser already uses for legacy plaintext d tags
|
||||
(see [`plans/bookmarks-tree-view.md`](../sovereign_browser/plans/bookmarks-tree-view.md:61)
|
||||
§"Why this is safe and nostr-ish" — "Legacy events with plaintext `d = "General"`
|
||||
will be detected on load... decrypted, and re-published in the new HMAC-`d`
|
||||
format; the old events get a kind 5 deletion.").
|
||||
|
||||
The migration is **self-healing**: each device migrates the events it sees, and
|
||||
once all devices have upgraded, no old two-step d tags remain on relays.
|
||||
|
||||
## n_signer changes
|
||||
|
||||
### 1. New verb constant
|
||||
|
||||
[`src/enforcement.c`](src/enforcement.c:209) (and the headerless decls block in
|
||||
every other src file that carries it):
|
||||
|
||||
```c
|
||||
#define VERB_DERIVE "derive"
|
||||
```
|
||||
|
||||
Add it to the algorithm-based verb set in
|
||||
[`is_algorithm_verb()`](src/dispatcher.c:817):
|
||||
|
||||
```c
|
||||
return (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 ||
|
||||
strcmp(method, VERB_GET_PUBLIC_KEY) == 0 ||
|
||||
strcmp(method, VERB_DERIVE) == 0);
|
||||
```
|
||||
|
||||
### 2. Enforcement
|
||||
|
||||
[`enforce_verb_algorithm()`](src/enforcement.c:765): `derive` is valid for
|
||||
`secp256k1` only (the privkey is a 32-byte scalar suitable as an HMAC key; PQ
|
||||
private keys are not). Add:
|
||||
|
||||
```c
|
||||
/* derive: HMAC-SHA256(privkey, data). secp256k1 only (32-byte scalar key). */
|
||||
if (strcmp(verb, VERB_DERIVE) == 0) {
|
||||
if (alg == CRYPTO_ALG_SECP256K1) {
|
||||
return ENFORCE_OK;
|
||||
}
|
||||
return ENFORCE_ERR_ALGORITHM;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Dispatcher handler
|
||||
|
||||
[`handle_algorithm_verb()`](src/dispatcher.c:880): add a new branch after the
|
||||
existing `derive_shared_secret` branch. Request shape:
|
||||
|
||||
```json
|
||||
{"id":"...","method":"derive","params":["<data>",{"algorithm":"secp256k1","index":0}]}
|
||||
```
|
||||
|
||||
- `params[0]` = the data string (UTF-8).
|
||||
- `options.algorithm` = `"secp256k1"` (required).
|
||||
- `options.index` = derivation index (**required** — no default). The dispatcher
|
||||
returns error `-32602 missing_index` if `index` is absent.
|
||||
|
||||
Handler:
|
||||
|
||||
```c
|
||||
/* ---- derive (secp256k1 HMAC-SHA256) ---- */
|
||||
if (strcmp(method, VERB_DERIVE) == 0) {
|
||||
cJSON *data_item = cJSON_GetArrayItem(params_item, 0);
|
||||
cJSON *index_item = NULL;
|
||||
const char *data_str;
|
||||
const unsigned char *priv;
|
||||
unsigned char mac[32];
|
||||
char mac_hex[65];
|
||||
char *result;
|
||||
|
||||
if (!cJSON_IsString(data_item) || data_item->valuestring == NULL) {
|
||||
return make_error_response(id_str, -32602, "invalid_params");
|
||||
}
|
||||
data_str = data_item->valuestring;
|
||||
|
||||
/* index is required for derive (no default). */
|
||||
if (!cJSON_IsObject(options_item)) {
|
||||
return make_error_response(id_str, -32602, "missing_index");
|
||||
}
|
||||
index_item = cJSON_GetObjectItemCaseSensitive(options_item, "index");
|
||||
if (!cJSON_IsNumber(index_item)) {
|
||||
return make_error_response(id_str, -32602, "missing_index");
|
||||
}
|
||||
index = index_item->valueint;
|
||||
|
||||
/* Derive the secp256k1 key on demand. */
|
||||
if (alg_key_cache_derive(ctx->alg_key_cache, ctx->mnemonic, alg, index) != 0) {
|
||||
return make_error_response(id_str, -32602, "key_derivation_failed");
|
||||
}
|
||||
key_entry = alg_key_cache_get(ctx->alg_key_cache, alg, index);
|
||||
if (key_entry == NULL || !key_entry->valid) {
|
||||
return make_error_response(id_str, -32602, "key_derivation_failed");
|
||||
}
|
||||
priv = (const unsigned char *)key_entry->private_key.data;
|
||||
|
||||
/* HMAC-SHA256(privkey, data) */
|
||||
if (nostr_hmac_sha256(priv, sz->priv_key_len,
|
||||
(const unsigned char *)data_str, strlen(data_str),
|
||||
mac) != 0) {
|
||||
return make_error_response(id_str, -32602, "hmac_failed");
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(mac, 32, mac_hex);
|
||||
secure_memzero(mac, sizeof(mac));
|
||||
|
||||
result = build_alg_result_json(alg_name, key_entry->key_id,
|
||||
"digest", mac_hex);
|
||||
if (result == NULL) {
|
||||
return make_error_response(id_str, -32602, "invalid_params");
|
||||
}
|
||||
return make_success_response(id_str, result);
|
||||
}
|
||||
```
|
||||
|
||||
`nostr_hmac_sha256` is already available via `<nostr_core/utils.h>` (linked into
|
||||
n_signer through nostr_core_lib). `secure_memzero` clears the stack-local mac.
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"id":"...","result":"{\"algorithm\":\"secp256k1\",\"key_id\":\"<16hex>\",\"digest\":\"<64hex>\"}"}
|
||||
```
|
||||
|
||||
### 4. Headerless decls
|
||||
|
||||
The `VERB_DERIVE` define must be added to the `NSIGNER_HEADERLESS_DECLS_BEGIN`
|
||||
block in every file that carries it. The same block already exists in:
|
||||
- [`src/dispatcher.c`](src/dispatcher.c:209)
|
||||
- [`src/enforcement.c`](src/enforcement.c:210)
|
||||
- [`src/key_store.c`](src/key_store.c:208)
|
||||
- [`src/main.c`](src/main.c:215)
|
||||
- [`src/policy.c`](src/policy.c:209)
|
||||
- [`src/selector.c`](src/selector.c:209)
|
||||
- [`src/server.c`](src/server.c:215)
|
||||
- [`src/role_table.c`](src/role_table.c:212)
|
||||
- [`src/mnemonic.c`](src/mnemonic.c:209)
|
||||
- [`src/secure_mem.c`](src/secure_mem.c:211)
|
||||
- [`src/socket_name.c`](src/socket_name.c:211)
|
||||
- [`src/pq_crypto.c`](src/pq_crypto.c:222)
|
||||
- [`tests/test_*.c`](tests/) (each test file that carries the block)
|
||||
|
||||
### 5. Tests
|
||||
|
||||
Add a test in [`tests/test_algorithm_api.c`](tests/test_algorithm_api.c) (or a
|
||||
new `tests/test_derive.c`):
|
||||
|
||||
1. Load a known mnemonic, derive secp256k1 key at index 0.
|
||||
2. Call `derive` with `data = "test-data"`.
|
||||
3. Independently compute `HMAC-SHA256(privkey, "test-data")` using
|
||||
`nostr_hmac_sha256` and compare to the RPC result.
|
||||
4. Assert determinism: same input → same digest.
|
||||
5. Assert opaqueness: different inputs → different digests.
|
||||
6. Assert rejection for non-secp256k1 algorithms (e.g. `algorithm: "ed25519"`
|
||||
→ error 1010 `algorithm_not_supported_for_verb`).
|
||||
7. Assert rejection when mnemonic is not loaded (error 1006).
|
||||
|
||||
### 6. Documentation
|
||||
|
||||
- [`README.md`](README.md) §4.3 verb table: add `derive` row.
|
||||
- [`README.md`](README.md) §4.4 algorithms: note `derive` is secp256k1-only.
|
||||
- [`api.md`](api.md): add worked example.
|
||||
- [`client/README.md`](client/README.md): add `derive` to the verb table.
|
||||
- [`documents/CLIENT_IMPLEMENTATION.md`](documents/CLIENT_IMPLEMENTATION.md):
|
||||
add `derive` to the algorithm-based verb list with example request/response.
|
||||
|
||||
## nostr_core_lib client changes
|
||||
|
||||
[`nostr_core_lib/nostr_core/nostr_signer.h`](../sovereign_browser/nostr_core_lib/nostr_core/nostr_signer.h)
|
||||
and
|
||||
[`nostr_core_lib/nostr_core/nostr_signer.c`](../sovereign_browser/nostr_core_lib/nostr_core/nostr_signer.c):
|
||||
|
||||
Add a new high-level API:
|
||||
|
||||
```c
|
||||
/* Compute HMAC-SHA256(privkey, data) using the signer's derived secp256k1
|
||||
* private key. Returns the 32-byte digest as 64 lowercase hex chars + NUL.
|
||||
* For the local backend, computes directly. For the nsigner remote backend,
|
||||
* calls the "derive" verb.
|
||||
* data must be a NUL-terminated UTF-8 string.
|
||||
* Returns NOSTR_SUCCESS or an error code. */
|
||||
int nostr_signer_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]);
|
||||
```
|
||||
|
||||
### Local backend
|
||||
|
||||
```c
|
||||
static int signer_local_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
unsigned char mac[32];
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
if (nostr_hmac_sha256(signer->u.local.private_key, 32,
|
||||
(const unsigned char*)data, strlen(data),
|
||||
mac) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
nostr_bytes_to_hex(mac, 32, out_digest_hex);
|
||||
memset(mac, 0, sizeof(mac));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
```
|
||||
|
||||
### Remote (nsigner) backend
|
||||
|
||||
```c
|
||||
static int signer_remote_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
cJSON* params;
|
||||
cJSON* result = NULL;
|
||||
const char* result_str;
|
||||
int rc;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(data));
|
||||
|
||||
params = signer_remote_params_with_selector(params,
|
||||
signer->u.remote.role,
|
||||
signer->u.remote.has_nostr_index,
|
||||
signer->u.remote.nostr_index);
|
||||
if (params == NULL) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
/* The remote derive verb needs algorithm:"secp256k1" in the options. */
|
||||
{
|
||||
cJSON* opts = cJSON_GetArrayItem(params, cJSON_GetArraySize(params) - 1);
|
||||
if (opts != NULL && cJSON_IsObject(opts)) {
|
||||
cJSON_AddStringToObject(opts, "algorithm", "secp256k1");
|
||||
}
|
||||
}
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "derive", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
/* result is a JSON string: {"algorithm":"secp256k1","key_id":"...","digest":"<64hex>"} */
|
||||
if (!cJSON_IsString(result) || result->valuestring == NULL) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
{
|
||||
cJSON* parsed = cJSON_Parse(result->valuestring);
|
||||
cJSON* digest_item;
|
||||
const char* digest_str;
|
||||
cJSON_Delete(result);
|
||||
if (parsed == NULL) return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
digest_item = cJSON_GetObjectItemCaseSensitive(parsed, "digest");
|
||||
if (!cJSON_IsString(digest_item) || digest_item->valuestring == NULL ||
|
||||
strlen(digest_item->valuestring) != 64) {
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
digest_str = digest_item->valuestring;
|
||||
memcpy(out_digest_hex, digest_str, 64);
|
||||
out_digest_hex[64] = '\0';
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
```
|
||||
|
||||
**Note on the options object:** the existing
|
||||
`signer_remote_params_with_selector` appends a selector object as the last
|
||||
params element. For algorithm-based verbs, the selector object must also carry
|
||||
`algorithm` and `index`. The implementation must ensure the options object
|
||||
already exists (or create one) before adding `algorithm`. If
|
||||
`has_nostr_index` is false and `role` is empty, `signer_remote_params_with_selector`
|
||||
does not append an options object — in that case the derive handler must append
|
||||
one with just `algorithm`. This is a small extension to the helper or a local
|
||||
construction in `signer_remote_derive_hmac`.
|
||||
|
||||
### Test
|
||||
|
||||
[`nostr_core_lib/tests/nsigner_client_test.c`](../sovereign_browser/nostr_core_lib/tests/nsigner_client_test.c):
|
||||
add a test that calls `nostr_signer_derive_hmac` on a local signer with a known
|
||||
privkey + known data, and compares against a reference HMAC-SHA256 computed
|
||||
with `nostr_hmac_sha256` directly.
|
||||
|
||||
## sovereign_browser changes
|
||||
|
||||
### 1. Switch to single-step d tag
|
||||
|
||||
[`src/bookmarks.c`](../sovereign_browser/src/bookmarks.c:194): replace
|
||||
`compute_hmac_key` + `path_to_d_tag` with a single function that calls the
|
||||
signer:
|
||||
|
||||
```c
|
||||
/* New label prefix — domain-separated, baked into the data string. */
|
||||
#define BOOKMARKS_HMAC_KEY_LABEL "sovereign-browser/bookmarks-folder-id-v1"
|
||||
|
||||
static char *path_to_d_tag(const char *path) {
|
||||
if (path == NULL) path = "";
|
||||
|
||||
/* Build "LABEL:" + path */
|
||||
char *data = g_strdup_printf("%s:%s", BOOKMARKS_HMAC_KEY_LABEL, path);
|
||||
char digest_hex[65];
|
||||
int rc;
|
||||
|
||||
rc = nostr_signer_derive_hmac(g_signer, data, digest_hex);
|
||||
g_free(data);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
/* No signer or derive failed — fall back to plaintext (read-only mode). */
|
||||
return g_strdup(path);
|
||||
}
|
||||
return g_strdup(digest_hex);
|
||||
}
|
||||
```
|
||||
|
||||
Remove `g_hmac_key`, `g_have_hmac_key`, `compute_hmac_key`, and the
|
||||
`g_privkey` / `g_have_privkey` state (the privkey is no longer needed in
|
||||
browser memory — the signer holds it). This is a **security improvement**: the
|
||||
browser no longer carries the privkey in its own RAM when using the nsigner
|
||||
backend.
|
||||
|
||||
### 2. Migration of existing two-step d tags
|
||||
|
||||
In `bookmarks_init` (or the relay-fetch load path), after decrypting an event
|
||||
and reading its `path`:
|
||||
|
||||
1. Compute the new single-step d tag: `path_to_d_tag(path)`.
|
||||
2. Compare to the event's actual `d` tag.
|
||||
3. If they differ (old two-step tag), re-publish the event with the new d tag
|
||||
and emit a kind-5 deletion for the old event id.
|
||||
|
||||
This reuses the existing legacy-plaintext-d-tag migration logic. The detection
|
||||
predicate changes from "d tag is not 64 hex chars" to "d tag is 64 hex chars
|
||||
but does not match `path_to_d_tag(path)`".
|
||||
|
||||
### 3. Remove privkey plumbing
|
||||
|
||||
- [`src/main.c`](../sovereign_browser/src/main.c:69): remove `privkey_hex` from
|
||||
`g_state` (or stop populating it).
|
||||
- [`src/main.c`](../sovereign_browser/src/main.c:652): remove the
|
||||
`strncpy(g_state.privkey_hex, ...)` line.
|
||||
- [`src/bookmarks.h`](../sovereign_browser/src/bookmarks.h): change
|
||||
`bookmarks_init` signature to drop the `privkey_hex` parameter.
|
||||
- All callers of `bookmarks_init` updated.
|
||||
|
||||
### 4. Test
|
||||
|
||||
[`tests/test_bookmarks_tree.c`](../sovereign_browser/tests/test_bookmarks_tree.c):
|
||||
update `path_to_d_tag` mirror to the single-step scheme:
|
||||
|
||||
```c
|
||||
static char *path_to_d_tag(const unsigned char *privkey, const char *path) {
|
||||
char *data = g_strdup_printf("%s:%s", LABEL, path ? path : "");
|
||||
unsigned char mac[32];
|
||||
char *hex;
|
||||
if (nostr_hmac_sha256(privkey, 32,
|
||||
(const unsigned char*)data, strlen(data),
|
||||
mac) != 0) {
|
||||
g_free(data);
|
||||
return NULL;
|
||||
}
|
||||
g_free(data);
|
||||
hex = g_strdup_printf(/* 64 hex chars */ ...);
|
||||
return hex;
|
||||
}
|
||||
```
|
||||
|
||||
All existing tests (determinism, opaqueness, 64-hex format, is_hmac_d_tag,
|
||||
per-user-ness, empty path) pass unchanged — they test properties, not the
|
||||
specific construction.
|
||||
|
||||
## Architecture diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Browser as sovereign_browser
|
||||
participant Signer as n_signer
|
||||
participant Relay as Nostr relay
|
||||
|
||||
Note over Browser: User saves bookmark to "Work/Projects/Secret"
|
||||
Browser->>Signer: derive("sovereign-browser/bookmarks-folder-id-v1:Work/Projects/Secret", {algorithm:"secp256k1", index:0})
|
||||
Signer->>Signer: alg_key_cache_derive(secp256k1, 0)
|
||||
Signer->>Signer: HMAC-SHA256(privkey, data)
|
||||
Signer-->>Browser: {"digest":"<64hex>"}
|
||||
Browser->>Signer: nostr_nip44_encrypt(self_pubkey, JSON{path, bookmarks})
|
||||
Signer-->>Browser: ciphertext
|
||||
Browser->>Relay: publish kind 30003, d=<64hex>, content=<ciphertext>
|
||||
|
||||
Note over Browser: Other device starts up
|
||||
Browser->>Relay: fetch kind 30003 by pubkey
|
||||
Relay-->>Browser: events
|
||||
Browser->>Signer: nostr_nip44_decrypt(self_pubkey, content)
|
||||
Signer-->>Browser: JSON{path:"Work/Projects/Secret", bookmarks:[...]}
|
||||
Note over Browser: path recovered from decrypted content, not from d tag
|
||||
```
|
||||
|
||||
## File change summary
|
||||
|
||||
### n_signer (this repo)
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`src/enforcement.c`](src/enforcement.c) | Add `VERB_DERIVE` define + `derive` case in `enforce_verb_algorithm` (secp256k1 only) |
|
||||
| [`src/dispatcher.c`](src/dispatcher.c) | Add `VERB_DERIVE` to `is_algorithm_verb` + `derive` branch in `handle_algorithm_verb` |
|
||||
| [`src/key_store.c`](src/key_store.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/main.c`](src/main.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/policy.c`](src/policy.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/selector.c`](src/selector.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/server.c`](src/server.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/role_table.c`](src/role_table.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/mnemonic.c`](src/mnemonic.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/secure_mem.c`](src/secure_mem.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/socket_name.c`](src/socket_name.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`src/pq_crypto.c`](src/pq_crypto.c) | Add `VERB_DERIVE` define (headerless decls) |
|
||||
| [`tests/test_algorithm_api.c`](tests/test_algorithm_api.c) | Add `derive` test cases |
|
||||
| [`README.md`](README.md) | Add `derive` to verb table + algorithm notes |
|
||||
| [`api.md`](api.md) | Add `derive` worked example |
|
||||
| [`client/README.md`](client/README.md) | Add `derive` to verb table |
|
||||
| [`documents/CLIENT_IMPLEMENTATION.md`](documents/CLIENT_IMPLEMENTATION.md) | Add `derive` section |
|
||||
|
||||
### nostr_core_lib (sovereign_browser repo)
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`nostr_core/nostr_signer.h`](../sovereign_browser/nostr_core_lib/nostr_core/nostr_signer.h) | Add `nostr_signer_derive_hmac` declaration |
|
||||
| [`nostr_core/nostr_signer.c`](../sovereign_browser/nostr_core_lib/nostr_core/nostr_signer.c) | Add local + remote `derive_hmac` implementations |
|
||||
| [`tests/nsigner_client_test.c`](../sovereign_browser/nostr_core_lib/tests/nsigner_client_test.c) | Add `derive_hmac` test |
|
||||
|
||||
### sovereign_browser
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`src/bookmarks.c`](../sovereign_browser/src/bookmarks.c) | Replace two-step `compute_hmac_key` + `path_to_d_tag` with single-step `path_to_d_tag` calling `nostr_signer_derive_hmac`; add two-step→single-step migration on load; remove `g_privkey`/`g_hmac_key` state |
|
||||
| [`src/bookmarks.h`](../sovereign_browser/src/bookmarks.h) | Drop `privkey_hex` param from `bookmarks_init` |
|
||||
| [`src/main.c`](../sovereign_browser/src/main.c) | Remove `privkey_hex` from `g_state` + stop populating it; update `bookmarks_init` call |
|
||||
| [`tests/test_bookmarks_tree.c`](../sovereign_browser/tests/test_bookmarks_tree.c) | Update `path_to_d_tag` mirror to single-step scheme |
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Should `derive` accept hex-encoded binary data, or only UTF-8 strings?**
|
||||
Default: UTF-8 strings only (simpler, covers the bookmark use case). If a
|
||||
caller needs binary data, they hex-encode it and we add a `data_encoding`
|
||||
option later. Decision can be deferred.
|
||||
|
||||
2. **`index` is required.** Unlike other algorithm-based verbs that default
|
||||
`index` to 0, `derive` requires the caller to specify `index` explicitly.
|
||||
This forces conscious selection of which derived key to use as the HMAC key,
|
||||
avoiding accidental cross-identity d-tag collisions. The dispatcher returns
|
||||
`-32602 missing_index` if `index` is absent.
|
||||
|
||||
3. **Should `derive` be added to the `--preapprove` algorithm-based policy
|
||||
syntax?**
|
||||
Yes — it flows through the same `policy_check_algorithm` path as other
|
||||
algorithm-based verbs. Operators can pre-approve
|
||||
`caller=...,algorithm=secp256k1,index=0,verb=derive`. No code change needed
|
||||
beyond the verb define; policy matching is by string.
|
||||
80
plans/legacy_verb_aliases.md
Normal file
80
plans/legacy_verb_aliases.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 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
|
||||
|
||||
`n_signer` is being moved to a clean, unified API (see [`api.md`](../api.md)). The current implementation in [`src/dispatcher.c`](../src/dispatcher.c) still uses the legacy unprefixed verb names. This document captures the old → new mapping so the implementation can be updated in one pass. Since this is a new project, there are no external clients to migrate — the legacy names are an internal cleanup item, not a long-term compatibility surface.
|
||||
|
||||
## Goal
|
||||
|
||||
Rename the verbs in [`src/dispatcher.c`](../src/dispatcher.c) to match [`api.md`](../api.md), remove the legacy aliases, and delete the alias-routing logic. No backward-compatibility shim is needed.
|
||||
|
||||
## Old → New Verb Mapping
|
||||
|
||||
### Nostr protocol verbs (add `nostr_` prefix)
|
||||
|
||||
| Legacy verb | New verb |
|
||||
|---------------------|---------------------------|
|
||||
| `sign_event` | `nostr_sign_event` |
|
||||
| `mine_event` | `nostr_mine_event` |
|
||||
| `nip04_encrypt` | `nostr_nip04_encrypt` |
|
||||
| `nip04_decrypt` | `nostr_nip04_decrypt` |
|
||||
| `nip44_encrypt` | `nostr_nip44_encrypt` |
|
||||
| `nip44_decrypt` | `nostr_nip44_decrypt` |
|
||||
|
||||
The role-based `get_public_key` (with `nostr_index`/`role`/`role_path` selector) becomes `nostr_get_public_key`. The algorithm-based `get_public_key` (with `algorithm` option) stays `get_public_key`.
|
||||
|
||||
### Algorithm-based verb aliases (collapse into canonical names)
|
||||
|
||||
| Legacy verb | Canonical verb | Default algorithm (when `algorithm` omitted) |
|
||||
|---------------------|--------------------|----------------------------------------------|
|
||||
| `sign_data` | `sign` | (from role) |
|
||||
| `ssh_sign` | `sign` | `ed25519` |
|
||||
| `verify_signature` | `verify` | (from role) |
|
||||
| `kem_encapsulate` | `encapsulate` | `ml-kem-768` |
|
||||
| `kem_decapsulate` | `decapsulate` | `ml-kem-768` |
|
||||
|
||||
After the rename, callers must always supply `algorithm` explicitly — the "default algorithm" fallbacks are removed. This makes the algorithm-based verbs uniform: every call specifies its algorithm.
|
||||
|
||||
### OTP verb aliases (collapse into `encrypt`/`decrypt`)
|
||||
|
||||
| Legacy verb | Canonical verb | Required option |
|
||||
|-----------------|----------------|--------------------------|
|
||||
| `otp_encrypt` | `encrypt` | `{"algorithm":"otp"}` |
|
||||
| `otp_decrypt` | `decrypt` | `{"algorithm":"otp"}` |
|
||||
|
||||
The general `encrypt`/`decrypt` with `curve:"otp"` routing is replaced by `algorithm:"otp"`.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **[`src/dispatcher.c`](../src/dispatcher.c)** — rename the `VERB_*` string constants:
|
||||
- `VERB_SIGN_EVENT` → `"nostr_sign_event"`
|
||||
- `VERB_MINE_EVENT` → `"nostr_mine_event"`
|
||||
- `VERB_NIP04_ENCRYPT` → `"nostr_nip04_encrypt"`
|
||||
- `VERB_NIP04_DECRYPT` → `"nostr_nip04_decrypt"`
|
||||
- `VERB_NIP44_ENCRYPT` → `"nostr_nip44_encrypt"`
|
||||
- `VERB_NIP44_DECRYPT` → `"nostr_nip44_decrypt"`
|
||||
- Add `VERB_NOSTR_GET_PUBLIC_KEY` = `"nostr_get_public_key"`; split the current `get_public_key` handler into two branches based on whether `algorithm` is present (algorithm-based) or `nostr_index`/`role`/`role_path` is present (Nostr).
|
||||
|
||||
2. **Remove alias routing** — delete [`is_algorithm_verb()`](../src/dispatcher.c:829), [`canonical_alg_verb()`](../src/dispatcher.c:846), and the alias-fallthrough logic in [`dispatcher_handle_request()`](../src/dispatcher.c:1559). The verbs `sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`, `otp_encrypt`, `otp_decrypt` are no longer recognized.
|
||||
|
||||
3. **Remove `curve:"otp"` routing** — the `encrypt`/`decrypt` handler no longer special-cases `curve:"otp"`; OTP is selected via `algorithm:"otp"` like every other algorithm.
|
||||
|
||||
4. **Update tests** — [`tests/test_dispatcher.c`](../tests/test_dispatcher.c), [`tests/test_integration.c`](../tests/test_integration.c), [`tests/test_algorithm_api.c`](../tests/test_algorithm_api.c), and any other test that uses the legacy verb names. Rename all call sites to the new verbs and add `algorithm` explicitly where it was previously defaulted.
|
||||
|
||||
5. **Update examples and clients**:
|
||||
- [`client/demo_c99.c`](../client/demo_c99.c)
|
||||
- [`client/demo_javascript.js`](../client/demo_javascript.js)
|
||||
- [`client/demo_python.py`](../client/demo_python.py)
|
||||
- [`examples/`](../examples/) — all example files using legacy verb names
|
||||
- [`README.md`](../README.md) — the §5 summary and any inline examples
|
||||
|
||||
6. **Update policy/preapprove** — [`src/policy.c`](../src/policy.c) and the `--preapprove` CLI parsing in [`src/main.c`](../src/main.c) should accept the new verb names. The role-based preapprove examples in [`api.md`](../api.md) §6 already use the `nostr_` prefix.
|
||||
|
||||
## Verification
|
||||
|
||||
- `make dev && ./build/nsigner --version` builds clean.
|
||||
- `make test` — all tests pass with the new verb names.
|
||||
- Manual smoke test over HTTP: each verb in [`api.md`](../api.md) §3 responds as documented.
|
||||
- `grep -rn "sign_event\|mine_event\|nip04_\|nip44_\|sign_data\|ssh_sign\|verify_signature\|kem_encapsulate\|kem_decapsulate\|otp_encrypt\|otp_decrypt" src/ tests/ client/ examples/` returns no matches (all legacy names gone).
|
||||
151
plans/tui_display_connections_on_demand.md
Normal file
151
plans/tui_display_connections_on_demand.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Plan: Move Connection Instructions to On-Demand Display
|
||||
|
||||
## Problem
|
||||
|
||||
The Connections section in the running TUI is verbose and confusing. It mixes server addresses with client command examples, and when multiple transports are active (Unix + TCP + HTTP) it takes up a large chunk of the status screen, pushing the Roles and Activity sections down. The connection instructions are only needed once when a user wants to know how to reach the signer — they don't need to be permanently visible.
|
||||
|
||||
## Solution
|
||||
|
||||
Remove the Connections section from the default `render_status()` display. Add a new hotkey **`d`** (display connection instructions) that prints the connection info on demand, then returns to the normal status view on the next refresh/keystroke.
|
||||
|
||||
## Design
|
||||
|
||||
### Default status display (after change)
|
||||
|
||||
The Connections section is removed. The display shows: Roles, Activity, status line, hotkey menu. A one-line hint at the top reminds the user that `d` shows connection instructions.
|
||||
|
||||
```text
|
||||
================================================================================
|
||||
n_signer v0.0.53
|
||||
================================================================================
|
||||
> Main Menu
|
||||
|
||||
Press d for connection instructions
|
||||
|
||||
Roles
|
||||
Role Purpose Curve Selector
|
||||
-------------------- ------------ ------------ ------------
|
||||
main nostr secp256k1 nostr_index
|
||||
ops nostr secp256k1 nostr_index
|
||||
backup bitcoin secp256k1 role_path
|
||||
|
||||
Activity (latest first)
|
||||
16:03:11 allow caller=uid:1000 method=get_public_key role=main
|
||||
16:02:44 prompt caller=uid:1000 method=sign_event role=ops
|
||||
16:02:46 allow caller=uid:1000 method=sign_event role=ops
|
||||
15:59:10 deny caller=uid:1001 method=sign_event error=unauthorized
|
||||
|
||||
session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
|
||||
|
||||
l lock/reunlock
|
||||
r refresh
|
||||
a toggle auto-approve
|
||||
d display connections
|
||||
q/x quit
|
||||
```
|
||||
|
||||
### On-demand connection display (press `d`)
|
||||
|
||||
When the user presses `d`, the screen clears and shows **only** the connection instructions, with clear spacing between transports. Each transport block shows the **connection string** (the address/endpoint to reach), a blank line, then an **Example:** with the client command indented beneath. No "Server:" / "Client:" labels — just the address and an example. A footer tells the user how to return.
|
||||
|
||||
```text
|
||||
================================================================================
|
||||
n_signer v0.0.53
|
||||
================================================================================
|
||||
> Connection Instructions
|
||||
|
||||
|
||||
Unix socket
|
||||
|
||||
@nsigner_hairy_dog
|
||||
|
||||
Example:
|
||||
nsigner --socket-name nsigner_hairy_dog client '<json>'
|
||||
|
||||
Qrexec (bridge-source-trusted):
|
||||
qrexec-client-vm <target_qube> qubes.NsignerRpc
|
||||
|
||||
|
||||
FIPS
|
||||
|
||||
http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111
|
||||
|
||||
Example:
|
||||
curl -X POST http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111/ \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '<json>'
|
||||
|
||||
|
||||
HTTP
|
||||
|
||||
http://127.0.0.1:11112
|
||||
|
||||
Example:
|
||||
curl -X POST http://127.0.0.1:11112/ \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '<json>'
|
||||
|
||||
|
||||
OTP pad: 333e9902db839d9d... (offset 288 / 1048576 bytes)
|
||||
|
||||
session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
|
||||
|
||||
Press any key to return
|
||||
```
|
||||
|
||||
### Key differences from current display
|
||||
|
||||
1. **Spacing between transports** — each transport gets its own titled block with blank lines separating it from the next, instead of a flat list of `Server:` / `Client:` lines.
|
||||
2. **Connection string + example, no labels** — each block shows the bare connection string (e.g. `http://127.0.0.1:11112`), then an `Example:` with the client command indented beneath. No confusing "Server:" / "Client:" labels.
|
||||
3. **On-demand only** — the default running display no longer shows connections at all, just a one-line hint. The user presses `d` when they need the instructions, reads them, then presses any key to return.
|
||||
4. **OTP pad status** — shown at the bottom of the connection display (it's connection-related: which pad is bound and how much has been consumed).
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **[`src/main.c`](src/main.c) — `render_status()`** (line 1451):
|
||||
- Remove the Connections section (lines 1464-1471).
|
||||
- Add a one-line hint after the top frame: `tui_print("Press d for connection instructions");`
|
||||
- Add a blank line after the hint.
|
||||
|
||||
2. **[`src/main.c`](src/main.c) — new function `render_connections()`**:
|
||||
- Clears the screen and renders the top frame.
|
||||
- Iterates `g_connection_info` but formats it with the spaced layout shown above. Since `g_connection_info` already stores formatted strings like `"Server: unix @nsigner_hairy_dog"` and `" Client: nsigner --socket-name ... client '<json>'"`, either:
|
||||
- **Option A**: Reformat the stored strings into blocks by detecting `Server:` lines as transport boundaries and printing blank lines + indentation.
|
||||
- **Option B**: Store connection info in a structured form (transport type, server address, client command(s)) and render the block layout from the structured data.
|
||||
- Option B is cleaner but requires changing `connection_info_add()` and all its call sites. Option A is a smaller change. **Recommend Option A** for now — parse the existing flat list into blocks.
|
||||
- **FIPS section**: The connection string is the npub-based FIPS URL (`http://<npub>.fips:<port>`), which FIPS resolves to the signer's TCP endpoint. The example uses `curl` since FIPS makes the endpoint HTTP-reachable for clients with FIPS installed. The raw TCP bind address (`tcp:[::]:11111`) is not shown — it's an implementation detail; the npub URL is what clients use.
|
||||
- Print OTP pad status if bound.
|
||||
- Print the status line at the bottom.
|
||||
- Print "Press any key to return" footer.
|
||||
|
||||
3. **[`src/main.c`](src/main.c) — `g_main_menu_items`** (line 825):
|
||||
- Add `{"^_d^: display connections", 'd'}` to the menu array.
|
||||
|
||||
4. **[`src/main.c`](src/main.c) — event loop** (line ~3068, the stdin keystroke handler):
|
||||
- Add a case for `'d'`:
|
||||
- Call `render_connections()`.
|
||||
- Read one keystroke from stdin (blocking `read()`).
|
||||
- Call `render_status()` to return to the normal display.
|
||||
|
||||
5. **[`README.md`](README.md) §3.2**:
|
||||
- Update the example TUI layout to match the new default display (no Connections section, `d` in hotkeys).
|
||||
- Add a note that pressing `d` shows the connection instructions.
|
||||
|
||||
6. **[`api.md`](api.md) §5 Transports**:
|
||||
- No changes needed — the transport documentation is already separate from the TUI display.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`src/main.c`](src/main.c) | Remove Connections from `render_status()`, add `render_connections()`, add `d` hotkey + menu item + event loop case |
|
||||
| [`README.md`](README.md) | Update §3.2 example display to match new layout |
|
||||
| [`api.md`](api.md) | No changes |
|
||||
|
||||
## Verification
|
||||
|
||||
- `make dev` builds clean.
|
||||
- Start nsigner with all transports selected. Default display shows no Connections section, just the "Press d for connection instructions" hint.
|
||||
- Press `d` — connection instructions appear with clear spacing between transports.
|
||||
- Press any key — returns to normal status display.
|
||||
- All other hotkeys (`l`, `r`, `a`, `q`) still work.
|
||||
745
src/dispatcher.c
745
src/dispatcher.c
@@ -204,45 +204,35 @@ 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_DERIVE "derive"
|
||||
#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"
|
||||
|
||||
/*
|
||||
* 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).
|
||||
*/
|
||||
@@ -823,7 +813,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 ||
|
||||
@@ -831,24 +822,8 @@ 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 ||
|
||||
strcmp(method, VERB_DERIVE) == 0);
|
||||
}
|
||||
|
||||
/* Parse the algorithm string from the options object.
|
||||
@@ -907,7 +882,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;
|
||||
@@ -915,25 +889,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");
|
||||
}
|
||||
@@ -941,7 +904,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");
|
||||
@@ -966,7 +929,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");
|
||||
}
|
||||
@@ -985,7 +948,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;
|
||||
@@ -1089,7 +1052,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;
|
||||
@@ -1193,7 +1156,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;
|
||||
@@ -1254,7 +1217,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;
|
||||
@@ -1320,7 +1283,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;
|
||||
@@ -1388,27 +1351,91 @@ static char *handle_algorithm_verb(dispatcher_ctx_t *ctx, const char *id_str,
|
||||
return make_success_response(id_str, out);
|
||||
}
|
||||
|
||||
/* ---- derive (secp256k1 HMAC-SHA256) ----
|
||||
*
|
||||
* Computes HMAC-SHA256(privkey, data) where privkey is the secp256k1
|
||||
* private key derived on demand at (algorithm:"secp256k1", index:N).
|
||||
* Returns the 32-byte digest as 64 lowercase hex chars.
|
||||
*
|
||||
* `index` is REQUIRED (no default) — forces conscious key selection.
|
||||
* Request:
|
||||
* {"method":"derive","params":["<data>",{"algorithm":"secp256k1","index":N}]}
|
||||
* Response:
|
||||
* {"result":"{\"algorithm\":\"secp256k1\",\"key_id\":\"<16hex>\",\"digest\":\"<64hex>\"}"}
|
||||
*/
|
||||
if (strcmp(method, VERB_DERIVE) == 0) {
|
||||
cJSON *data_item = cJSON_GetArrayItem(params_item, 0);
|
||||
cJSON *index_item = NULL;
|
||||
const char *data_str;
|
||||
const unsigned char *priv;
|
||||
unsigned char mac[32];
|
||||
char mac_hex[65];
|
||||
char *result;
|
||||
|
||||
if (!cJSON_IsString(data_item) || data_item->valuestring == NULL) {
|
||||
return make_error_response(id_str, -32602, "invalid_params");
|
||||
}
|
||||
data_str = data_item->valuestring;
|
||||
|
||||
/* index is required for derive (no default). */
|
||||
if (!cJSON_IsObject(options_item)) {
|
||||
return make_error_response(id_str, -32602, "missing_index");
|
||||
}
|
||||
index_item = cJSON_GetObjectItemCaseSensitive(options_item, "index");
|
||||
if (!cJSON_IsNumber(index_item)) {
|
||||
return make_error_response(id_str, -32602, "missing_index");
|
||||
}
|
||||
index = index_item->valueint;
|
||||
|
||||
/* Derive the secp256k1 key on demand. */
|
||||
if (alg_key_cache_derive(ctx->alg_key_cache, ctx->mnemonic, alg, index) != 0) {
|
||||
return make_error_response(id_str, -32602, "key_derivation_failed");
|
||||
}
|
||||
key_entry = alg_key_cache_get(ctx->alg_key_cache, alg, index);
|
||||
if (key_entry == NULL || !key_entry->valid) {
|
||||
return make_error_response(id_str, -32602, "key_derivation_failed");
|
||||
}
|
||||
priv = (const unsigned char *)key_entry->private_key.data;
|
||||
|
||||
/* HMAC-SHA256(privkey, data) */
|
||||
if (nostr_hmac_sha256(priv, sz->priv_key_len,
|
||||
(const unsigned char *)data_str, strlen(data_str),
|
||||
mac) != 0) {
|
||||
return make_error_response(id_str, -32602, "hmac_failed");
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(mac, 32, mac_hex);
|
||||
secure_memzero(mac, sizeof(mac));
|
||||
|
||||
result = build_alg_result_json(alg_name, key_entry->key_id,
|
||||
"digest", mac_hex);
|
||||
if (result == NULL) {
|
||||
return make_error_response(id_str, -32602, "invalid_params");
|
||||
}
|
||||
return make_success_response(id_str, result);
|
||||
}
|
||||
|
||||
/* Should not reach here (is_algorithm_verb filtered). */
|
||||
return make_error_response(id_str, -32601, "method_not_found");
|
||||
}
|
||||
|
||||
/* ---- 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) {
|
||||
@@ -1431,7 +1458,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);
|
||||
@@ -1497,7 +1524,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;
|
||||
@@ -1571,7 +1598,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");
|
||||
@@ -1612,67 +1638,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);
|
||||
if (strcmp(method, VERB_ENCRYPT) == 0 ||
|
||||
strcmp(method, VERB_DECRYPT) == 0) {
|
||||
if (cJSON_IsObject(options_item)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
/* Other algorithms: not valid for encrypt/decrypt in the new API. */
|
||||
cJSON_Delete(root);
|
||||
return otp_response;
|
||||
return make_error_response(id_str, 1010, "algorithm_not_supported_for_verb");
|
||||
}
|
||||
|
||||
if (cJSON_IsObject(options_item)) {
|
||||
@@ -1751,64 +1749,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;
|
||||
|
||||
@@ -1848,14 +1801,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);
|
||||
@@ -1868,7 +1821,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;
|
||||
@@ -1922,7 +1875,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 ||
|
||||
@@ -1936,7 +1889,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 ||
|
||||
@@ -1950,7 +1903,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 ||
|
||||
@@ -1964,7 +1917,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 ||
|
||||
@@ -1978,407 +1931,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_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");
|
||||
}
|
||||
|
||||
@@ -202,43 +202,35 @@ 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_DERIVE "derive"
|
||||
#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"
|
||||
|
||||
/*
|
||||
* 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).
|
||||
*/
|
||||
@@ -710,69 +702,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) {
|
||||
@@ -780,14 +720,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;
|
||||
}
|
||||
|
||||
@@ -802,41 +743,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);
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
@@ -878,17 +803,22 @@ int enforce_verb_algorithm(const char *verb, crypto_alg_t alg) {
|
||||
return ENFORCE_ERR_ALGORITHM;
|
||||
}
|
||||
|
||||
/* Nostr verbs: always secp256k1 (protocol requirement). */
|
||||
if (is_nostr_verb(verb) || strcmp(verb, VERB_MINE_EVENT) == 0) {
|
||||
/* derive: HMAC-SHA256(privkey, data). secp256k1 only (32-byte scalar key). */
|
||||
if (strcmp(verb, VERB_DERIVE) == 0) {
|
||||
if (alg == CRYPTO_ALG_SECP256K1) {
|
||||
return ENFORCE_OK;
|
||||
}
|
||||
return ENFORCE_ERR_ALGORITHM;
|
||||
}
|
||||
|
||||
/* Nostr verbs: always secp256k1 (protocol requirement). */
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -210,20 +210,17 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
/* 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`.
|
||||
|
||||
332
src/main.c
332
src/main.c
@@ -214,6 +214,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -223,22 +224,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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 +762,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 48
|
||||
#define NSIGNER_VERSION "v0.0.48"
|
||||
#define NSIGNER_VERSION_PATCH 58
|
||||
#define NSIGNER_VERSION "v0.0.58"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
@@ -797,12 +800,24 @@ int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
||||
#define NSIGNER_QREXEC_SERVICE_NAME "qubes.NsignerRpc"
|
||||
#define ACTIVITY_LOG_CAP 16
|
||||
#define CONNECTION_INFO_CAP 8
|
||||
#define CONNECTION_INFO_STR_MAX 256
|
||||
|
||||
typedef struct {
|
||||
char lines[ACTIVITY_LOG_CAP][256];
|
||||
int count;
|
||||
} activity_log_t;
|
||||
|
||||
/* Structured connection info for the on-demand connection display.
|
||||
* Each entry represents one transport block: a title (e.g. "Unix socket"),
|
||||
* a connection string (the address/endpoint), an optional example command,
|
||||
* and an optional extra line (e.g. qrexec bridge info under Unix). */
|
||||
typedef struct {
|
||||
char title[64];
|
||||
char connection_string[CONNECTION_INFO_STR_MAX];
|
||||
char example[CONNECTION_INFO_STR_MAX]; /* empty if none */
|
||||
char extra[CONNECTION_INFO_STR_MAX]; /* empty if none */
|
||||
} connection_info_entry_t;
|
||||
|
||||
static volatile sig_atomic_t g_running = 1;
|
||||
static activity_log_t g_activity_log;
|
||||
static int g_auto_approve = 0;
|
||||
@@ -819,13 +834,16 @@ typedef struct {
|
||||
} mnemonic_source_t;
|
||||
|
||||
static mnemonic_source_kind_t g_startup_mnemonic_source_kind = MNEMONIC_SOURCE_TUI;
|
||||
static char g_connection_info[CONNECTION_INFO_CAP][192];
|
||||
static connection_info_entry_t g_connection_info[CONNECTION_INFO_CAP];
|
||||
static int g_connection_info_count = 0;
|
||||
/* OTP pad status line (shown at bottom of connection display) */
|
||||
static char g_otp_pad_status[CONNECTION_INFO_STR_MAX];
|
||||
|
||||
static const TuiMenuItem g_main_menu_items[] = {
|
||||
{"^_l^: lock/reunlock", 'l'},
|
||||
{"^_r^: refresh", 'r'},
|
||||
{"^_a^: toggle auto-approve", 'a'},
|
||||
{"^_d^: display connections", 'd'},
|
||||
{"^_q^:/x quit", 'q'}
|
||||
};
|
||||
|
||||
@@ -845,28 +863,49 @@ static void render_status(const role_table_t *role_table,
|
||||
int derived_count,
|
||||
const char *socket_name);
|
||||
|
||||
static void render_connections(const role_table_t *role_table,
|
||||
const mnemonic_state_t *mnemonic,
|
||||
int derived_count,
|
||||
const char *socket_name);
|
||||
|
||||
static void connection_info_clear(void) {
|
||||
g_connection_info_count = 0;
|
||||
memset(g_connection_info, 0, sizeof(g_connection_info));
|
||||
g_otp_pad_status[0] = '\0';
|
||||
}
|
||||
|
||||
static void connection_info_add(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
/* Add a transport block to the connection info.
|
||||
* title: transport name (e.g. "Unix socket", "FIPS", "HTTP")
|
||||
* connection_str: the address/endpoint clients use to reach the signer
|
||||
* example: example client command (empty string "" if none)
|
||||
* extra: optional extra line under the block (e.g. qrexec info; "" if none) */
|
||||
static void connection_info_add_transport(const char *title,
|
||||
const char *connection_str,
|
||||
const char *example,
|
||||
const char *extra) {
|
||||
connection_info_entry_t *e;
|
||||
|
||||
if (fmt == NULL || g_connection_info_count >= CONNECTION_INFO_CAP) {
|
||||
if (g_connection_info_count >= CONNECTION_INFO_CAP) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
(void)vsnprintf(g_connection_info[g_connection_info_count],
|
||||
sizeof(g_connection_info[g_connection_info_count]),
|
||||
fmt,
|
||||
ap);
|
||||
va_end(ap);
|
||||
|
||||
e = &g_connection_info[g_connection_info_count];
|
||||
memset(e, 0, sizeof(*e));
|
||||
if (title) { strncpy(e->title, title, sizeof(e->title) - 1); }
|
||||
if (connection_str) { strncpy(e->connection_string, connection_str, sizeof(e->connection_string) - 1); }
|
||||
if (example) { strncpy(e->example, example, sizeof(e->example) - 1); }
|
||||
if (extra) { strncpy(e->extra, extra, sizeof(e->extra) - 1); }
|
||||
g_connection_info_count++;
|
||||
}
|
||||
|
||||
/* Set the OTP pad status line (shown at bottom of connection display). */
|
||||
static void connection_info_set_otp(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
if (fmt == NULL) { g_otp_pad_status[0] = '\0'; return; }
|
||||
va_start(ap, fmt);
|
||||
(void)vsnprintf(g_otp_pad_status, sizeof(g_otp_pad_status), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static void handle_signal(int sig) {
|
||||
(void)sig;
|
||||
g_running = 0;
|
||||
@@ -1440,7 +1479,11 @@ static void role_table_get_cell(int row, int col, char *out, size_t out_size, vo
|
||||
(void)snprintf(out, out_size, "%s", role_curve_to_str(r->curve));
|
||||
break;
|
||||
case 3:
|
||||
(void)snprintf(out, out_size, "%s", (r->selector_type == SELECTOR_NOSTR_INDEX) ? "nostr_index" : "role_path");
|
||||
if (r->selector_type == SELECTOR_NOSTR_INDEX) {
|
||||
(void)snprintf(out, out_size, "m/44'/1237'/%d'/0/0", r->nostr_index);
|
||||
} else {
|
||||
(void)snprintf(out, out_size, "%s", r->role_path);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
out[0] = '\0';
|
||||
@@ -1448,6 +1491,104 @@ static void role_table_get_cell(int row, int col, char *out, size_t out_size, vo
|
||||
}
|
||||
}
|
||||
|
||||
/* On-demand connection instructions display (press 'd').
|
||||
* Shows each transport as a titled block with the connection string
|
||||
* and an example client command, with blank lines between transports. */
|
||||
static void render_connections(const role_table_t *role_table,
|
||||
const mnemonic_state_t *mnemonic,
|
||||
int derived_count,
|
||||
const char *socket_name) {
|
||||
TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Connection Instructions" };
|
||||
TuiSize size = tui_terminal_size();
|
||||
char status_buf[256];
|
||||
int i;
|
||||
|
||||
/* Use a full screen clear (not tui_clear_continuous) because the
|
||||
* connections display is a modal view that may exceed the terminal
|
||||
* height. tui_clear_continuous assumes the content fits within
|
||||
* term_height lines; if it doesn't, the extra lines scroll past and
|
||||
* the cursor-up can't reach them. A full clear + home cursor works
|
||||
* regardless of content height. */
|
||||
printf("\033[2J\033[H");
|
||||
fflush(stdout);
|
||||
tui_render_top_frame(&frame, size.width);
|
||||
tui_print("");
|
||||
|
||||
for (i = 0; i < g_connection_info_count; i++) {
|
||||
connection_info_entry_t *e = &g_connection_info[i];
|
||||
|
||||
/* Transport title */
|
||||
tui_print("^*%s^:", e->title);
|
||||
tui_print("");
|
||||
|
||||
/* Connection string (indented) */
|
||||
tui_print(" %s", e->connection_string);
|
||||
tui_print("");
|
||||
|
||||
/* Example command (if present) */
|
||||
if (e->example[0] != '\0') {
|
||||
tui_print(" Example:");
|
||||
/* The example may contain embedded newlines for multi-line commands */
|
||||
{
|
||||
const char *p = e->example;
|
||||
const char *nl;
|
||||
while (*p != '\0') {
|
||||
nl = strchr(p, '\n');
|
||||
if (nl != NULL) {
|
||||
tui_print(" %.*s", (int)(nl - p), p);
|
||||
p = nl + 1;
|
||||
} else {
|
||||
tui_print(" %s", p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
/* Extra info (if present, e.g. qrexec bridge) */
|
||||
if (e->extra[0] != '\0') {
|
||||
const char *p = e->extra;
|
||||
const char *nl;
|
||||
while (*p != '\0') {
|
||||
nl = strchr(p, '\n');
|
||||
if (nl != NULL) {
|
||||
tui_print(" %.*s", (int)(nl - p), p);
|
||||
p = nl + 1;
|
||||
} else {
|
||||
tui_print(" %s", p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
/* OTP pad status */
|
||||
if (g_otp_pad_status[0] != '\0') {
|
||||
tui_print("%s", g_otp_pad_status);
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
/* Status line */
|
||||
(void)snprintf(status_buf,
|
||||
sizeof(status_buf),
|
||||
"session=%s (%d words) signer=%s derived=%d auto-approve=%s",
|
||||
mnemonic_is_loaded(mnemonic) ? "unlocked" : "locked",
|
||||
(mnemonic != NULL) ? mnemonic->word_count : 0,
|
||||
(socket_name != NULL) ? socket_name : "(none)",
|
||||
derived_count,
|
||||
g_auto_approve ? "ON" : "OFF");
|
||||
tui_print("%s", status_buf);
|
||||
tui_print("");
|
||||
tui_print("Press any key to return");
|
||||
|
||||
tui_anchor_prompt(0, 0);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void render_status(const role_table_t *role_table,
|
||||
const mnemonic_state_t *mnemonic,
|
||||
int derived_count,
|
||||
@@ -1461,16 +1602,6 @@ static void render_status(const role_table_t *role_table,
|
||||
tui_clear_continuous(size.height);
|
||||
tui_render_top_frame(&frame, size.width);
|
||||
|
||||
tui_print("^*Connections^:");
|
||||
if (g_connection_info_count == 0) {
|
||||
tui_print("(none)");
|
||||
} else {
|
||||
for (int i = 0; i < g_connection_info_count; ++i) {
|
||||
tui_print("%s", g_connection_info[i]);
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("");
|
||||
tui_print("^*Roles^:");
|
||||
if (role_table == NULL || role_table->count == 0) {
|
||||
tui_print("(none)");
|
||||
@@ -1479,7 +1610,7 @@ static void render_status(const role_table_t *role_table,
|
||||
{"Role", 20, 0},
|
||||
{"Purpose", 12, 0},
|
||||
{"Curve", 12, 0},
|
||||
{"Selector", 12, 0}
|
||||
{"Derivation path", 24, 0}
|
||||
};
|
||||
role_table_view_data_t view;
|
||||
TuiTable table;
|
||||
@@ -2325,7 +2456,7 @@ int main(int argc, char *argv[]) {
|
||||
server_ctx_t server;
|
||||
key_store_t key_store;
|
||||
static algorithm_key_cache_t alg_key_cache;
|
||||
struct pollfd pfds[3]; /* max: 2 listeners (unix+tcp) + stdin */
|
||||
struct pollfd pfds[4]; /* max: 3 listeners (unix+tcp+http) + stdin */
|
||||
uid_t owner_uid;
|
||||
int derived_count;
|
||||
const char *socket_name = NSIGNER_DEFAULT_SOCKET_NAME;
|
||||
@@ -2886,71 +3017,122 @@ int main(int argc, char *argv[]) {
|
||||
if (multi_listen) {
|
||||
/* Multi-listener mode: show all active transports */
|
||||
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
|
||||
connection_info_add("Server: unix @%s%s", socket_name,
|
||||
bridge_source_trusted ? " (bridge-source-trusted)" : "");
|
||||
connection_info_add(" Client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||
char unix_conn[128];
|
||||
char unix_example[256];
|
||||
char unix_extra[256] = "";
|
||||
snprintf(unix_conn, sizeof(unix_conn), "@%s", socket_name);
|
||||
snprintf(unix_example, sizeof(unix_example),
|
||||
"nsigner --socket-name %s client '<json>'", socket_name);
|
||||
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
|
||||
connection_info_add(" Qrexec: qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
snprintf(unix_extra, sizeof(unix_extra),
|
||||
"Qrexec (bridge-source-trusted):\n"
|
||||
" qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||
}
|
||||
connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
}
|
||||
if (transport_mask & TRANSPORT_TCP) {
|
||||
connection_info_add("Server: FIPS/TCP [::]:11111");
|
||||
connection_info_add(" Client: nsigner --listen tcp:[::]:11111 client '<json>'");
|
||||
printf("System is ready and waiting for connections on tcp:[::]:11111.\n");
|
||||
const char *tcp_addr = servers[tcp_server_idx].socket_name;
|
||||
char fips_conn[256] = "";
|
||||
char fips_example[256] = "";
|
||||
/* If we have a FIPS identity, show the npub-based URL */
|
||||
if (have_fips_identity &&
|
||||
extract_listen_port(tcp_addr, listen_port, sizeof(listen_port)) == 0) {
|
||||
snprintf(fips_conn, sizeof(fips_conn),
|
||||
"http://%s.fips:%s", fips_npub, listen_port);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"curl -X POST http://%s.fips:%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", fips_npub, listen_port);
|
||||
printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port);
|
||||
} else {
|
||||
snprintf(fips_conn, sizeof(fips_conn), "%s", tcp_addr);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"nsigner --listen %s client '<json>'", tcp_addr);
|
||||
}
|
||||
connection_info_add_transport("FIPS", fips_conn, fips_example, "");
|
||||
printf("System is ready and waiting for connections on %s.\n", tcp_addr);
|
||||
}
|
||||
if (transport_mask & TRANSPORT_HTTP) {
|
||||
connection_info_add("Server: HTTP 127.0.0.1:11111");
|
||||
connection_info_add(" Client: curl -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' -d '<json>'");
|
||||
printf("System is ready and waiting for connections on http://127.0.0.1:11111.\n");
|
||||
const char *http_addr = servers[http_server_idx].socket_name;
|
||||
char http_conn[256];
|
||||
char http_example[256];
|
||||
snprintf(http_conn, sizeof(http_conn), "http://%s", http_addr + 5);
|
||||
snprintf(http_example, sizeof(http_example),
|
||||
"curl -X POST http://%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", http_addr + 5);
|
||||
connection_info_add_transport("HTTP", http_conn, http_example, "");
|
||||
printf("System is ready and waiting for connections on http://%s.\n", http_addr + 5);
|
||||
}
|
||||
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
connection_info_add("Server: unix @%s", socket_name);
|
||||
connection_info_add(" Client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||
char unix_conn[128];
|
||||
char unix_example[256];
|
||||
char unix_extra[256] = "";
|
||||
snprintf(unix_conn, sizeof(unix_conn), "@%s", socket_name);
|
||||
snprintf(unix_example, sizeof(unix_example),
|
||||
"nsigner --socket-name %s client '<json>'", socket_name);
|
||||
if (bridge_source_trusted) {
|
||||
connection_info_add(" Qrexec: qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
snprintf(unix_extra, sizeof(unix_extra),
|
||||
"Qrexec (bridge-source-trusted):\n"
|
||||
" qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||
}
|
||||
connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
connection_info_add("Server: %s", listen_target);
|
||||
connection_info_add(" Client: nsigner --listen %s client '<json>'", listen_target);
|
||||
printf("System is ready and waiting for connections on %s.\n", listen_target);
|
||||
const char *actual_addr = server.socket_name;
|
||||
char fips_conn[256] = "";
|
||||
char fips_example[256] = "";
|
||||
if (have_fips_identity &&
|
||||
extract_listen_port(actual_addr, listen_port, sizeof(listen_port)) == 0) {
|
||||
snprintf(fips_conn, sizeof(fips_conn),
|
||||
"http://%s.fips:%s", fips_npub, listen_port);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"curl -X POST http://%s.fips:%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", fips_npub, listen_port);
|
||||
printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port);
|
||||
} else {
|
||||
snprintf(fips_conn, sizeof(fips_conn), "%s", actual_addr);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"nsigner --listen %s client '<json>'", actual_addr);
|
||||
}
|
||||
connection_info_add_transport("FIPS", fips_conn, fips_example, "");
|
||||
printf("System is ready and waiting for connections on %s.\n", actual_addr);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_HTTP) {
|
||||
connection_info_add("Server: %s", listen_target);
|
||||
connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d '<json>'",
|
||||
listen_target + 5); /* skip "http:" prefix */
|
||||
printf("System is ready and waiting for connections on %s.\n", listen_target);
|
||||
const char *actual_addr = server.socket_name;
|
||||
char http_conn[256];
|
||||
char http_example[256];
|
||||
snprintf(http_conn, sizeof(http_conn), "http://%s", actual_addr + 5);
|
||||
snprintf(http_example, sizeof(http_example),
|
||||
"curl -X POST http://%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", actual_addr + 5);
|
||||
connection_info_add_transport("HTTP", http_conn, http_example, "");
|
||||
printf("System is ready and waiting for connections on %s.\n", actual_addr);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_QREXEC) {
|
||||
connection_info_add("Server: qrexec (one request per invocation)");
|
||||
connection_info_add_transport("Qrexec", "qrexec (one request per invocation)", "", "");
|
||||
printf("System is ready and waiting for a qrexec request.\n");
|
||||
} else {
|
||||
connection_info_add("Server: stdio (one request per invocation)");
|
||||
connection_info_add_transport("Stdio", "stdio (one request per invocation)", "", "");
|
||||
printf("System is ready and waiting for a stdio request.\n");
|
||||
}
|
||||
|
||||
/* Show OTP pad status if bound. */
|
||||
if (otp_pad_is_bound()) {
|
||||
connection_info_add("OTP pad: %s (offset %llu / %llu bytes)",
|
||||
otp_pad_chksum(),
|
||||
(unsigned long long)otp_pad_current_offset(),
|
||||
(unsigned long long)otp_pad_size());
|
||||
connection_info_set_otp("OTP pad: %s (offset %llu / %llu bytes)",
|
||||
otp_pad_chksum(),
|
||||
(unsigned long long)otp_pad_current_offset(),
|
||||
(unsigned long long)otp_pad_size());
|
||||
}
|
||||
|
||||
if (have_fips_identity) {
|
||||
connection_info_add("fips ipv6: %s", fips_ipv6);
|
||||
connection_info_add("fips npub: %s", fips_npub);
|
||||
printf("fips ipv6: %s\n", fips_ipv6);
|
||||
printf("fips npub: %s\n", fips_npub);
|
||||
|
||||
if (listen_mode == NSIGNER_LISTEN_TCP &&
|
||||
extract_listen_port(listen_target, listen_port, sizeof(listen_port)) == 0) {
|
||||
connection_info_add("fips address: http://%s.fips:%s", fips_npub, listen_port);
|
||||
printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port);
|
||||
}
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
@@ -3072,6 +3254,16 @@ int main(int argc, char *argv[]) {
|
||||
g_auto_approve = g_auto_approve ? 0 : 1;
|
||||
server_set_prompt_always_allow(g_auto_approve);
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
} else if (lower == 'd') {
|
||||
/* Display connection instructions on demand.
|
||||
* Show the connections screen, wait for any key, then
|
||||
* return to the normal status display. */
|
||||
render_connections(&role_table, &mnemonic, derived_count, socket_name);
|
||||
{
|
||||
char dch = '\0';
|
||||
(void)read(STDIN_FILENO, &dch, 1);
|
||||
}
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
} else if (lower == 'l') {
|
||||
mnemonic_source_t relock_source;
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
17
src/policy.c
17
src/policy.c
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -221,6 +221,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -230,22 +231,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -211,6 +211,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -220,22 +221,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -210,6 +210,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -219,22 +220,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
156
src/server.c
156
src/server.c
@@ -214,6 +214,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -223,22 +224,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
@@ -1457,6 +1460,8 @@ int server_start(server_ctx_t *ctx) {
|
||||
uint16_t port;
|
||||
char host[64];
|
||||
int one = 1;
|
||||
int bind_attempt;
|
||||
int bound = 0;
|
||||
|
||||
/* HTTP mode uses the same TCP socket setup, just with an "http:" prefix
|
||||
* instead of "tcp:". Convert to tcp: for parse_tcp_target. */
|
||||
@@ -1476,54 +1481,95 @@ int server_start(server_ctx_t *ctx) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = socket(family, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"socket(tcp) failed: %s",
|
||||
strerror(errno));
|
||||
/* Try the requested port, then increment up to 5 times if it's in use.
|
||||
* This handles both real conflicts and false positives (e.g. when TCP
|
||||
* [::]:11111 and HTTP 127.0.0.1:11111 are started together in
|
||||
* multi-listen mode, the second bind can fail with EADDRINUSE even
|
||||
* though no other process holds the port). */
|
||||
for (bind_attempt = 0; bind_attempt < 5 && !bound; ++bind_attempt) {
|
||||
uint16_t try_port = (uint16_t)(port + bind_attempt);
|
||||
if (try_port < port) {
|
||||
/* port wrapped past 65535 — stop retrying */
|
||||
break;
|
||||
}
|
||||
|
||||
fd = socket(family, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"socket(tcp) failed: %s",
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
|
||||
if (family == AF_INET) {
|
||||
struct sockaddr_in addr4;
|
||||
memset(&addr4, 0, sizeof(addr4));
|
||||
addr4.sin_family = AF_INET;
|
||||
addr4.sin_port = htons(try_port);
|
||||
if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
if (errno == EADDRINUSE) {
|
||||
continue; /* try next port */
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
struct sockaddr_in6 addr6;
|
||||
memset(&addr6, 0, sizeof(addr6));
|
||||
addr6.sin6_family = AF_INET6;
|
||||
addr6.sin6_port = htons(try_port);
|
||||
if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
if (errno == EADDRINUSE) {
|
||||
continue; /* try next port */
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
bound = 1;
|
||||
port = try_port;
|
||||
}
|
||||
|
||||
if (!bound) {
|
||||
/* All 5 attempts failed with EADDRINUSE. ctx->last_error already
|
||||
* holds the most recent bind failure message. */
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
|
||||
if (family == AF_INET) {
|
||||
struct sockaddr_in addr4;
|
||||
memset(&addr4, 0, sizeof(addr4));
|
||||
addr4.sin_family = AF_INET;
|
||||
addr4.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
struct sockaddr_in6 addr6;
|
||||
memset(&addr6, 0, sizeof(addr6));
|
||||
addr6.sin6_family = AF_INET6;
|
||||
addr6.sin6_port = htons(port);
|
||||
if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
/* Update ctx->socket_name to reflect the actual bound port, so the
|
||||
* status display and error messages show the real address. */
|
||||
{
|
||||
const char *prefix = (ctx->listen_mode == NSIGNER_LISTEN_HTTP) ? "http:" : "tcp:";
|
||||
if (family == AF_INET6) {
|
||||
snprintf(ctx->socket_name, sizeof(ctx->socket_name),
|
||||
"%s[%s]:%u", prefix, host, (unsigned)port);
|
||||
} else {
|
||||
snprintf(ctx->socket_name, sizeof(ctx->socket_name),
|
||||
"%s%s:%u", prefix, host, (unsigned)port);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1717,7 +1763,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;
|
||||
@@ -2089,12 +2135,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;
|
||||
|
||||
@@ -210,6 +210,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -219,22 +220,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -74,18 +74,15 @@ 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_DERIVE "derive"
|
||||
#define VERB_GET_PUBLIC_KEY "get_public_key"
|
||||
#define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key"
|
||||
#define VERB_NOSTR_SIGN_EVENT "nostr_sign_event"
|
||||
#define VERB_NOSTR_MINE_EVENT "nostr_mine_event"
|
||||
#define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt"
|
||||
#define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt"
|
||||
#define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt"
|
||||
#define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt"
|
||||
int enforce_verb_role(const char *verb, const role_entry_t *role);
|
||||
const char *enforce_strerror(int err);
|
||||
|
||||
@@ -367,12 +364,20 @@ int main(void) {
|
||||
enforce_verb_algorithm(VERB_DERIVE_SHARED, CRYPTO_ALG_X25519) == ENFORCE_OK);
|
||||
check("enforce derive_shared_secret + ed25519 -> ALGORITHM err",
|
||||
enforce_verb_algorithm(VERB_DERIVE_SHARED, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
|
||||
check("enforce derive + secp256k1 -> OK",
|
||||
enforce_verb_algorithm(VERB_DERIVE, CRYPTO_ALG_SECP256K1) == ENFORCE_OK);
|
||||
check("enforce derive + ed25519 -> ALGORITHM err",
|
||||
enforce_verb_algorithm(VERB_DERIVE, CRYPTO_ALG_ED25519) == ENFORCE_ERR_ALGORITHM);
|
||||
check("enforce derive + x25519 -> ALGORITHM err",
|
||||
enforce_verb_algorithm(VERB_DERIVE, CRYPTO_ALG_X25519) == ENFORCE_ERR_ALGORITHM);
|
||||
check("enforce derive + ml-kem-768 -> ALGORITHM err",
|
||||
enforce_verb_algorithm(VERB_DERIVE, CRYPTO_ALG_ML_KEM_768) == 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 +593,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);
|
||||
|
||||
@@ -682,6 +687,96 @@ int main(void) {
|
||||
check("policy_check_algorithm: wrong caller -> NO_MATCH", rc == POLICY_NO_MATCH);
|
||||
}
|
||||
|
||||
/* ---- derive: HMAC-SHA256(privkey, data) — secp256k1 ---- */
|
||||
/* Successful call returns a 64-hex digest. */
|
||||
resp = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dv1\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\",\"index\":0}]}");
|
||||
check("derive secp256k1 returns result", resp_has(resp, "\"result\""));
|
||||
check("derive secp256k1 has digest field", resp_has(resp, "digest"));
|
||||
check("derive secp256k1 has algorithm secp256k1", resp_has(resp, "secp256k1"));
|
||||
{
|
||||
char *digest_hex = extract_result_field(resp, "digest");
|
||||
check("derive secp256k1 digest is 64 hex chars",
|
||||
digest_hex != NULL && strlen(digest_hex) == 64);
|
||||
free(digest_hex);
|
||||
}
|
||||
free(resp);
|
||||
|
||||
/* Determinism: same input -> same digest. */
|
||||
resp = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dv2\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\",\"index\":0}]}");
|
||||
{
|
||||
char *digest2 = extract_result_field(resp, "digest");
|
||||
/* Re-run first call to compare (deterministic). */
|
||||
char *resp_a = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dv1b\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\",\"index\":0}]}");
|
||||
char *digest_a = extract_result_field(resp_a, "digest");
|
||||
check("derive determinism: same input -> same digest",
|
||||
digest2 != NULL && digest_a != NULL && strcmp(digest2, digest_a) == 0);
|
||||
free(digest2); free(digest_a); free(resp_a);
|
||||
}
|
||||
free(resp);
|
||||
|
||||
/* Opaqueness: different inputs -> different digests. */
|
||||
{
|
||||
char *resp_b = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dv3\",\"method\":\"derive\",\"params\":[\"other-data\",{\"algorithm\":\"secp256k1\",\"index\":0}]}");
|
||||
char *resp_c = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dv1c\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\",\"index\":0}]}");
|
||||
char *dig_b = extract_result_field(resp_b, "digest");
|
||||
char *dig_c = extract_result_field(resp_c, "digest");
|
||||
check("derive opaqueness: different inputs -> different digests",
|
||||
dig_b != NULL && dig_c != NULL && strcmp(dig_b, dig_c) != 0);
|
||||
free(dig_b); free(dig_c); free(resp_b); free(resp_c);
|
||||
}
|
||||
|
||||
/* Cross-check against nostr_hmac_sha256 with the derived private key. */
|
||||
{
|
||||
const unsigned char *priv = crypto_get_private_key(&g_key_store, 0);
|
||||
unsigned char ref_mac[32];
|
||||
char ref_hex[65];
|
||||
char *resp_ref = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dvref\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\",\"index\":0}]}");
|
||||
char *got_hex = extract_result_field(resp_ref, "digest");
|
||||
if (priv != NULL && nostr_hmac_sha256(priv, 32,
|
||||
(const unsigned char *)"test-data", strlen("test-data"),
|
||||
ref_mac) == 0) {
|
||||
nostr_bytes_to_hex(ref_mac, 32, ref_hex);
|
||||
check("derive matches reference HMAC-SHA256(privkey, data)",
|
||||
got_hex != NULL && strcmp(got_hex, ref_hex) == 0);
|
||||
} else {
|
||||
check("derive reference HMAC computation available", 0);
|
||||
}
|
||||
free(got_hex); free(resp_ref);
|
||||
}
|
||||
|
||||
/* index is required: omitting it returns missing_index. */
|
||||
resp = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dv4\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\"}]}");
|
||||
check("derive without index returns missing_index",
|
||||
resp_has(resp, "missing_index"));
|
||||
free(resp);
|
||||
|
||||
/* Non-secp256k1 algorithm is rejected. */
|
||||
resp = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dv5\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"ed25519\",\"index\":0}]}");
|
||||
check("derive ed25519 returns algorithm error",
|
||||
resp_has(resp, "algorithm_not_supported_for_verb") || resp_has(resp, "\"code\":1010"));
|
||||
free(resp);
|
||||
|
||||
/* Different index -> different digest (different privkey). */
|
||||
{
|
||||
char *r0 = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dvi0\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\",\"index\":0}]}");
|
||||
char *r1 = dispatcher_handle_request(&g_dispatcher,
|
||||
"{\"id\":\"dvi1\",\"method\":\"derive\",\"params\":[\"test-data\",{\"algorithm\":\"secp256k1\",\"index\":1}]}");
|
||||
char *d0 = extract_result_field(r0, "digest");
|
||||
char *d1 = extract_result_field(r1, "digest");
|
||||
check("derive different index -> different digest",
|
||||
d0 != NULL && d1 != NULL && strcmp(d0, d1) != 0);
|
||||
free(d0); free(d1); free(r0); free(r1);
|
||||
}
|
||||
|
||||
/* ---- Cleanup ---- */
|
||||
crypto_wipe(&g_key_store);
|
||||
alg_key_cache_wipe(&g_alg_key_cache);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -211,6 +211,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -220,22 +221,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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 +816,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 +875,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);
|
||||
|
||||
@@ -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,22 @@ 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_DERIVE "derive"
|
||||
#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 +695,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 +720,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 +748,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 +788,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 +802,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 +814,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 +843,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 +871,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);
|
||||
|
||||
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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 +742,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 +787,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);
|
||||
|
||||
@@ -210,6 +210,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -219,22 +220,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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 +1004,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 +1021,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 +1032,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 +1055,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 +1065,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 +1077,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 +1087,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);
|
||||
|
||||
@@ -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}]}");
|
||||
|
||||
|
||||
@@ -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,22 @@ 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_DERIVE "derive"
|
||||
#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 +685,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 +708,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 +725,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 +752,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 +780,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 +809,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 */
|
||||
|
||||
@@ -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,22 @@ 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_DERIVE "derive"
|
||||
#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 +524,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 +555,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 +587,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 +629,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 +644,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 ---- */
|
||||
|
||||
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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 +804,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 +823,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 +834,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 +934,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -211,6 +211,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -220,22 +221,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
@@ -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,22 @@ 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_DERIVE "derive"
|
||||
#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 +685,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 +708,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 +725,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 +755,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 +783,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 +813,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 ---- */
|
||||
|
||||
@@ -208,6 +208,7 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
#define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */
|
||||
|
||||
@@ -217,22 +218,24 @@ const char *selector_strerror(int err);
|
||||
#define VERB_ENCAPSULATE "encapsulate"
|
||||
#define VERB_DECAPSULATE "decapsulate"
|
||||
#define VERB_DERIVE_SHARED "derive_shared_secret"
|
||||
#define VERB_DERIVE "derive"
|
||||
|
||||
|
||||
/* 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).
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user