Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64fbd5c874 | ||
|
|
ca18e1e42d | ||
|
|
b3421c3e40 | ||
|
|
96ab9741ef | ||
|
|
2af12868e2 | ||
|
|
a0a5987ffa | ||
|
|
0b0ec5eb1a |
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)
|
||||
|
||||
387
api.md
387
api.md
@@ -1,382 +1,15 @@
|
||||
# n_signer API
|
||||
|
||||
`n_signer` exposes a JSON-RPC 2.0-style request/response protocol. Every request is a single JSON object; every response is a single JSON object. This document is the complete, authoritative description of the API.
|
||||
The complete, authoritative API reference is now in [`README.md`](README.md) §4 (API).
|
||||
|
||||
For the security model, transports, and operational behavior, see [`README.md`](README.md). For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).
|
||||
It covers:
|
||||
|
||||
---
|
||||
- **§4.1 Request format** — JSON-RPC 2.0-style request shape.
|
||||
- **§4.2 Response format** — success/error shapes and the full error-code table.
|
||||
- **§4.3 Verbs** — the verb table (positional params + options), the `scheme` option for secp256k1, and the enforcement matrix.
|
||||
- **§4.4 Algorithms** — the algorithm table (secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768, otp), derivation paths, key sizes, and the OTP one-time-pad model.
|
||||
- **§4.5 Examples** — worked request/response examples for every verb.
|
||||
- **§4.6 Role-based selectors** — `nostr_index` / `role` / `role_path` for the `nostr_*` verbs.
|
||||
- **§4.7 Pre-approval** — `--preapprove` syntax for algorithm-based and Nostr verbs.
|
||||
|
||||
## 1. API format
|
||||
|
||||
### 1.1 Request
|
||||
|
||||
```json
|
||||
{ "id": "<string>", "method": "<verb>", "params": [ <arg0>, <arg1>, ..., { <options> } ] }
|
||||
```
|
||||
|
||||
- `id` — caller-supplied string echoed verbatim in the response. Used to match requests to responses.
|
||||
- `method` — the verb name (see [§2](#2-verbs)).
|
||||
- `params` — a JSON array. Positional arguments come first; the **last array element** is conventionally an options object. The options object is optional for most verbs.
|
||||
|
||||
### 1.2 Response (success)
|
||||
|
||||
```json
|
||||
{ "id": "<string>", "result": <value> }
|
||||
```
|
||||
|
||||
`result` is a JSON string. For structured verbs the string is itself a serialized JSON object — clients should `JSON.parse` it.
|
||||
|
||||
### 1.3 Response (error)
|
||||
|
||||
```json
|
||||
{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } }
|
||||
```
|
||||
|
||||
### 1.4 Error codes
|
||||
|
||||
| Code | Message | Meaning |
|
||||
|-------|-------------------------------|--------------------------------------------------------------------|
|
||||
| -32700| `parse_error` | Request was not valid JSON. |
|
||||
| -32600| `invalid_request` | Missing `id`, `method`, or `params`, or `params` is not an array. |
|
||||
| -32601| `method_not_found` | Unknown verb, or verb not valid for the selected algorithm. |
|
||||
| -32602| `invalid_params` | Malformed arguments (bad hex, wrong length, missing field, etc.). |
|
||||
| 1001 | `ambiguous_role_selector` | More than one role selector was supplied. |
|
||||
| 1002 | `unknown_role` | No role matched the selector. |
|
||||
| 1003 | `no_default_role` | No selector given and no `main` role exists. |
|
||||
| 1004 | `purpose_mismatch` | Role's purpose is not valid for this verb. |
|
||||
| 1005 | `curve_mismatch` | Role's curve is not valid for this verb. |
|
||||
| 1006 | `mnemonic_not_loaded` | No mnemonic is loaded in the signer. |
|
||||
| 1007 | `no_termination_condition` | `nostr_mine_event` called without `difficulty` or `timeout_sec`. |
|
||||
| 1008 | `mining_failed` | Internal error during proof-of-work mining. |
|
||||
| 1009 | `not_yet_implemented` | Verb+algorithm combination is reserved but not yet implemented. |
|
||||
| 1010 | `algorithm_not_supported_for_verb` | The `algorithm` value is not valid for this verb. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Verbs
|
||||
|
||||
All verbs take their arguments as positional `params` and their options in a trailing options object. Most verbs select a key via the `algorithm` + `index` options (see [§3](#3-algorithms)). The `nostr_*` verbs select a secp256k1 NIP-06 key via `nostr_index` and implement Nostr-protocol-specific serialization on top of the raw crypto.
|
||||
|
||||
| Verb | Algorithms | Positional params | Options |
|
||||
|-------------------------|-----------------------------------------------|----------------------------------|----------------------------------|
|
||||
| `get_public_key` | all key-deriving algorithms | — | `algorithm`, `index` |
|
||||
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>` | `algorithm`, `index`, `scheme`* |
|
||||
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>`, `<signature_hex>` | `algorithm`, `index`, `scheme`* |
|
||||
| `encapsulate` | ml-kem-768 | `<peer_pubkey_hex>` | `algorithm` |
|
||||
| `decapsulate` | ml-kem-768 | `<ciphertext_hex>` | `algorithm`, `index` |
|
||||
| `derive_shared_secret` | x25519 | `<peer_pubkey_hex>` | `algorithm`, `index` |
|
||||
| `encrypt` | otp | `<plaintext_base64>` | `algorithm`, `encoding` |
|
||||
| `decrypt` | otp | `<ciphertext>` | `algorithm`, `encoding` |
|
||||
| `nostr_get_public_key` | secp256k1 (NIP-06) | — | `nostr_index`, `format` |
|
||||
| `nostr_sign_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index` |
|
||||
| `nostr_mine_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` |
|
||||
| `nostr_nip04_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
|
||||
| `nostr_nip04_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
|
||||
| `nostr_nip44_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
|
||||
| `nostr_nip44_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
|
||||
|
||||
\* `scheme` is secp256k1-only: `"schnorr"` (default, BIP-340) or `"ecdsa"`.
|
||||
|
||||
### 2.1 Enforcement matrix
|
||||
|
||||
| Verb | Valid algorithms |
|
||||
|----------------------------|-----------------------------------------------|
|
||||
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `get_public_key` | all key-deriving algorithms |
|
||||
| `nostr_*` | secp256k1 (Nostr protocol) |
|
||||
|
||||
Any unlisted `(verb, algorithm)` pair is rejected with `algorithm_not_supported_for_verb` (1010).
|
||||
|
||||
---
|
||||
|
||||
## 3. Algorithms
|
||||
|
||||
All keys derive deterministically from the loaded BIP-39 mnemonic. The caller selects an algorithm by name and a derivation `index` (an integer `<n>` substituted into the algorithm's derivation path). OTP is the exception — it does not derive a key, it consumes a bound one-time pad (see [§3.3](#33-otp)).
|
||||
|
||||
### 3.1 Algorithm table
|
||||
|
||||
| Algorithm | Key type | FIPS standard | Derivation path | Key sizes (priv / pub, bytes) |
|
||||
|-----------------|-----------------|---------------|---------------------------------------|-------------------------------|
|
||||
| `secp256k1` | Signature | — | `m/44'/1237'/<n>'/0/0` (NIP-06) | 32 / 32 |
|
||||
| `ed25519` | Signature | — | `m/44'/102001'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
|
||||
| `x25519` | Key agreement | — | `m/44'/102002'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
|
||||
| `ml-dsa-65` | PQ signature | FIPS 204 | `m/44'/102003'/<n>'/0/0'` → DRBG | 4032 / 1952 |
|
||||
| `slh-dsa-128s` | PQ signature | FIPS 205 | `m/44'/102004'/<n>'/0/0'` → DRBG | 64 / 32 |
|
||||
| `ml-kem-768` | PQ KEM | FIPS 203 | `m/44'/102005'/<n>'/0/0'` → DRBG | 2400 / 1184 |
|
||||
| `otp` | One-time pad | — | (no key — bound USB pad) | n/a |
|
||||
|
||||
### 3.2 Key derivation
|
||||
|
||||
- **secp256k1** uses standard BIP-32/NIP-06 derivation. The 32-byte path output is the private key scalar.
|
||||
- **ed25519 / x25519** use SLIP-0010 HMAC-SHA512 derivation (all-hardened paths, as required by SLIP-0010 for ed25519). The 32-byte output is the private key.
|
||||
- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. Same mnemonic, same index, same key pair every time.
|
||||
- **otp** does not derive a key. A pad is bound at signer startup (`--otp-pad-dir` + `--otp-pad`); the pad offset advances monotonically across requests.
|
||||
|
||||
### 3.3 OTP
|
||||
|
||||
The `otp` algorithm is a stream-style one-time pad, not a key-derivation scheme. It is selected like any other algorithm via `{"algorithm":"otp"}` and works with the `encrypt` / `decrypt` verbs. One pad per session; the pad offset advances monotonically across requests and is reported in every response.
|
||||
|
||||
---
|
||||
|
||||
## 4. Examples
|
||||
|
||||
### 4.1 `get_public_key`
|
||||
|
||||
```json
|
||||
{ "id": "1", "method": "get_public_key", "params": [ { "algorithm": "ml-dsa-65", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "1", "result": "{\"algorithm\":\"ml-dsa-65\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}" }
|
||||
```
|
||||
|
||||
`key_id` is the first 16 hex characters of the public key — a short display identifier.
|
||||
|
||||
### 4.2 `sign`
|
||||
|
||||
```json
|
||||
{ "id": "2", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "ed25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "2", "result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\",\"key_id\":\"<16 hex>\"}" }
|
||||
```
|
||||
|
||||
The first positional argument is the message as hex. For `secp256k1` the `scheme` option selects `"schnorr"` (default, BIP-340) or `"ecdsa"`:
|
||||
|
||||
```json
|
||||
{ "id": "3", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "secp256k1", "index": 0, "scheme": "ecdsa" } ] }
|
||||
```
|
||||
|
||||
### 4.3 `verify`
|
||||
|
||||
```json
|
||||
{ "id": "4", "method": "verify", "params": [ "<message_hex>", "<signature_hex>", { "algorithm": "ed25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "4", "result": "{\"valid\":true,\"algorithm\":\"ed25519\"}" }
|
||||
```
|
||||
|
||||
The signer derives its own public key from `(algorithm, index)` and verifies against it. To verify an arbitrary third-party key, use a client-side library.
|
||||
|
||||
### 4.4 `encapsulate` (ML-KEM-768)
|
||||
|
||||
```json
|
||||
{ "id": "5", "method": "encapsulate", "params": [ "<peer_pubkey_hex>", { "algorithm": "ml-kem-768" } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "5", "result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
|
||||
```
|
||||
|
||||
`peer_pubkey_hex` is the recipient's ML-KEM-768 public key (1184 bytes → 2368 hex chars). Send the returned `ciphertext` to the recipient; both sides end up with the same `shared_secret`.
|
||||
|
||||
### 4.5 `decapsulate` (ML-KEM-768)
|
||||
|
||||
```json
|
||||
{ "id": "6", "method": "decapsulate", "params": [ "<ciphertext_hex>", { "algorithm": "ml-kem-768", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "6", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
|
||||
```
|
||||
|
||||
### 4.6 `derive_shared_secret` (X25519)
|
||||
|
||||
```json
|
||||
{ "id": "7", "method": "derive_shared_secret", "params": [ "<peer_pubkey_hex>", { "algorithm": "x25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "7", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"x25519\"}" }
|
||||
```
|
||||
|
||||
`peer_pubkey_hex` is the peer's 32-byte X25519 public key (64 hex chars). Feed the returned `shared_secret` into your own symmetric cipher (e.g. AES-GCM, ChaCha20-Poly1305).
|
||||
|
||||
### 4.7 `encrypt` / `decrypt` (OTP)
|
||||
|
||||
```json
|
||||
{ "id": "8", "method": "encrypt", "params": [ "<plaintext_base64>", { "algorithm": "otp", "encoding": "ascii" } ] }
|
||||
{ "id": "9", "method": "decrypt", "params": [ "<ciphertext>", { "algorithm": "otp", "encoding": "ascii" } ] }
|
||||
```
|
||||
|
||||
`encoding` is `"ascii"` (ASCII-armored, default) or `"binary"` (base64-encoded raw `.otp` blob). If omitted on `decrypt`, auto-detection by magic bytes is used.
|
||||
|
||||
`encrypt` response:
|
||||
```json
|
||||
{
|
||||
"id": "8",
|
||||
"result": "{\"ciphertext\":\"<ascii-armor-or-base64-blob>\",\"encoding\":\"ascii\",\"pad_chksum\":\"<64 hex>\",\"pad_offset_before\":288,\"pad_offset_after\":416}"
|
||||
}
|
||||
```
|
||||
|
||||
`decrypt` response:
|
||||
```json
|
||||
{ "id": "9", "result": "{\"plaintext\":\"<base64>\",\"pad_chksum\":\"<64 hex>\"}" }
|
||||
```
|
||||
|
||||
If no pad is bound at startup, the error is `-32601` `otp_pad_not_bound`.
|
||||
|
||||
### 4.8 `nostr_get_public_key`
|
||||
|
||||
```json
|
||||
{ "id": "10", "method": "nostr_get_public_key", "params": [ { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
Response (default): a plain 64-hex-char secp256k1 public key string.
|
||||
Response with `{"format":"structured"}` in options: `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`.
|
||||
|
||||
### 4.9 `nostr_sign_event`
|
||||
|
||||
Serializes the event to canonical form (`[0, pubkey, created_at, kind, tags, content]`), SHA-256 hashes it to produce the event `id`, signs the hash with BIP-340 Schnorr, and returns the complete signed event.
|
||||
|
||||
```json
|
||||
{ "id": "11", "method": "nostr_sign_event", "params": [ "<event_json>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`<event_json>` is the unsigned event object:
|
||||
```json
|
||||
{ "pubkey": "...", "created_at": 1234567890, "kind": 1, "tags": [], "content": "hello" }
|
||||
```
|
||||
|
||||
Response: the signed event JSON string, with `id` and `sig` populated.
|
||||
|
||||
### 4.10 `nostr_mine_event`
|
||||
|
||||
Mines NIP-13 proof-of-work (adds a `nonce` tag) and signs the event in one step. Mining runs in a detached thread so the signer stays responsive.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"method": "nostr_mine_event",
|
||||
"params": [ "<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 } ]
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Required | Default | Meaning |
|
||||
|---------------|-------------------------------|---------|---------------------------------------------------------------|
|
||||
| `difficulty` | one of `difficulty`/`timeout` | 0 | Target leading zero bits. Stops early if reached. |
|
||||
| `timeout_sec` | one of `difficulty`/`timeout` | 600 | Time budget in seconds. Always returns the best event found. |
|
||||
| `threads` | no | 1 | Mining threads (clamped to 1..32). |
|
||||
|
||||
At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when **either** condition is met. Timeout is never an error — the best event found is always returned.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"result": "{\"event\":\"<signed event JSON with nonce tag>\",\"achieved_difficulty\":18,\"target_difficulty\":20,\"target_reached\":false,\"elapsed_sec\":30,\"attempts\":4523456}"
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `1007` `no_termination_condition` — neither `difficulty` nor `timeout_sec` given.
|
||||
- `1008` `mining_failed` — internal mining error.
|
||||
|
||||
### 4.11 `nostr_nip04_encrypt` / `nostr_nip04_decrypt`
|
||||
|
||||
NIP-04 encryption (deprecated in Nostr but still widely used): ECDH + AES-256-CBC, base64 payload.
|
||||
|
||||
```json
|
||||
{ "id": "13", "method": "nostr_nip04_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
|
||||
{ "id": "14", "method": "nostr_nip04_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`encrypt` returns the NIP-04 ciphertext string; `decrypt` returns the plaintext string.
|
||||
|
||||
### 4.12 `nostr_nip44_encrypt` / `nostr_nip44_decrypt`
|
||||
|
||||
NIP-44 encryption (current Nostr standard): ECDH + HKDF + ChaCha20-Poly1305 + specific payload format.
|
||||
|
||||
```json
|
||||
{ "id": "15", "method": "nostr_nip44_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
|
||||
{ "id": "16", "method": "nostr_nip44_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`encrypt` returns the NIP-44 ciphertext string; `decrypt` returns the plaintext string.
|
||||
|
||||
---
|
||||
|
||||
## 5. Transports
|
||||
|
||||
The API is transport-independent. The same JSON request works over every transport; only the framing differs.
|
||||
|
||||
| Transport | `--listen` flag | Framing | Caller identity |
|
||||
|-----------|--------------------------------|------------------------------------------|----------------------------|
|
||||
| Unix socket (abstract) | `unix` (default on desktop) | Length-prefixed framed JSON | `SO_PEERCRED` → `uid:<n>` |
|
||||
| stdio | `stdio` | One framed request/response over stdin/stdout | inherited uid |
|
||||
| qrexec | `qrexec` | Same as stdio; caller from `QREXEC_REMOTE_DOMAIN` | `qubes:<vm>` |
|
||||
| FIPS/TCP | `tcp:[host]:port` | Length-prefixed framed JSON | (transport-defined) |
|
||||
| HTTP | `http:host:port` | Standard HTTP POST, JSON body, no custom framing. CORS enabled. | (transport-defined) |
|
||||
|
||||
### 5.1 HTTP examples
|
||||
|
||||
Start the signer:
|
||||
```bash
|
||||
nsigner --listen http:127.0.0.1:11111 --allow-all
|
||||
```
|
||||
|
||||
Get a public key:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
|
||||
```
|
||||
|
||||
Sign a Nostr event:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
|
||||
```
|
||||
|
||||
OTP encrypt:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"algorithm":"otp","encoding":"ascii"}]}'
|
||||
```
|
||||
|
||||
### 5.2 Unix socket examples (framed mode)
|
||||
|
||||
```bash
|
||||
# get_public_key
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
|
||||
|
||||
# Sign a Nostr event
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"2","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
|
||||
|
||||
# ed25519 sign
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"3","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Pre-approval
|
||||
|
||||
Pre-approval entries skip the interactive prompt for matching requests. They are configured at startup with `--preapprove`.
|
||||
|
||||
### 6.1 Algorithm-based
|
||||
|
||||
```bash
|
||||
nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign,verify
|
||||
nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate
|
||||
```
|
||||
|
||||
### 6.2 Nostr
|
||||
|
||||
```bash
|
||||
nsigner --preapprove caller=uid:1000,nostr_index=0,verb=nostr_sign_event,nostr_get_public_key
|
||||
```
|
||||
|
||||
A `*` wildcard matches any caller, role, or verb. Index ranges use `min-max` syntax. Unmatched requests fall through to the default policy (prompt for same-uid, deny for others).
|
||||
For the security model, transports, and operational behavior, see [`README.md`](README.md) §1–§3 and §5–§8. For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).
|
||||
|
||||
@@ -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`
|
||||
|
||||
535
examples/cyd_webserial_demo.html
Normal file
535
examples/cyd_webserial_demo.html
Normal file
@@ -0,0 +1,535 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>n_signer CYD Web Serial Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--panel: #121821;
|
||||
--panel-2: #182231;
|
||||
--text: #e6edf3;
|
||||
--muted: #9fb0c3;
|
||||
--accent: #58a6ff;
|
||||
--good: #3fb950;
|
||||
--bad: #f85149;
|
||||
--border: #263448;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrap { max-width: 980px; margin: 20px auto; padding: 0 14px 24px; }
|
||||
h1 { margin: 0 0 8px; font-size: 1.45rem; }
|
||||
p.note { margin: 0 0 14px; color: var(--muted); }
|
||||
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; margin-top: 12px; }
|
||||
.card { background: linear-gradient(180deg, var(--panel), var(--panel-2)); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
|
||||
.card h2 { font-size: 1rem; margin: 0 0 10px; }
|
||||
label { font-size: 0.86rem; color: var(--muted); display: block; margin: 6px 0 4px; }
|
||||
input, textarea, select, button { font: inherit; border-radius: 8px; border: 1px solid var(--border); }
|
||||
input, textarea, select { width: 100%; background: #0c131d; color: var(--text); padding: 8px 10px; }
|
||||
textarea { min-height: 64px; resize: vertical; }
|
||||
input[type="number"] { max-width: 130px; }
|
||||
button { background: #1f6feb; color: white; padding: 8px 12px; cursor: pointer; border: 0; }
|
||||
button[disabled] { opacity: 0.55; cursor: not-allowed; }
|
||||
.secondary { background: #334155; }
|
||||
.status { padding: 6px 10px; border-radius: 999px; background: #2a3648; color: var(--muted); font-size: 0.85rem; border: 1px solid var(--border); }
|
||||
.status.ok { color: var(--good); border-color: #2f5a3a; }
|
||||
.status.err { color: var(--bad); border-color: #6a3131; }
|
||||
pre { margin: 8px 0 0; background: #0a1018; border: 1px solid #1b2636; color: #d7e2ee; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; font-size: 12px; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.warn { color: #d29922; font-size: 0.8rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>n_signer CYD Web Serial Demo</h1>
|
||||
<p class="note">Connect to the CYD (CH340 serial) via Web Serial, then exercise every algorithm and verb in the n_signer API. Chrome/Edge/Brave/Opera only.</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<button id="connectBtn">Connect Web Serial</button>
|
||||
<button id="disconnectBtn" class="secondary" disabled>Disconnect</button>
|
||||
<span id="connStatus" class="status">Disconnected</span>
|
||||
</div>
|
||||
<pre id="log" class="mono"></pre>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Get Public Key (algorithm-based) -->
|
||||
<section class="card">
|
||||
<h2>Get Public Key (algorithm)</h2>
|
||||
<label for="gpkAlg">Algorithm</label>
|
||||
<select id="gpkAlg">
|
||||
<option>secp256k1</option><option>ed25519</option><option>x25519</option>
|
||||
<option>ml-dsa-65</option><option>slh-dsa-128s</option><option>ml-kem-768</option>
|
||||
</select>
|
||||
<label for="gpkIdx">Index</label>
|
||||
<input id="gpkIdx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="gpkBtn" disabled>get_public_key</button>
|
||||
</div>
|
||||
<pre id="gpkOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Nostr Get Public Key -->
|
||||
<section class="card">
|
||||
<h2>nostr_get_public_key</h2>
|
||||
<label for="ngpkIdx">nostr_index</label>
|
||||
<input id="ngpkIdx" type="number" value="0" min="0" />
|
||||
<label for="ngpkFmt">format</label>
|
||||
<select id="ngpkFmt"><option>bare</option><option>structured</option></select>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="ngpkBtn" disabled>nostr_get_public_key</button>
|
||||
</div>
|
||||
<pre id="ngpkOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Sign / Verify -->
|
||||
<section class="card">
|
||||
<h2>sign / verify</h2>
|
||||
<label for="signAlg">Algorithm</label>
|
||||
<select id="signAlg">
|
||||
<option>secp256k1</option><option>ed25519</option><option>ml-dsa-65</option><option>slh-dsa-128s</option>
|
||||
</select>
|
||||
<label for="signIdx">Index</label>
|
||||
<input id="signIdx" type="number" value="0" min="0" />
|
||||
<label for="signScheme">scheme (secp256k1 only)</label>
|
||||
<select id="signScheme"><option>schnorr</option><option>ecdsa</option></select>
|
||||
<label for="signMsg">message (hex)</label>
|
||||
<input id="signMsg" value="68656c6c6f" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="signBtn" disabled>sign</button>
|
||||
<button id="verifyBtn" disabled>verify</button>
|
||||
</div>
|
||||
<pre id="signOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- KEM encapsulate / decapsulate -->
|
||||
<section class="card">
|
||||
<h2>encapsulate / decapsulate (ml-kem-768)</h2>
|
||||
<label for="kemPeer">peer pubkey hex (1184 bytes / 2368 hex) — leave empty to use self pubkey</label>
|
||||
<textarea id="kemPeer" placeholder="auto: uses ml-kem-768 get_public_key"></textarea>
|
||||
<label for="kemIdx">index (for decapsulate)</label>
|
||||
<input id="kemIdx" type="number" value="0" min="0" />
|
||||
<label for="kemCt">ciphertext hex (for decapsulate)</label>
|
||||
<textarea id="kemCt" placeholder="filled by encapsulate"></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="encapBtn" disabled>encapsulate</button>
|
||||
<button id="decapBtn" disabled>decapsulate</button>
|
||||
</div>
|
||||
<pre id="kemOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- derive_shared_secret (x25519) -->
|
||||
<section class="card">
|
||||
<h2>derive_shared_secret (x25519)</h2>
|
||||
<label for="x25519Peer">peer pubkey hex (32 bytes / 64 hex)</label>
|
||||
<input id="x25519Peer" placeholder="64 hex chars" />
|
||||
<label for="x25519Idx">index</label>
|
||||
<input id="x25519Idx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="x25519Btn" disabled>derive_shared_secret</button>
|
||||
</div>
|
||||
<pre id="x25519Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- derive (HMAC) -->
|
||||
<section class="card">
|
||||
<h2>derive (secp256k1 HMAC-SHA256)</h2>
|
||||
<label for="deriveData">data (UTF-8 string)</label>
|
||||
<input id="deriveData" value="hello-derive" />
|
||||
<label for="deriveIdx">index (required)</label>
|
||||
<input id="deriveIdx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="deriveBtn" disabled>derive</button>
|
||||
</div>
|
||||
<pre id="deriveOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Nostr Sign Event -->
|
||||
<section class="card">
|
||||
<h2>nostr_sign_event</h2>
|
||||
<label for="nseContent">content</label>
|
||||
<textarea id="nseContent">hello from cyd webserial demo</textarea>
|
||||
<label for="nseIdx">nostr_index</label>
|
||||
<input id="nseIdx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nseBtn" disabled>nostr_sign_event</button>
|
||||
</div>
|
||||
<pre id="nseOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Nostr Mine Event -->
|
||||
<section class="card">
|
||||
<h2>nostr_mine_event</h2>
|
||||
<p class="warn">Slow on ESP32 — uses single-threaded PoW. Keep difficulty low.</p>
|
||||
<label for="nmeContent">content</label>
|
||||
<textarea id="nmeContent">mined by cyd</textarea>
|
||||
<label for="nmeIdx">nostr_index</label>
|
||||
<input id="nmeIdx" type="number" value="0" min="0" />
|
||||
<label for="nmeDiff">difficulty (leading zero bits)</label>
|
||||
<input id="nmeDiff" type="number" value="4" min="1" max="16" />
|
||||
<label for="nmeTimeout">timeout (sec)</label>
|
||||
<input id="nmeTimeout" type="number" value="30" min="1" max="60" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nmeBtn" disabled>nostr_mine_event</button>
|
||||
</div>
|
||||
<pre id="nmeOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- NIP-04 -->
|
||||
<section class="card">
|
||||
<h2>nostr_nip04_encrypt / decrypt</h2>
|
||||
<label for="nip04Peer">peer pubkey hex (32-byte x-only)</label>
|
||||
<input id="nip04Peer" placeholder="64 hex chars" />
|
||||
<label for="nip04Msg">plaintext</label>
|
||||
<textarea id="nip04Msg">hello via nip04</textarea>
|
||||
<label for="nip04Cipher">ciphertext (for decrypt)</label>
|
||||
<textarea id="nip04Cipher" placeholder="ciphertext?iv=..."></textarea>
|
||||
<label for="nip04Idx">nostr_index</label>
|
||||
<input id="nip04Idx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04EncBtn" disabled>encrypt</button>
|
||||
<button id="nip04DecBtn" disabled>decrypt</button>
|
||||
</div>
|
||||
<pre id="nip04Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- NIP-44 -->
|
||||
<section class="card">
|
||||
<h2>nostr_nip44_encrypt / decrypt</h2>
|
||||
<label for="nip44Peer">peer pubkey hex (32-byte x-only)</label>
|
||||
<input id="nip44Peer" placeholder="64 hex chars" />
|
||||
<label for="nip44Msg">plaintext</label>
|
||||
<textarea id="nip44Msg">hello via nip44</textarea>
|
||||
<label for="nip44Cipher">ciphertext (for decrypt)</label>
|
||||
<textarea id="nip44Cipher" placeholder="base64 payload"></textarea>
|
||||
<label for="nip44Idx">nostr_index</label>
|
||||
<input id="nip44Idx" type="number" value="0" min="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44EncBtn" disabled>encrypt</button>
|
||||
<button id="nip44DecBtn" disabled>decrypt</button>
|
||||
</div>
|
||||
<pre id="nip44Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<!-- OTP encrypt / decrypt -->
|
||||
<section class="card">
|
||||
<h2>encrypt / decrypt (otp)</h2>
|
||||
<p class="note">OTP pad is derived from the mnemonic on the CYD. Offset advances monotonically.</p>
|
||||
<label for="otpPlain">plaintext (base64)</label>
|
||||
<textarea id="otpPlain">SGVsbG8sIE9UUCB3b3JsZCE=</textarea>
|
||||
<label for="otpCipher">ciphertext (for decrypt, base64)</label>
|
||||
<textarea id="otpCipher" placeholder="filled by encrypt"></textarea>
|
||||
<label for="otpEnc">encoding</label>
|
||||
<select id="otpEnc"><option>ascii</option><option>binary</option></select>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="otpEncBtn" disabled>encrypt</button>
|
||||
<button id="otpDecBtn" disabled>decrypt</button>
|
||||
</div>
|
||||
<pre id="otpOut" class="mono"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
|
||||
|
||||
const logEl = document.getElementById("log");
|
||||
const connStatusEl = document.getElementById("connStatus");
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const disconnectBtn = document.getElementById("disconnectBtn");
|
||||
|
||||
let port = null;
|
||||
let reader = null;
|
||||
let writer = null;
|
||||
let readLoopRunning = false;
|
||||
let rxBuffer = new Uint8Array(0);
|
||||
let pendingResolve = null;
|
||||
|
||||
function log(...args) {
|
||||
logEl.textContent += args.join(" ") + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
function setStatus(text, mode = "") {
|
||||
connStatusEl.textContent = text;
|
||||
connStatusEl.className = `status ${mode}`.trim();
|
||||
}
|
||||
function hex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
function utf8(s) { return new TextEncoder().encode(s); }
|
||||
function be32(n) {
|
||||
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
async function sha256Hex(dataBytes) {
|
||||
const h = await crypto.subtle.digest("SHA-256", dataBytes);
|
||||
return hex(new Uint8Array(h));
|
||||
}
|
||||
function pretty(value) {
|
||||
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
||||
}
|
||||
|
||||
async function buildAuth(method, params) {
|
||||
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
|
||||
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
const paramsJson = JSON.stringify(params);
|
||||
const bodyHash = await sha256Hex(utf8(paramsJson));
|
||||
const tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
const content = "cyd-webserial-demo";
|
||||
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
|
||||
const id = await sha256Hex(utf8(ser));
|
||||
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
|
||||
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
|
||||
return { id, pubkey: callerPubX, created_at: createdAt, kind: 27235, tags, content, sig: sigHex };
|
||||
}
|
||||
|
||||
/* ---- Web Serial transport ---- */
|
||||
async function readLoop() {
|
||||
readLoopRunning = true;
|
||||
while (readLoopRunning && reader) {
|
||||
try {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) {
|
||||
const next = new Uint8Array(rxBuffer.length + value.length);
|
||||
next.set(rxBuffer, 0);
|
||||
next.set(value, rxBuffer.length);
|
||||
rxBuffer = next;
|
||||
tryDeliver();
|
||||
}
|
||||
} catch (e) {
|
||||
log("read error:", e.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
readLoopRunning = false;
|
||||
}
|
||||
|
||||
function tryDeliver() {
|
||||
if (pendingResolve === null) return;
|
||||
while (rxBuffer.length >= 4) {
|
||||
const len = (rxBuffer[0] << 24) | (rxBuffer[1] << 16) | (rxBuffer[2] << 8) | rxBuffer[3];
|
||||
if (len <= 0 || len > 16384) {
|
||||
/* resync: drop one byte */
|
||||
rxBuffer = rxBuffer.slice(1);
|
||||
continue;
|
||||
}
|
||||
if (rxBuffer.length < 4 + len) return;
|
||||
const payload = rxBuffer.slice(4, 4 + len);
|
||||
rxBuffer = rxBuffer.slice(4 + len);
|
||||
const text = new TextDecoder().decode(payload);
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); } catch { parsed = text; }
|
||||
const r = pendingResolve;
|
||||
pendingResolve = null;
|
||||
r(parsed);
|
||||
if (pendingResolve === null) return;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendRpc(reqObj) {
|
||||
if (!writer) throw new Error("not connected");
|
||||
const body = utf8(JSON.stringify(reqObj));
|
||||
const frame = new Uint8Array(4 + body.length);
|
||||
frame.set(be32(body.length), 0);
|
||||
frame.set(body, 4);
|
||||
await writer.write(frame);
|
||||
const resp = await new Promise((resolve, reject) => {
|
||||
pendingResolve = resolve;
|
||||
setTimeout(() => {
|
||||
if (pendingResolve === resolve) {
|
||||
pendingResolve = null;
|
||||
reject(new Error("timeout (30s) — check the CYD screen for an approval prompt"));
|
||||
}
|
||||
}, 65000);
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
|
||||
async function callVerb(method, params, outEl) {
|
||||
outEl.textContent = "→ " + method + " " + JSON.stringify(params);
|
||||
try {
|
||||
const auth = await buildAuth(method, params);
|
||||
const req = { id: String(Math.floor(Math.random() * 1e9)), method, params, auth };
|
||||
const resp = await sendRpc(req);
|
||||
outEl.textContent += "\n← " + pretty(resp);
|
||||
} catch (e) {
|
||||
outEl.textContent += "\n✗ " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Connect / Disconnect ---- */
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
port = await navigator.serial.requestPort();
|
||||
await port.open({ baudRate: 115200 });
|
||||
reader = port.readable.getReader();
|
||||
writer = port.writable.getWriter();
|
||||
rxBuffer = new Uint8Array(0);
|
||||
readLoop();
|
||||
setStatus("Connected", "ok");
|
||||
log("Connected to CYD via Web Serial @ 115200 baud");
|
||||
document.querySelectorAll("button[id$='Btn']").forEach(b => {
|
||||
if (b !== connectBtn && b !== disconnectBtn) b.disabled = false;
|
||||
});
|
||||
disconnectBtn.disabled = false;
|
||||
connectBtn.disabled = true;
|
||||
} catch (e) {
|
||||
setStatus("Error", "err");
|
||||
log("connect failed:", e.message);
|
||||
}
|
||||
});
|
||||
|
||||
disconnectBtn.addEventListener("click", async () => {
|
||||
readLoopRunning = false;
|
||||
try { if (reader) await reader.cancel(); } catch {}
|
||||
try { if (writer) await writer.releaseLock(); } catch {}
|
||||
try { if (port) await port.close(); } catch {}
|
||||
reader = null; writer = null; port = null;
|
||||
setStatus("Disconnected");
|
||||
document.querySelectorAll("button[id$='Btn']").forEach(b => {
|
||||
if (b !== connectBtn) b.disabled = true;
|
||||
});
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
log("Disconnected");
|
||||
});
|
||||
|
||||
/* ---- Verb wiring ---- */
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
$("gpkBtn").addEventListener("click", () => {
|
||||
const alg = $("gpkAlg").value, idx = Number($("gpkIdx").value || 0);
|
||||
callVerb("get_public_key", [{ algorithm: alg, index: idx }], $("gpkOut"));
|
||||
});
|
||||
$("ngpkBtn").addEventListener("click", () => {
|
||||
const idx = Number($("ngpkIdx").value || 0), fmt = $("ngpkFmt").value;
|
||||
const opts = { nostr_index: idx };
|
||||
if (fmt === "structured") opts.format = "structured";
|
||||
callVerb("nostr_get_public_key", [opts], $("ngpkOut"));
|
||||
});
|
||||
|
||||
$("signBtn").addEventListener("click", () => {
|
||||
const alg = $("signAlg").value, idx = Number($("signIdx").value || 0);
|
||||
const scheme = $("signScheme").value, msg = $("signMsg").value;
|
||||
const opts = { algorithm: alg, index: idx };
|
||||
if (alg === "secp256k1") opts.scheme = scheme;
|
||||
callVerb("sign", [msg, opts], $("signOut"));
|
||||
});
|
||||
$("verifyBtn").addEventListener("click", async () => {
|
||||
const alg = $("signAlg").value, idx = Number($("signIdx").value || 0);
|
||||
const scheme = $("signScheme").value, msg = $("signMsg").value;
|
||||
const opts = { algorithm: alg, index: idx };
|
||||
if (alg === "secp256k1") opts.scheme = scheme;
|
||||
/* parse the last sign result to get the signature */
|
||||
const outText = $("signOut").textContent;
|
||||
const m = outText.match(/"signature"\s*:\s*"([0-9a-f]+)"/);
|
||||
if (!m) { $("signOut").textContent += "\n✗ no signature found — run sign first"; return; }
|
||||
callVerb("verify", [msg, m[1], opts], $("signOut"));
|
||||
});
|
||||
|
||||
$("encapBtn").addEventListener("click", async () => {
|
||||
let peer = $("kemPeer").value.trim();
|
||||
if (!peer) {
|
||||
/* fetch self ml-kem-768 pubkey first */
|
||||
const opts = [{ algorithm: "ml-kem-768", index: Number($("kemIdx").value || 0) }];
|
||||
const auth = await buildAuth("get_public_key", opts);
|
||||
const resp = await sendRpc({ id: String(Math.floor(Math.random()*1e9)), method: "get_public_key", params: opts, auth });
|
||||
peer = resp.result && JSON.parse(resp.result).public_key;
|
||||
if (!peer) { $("kemOut").textContent = "✗ could not fetch self pubkey"; return; }
|
||||
$("kemPeer").value = peer;
|
||||
}
|
||||
callVerb("encapsulate", [peer, { algorithm: "ml-kem-768" }], $("kemOut"));
|
||||
});
|
||||
$("decapBtn").addEventListener("click", () => {
|
||||
const ct = $("kemCt").value.trim();
|
||||
const idx = Number($("kemIdx").value || 0);
|
||||
if (!ct) { $("kemOut").textContent = "✗ paste a ciphertext first (from encapsulate)"; return; }
|
||||
callVerb("decapsulate", [ct, { algorithm: "ml-kem-768", index: idx }], $("kemOut"));
|
||||
});
|
||||
|
||||
$("x25519Btn").addEventListener("click", () => {
|
||||
const peer = $("x25519Peer").value.trim();
|
||||
const idx = Number($("x25519Idx").value || 0);
|
||||
if (!peer) { $("x25519Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("derive_shared_secret", [peer, { algorithm: "x25519", index: idx }], $("x25519Out"));
|
||||
});
|
||||
|
||||
$("deriveBtn").addEventListener("click", () => {
|
||||
const data = $("deriveData").value;
|
||||
const idx = Number($("deriveIdx").value || 0);
|
||||
callVerb("derive", [data, { algorithm: "secp256k1", index: idx }], $("deriveOut"));
|
||||
});
|
||||
|
||||
$("nseBtn").addEventListener("click", () => {
|
||||
const content = $("nseContent").value;
|
||||
const idx = Number($("nseIdx").value || 0);
|
||||
const event = { kind: 1, created_at: Math.floor(Date.now()/1000), tags: [], content };
|
||||
callVerb("nostr_sign_event", [event, { nostr_index: idx }], $("nseOut"));
|
||||
});
|
||||
|
||||
$("nmeBtn").addEventListener("click", () => {
|
||||
const content = $("nmeContent").value;
|
||||
const idx = Number($("nmeIdx").value || 0);
|
||||
const diff = Number($("nmeDiff").value || 4);
|
||||
const timeout = Number($("nmeTimeout").value || 30);
|
||||
const event = { kind: 1, created_at: Math.floor(Date.now()/1000), tags: [], content };
|
||||
callVerb("nostr_mine_event", [event, { nostr_index: idx, difficulty: diff, timeout_sec: timeout }], $("nmeOut"));
|
||||
});
|
||||
|
||||
const nip04Enc = () => {
|
||||
const peer = $("nip04Peer").value.trim(), msg = $("nip04Msg").value, idx = Number($("nip04Idx").value || 0);
|
||||
if (!peer) { $("nip04Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("nostr_nip04_encrypt", [peer, msg, { nostr_index: idx }], $("nip04Out"));
|
||||
};
|
||||
const nip04Dec = () => {
|
||||
const peer = $("nip04Peer").value.trim(), ct = $("nip04Cipher").value, idx = Number($("nip04Idx").value || 0);
|
||||
if (!peer || !ct) { $("nip04Out").textContent = "✗ enter peer pubkey + ciphertext"; return; }
|
||||
callVerb("nostr_nip04_decrypt", [peer, ct, { nostr_index: idx }], $("nip04Out"));
|
||||
};
|
||||
$("nip04EncBtn").addEventListener("click", nip04Enc);
|
||||
$("nip04DecBtn").addEventListener("click", nip04Dec);
|
||||
|
||||
const nip44Enc = () => {
|
||||
const peer = $("nip44Peer").value.trim(), msg = $("nip44Msg").value, idx = Number($("nip44Idx").value || 0);
|
||||
if (!peer) { $("nip44Out").textContent = "✗ enter peer pubkey"; return; }
|
||||
callVerb("nostr_nip44_encrypt", [peer, msg, { nostr_index: idx }], $("nip44Out"));
|
||||
};
|
||||
const nip44Dec = () => {
|
||||
const peer = $("nip44Peer").value.trim(), ct = $("nip44Cipher").value, idx = Number($("nip44Idx").value || 0);
|
||||
if (!peer || !ct) { $("nip44Out").textContent = "✗ enter peer pubkey + ciphertext"; return; }
|
||||
callVerb("nostr_nip44_decrypt", [peer, ct, { nostr_index: idx }], $("nip44Out"));
|
||||
};
|
||||
$("nip44EncBtn").addEventListener("click", nip44Enc);
|
||||
$("nip44DecBtn").addEventListener("click", nip44Dec);
|
||||
|
||||
$("otpEncBtn").addEventListener("click", () => {
|
||||
const pt = $("otpPlain").value, enc = $("otpEnc").value;
|
||||
callVerb("encrypt", [pt, { algorithm: "otp", encoding: enc }], $("otpOut"));
|
||||
});
|
||||
$("otpDecBtn").addEventListener("click", () => {
|
||||
const ct = $("otpCipher").value, enc = $("otpEnc").value;
|
||||
if (!ct) { $("otpOut").textContent = "✗ paste ciphertext first (from encrypt)"; return; }
|
||||
callVerb("decrypt", [ct, { algorithm: "otp", encoding: enc }], $("otpOut"));
|
||||
});
|
||||
|
||||
if (!("serial" in navigator)) {
|
||||
log("Web Serial not supported in this browser. Use Chrome/Edge/Brave/Opera.");
|
||||
connectBtn.disabled = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,26 @@ WebUSB path:
|
||||
- Run `get_public_key`
|
||||
- Confirm pubkey matches CDC result
|
||||
|
||||
## CYD (ESP32-2432S028) validation — Web Serial
|
||||
|
||||
The CYD has no native USB; its CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html):
|
||||
|
||||
- Open [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) in Chrome/Edge
|
||||
- Click **Connect Web Serial**, select the CH340 port (`1a86:7523`)
|
||||
- Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`,
|
||||
`nostr_nip04`/`nostr_nip44` encrypt+decrypt, and `encrypt`/`decrypt` (otp)
|
||||
- Each request shows the raw JSON-RPC request and response
|
||||
|
||||
The CYD firmware (v0.0.2+) speaks the same algorithm-based API as the host
|
||||
([`README.md`](../README.md) §4). The OTP pad is derived from the mnemonic
|
||||
seed (no USB pad on this board); the offset advances monotonically and is
|
||||
reported in every `encrypt`/`decrypt` response.
|
||||
|
||||
## Linux WebUSB host setup (one-time)
|
||||
|
||||
Chrome and Edge need permission to open the device on Linux. Install a udev rule for the firmware VID:PID and reload rules:
|
||||
@@ -123,33 +143,34 @@ firmware uses a **crypto backend abstraction** ([`resources/pqclean/common/crypt
|
||||
with two implementations:
|
||||
|
||||
- [`resources/pqclean/common/crypto_backend_openssl.c`](../resources/pqclean/common/crypto_backend_openssl.c) — host build (OpenSSL EVP)
|
||||
- [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — ESP32 firmware (mbedtls)
|
||||
- [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — ESP32 firmware (mbedtls + vendored Keccak)
|
||||
|
||||
The mbedtls backend uses:
|
||||
- `mbedtls_sha256_ret()` for SHA-256 (ESP32-S3 hardware accelerated)
|
||||
- `mbedtls_sha512_ret()` for SHA-512 (ESP32-S3 hardware accelerated)
|
||||
- `mbedtls_md` API for SHA3-256, SHA3-512, SHAKE-128, SHAKE-256
|
||||
- `mbedtls_sha256()` for SHA-256 (ESP32 hardware accelerated where available)
|
||||
- `mbedtls_sha512()` for SHA-512 (ESP32 hardware accelerated where available)
|
||||
- A **self-contained Keccak-f[1600]** implementation (FIPS 202, public domain)
|
||||
for SHA3-256, SHA3-512, SHAKE-128, and SHAKE-256. This is vendored directly
|
||||
in `crypto_backend_mbedtls.c` because ESP-IDF v5.x mbedtls does not expose
|
||||
SHAKE (and SHA3 is only available when `CONFIG_MBEDTLS_SHA3_C` is set) through
|
||||
the `mbedtls_md` API. Carrying the Keccak core avoids any mbedtls config
|
||||
dependency for the PQ algorithms.
|
||||
|
||||
### Enabling SHA3/SHAKE in menuconfig
|
||||
### ed25519 / x25519 via PSA crypto
|
||||
|
||||
mbedtls does not enable SHA3/SHAKE by default. You must enable them in
|
||||
menuconfig before building:
|
||||
ESP-IDF v5.x mbedtls removed the low-level `mbedtls_ed25519_*` functions. The
|
||||
firmware uses the **PSA Crypto API** for ed25519 sign/verify/key-derivation and
|
||||
x25519 key derivation + ECDH. Enable PSA in `sdkconfig.defaults`:
|
||||
|
||||
```
|
||||
Component config → mbedTLS → Hash functions → SHA-3
|
||||
Component config → mbedTLS → Hash functions → SHAKE
|
||||
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
|
||||
CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y
|
||||
```
|
||||
|
||||
Or add to `sdkconfig.defaults`:
|
||||
```
|
||||
CONFIG_MBEDTLS_SHA3_C=y
|
||||
CONFIG_MBEDTLS_SHAKE_C=y
|
||||
```
|
||||
### No SHA3/SHAKE menuconfig requirement
|
||||
|
||||
If mbedtls does not have SHAKE support, the firmware build will fail at link
|
||||
time with unresolved `mbedtls_md_info_from_type(MBEDTLS_MD_SHAKE128)`. In that
|
||||
case, either enable the config options above or patch mbedtls with a
|
||||
Keccak/SHAKE contribution.
|
||||
Because SHA3/SHAKE are provided by the vendored Keccak core (not mbedtls), you
|
||||
do **not** need to enable `CONFIG_MBEDTLS_SHA3_C` or any SHAKE config. The PQ
|
||||
algorithms build and run with the default mbedtls configuration.
|
||||
|
||||
### PQClean component
|
||||
|
||||
|
||||
81
firmware/ble_wearable_signer/README.md
Normal file
81
firmware/ble_wearable_signer/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# n_signer BLE Wearable Signer
|
||||
|
||||
**Status:** Concept — brainstorming. No plan yet.
|
||||
|
||||
A small, battery-powered wearable hardware signer that communicates with a host
|
||||
over **Bluetooth Low Energy (BLE)**. The host sends JSON-RPC requests over a
|
||||
BLE GATT characteristic; the signer shows an approval prompt on a tiny display;
|
||||
the user taps a button to approve; the signed response goes back over BLE.
|
||||
|
||||
## Concept
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Host[Host: phone/laptop<br/>n_signer client] -->|BLE GATT| Signer[Wearable signer<br/>nRF52840 + OLED]
|
||||
Signer -->|approve/deny button| User[User]
|
||||
Signer -->|BLE GATT response| Host
|
||||
```
|
||||
|
||||
The signer speaks the same algorithm-based API as the host and the CYD/Teensy
|
||||
firmware ([`README.md`](../../README.md) §4). The auth envelope (kind 27235)
|
||||
protects the BLE wire — even if BLE is sniffed, an attacker can't forge
|
||||
requests without the caller's secp256k1 private key.
|
||||
|
||||
## Why BLE
|
||||
|
||||
- **Wearable form factor** — always with you (wristband, pendant, card)
|
||||
- **No physical connection** — no USB cable, no host-side driver, no dongle
|
||||
- **Universal host support** — phones, laptops, tablets all have BT
|
||||
- **Low power** — nRF52840 draws ~5 mA active, ~1 µA sleep
|
||||
|
||||
## Hardware (preliminary)
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **nRF52840** (Nordic) | Cortex-M4 @ 64 MHz, 1 MB flash, 256 KB RAM, BT 5.0, hardware AES/ECC, USB device, NFC-A. ~$5-8. |
|
||||
| Display | 0.96" or 1.3" SSD1306 OLED (I2C) or 1.02" e-paper | Small is fine — only shows "approve kind 1 from <caller>?" |
|
||||
| Input | 2-3 tactile buttons (approve/deny/back) | No touch at this size |
|
||||
| Power | 200 mAh coin cell or small LiPo | Weeks of battery life |
|
||||
| Mnemonic entry | Buttons (scroll words), NFC from phone, or generate-on-device | The hard UX problem |
|
||||
|
||||
## Security considerations
|
||||
|
||||
- **BT stack attack surface:** BLE has a large stack (pairing, GATT, L2CAP, SMP).
|
||||
A stack bug could allow code execution. Mitigations: use Nordic's audited
|
||||
SoftDevice, disable unnecessary services, require LE Secure Connections pairing.
|
||||
- **Radio range (~10 m):** an attacker in the same room could potentially
|
||||
interact with the signer. The auth envelope + approval prompt protect against
|
||||
this, but the radio is omnidirectional.
|
||||
- **Pairing UX:** BT pairing can be frustrating. LE Secure Connections (Numeric
|
||||
Comparison) is the most secure and user-friendly pairing method.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Mnemonic entry on a tiny screen:** scroll through 2048 BIP-39 words with
|
||||
up/down buttons (like Coldcard)? Load via NFC from a phone? Generate on-device
|
||||
and display for the user to write down?
|
||||
- **PQ crypto on nRF52840:** 256 KB RAM is enough for ML-DSA-65 (~6 KB heap)
|
||||
but SLH-DSA-128s is heavy. May need to limit the PQ algorithm set or stream
|
||||
the keygen.
|
||||
- **Display choice:** OLED (fast refresh, high power) vs e-paper (slow refresh,
|
||||
zero power when static, persistent display).
|
||||
- **Form factor:** wristband? pendant? card? What's the target use case —
|
||||
daily signing, emergency key access, or a backup signer?
|
||||
|
||||
## Comparison to the IR air-gap signer
|
||||
|
||||
| | BLE wearable | IR air-gap |
|
||||
|---|---|---|
|
||||
| Air-gap | Medium (radio, ~10 m, omnidirectional) | High (light, line-of-sight, ~1 m) |
|
||||
| Attack surface | Large (BT stack) | Small (no BT, dumb dongle) |
|
||||
| Host compatibility | Universal (phones, laptops) | Requires USB dongle |
|
||||
| Form factor | Wearable | Handheld (point at dongle) |
|
||||
| Throughput | ~250 KB/s (BLE 5) | ~11 KB/s (raw IR) or ~400 KB/s (IrDA) |
|
||||
| Novelty | Conventional | Novel (no hardware wallet uses IR) |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide on the MCU (nRF52840 vs RP2040+BT-module)
|
||||
- Decide on mnemonic entry method
|
||||
- Decide on display (OLED vs e-paper)
|
||||
- Write a port plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))
|
||||
175
firmware/cyd_esp32_2432s028/README.md
Normal file
175
firmware/cyd_esp32_2432s028/README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# n_signer CYD Firmware (ESP32-2432S028)
|
||||
|
||||
The **Cheap Yellow Display** (ESP32-2432S028) is a $15 ESP32-WROOM-32 board with
|
||||
a 2.8" 320×240 ILI9341 resistive-touch display, CH340 USB-UART bridge, and a
|
||||
Micro SD card slot. This firmware turns it into a hardware n_signer that speaks
|
||||
the same algorithm-based API as the host ([`README.md`](../../README.md) §4).
|
||||
|
||||
**Firmware version:** 0.0.2 (algorithm-based API)
|
||||
|
||||
## Hardware summary
|
||||
|
||||
| Concern | Value |
|
||||
|---|---|
|
||||
| MCU | ESP32-WROOM-32 (classic, dual-core Xtensa, 512 KB SRAM, no PSRAM) |
|
||||
| USB-UART | CH340 (`1a86:7523`) → `/dev/ttyUSB0` |
|
||||
| Flash | 4 MB |
|
||||
| Display | 2.8" 320×240 ILI9341 (HSPI: DC=IO2, CS=IO15, SCK=IO14, MOSI=IO13, MISO=IO12, BL=IO21) |
|
||||
| Touch | XPT2046 resistive (bit-banged SPI: CLK=IO25, MOSI=IO32, CS=IO33, MISO=IO39, IRQ=IO36) |
|
||||
| SD card | Micro SD, VSPI (CS=IO5, SCK=IO18, MISO=IO19, MOSI=IO23) |
|
||||
| RGB LED | R=IO4, G=IO16, B=IO17 (active LOW) |
|
||||
| LDR | IO34 |
|
||||
| Speaker | IO26 (DAC) |
|
||||
| BOOT button | IO0 |
|
||||
| GUI | LVGL 8.3 |
|
||||
|
||||
For the full pin map, connectors (P1/P3/CN1), and add-ons, see the upstream
|
||||
hardware docs copied to [`docs/`](docs/) — especially
|
||||
[`docs/PINS.md`](docs/PINS.md) and [`docs/SETUP.md`](docs/SETUP.md).
|
||||
|
||||
## SD card — size limits and OTP pad storage
|
||||
|
||||
The CYD's Micro SD slot is wired to VSPI (IO5/18/19/23). ESP-IDF drives it via
|
||||
the SDSPI host + FATFS filesystem. The proven bring-up example is
|
||||
`07_sd_card` in the `esp32_playground/cyb-esp32-2432s028/` workspace.
|
||||
|
||||
**Size limits:**
|
||||
|
||||
- **SDSC (≤ 2 GB):** supported.
|
||||
- **SDHC (2 GB – 32 GB):** supported — this is the recommended range. The
|
||||
Makerfabs CYD ships with a 16 GB card, which works.
|
||||
- **SDXC (> 32 GB):** **not supported** out of the box. SDXC cards ship
|
||||
formatted as exFAT, and ESP-IDF's FATFS does not include exFAT. An SDXC card
|
||||
reformatted to FAT32 will work up to 32 GB; beyond that, FAT32's 32 GB limit
|
||||
applies. For OTP pad storage, 32 GB is vastly more than enough (see below).
|
||||
|
||||
**Recommendation:** use any **SDHC card from 4–32 GB** formatted **FAT32**.
|
||||
|
||||
### Using the SD card for the OTP pad
|
||||
|
||||
The current v0.0.2 firmware derives the OTP pad from the mnemonic seed via
|
||||
HKDF-SHA256 into a 1024-byte in-RAM pad (no SD card required). This keeps the
|
||||
wire contract identical to the host's `encrypt`/`decrypt` (algorithm:"otp")
|
||||
verbs but limits the pad to 1024 bytes per session.
|
||||
|
||||
To hold a **large OTP pad** (the original n_signer host design binds a pad file
|
||||
from `--otp-pad-dir`), the SD card is the right storage. The plan:
|
||||
|
||||
1. Format the SD card as FAT32.
|
||||
2. Place a pad file (e.g. `nsigner.pad`) on it — any size up to the card's free
|
||||
space. A 1 GB pad gives ~1 billion one-time-pad bytes before exhaustion.
|
||||
3. The firmware mounts the SD card at boot via `esp_vfs_fat_sdmmc_mount()` on
|
||||
the SDSPI host, opens the pad file, and reads pad bytes on demand into a
|
||||
small ring buffer, advancing a persistent offset (stored in a small
|
||||
`nsigner.offset` file on the SD so the offset survives power cycles).
|
||||
4. The `encrypt`/`decrypt` verbs XOR against the SD-backed pad instead of the
|
||||
HKDF-derived in-RAM pad.
|
||||
|
||||
This is a planned enhancement (see [`plans/cyd_algorithm_api_upgrade.md`](../../plans/cyd_algorithm_api_upgrade.md)
|
||||
§13 — the current implementation uses the mnemonic-derived pad as the embedded
|
||||
fallback). The SD card slot is confirmed working and the pin map is in
|
||||
[`docs/PINS.md`](docs/PINS.md).
|
||||
|
||||
**Note on simultaneous display + touch + SD:** The CYD's display (HSPI), touch
|
||||
(bit-banged), and SD (VSPI) use three different SPI buses. All three can run at
|
||||
the same time — the touch is bit-banged precisely so it doesn't contend with
|
||||
the other two hardware SPI buses (see [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md)).
|
||||
|
||||
## Building and flashing
|
||||
|
||||
Requires ESP-IDF v5.x (tested with v5.4.2). The classic ESP32 target uses the
|
||||
`xtensa-esp-elf` unified toolchain.
|
||||
|
||||
```bash
|
||||
source /home/user/esp/esp-idf/export.sh
|
||||
cd firmware/cyd_esp32_2432s028
|
||||
idf.py build
|
||||
idf.py -p /dev/ttyUSB0 flash
|
||||
```
|
||||
|
||||
If flashing fails with `Wrong boot mode detected (0x13)`, see the serial-reset
|
||||
hardware note below.
|
||||
|
||||
## Validation — Web Serial
|
||||
|
||||
The CYD has no native USB; the CH340 bridge exposes a serial port. The browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test
|
||||
page covering every algorithm and verb lives at
|
||||
[`examples/cyd_webserial_demo.html`](../../examples/cyd_webserial_demo.html):
|
||||
|
||||
1. Open [`examples/cyd_webserial_demo.html`](../../examples/cyd_webserial_demo.html) in Chrome/Edge.
|
||||
2. Click **Connect Web Serial**, select the CH340 port (`1a86:7523`).
|
||||
3. On the CYD touchscreen, enter or generate a mnemonic to reach the "ready" state.
|
||||
4. Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`,
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`,
|
||||
`nostr_nip04`/`nostr_nip44` encrypt+decrypt, and `encrypt`/`decrypt` (otp).
|
||||
|
||||
## API
|
||||
|
||||
The CYD firmware speaks the same algorithm-based API as the host n_signer
|
||||
([`README.md`](../../README.md) §4). Supported verbs:
|
||||
|
||||
| Verb | Algorithms |
|
||||
|---|---|
|
||||
| `get_public_key` | secp256k1, ed25519, x25519, ml-dsa-65, slh-dsa-128s, ml-kem-768 |
|
||||
| `sign` / `verify` | secp256k1 (schnorr/ecdsa), ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `derive` | secp256k1 (HMAC-SHA256) |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `nostr_get_public_key` | secp256k1 (NIP-06) |
|
||||
| `nostr_sign_event` | secp256k1 (NIP-06) |
|
||||
| `nostr_mine_event` | secp256k1 (NIP-06, single-threaded PoW) |
|
||||
| `nostr_nip04_encrypt` / `decrypt` | secp256k1 (NIP-06) |
|
||||
| `nostr_nip44_encrypt` / `decrypt` | secp256k1 (NIP-06) |
|
||||
|
||||
All requests require an auth envelope (kind 27235). The `key_id` in every
|
||||
structured result is the first 16 hex characters of the public key, matching
|
||||
the host. Invalid `(verb, algorithm)` pairs return error `1010`.
|
||||
|
||||
### Embedded-specific notes
|
||||
|
||||
- **OTP pad:** derived from the mnemonic seed (HKDF-SHA256, 1024 bytes) in
|
||||
v0.0.2. The offset advances monotonically and is reported in every
|
||||
`encrypt`/`decrypt` response. SD-card-backed pad is a planned enhancement
|
||||
(see above).
|
||||
- **`nostr_mine_event`:** single-threaded, hard 30 s default timeout, shows a
|
||||
"mining…" screen. Keep difficulty low (≤ 8) on ESP32.
|
||||
- **SLH-DSA-128s:** keygen and signing take 5–30 s. The UI shows a "deriving
|
||||
key…" / "signing…" indicator. ML-DSA-65 is much faster (~100 ms) and is the
|
||||
recommended PQ signature algorithm for interactive use.
|
||||
|
||||
## Crypto backend
|
||||
|
||||
- **SHA-256 / SHA-512:** mbedtls (ESP32 hardware accelerated).
|
||||
- **SHA3 / SHAKE-128 / SHAKE-256:** vendored Keccak-f[1600] (FIPS 202) in
|
||||
[`resources/pqclean/common/crypto_backend_mbedtls.c`](../../resources/pqclean/common/crypto_backend_mbedtls.c).
|
||||
No `CONFIG_MBEDTLS_SHA3_C` or SHAKE menuconfig dependency.
|
||||
- **ed25519 / x25519:** PSA Crypto API (`psa_import_key`, `psa_sign_message`,
|
||||
`psa_raw_key_agreement`, etc.) — ESP-IDF v5.x mbedtls removed the
|
||||
`mbedtls_ed25519_*` functions. Requires `CONFIG_MBEDTLS_PSA_CRYPTO_C=y`
|
||||
(set in [`sdkconfig.defaults`](sdkconfig.defaults)).
|
||||
- **secp256k1:** the vendored secp256k1 component (schnorr + ECDSA).
|
||||
- **PQ (ML-DSA-65, SLH-DSA-128s, ML-KEM-768):** PQClean via the
|
||||
[`components/pqclean/`](components/pqclean/) component.
|
||||
|
||||
## Serial-reset hardware note (CH340 auto-reset)
|
||||
|
||||
Opening `/dev/ttyUSB0` can reset the ESP32 because the CH340's DTR/RTS lines
|
||||
are wired into the ESP32 auto-reset circuit. Symptoms: the device returns to
|
||||
the startup menu when a host app opens the serial port.
|
||||
|
||||
**Mitigation:** add a **10 µF capacitor between EN and GND** on the CYD board
|
||||
(negative leg to GND). Typical working range is 4.7–22 µF. This also fixes the
|
||||
`Wrong boot mode detected (0x13)` flashing error. See
|
||||
[`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) and the
|
||||
[`firmware/README.md`](../README.md) CYD section for details.
|
||||
|
||||
## Reference documentation
|
||||
|
||||
- [`docs/`](docs/) — upstream CYD hardware docs (PINS, SETUP, TROUBLESHOOTING, ADDONS, etc.)
|
||||
- [`plans/cyd_signer_port.md`](../../plans/cyd_signer_port.md) — original port plan (hardware comparison, architecture, UI flow)
|
||||
- [`plans/cyd_algorithm_api_upgrade.md`](../../plans/cyd_algorithm_api_upgrade.md) — v0.0.2 API upgrade plan
|
||||
- [`firmware/README.md`](../README.md) — shared firmware README (PQ crypto, mbedtls backend, feather target)
|
||||
- [`README.md`](../../README.md) §4 — the authoritative n_signer API reference
|
||||
86
firmware/cyd_esp32_2432s028/docs/ADDONS.md
Normal file
86
firmware/cyd_esp32_2432s028/docs/ADDONS.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Add-Ons
|
||||
|
||||
Here is a list of additional hardware add-ons that can add functionality to your CYD
|
||||
|
||||
## SD Card Sniffer
|
||||
|
||||
If you want to use the pins of the SD card for a different purpose, the easiest way to do that is with an "SD card sniffer", which basically plugs into the SD card slot and breaks out the pins. It's particularly useful for SPI devices.
|
||||
|
||||
## Pin-out of the Sniffer board
|
||||
|
||||
| Sniffer Board Label | ESP32 Pin | SPI Use |
|
||||
| ------------------- | --------- | --------- |
|
||||
| DAT2 | - | - |
|
||||
| CD | IO5 | CS |
|
||||
| CMD | IO23 | DI / MOSI |
|
||||
| GND | GND | - |
|
||||
| VCC | 3.3V | - |
|
||||
| CLK | IO18 | SCLK |
|
||||
| DAT0 | IO19 | DO / MISO |
|
||||
| DAT1 | - | - |
|
||||
|
||||
### Links
|
||||
|
||||
- [Micro SD Card Sniffer - Aliexpress\*](https://s.click.aliexpress.com/e/_Ddwcy9h)
|
||||
|
||||
## Nintendo Wii Nunchuck
|
||||
|
||||
A Nunchuck controller from a Nintendo Wii is a great input device for CYD projects as they are inexpensive and, since they use i2c for communication, they only require 2 GPIO pins to connect them up.
|
||||
|
||||
For these two pins you get:
|
||||
|
||||
- An analog stick
|
||||
- 2 Buttons
|
||||
- An accelerometer
|
||||
|
||||
### Hardware Required
|
||||
|
||||
#### Nunchuck controllers
|
||||
|
||||
Official Nintendo ones are generally better (maybe try second-hand options), but third-party ones also work fine.
|
||||
|
||||
- [Amazon.co.uk Search\*](https://amzn.to/3nQrXcE)
|
||||
- [Amazon.com Search\*](https://amzn.to/3nRJTUd)
|
||||
- [Aliexpress (Third Party)\*](https://s.click.aliexpress.com/e/_AaQbXh)
|
||||
|
||||
#### Nunchuck Adaptors
|
||||
|
||||
There are many different options available for these, even the cheap ones from Aliexpress work perfectly.
|
||||
|
||||
- [Aliexpress](https://s.click.aliexpress.com/e/_AEEtc3)
|
||||
- [My Open source one from Oshpark](https://oshpark.com/shared_projects/RcIxSx2D)
|
||||
- [Adafaruit](https://www.adafruit.com/product/4836)
|
||||
|
||||
### Wiring
|
||||
|
||||
The easiest way to wire this up is to use the wire that came with the CYD and the **CN1** JST connector (the one closest to the Micro SD card slot)
|
||||
|
||||
Connect the wire to your breakout board as follows:
|
||||
|
||||
| CYD CN1 | Adapter | Note |
|
||||
| ------- | ----------- | ------------------ |
|
||||
| GND | - (AKA GND) | Black wire for me |
|
||||
| 3.3V | + (AKA 3V) | Red wire for me |
|
||||
| IO22 | d (AKA SDA) | Blue wire for me |
|
||||
| IO27 | c (AKA SCL) | Yellow wire for me |
|
||||
|
||||
Note: I have found pull-ups resistors are not required on SDA and SCL
|
||||
|
||||
### Example
|
||||
|
||||
Check out the [NunchuckTest](/Examples/InputTests/NunchuckTest) example for code how to use it.
|
||||
|
||||
## Speakers
|
||||
|
||||
A speaker can be attached to the display with a 1.25mm JST connector to the connector labeled "SPEAK" (or soldered)
|
||||
|
||||
Check out the [HelloRadio](/Examples/Basics/7-HelloRadio) example for the code on how to use it.
|
||||
|
||||
Most small 8 Ohm speakers should work. Maybe worth adding a 1.25mm JST connector to it to make it easy to add remove.
|
||||
|
||||
### Links
|
||||
|
||||
- [Speaker with 1.25mm JST connector (2pcs) - Aliexpress\*](https://s.click.aliexpress.com/e/_DBOJoh7) - Tested, works right out of the package.
|
||||
- [2pin 1.25mm JST connectors - Aliexpress\*](https://s.click.aliexpress.com/e/_DlbPkWH) - Not purchased by me, but should work
|
||||
|
||||
\* = Affiliate Link - It doesn't cost you any extra but I receive a small portion of the sale.
|
||||
14
firmware/cyd_esp32_2432s028/docs/MEDIA.md
Normal file
14
firmware/cyd_esp32_2432s028/docs/MEDIA.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Media and Mentions
|
||||
|
||||
This page can document any times the CYD project was mentioned somewhere!
|
||||
|
||||
## Videos
|
||||
|
||||
- [Brian Lough (hey, thats me!) - Cheap and Easy to Use ESP32 Screen!](https://www.youtube.com/watch?v=0AVyvwv0agk)
|
||||
- [Talking Sasquach - Don't be Fooled!! This Cheap Yellow Display Can Do a LOT!!](https://youtu.be/PsqMCoCTgTg?feature=shared)
|
||||
- [Teaching Tech - Cheap and easy Klipper touch interface with CYD Klipper](https://youtu.be/R3o0MGYW1ZU?feature=shared)
|
||||
|
||||
## Articles
|
||||
|
||||
- [Hackaday.com - “Cheap Yellow Display” Builds Community Through Hardware](https://hackaday.com/2023/10/28/cheap-yellow-display-builds-community-through-hardware/)
|
||||
- [Hackster.io - Brian Lough Looks to Build a Community Around the Espressif ESP32-Powered "Cheap Yellow Display"](https://www.hackster.io/news/brian-lough-looks-to-build-a-community-around-the-espressif-esp32-powered-cheap-yellow-display-66d23972910d)
|
||||
140
firmware/cyd_esp32_2432s028/docs/PINS.md
Normal file
140
firmware/cyd_esp32_2432s028/docs/PINS.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Pins
|
||||
|
||||
This page talks about the pins on the CYD.
|
||||
|
||||
## Connector types
|
||||
|
||||
The connectors are often called "1.25mm JST" but the correct name is "Molex PicoBlade".
|
||||
Chinese clones are sometimes called "mx1.25".
|
||||
|
||||
|Connector|Type |Note |
|
||||
|--- |--- |---- |
|
||||
|[**P1**](#p1) |4P 1.25mm Molex PicoBlade|Serial |
|
||||
|[**P3**](#p3) |4P 1.25mm Molex PicoBlade|GPIO |
|
||||
|[**P4**](#p4) |2P 1.25mm Molex PicoBlade|Speaker |
|
||||
|[**CN1**](#cn1)|4P 1.25mm Molex PicoBlade|GPIO (I2C) |
|
||||
|
||||
## What pins are available on the CYD?
|
||||
|
||||
There are 3 easily accessible GPIO pins
|
||||
|
||||
|Pin|Location|Note|
|
||||
|---|---|----|
|
||||
|IO35|**P3** Molex PicoBlade connector|Input only pin, no internal pull-ups available|
|
||||
|IO22|**P3** and **CN1** Molex PicoBlade connector||
|
||||
|IO27|**CN1** Molex PicoBlade connector||
|
||||
|
||||
If you need more than that, you need to start taking them from something else. An SD Card sniffer like mentioned in the [Add-ons](/ADDONS.md) is probably the next easiest.
|
||||
|
||||
After that you're probably de-soldering something!
|
||||
|
||||
## Broken Out Pins
|
||||
|
||||
There are three 4P 1.25mm Molex PicoBlade connectors on the board.
|
||||
|
||||
### P3
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|GND|||
|
||||
|IO35||Input only pin, no internal pull-ups available|
|
||||
|IO22||Also on the **CN1** connector|
|
||||
|IO21||Used for the TFT Backlight, so not really usable|
|
||||
|
||||
### CN1
|
||||
This is a great candidate for I2C devices
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|GND|||
|
||||
|IO22||Also on **P3** connector|
|
||||
|IO27|||
|
||||
|3.3V|||
|
||||
|
||||
### P1
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|VIN|||
|
||||
|IO1(?)|TX|Maybe possible to use as a GPIO?|
|
||||
|IO3(?)|RX|Maybe possible to use as a GPIO?|
|
||||
|GND|||
|
||||
|
||||
|
||||
## Buttons
|
||||
|
||||
The CYD has two buttons, reset and boot.
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO0|BOOT|Can be used as an input in sketches|
|
||||
|
||||
## Speaker
|
||||
|
||||
The speaker connector is a 2P 1.25mm Molex PicoBlade connector that is connected to the amplifier, so not usable as GPIO at the speaker connector
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO26|Connected to amp|`i2s_set_dac_mode(I2S_DAC_CHANNEL_LEFT_EN);`|
|
||||
|
||||
## RGB LED
|
||||
|
||||
If your project requires additional pins to what is available elsewhere, this might be a good candidate to sacrifice.
|
||||
|
||||
Note: LEDs are "active low", meaning HIGH == off, LOW == on
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO4|Red LED||
|
||||
|IO16|Green LED||
|
||||
|IO17|Blue LED||
|
||||
|
||||
## SD Card
|
||||
Uses the VSPI
|
||||
Pin names are predefined in SPI.h
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO5|SS||
|
||||
|IO18|SCK||
|
||||
|IO19|MISO||
|
||||
|IO23|MOSI||
|
||||
|
||||
## Touch Screen
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO25|XPT2046_CLK||
|
||||
|IO32|XPT2046_MOSI||
|
||||
|IO33|XPT2046_CS||
|
||||
|IO36|XPT2046_IRQ||
|
||||
|IO39|XPT2046_MISO||
|
||||
|
||||
## LDR (Light Sensor)
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO34|||
|
||||
|
||||
## Display
|
||||
Uses the HSPI
|
||||
|
||||
|Pin|Use|Note|
|
||||
|---|---|----|
|
||||
|IO2|TFT_RS|AKA: TFT_DC|
|
||||
|IO12|TFT_SDO|AKA: TFT_MISO|
|
||||
|IO13|TFT_SDI|AKA: TFT_MOSI|
|
||||
|IO14|TFT_SCK||
|
||||
|IO15|TFT_CS||
|
||||
|IO21|TFT_BL|Also on P3 connector, for some reason|
|
||||
|
||||
## Test points
|
||||
|Pad|Use|Note|
|
||||
|---|---|----|
|
||||
|S1|GND|near USB-SERIAL|
|
||||
|S2|3.3v|for ESP32|
|
||||
|S3|5v|near USB-SERIAL|
|
||||
|S4|GND|for ESP32|
|
||||
|S5|3.3v|for TFT|
|
||||
|JP0 (pad nearest USB socket)|5v|TFT LDO|
|
||||
|JP0|3.3v|TFT LDO|
|
||||
|JP3 (pad nearest USB socket)|5v|ESP32 LDO|
|
||||
|JP3|3.3v|ESP32 LDO|
|
||||
60
firmware/cyd_esp32_2432s028/docs/PROJECTS.md
Normal file
60
firmware/cyd_esp32_2432s028/docs/PROJECTS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Projects
|
||||
|
||||
Because the CYD is a common platform, it makes it really useful for sharing projects. This page will be a list of projects that are available on the CYD.
|
||||
|
||||
## Disclaimer!
|
||||
|
||||
Projects appearing on here is not necessarily a seal of approval from me, I will not be test each project that gets added, so please install these projects at your own risk!
|
||||
|
||||
## Projects
|
||||
|
||||
| Name | Description | Author | Additional Hardware? | Project Page | WebFlash |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
||||
| Spotify DIY Thing | A device for displaying your currently playing Spotify track | [Brian Lough](https://github.com/witnessmenow) | | [Github](https://github.com/witnessmenow/Spotify-Diy-Thing) | [WebFlash](https://witnessmenow.github.io/Spotify-Diy-Thing/) |
|
||||
| F1 Notifier | Displays and notifies you of the F1 session times(in your local timezone) | [Brian Lough](https://github.com/witnessmenow) | | [Github](https://github.com/witnessmenow/F1-Arduino-Notifications) | [WebFlash](https://witnessmenow.github.io/F1-Arduino-Notifications/) |
|
||||
| Tetris with Nunchuck | A version of Tetris using a Nintendo wii Nunchuck | [Brian Lough](https://github.com/witnessmenow) | A nunchuck and an adaptor for connecting it | [Code](/Examples/Projects/TetrisWithNunchuck) | |
|
||||
| Galagino | An emulator for some classic arcade games (Galaga, Donkey Kong, Pacman, Digdug and Frogger) | [Till Harbaum](https://github.com/harbaum) | A nunchuck and an adaptor for connecting it. Speaker if you want sound | [Github](https://github.com/harbaum/galagino) | |
|
||||
| ESP32-fluid-simulation | A small fluid simulation with touch input | [Kenny Peng](https://github.com/colonelwatch) | | [Github](https://github.com/colonelwatch/ESP32-fluid-simulation) | |
|
||||
| ESP32-TV | Play Video Files on the ESP32 | [atomic14](https://github.com/atomic14) | Speaker if you want sound and possibly an IR receiver | [Github](https://github.com/atomic14/esp32-tv) | |
|
||||
| xtouch | "The xtouch screen is a revolutionary addition to your BambuLab Printer" | [xperiments-in](https://github.com/xperiments-in) (\#) | | [Github](https://github.com/xperiments-in/xtouch) | [Webflash](https://github.com/xperiments-in/xtouch#online-web-installer) |
|
||||
| CYD-Klipper | An implementation of a wireless Klipper status display on an ESP32 + screen | [Sims](https://github.com/suchmememanyskill) | | [Github](https://github.com/suchmememanyskill/CYD-Klipper) | [Webflash](https://suchmememanyskill.github.io/CYD-Klipper/) |
|
||||
| DRO (for lathe / mill) | A DRO (digital readout) for your lathe or mill | [Alanesq](https://github.com/alanesq) | It uses cheap digital caliper, requires a very basic interface | [Github](https://github.com/alanesq/DRO) | |
|
||||
| ESP32Marauder-CYD | A suite of WiFi/Bluetooth offensive and defensive tools for the ESP32 | [Fr4nkFletcher](https://github.com/Fr4nkFletcher) | GPS if you want BT/Wifi wardriving options | [Github](https://github.com/Fr4nkFletcher/ESP32-Marauder-Cheap-Yellow-Display) | [Webflash](https://fr4nkfletcher.github.io/Adafruit_WebSerial_ESPTool/) |
|
||||
| NerdMiner_v2 | A project that lets you try to solve a bitcoin block with a small piece of hardware. | [Fr4nkFletcher](https://github.com/Fr4nkFletcher) | | [Github](https://github.com/Fr4nkFletcher/NerdMiner_v2-Cheap-Yellow-Display) | [Webflash](https://fr4nkfletcher.github.io/NerdMiner_v2-Cheap-Yellow-Display/flash.html) |
|
||||
| Tasmota | Tasmota (with UI) on the CYD | ? (\#) | | [Templates](https://templates.blakadder.com/sunton_ESP32-2432S028.html) | [Webflash](https://tasmota.github.io/install/) |
|
||||
| BAM | A game engine featuring smooth scrolling tile map, sprites in layers with pixel precision on-screen collision detection, intuitive definition of game objects and logic, decent performance, ~30 frames per second on the device | [calint](https://github.com/calint) | | [Github](https://github.com/calint/bam) | |
|
||||
|London Underground Arrivals| A highly configurable application that replicates the train arrivals boards found in [TFL](https://tfl.gov.uk/) stations. All variable data is encoded in a json file that may be updated at any time without the need to recompile the application e.g. the station to be displayed or the time to refresh data from TFL. The source code already supports 2 variants of CYD and, I hope, contains clear instructions how to handle any other variant.| [David Henry](https://github.com/mgaman) | | [Github](https://github.com/mgaman/TFL-tube-arrivals-board-ESP32-TFT-Arduino) |
|
||||
|GitHub-Stats| This Arduino project fetches and displays GitHub repository statistics such as star count, open issues, forks and notifactions on a CYD or via serial communication. Ideal for developers to monitor project metrics in real time.| [ATOMNFT](https://github.com/ATOMNFT) | | [Github](https://github.com/ATOMNFT/ESP32-CYD-Projects/tree/main/GitHub-Stats) | |
|
||||
| Midbar-Firebase-Edition | An advanced password vault that stores the encrypted data in the cloud while keeping the cryptographic keys on the edge! | [Northstrix](https://github.com/Northstrix) | PS/2 keyboard and an optional STM32F103C8T6 (if you want it to emulate the USB keyboard) | [SourceForge](https://sourceforge.net/projects/midbar-firebase-edition/) [Github](https://github.com/Northstrix/Midbar-Firebase-Edition)
|
||||
| Electronic-Shelf-Label-Management-System | A simple device for displaying relevant product information. It gets the encrypted images via UDP. | [Northstrix](https://github.com/Northstrix)| | [SourceForge](https://sourceforge.net/projects/esl-management-system/) [Github](https://github.com/Northstrix/Electronic-Shelf-Label-Management-System)
|
||||
| ESP32-Tetris-With-Nintendo-64-Controller | Tetris for ESP32 with Nintendo 64 controller support | [Northstrix](https://github.com/Northstrix) | Nintendo 64 Controller and Arduino Nano | [SourceForge](https://sourceforge.net/projects/esp32-tetris/) [Github](https://github.com/Northstrix/ESP32-Tetris-With-Nintendo-64-Controller)
|
||||
| Midbar ESP32 CYD | A version of Midbar data vault tweaked specifically for the ESP32 Cheap Yellow Display. | [Northstrix](https://github.com/Northstrix) | PS/2 Keyboard | [SourceForge](https://sourceforge.net/projects/midbar-esp32-cyd/) [Github](https://github.com/Northstrix/Midbar-ESP32-CYD)
|
||||
| ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase | An ESP32 CYD-based Electronic Shelf Label that makes use of the Google Firebase and AES-256. | [Northstrix](https://github.com/Northstrix) | | [SourceForge](https://sourceforge.net/projects/esp32-cyd-esl-with-firebase/) [Github](https://github.com/Northstrix/ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase) | [WebFlash](https://northstrix.github.io/ESP32-Cheap-Yellow-Display-Electronic-Shelf-Label-with-Google-Firebase/flash.html) </br>!!! Format Flash area designated for SPIFFS with [ESP32 Filesystem Uploader](https://github.com/me-no-dev/arduino-esp32fs-plugin/releases/) after using the WebFlash
|
||||
| Addressable RGB LED Strip Controller (The Lantern Project) | DIY Addressable RGB LED Strip Controller that utilizes the ESP32, ESP8266, and the WS2812 LED Strip. | [Northstrix](https://github.com/Northstrix) | Nintendo Wii Nunchuk, WiiChuck Nunchuck Adapter (PCB Board), ESP8266, 580 Ohm resistor, WS2812 LED Strip | [SourceForge](https://sourceforge.net/projects/the-lantern-project/) [Github](https://github.com/Northstrix/Lantern)
|
||||
| Midbar ESP32 CYD Firebase Edition | A version of Midbar data vault adapted for the ESP32 CYD and WebFlash. It keeps the cryptographic keys in the ESP32 RAM and stores the ciphertexts (encrypted data) in the Google Firebase. | [Northstrix](https://github.com/Northstrix) (Adapted for CYD2USB by [Rovel](https://github.com/Rovel))| PS2 Keyboard, PS2 Port *optional | [SourceForge](https://sourceforge.net/projects/midbar-esp32-cyd-firebase/) [Github (CYD)](https://github.com/Northstrix/Midbar-ESP32-CYD-Firebase-Edition) [Github (CYD2USB)](https://github.com/Northstrix/Midbar-ESP32-CYD2USB-Firebase-Edition) | [WebFlash (CYD)](https://northstrix.github.io/Midbar-ESP32-CYD-Firebase-Edition/flash) [WebFlash (CYD2USB)](https://northstrix.github.io/Midbar-ESP32-CYD2USB-Firebase-Edition/flash)
|
||||
| cydOS (WIP) | cydOS is a GUI app that is able to manage various aspects of the CYD, like SD browsing and file mangement, on board flashing of .bin files for rapid firmware switching, on board device settings(WIP) | [orlandobianco](https://github.com/orlandobianco) | | [Github]((https://github.com/orlandobianco/cydOS)) | |
|
||||
| ESP32 MFA Authenticator | Turn the CYD into a MFA Authenticator | [AllanOricil](https://github.com/AllanOricil) | | [Github](https://github.com/AllanOricil/esp32-mfa-authenticator) | [Webflash](https://allanoricil.github.io/esp32-mfa-authenticator/)
|
||||
| cydWeatherStation | cyd Weather station | [gustheseventh](https://github.com/gustheseventh) (#) | | [Github](https://github.com/gustheseventh/cyd-Weather-Station) | |
|
||||
| PhilRadio | CYD Wifi Radio project. Re-using an old radio as hardware. Exposing a webserver on local network to configure the radio stations. Persistent storage. | [mogrikid](https://github.com/mogrikid) | Required: A speaker. Recommended: Speaker, potentiometer, 10kohm resistor, female usb port, switch | [Github](https://github.com/mogrikid/PhilRadio)
|
||||
| cydWeeWX | Simple CYD Weather Display for the open source [WeeWX](https://www.weewx.com/) weather station server. | [hcomet](https://hcomet.github.io/) | | [Github](https://github.com/hcomet/cydWeeWX)| [Webflash](https://hcomet.github.io/cydWeeWX/cydWeeWXFlash.html) |
|
||||
| CYD Stream Deck | A customizable touch-based Bluetooth HID controller using CYD. | [gahingwoo](https://github.com/gahingwoo) | | [GitHub](https://github.com/gahingwoo/cyd-stream-deck) | [Webflash](https://gahingwoo.github.io/cyd-stream-deck/webflash/index.html) |
|
||||
| CYD DHT22 Weather Clock | A weather and time display using CYD. | [gahingwoo](https://github.com/gahingwoo) | DHT22 sensor | [GitHub](https://github.com/gahingwoo/cyd-dht22-weather-clock) | |
|
||||
| ESP CYD MCP | Model Context Protocol (MCP) server implementation for the ESP32 CYD | [OfryL](https://github.com/OfryL) | | [Github](https://github.com/OfryL/esp-cyd-mcp) | |
|
||||
| Aura | Smart weather forecast device (OpenMeteo) | [Surrey-Homeware](https://github.com/Surrey-Homeware/) (\#) | [3D printed case](https://makerworld.com/en/models/1382304-aura-smart-weather-forecast-display#profileId-1430951) | [Github](https://github.com/Surrey-Homeware/Aura) | [Webflash](https://surrey-homeware.github.io/aura-installer/) |
|
||||
| SmartEnergyMeter | An ESPHome display for Energy in the house (solar, battery, etc) | [anthony-spruyt](https://github.com/anthony-spruyt) (#) | | [Github](https://github.com/anthony-spruyt/ESPHOME-ESP32_CYD_V2-SmartEnergyMeter) | |
|
||||
| Navi Phone | Relica of the Mobile Phones used int the anime Serial Experments Lain | [Aquafrostbyte](https://github.com/AquaFrostByte) (#) | A Sd card is required, Speaker and Wifi is optional | [Github](https://github.com/AquaFrostByte/Navi-Phone) | |
|
||||
| OASMan | Open-source Air Suspension Management - Worlds first DIY Digital Air suspension controller for your car! | [gopro_2027](https://github.com/gopro2027/) | Ideally you would build the manifold and install it in your car, but if you just want to test the connection you can use an original esp32 dev board and flash the manifold code through platformio. We also have a [3d printable case](https://github.com/gopro2027/ArduinoAirSuspensionController/blob/main/3d%20Prints/other/3.2%20inch%20screen%20case/3.2%20inch%20CYD%20screen%20container%20v19%20-%20gopro_2027's%20design.stl) | [Github](https://github.com/gopro2027/ArduinoAirSuspensionController) | [Webflash](https://oasman.dev/oasman/flash/) |
|
||||
| Sonos Remote Control | Use your Sonos Speakers as Internet Radio with Station Buttons | Florian Lenz | https://github.com/SpringTideSystems | [GitHub](https://github.com/SpringTideSystems/CYD_Sonos-RemoteControl) | |
|
||||
|
||||
(\#) = Project not added by original author
|
||||
|
||||
## Adding a project
|
||||
|
||||
If you have a project that you would like to add, please feel free to add it to the list!
|
||||
|
||||
New projects should be added to bottom of the list.
|
||||
|
||||
Some rules:
|
||||
|
||||
- Project must be open source
|
||||
- Project must be functional - It's ok for it to not be finished, but it should do what it says!
|
||||
114
firmware/cyd_esp32_2432s028/docs/README.md
Normal file
114
firmware/cyd_esp32_2432s028/docs/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# ESP32-Cheap-Yellow-Display
|
||||
|
||||
There is an ESP32 with a built in 320 x 240 2.8" LCD display with a touch screen called the "ESP32-2432S028R", since this doesn't roll of the tongue, I propose it should be renamed the "Cheap Yellow Display" or CYD for short. This display is only about $15 delivered so I think it's really good value.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
The CYD has the following features:
|
||||
|
||||
- ESP32 (With Wifi and Bluetooth)
|
||||
- 320 x 240 LCD Display (2.8")
|
||||
- Touch Screen (Resistive)
|
||||
- USB for powering and programming
|
||||
- SD Card Slot, LED and some additional pins broken out
|
||||
|
||||
## Who is it good for?
|
||||
|
||||
I think it's useful for the following types of people:
|
||||
|
||||
- **People just getting started with working hardware** - as everything is already connected, there is no soldering or additional components required
|
||||
- **People who are familiar with working with hardware, but are lazy** - (like me) Sometimes you just want to build a project without having to assemble any hardware
|
||||
- **People who aren't really looking to learn anything, but just want to build some cool things** - More about this later.
|
||||
|
||||
## What is the purpose of this page?
|
||||
|
||||
So this is pretty nice hardware and a cheap price, but the software instructions/support around it is pretty poor. Just a single link to a zip file on a random website.
|
||||
|
||||
A couple of years ago I released the [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity), which is an open source ESP32 board for controlling Matrix panels. I think the main benefit people get out of the work I did on the Trinity is not the hardware, but the documentation, example code and ready to go projects.
|
||||
|
||||
I'm no longer creating hardware products, but I think it would be interesting if we could create the same kind of community around this display, where people can share examples and projects made for this display.
|
||||
|
||||
## How do I know if a display is a CYD?
|
||||

|
||||
|
||||
## Where to buy?
|
||||
|
||||
Buy from wherever works out cheapest for you:
|
||||
|
||||
- [Aliexpress\*](https://s.click.aliexpress.com/e/_DkSpIjB)
|
||||
- [Aliexpress\*](https://s.click.aliexpress.com/e/_DkcmuCh)
|
||||
- [Aliexpress](https://www.aliexpress.com/item/1005004502250619.html)
|
||||
- [Makerfabs](https://www.makerfabs.com/sunton-esp32-2-8-inch-tft-with-touch.html) - Seems to come with a 16GB SD card. Makerfabs also stock my [ESP32 Trinity](https://github.com/witnessmenow/ESP32-Trinity) (NOTE there will be import due in the EU from makerfabs)
|
||||
|
||||
\* = Affiliate Link
|
||||
|
||||
## Getting Started With Your CYD
|
||||
|
||||
For details on how to get started with your CYD, please check out the [Setup and Configuration](/SETUP.md) page
|
||||
|
||||
## Code Examples
|
||||
|
||||
### The Basics
|
||||
|
||||
A collection of examples demonstrating how to use the different features of the CYD, this is a good place to get started. [Check them out here.](/Examples/Basics)
|
||||
|
||||
### Alternative Display Libraries
|
||||
|
||||
The basics examples are based on the TFT_eSPI display library, but the CYD also works with other display libraries too. Here is some example code if you prefer to use an alternative Arduino library. [Check them out here.](/Examples/AlternativeLibraries)
|
||||
|
||||
### ESPHome
|
||||
|
||||
Some examples for using the CYD in ESPHome. [Check them out here.](/Examples/ESPHome)
|
||||
|
||||
## Additional Info and Links
|
||||
|
||||
### Discord
|
||||
|
||||
Join the CYD discussion on [my Discord channel](https://discord.gg/nnezpvq)
|
||||
|
||||
### 3DPrinting
|
||||
|
||||
Some examples of 3D printed stands and cases. [Check them out here.](/3dModels)
|
||||
|
||||
### Pin Information
|
||||
|
||||
[This page](/PINS.md) contains information about what pins are used where, and what ones are free to use.
|
||||
|
||||
### Add-ons
|
||||
|
||||
[This page](/ADDONS.md) contains information about additional hardware add-ons that can add functionality to your CYD
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
[This page](/TROUBLESHOOTING.md) contains information about how to troubleshoot your CYD device
|
||||
|
||||
### Hardware Mods
|
||||
|
||||
[This page](/Mods/README.md) contains information about some hardware mods that can be performed on the CYD to improve or change some of its functionality
|
||||
|
||||
### Media and Video Mentions
|
||||
|
||||
[This page](/MEDIA.md) lists any times the CYD project was mentioned somewhere!
|
||||
|
||||
## License Info
|
||||
|
||||
This project is licensed as MIT as per the [license file](/LICENSE)
|
||||
|
||||
The one exception to this is the [OriginalDocumentation](/OriginalDocumentation/) folder, that I do not have the right to license
|
||||
|
||||
## Other Languages
|
||||
|
||||
Some members of the community have ported some of this information to other languages!
|
||||
|
||||
Please note: I can't gaurantee the accuracy of the translation, how up to date they are or the content on them in general.
|
||||
|
||||
- [French / Française](https://github.com/usini/ESP32-Cheap-Yellow-Display-Documentation-FR)
|
||||
- [German / Deutsch](https://github.com/paelzer/ESP32-Cheap-Yellow-Display-Documentation-DE)
|
||||
|
||||
If you would like to contribure a translation, please name the repo with the language name or code in the repo name and you can link it here.
|
||||
|
||||
## Help Support what I do!
|
||||
|
||||
[If you enjoy my work, please consider becoming a Github sponsor!](https://github.com/sponsors/witnessmenow/)
|
||||
39
firmware/cyd_esp32_2432s028/docs/SETUP.md
Normal file
39
firmware/cyd_esp32_2432s028/docs/SETUP.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Setup and Configuration options
|
||||
|
||||
This page will cover the basics of setting up the CYD
|
||||
|
||||
## Hardware Setup
|
||||
|
||||
There really is nothing to setup here, just connect the CYD to a computer using a micro USB cable (it even comes with one)
|
||||
|
||||
## Software Setup
|
||||
|
||||
The driver needs to be setup for uploading to the CYD, including webflashing projects.
|
||||
|
||||
### Driver
|
||||
|
||||
The CYD uses the CH340 USB to UART chip. If you do not have a driver already installed for this chip you may need to install one. Check out [Sparkfun's guide for installation instruction](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all)
|
||||
|
||||
## Coding Setup
|
||||
|
||||
Follow these instructions if you want to write new code for the CYD
|
||||
|
||||
### Board definition
|
||||
|
||||
You will need to have the ESP32 setup for your Arduino IDE, [instructions can be found here](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html).
|
||||
|
||||
You can then select basically any ESP32 board in the boards menu. (I usually use "ESP32 Dev Module", but it doesn't really matter)
|
||||
|
||||
If you see errors uploading a sketch, try setting board upload speed to `115200`
|
||||
|
||||
### Library Configuration
|
||||
|
||||
The CYD can work with a selection of different libraries, but the main one this repo will focus on is [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) as it is a fairly popular library for working with these types of displays and there are lots of examples.
|
||||
|
||||
This can be installed from the library manager by searching for "TFT_eSPI".
|
||||
|
||||
> Note: After install of the library, copy the file [User_Setup.h](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/DisplayConfig/User_Setup.h) to the `libraries\TFT_eSPI` Arduino folder. This sets up the library for use with this display.
|
||||
|
||||
### Examples
|
||||
|
||||
I have provided examples for you to try out to get some ideas or inspiration. [Check them out here.](/Examples/)
|
||||
45
firmware/cyd_esp32_2432s028/docs/TROUBLESHOOTING.md
Normal file
45
firmware/cyd_esp32_2432s028/docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# First, Make sure it's a CYD!
|
||||
|
||||
If you are having any issues, this is the first thing I would check!
|
||||
|
||||
The examples and information contained on this repo are for the **ESP32-2432S028** display only. The model number is written on the back of the display in gold writting, beside the speaker connector.
|
||||
|
||||
# Display is not turning on
|
||||
|
||||
If you are having issues getting the display working, the first thing I would try is [webflashing an existing project](/PROJECTS.md#projects-1). These will be known working code, and if it works correctly, it points to a software issue, not a hardware one.
|
||||
|
||||
## If the webflash project displays something on the screen
|
||||
|
||||
- Make sure you have put the [User_Setup.h](DisplayConfig/User_Setup.h) file in the correct location [as described here](/SETUP.md#library-configuration)
|
||||
- Pin 21 is the backlight pin, make sure you are not using it for something else in your sketch.
|
||||
|
||||
## The webflash project doesn't display on screen
|
||||
|
||||
- Make sure you are not connecting Pin 21 to anything. It is broken out on the connector labeled `P3`
|
||||
- Try a different USB supply and or cable
|
||||
- If nothing else worked, your CYD could be faulty. Contact the seller.
|
||||
|
||||
# Display, Touch and SD card are not working at the same time
|
||||
|
||||
The ESP32 offers two usable hardware SPI buses, but on the CYD each of display, touch and SD card use a different bus. To use all three devices at the same time, for one of them the SPI has to be "simulated" in software. Usually this is done for the touch device, since it doesn't require a high bandwidth. Therefor use a software SPI implementation like [XPT2046_Bitbang_Slim](https://github.com/TheNitek/XPT2046_Bitbang_Arduino_Library) and follow the [button example](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/tree/main/Examples/Basics/8-Buttons)
|
||||
|
||||
# Display is flickering
|
||||
|
||||
- Try a different USB supply and or cable
|
||||
- Go through the [Display is not turning on](#display-is-not-turning-on) steps
|
||||
- If nothing else worked, your CYD could be faulty. Contact the seller.
|
||||
|
||||
# Cannot upload
|
||||
- On Ubuntu and flavors disable or uninstall service `brltty` and make sure user is in group `dialout`
|
||||
|
||||
# Automatic flash with esptool failed: Wrong boot mode detected (0x13)
|
||||
|
||||
This is the well-known problem of flashing ESP32 through USB-UART converter, when DTR and RTS signals are used to switch the chip to the bootloader mode (with additional 2xNPN transistor digital protection logic). On some PC, OS, driver version it works, on another it doesn't:
|
||||
|
||||
```
|
||||
A fatal error occurred: Failed to connect to ESP32: Wrong boot mode detected (0x13)! The chip needs to be in download mode. For troubleshooting steps visit: https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html
|
||||
```
|
||||
|
||||
The solution is to replace a capacitor between EN (RST) and GND from 0.1uF, installed on CYD, to something in range 1uF and 10uF.
|
||||
|
||||
**NOTE:** In schematic, this is C4, but at least on Type-C version of CYD it is C5 actually.
|
||||
43
firmware/cyd_esp32_2432s028/docs/cyd.md
Normal file
43
firmware/cyd_esp32_2432s028/docs/cyd.md
Normal file
@@ -0,0 +1,43 @@
|
||||
## What is a Cheap Yellow Display (CYD)?
|
||||
|
||||
A CYD is a ESP32-2432S028, an ESP32 development board with a 2.8" display with a resistive touch screen,
|
||||
|
||||
There are other boards with different sizes displays that look similar but **are not** a CYD. This isn't to try exclude anyone, but there so many different displays and types that it would be incredibly difficult and very confusing to support all of them.
|
||||
|
||||
You can verify you have the correct board by checking the number on the back of the display.
|
||||
|
||||

|
||||
|
||||
## My CYD has two USB ports
|
||||
|
||||
The original CYD only has a micro USB port, but there is a device that is also labelled a _ESP32-2432S028_ that has two USB ports, one micro USB and one USB-C.
|
||||
|
||||
Having an additional USB port would be a minor problem if that was the only difference, but unfortunately the display also works differently, the colours are inverted on the display.
|
||||
|
||||
It can be fixed in a couple of ways:
|
||||
|
||||
- Use platformio - The examples on the Github have all been updated so they can be used with platformio, and you can simply select CYD or CYD2USB and it will just work
|
||||
- Use the [CYD2USB specific User_setup.h](/DisplayConfig/CYD2USB/) that is on the repo, you can now use all the examples like normal
|
||||
- Invert the display at the code level using the `tft.invertDisplay(1);` method
|
||||
|
||||
### The USB-C port doesn't work
|
||||
|
||||
The USB-C port has a flaw in it, it doesn’t have the resistors on the CC lines. This means it will not work with USB-C to USB-C cables. If your computer only has USB-C ports, you can use it through a USB-C to USB-A adaptor.
|
||||
|
||||
### The Display doesn't look as good
|
||||
|
||||
There seems to be a gamma issue with the CYD2USB (I don't even know what gamma is)
|
||||
|
||||
Adding this to the code seems to help
|
||||
|
||||
```
|
||||
tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected
|
||||
tft.writedata(2);
|
||||
delay(120);
|
||||
tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected
|
||||
tft.writedata(1);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "mbedtls/md.h"
|
||||
#include "mbedtls/ed25519.h"
|
||||
#include "mbedtls/ecp.h"
|
||||
#include "mbedtls/pk.h"
|
||||
#include "psa/crypto.h"
|
||||
|
||||
#include "secp256k1.h"
|
||||
#include "secp256k1_extrakeys.h"
|
||||
@@ -381,18 +381,33 @@ int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* The SLIP-0010 derived 32 bytes IS the ed25519 private key.
|
||||
* Use mbedtls to derive the public key. */
|
||||
/* The SLIP-0010 derived 32 bytes IS the ed25519 private key. */
|
||||
memcpy(privkey, derived_seed, 32);
|
||||
|
||||
/* mbedtls_ed25519_make_public: derive pub from priv */
|
||||
/* Note: mbedtls ed25519 API may vary by version. The ESP-IDF mbedtls
|
||||
* component provides mbedtls_ed25519_make_public (or via the PK API).
|
||||
* We use the low-level function if available. */
|
||||
int ret = mbedtls_ed25519_make_public((unsigned char *)pubkey, 32,
|
||||
(const unsigned char *)privkey, 32);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 make_public failed: %d", ret);
|
||||
/* Derive the ed25519 public key via PSA crypto (IDF v5.x mbedtls has no
|
||||
* mbedtls_ed25519_make_public). Import the private key, export the pub. */
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t pub_len = 0;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT | PSA_KEY_USAGE_SIGN_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_import failed: %d", (int)status);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
status = psa_export_public_key(key_id, pubkey, 32, &pub_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || pub_len != 32) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_export_public failed: %d", (int)status);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
@@ -402,20 +417,64 @@ int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ed25519 sign via PSA (PureEdDSA — signs the raw message, not pre-hashed). */
|
||||
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
|
||||
uint8_t sig64[64]) {
|
||||
/* mbedtls_ed25519_sign: sign a message (not pre-hashed) */
|
||||
int ret = mbedtls_ed25519_sign((unsigned char *)sig64, 64,
|
||||
(const unsigned char *)msg32, 32,
|
||||
(const unsigned char *)privkey, 32,
|
||||
NULL, NULL);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 sign failed: %d", ret);
|
||||
return ed25519_sign_msg(privkey, msg32, 32, sig64);
|
||||
}
|
||||
|
||||
int ed25519_sign_msg(const uint8_t privkey[32], const uint8_t *msg, size_t msg_len,
|
||||
uint8_t sig64[64]) {
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t sig_len = 0;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_SIGN_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 sign psa_import failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
status = psa_sign_message(key_id, PSA_ALG_PURE_EDDSA,
|
||||
msg, msg_len, sig64, 64, &sig_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || sig_len != 64) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 psa_sign_message failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ed25519_verify_msg(const uint8_t *sig, size_t sig_len,
|
||||
const uint8_t *msg, size_t msg_len,
|
||||
const uint8_t pubkey[32]) {
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_TWISTED_EDWARDS));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_VERIFY_MESSAGE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_PURE_EDDSA);
|
||||
|
||||
status = psa_import_key(&attrs, pubkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "ed25519 verify psa_import failed: %d", (int)status);
|
||||
return -1;
|
||||
}
|
||||
status = psa_verify_message(key_id, PSA_ALG_PURE_EDDSA,
|
||||
msg, msg_len, sig, sig_len);
|
||||
psa_destroy_key(key_id);
|
||||
return (status == PSA_SUCCESS) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* --- x25519 --- */
|
||||
|
||||
int derive_x25519_key(const uint8_t seed[64], uint32_t index,
|
||||
@@ -429,10 +488,10 @@ int derive_x25519_key(const uint8_t seed[64], uint32_t index,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
};
|
||||
uint8_t derived_seed[32];
|
||||
mbedtls_ecp_group grp;
|
||||
mbedtls_mpi d;
|
||||
mbedtls_ecp_point Q;
|
||||
int ret;
|
||||
psa_status_t status;
|
||||
psa_key_id_t key_id = 0;
|
||||
psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT;
|
||||
size_t pub_len = 0;
|
||||
|
||||
if (seed == NULL || privkey == NULL || pubkey == NULL) {
|
||||
return -1;
|
||||
@@ -443,54 +502,30 @@ int derive_x25519_key(const uint8_t seed[64], uint32_t index,
|
||||
}
|
||||
|
||||
/* The SLIP-0010 derived 32 bytes IS the x25519 private key.
|
||||
* Clamp it per RFC 7748 and derive the public key via mbedtls ECDH. */
|
||||
* PSA imports it and exports the public key (PSA handles clamping). */
|
||||
memcpy(privkey, derived_seed, 32);
|
||||
memset(derived_seed, 0, sizeof(derived_seed));
|
||||
|
||||
/* x25519 clamping: priv[0] &= 248, priv[31] &= 127, priv[31] |= 64 */
|
||||
privkey[0] &= 248;
|
||||
privkey[31] &= 127;
|
||||
privkey[31] |= 64;
|
||||
psa_crypto_init();
|
||||
psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY));
|
||||
psa_set_key_bits(&attrs, 255);
|
||||
psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT | PSA_KEY_USAGE_DERIVE);
|
||||
psa_set_key_algorithm(&attrs, PSA_ALG_ECDH);
|
||||
|
||||
mbedtls_ecp_group_init(&grp);
|
||||
mbedtls_mpi_init(&d);
|
||||
mbedtls_ecp_point_init(&Q);
|
||||
|
||||
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(KD_TAG, "x25519 group load failed: %d", ret);
|
||||
goto cleanup;
|
||||
status = psa_import_key(&attrs, privkey, 32, &key_id);
|
||||
if (status != PSA_SUCCESS) {
|
||||
ESP_LOGE(KD_TAG, "x25519 psa_import failed: %d", (int)status);
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = mbedtls_mpi_read_binary_le(d, privkey, 32);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(KD_TAG, "x25519 mpi read failed: %d", ret);
|
||||
goto cleanup;
|
||||
status = psa_export_public_key(key_id, pubkey, 32, &pub_len);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || pub_len != 32) {
|
||||
ESP_LOGE(KD_TAG, "x25519 psa_export_public failed: %d", (int)status);
|
||||
memset(privkey, 0, 32);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = mbedtls_ecp_mul(&grp, &Q, d, &grp.G, NULL, NULL);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(KD_TAG, "x25519 ecp_mul failed: %d", ret);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Serialize the public key as raw 32 bytes (little-endian) */
|
||||
{
|
||||
size_t olen = 0;
|
||||
ret = mbedtls_ecp_point_write_binary(&grp, &Q,
|
||||
MBEDTLS_ECP_PF_COMPRESSED,
|
||||
&olen, pubkey, 32);
|
||||
if (ret != 0 || olen != 32) {
|
||||
ESP_LOGE(KD_TAG, "x25519 pub serialize failed: %d", ret);
|
||||
ret = -1;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
mbedtls_ecp_group_free(&grp);
|
||||
mbedtls_mpi_free(&d);
|
||||
mbedtls_ecp_point_free(&Q);
|
||||
return (ret == 0) ? 0 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- ML-DSA-65 --- */
|
||||
|
||||
@@ -17,12 +17,21 @@ int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t s
|
||||
int derive_ed25519_key(const uint8_t seed[64], uint32_t index,
|
||||
uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
|
||||
/* Signs a 32-byte message digest with ed25519.
|
||||
/* Signs a 32-byte message digest with ed25519 (PureEdDSA, raw message).
|
||||
* sig: 64-byte ed25519 signature
|
||||
* Returns 0 on success, -1 on error. */
|
||||
int ed25519_sign32(const uint8_t privkey[32], const uint8_t msg32[32],
|
||||
uint8_t sig64[64]);
|
||||
|
||||
/* Signs a variable-length message with ed25519 (PureEdDSA). */
|
||||
int ed25519_sign_msg(const uint8_t privkey[32], const uint8_t *msg, size_t msg_len,
|
||||
uint8_t sig64[64]);
|
||||
|
||||
/* Verifies an ed25519 signature over a variable-length message. */
|
||||
int ed25519_verify_msg(const uint8_t *sig, size_t sig_len,
|
||||
const uint8_t *msg, size_t msg_len,
|
||||
const uint8_t pubkey[32]);
|
||||
|
||||
/* --- x25519 (age encryption / key agreement) --- */
|
||||
/* Derives an x25519 keypair from the mnemonic seed using SLIP-0010
|
||||
* all-hardened derivation: m/44'/102002'/<n>'/0'/0'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,3 +8,8 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
|
||||
# PSA crypto for ed25519 sign/verify (IDF v5.x mbedtls has no mbedtls_ed25519_*).
|
||||
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
|
||||
# Curve25519 / Ed25519 ECP domain parameter (already enabled, kept for clarity).
|
||||
CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y
|
||||
|
||||
192
firmware/fpga_signer/README.md
Normal file
192
firmware/fpga_signer/README.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# n_signer FPGA Signing Core
|
||||
|
||||
**Status:** Concept — brainstorming. No plan yet.
|
||||
|
||||
An FPGA-based secp256k1 signing core that provides the **maximum physical
|
||||
security** possible for Nostr signing: constant-time crypto (no instruction
|
||||
timing leakage), key material in FPGA fabric (no bus access to the key), and no
|
||||
firmware (no malware injection surface). The FPGA is a **signing oracle** — a
|
||||
small, auditable hardware module that does one thing (secp256k1 schnorr/ECDSA
|
||||
signing) with side-channel resistance that software on an MCU cannot match.
|
||||
|
||||
## The secure element gap
|
||||
|
||||
Commercial secure elements (NXP JCOP, Infineon, Microchip ATECC) support NIST
|
||||
curves (P-256, P-384) and RSA, but **not secp256k1** — the smart card industry
|
||||
standardized on NIST curves, and secp256k1 was treated as a "Bitcoin curve"
|
||||
that didn't get hardware support. This is why every Nostr/Bitcoin hardware
|
||||
wallet (Coldcard, Ledger, Trezor, Keystone) uses a **general-purpose MCU**
|
||||
running software secp256k1, not a secure element.
|
||||
|
||||
An FPGA fills this gap: it gives us **hardware-level secp256k1** without relying
|
||||
on a secure element vendor to support the curve. We write the secp256k1 core
|
||||
ourselves in Verilog, with full control over the timing, the key storage, and
|
||||
the side-channel resistance.
|
||||
|
||||
## Why an FPGA
|
||||
|
||||
| Property | MCU (software) | FPGA (hardware) |
|
||||
|---|---|---|
|
||||
| Timing leakage | Branch prediction, cache, instruction timing | **None** — fixed datapath, every op takes the same cycles |
|
||||
| Key storage | RAM (accessible via bus/debug) | **FPGA fabric / BRAM** (no external bus access) |
|
||||
| Firmware attacks | OS, USB stack, BT stack = injection surface | **No firmware** — bitstream is the entire program |
|
||||
| Debug access | JTAG/SWD can read RAM | **No debug path to key** if not routed |
|
||||
| Auditability | Large codebase (thousands of lines of C) | **Small Verilog core** (~2000 lines, auditable) |
|
||||
| PQ crypto | Yes (software) | No (too complex for FPGA) |
|
||||
|
||||
## Architecture: hybrid FPGA + MCU
|
||||
|
||||
The practical design is a **two-chip hybrid**: the FPGA is the signing oracle,
|
||||
the MCU handles the protocol/UI/transport. The MCU sends a message hash + key
|
||||
index to the FPGA over SPI; the FPGA signs with the key in fabric; the FPGA
|
||||
returns the 64-byte signature. The private key never leaves the FPGA.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Host_Side
|
||||
Host[Host: laptop/phone<br/>n_signer client]
|
||||
end
|
||||
subgraph Signer_Device
|
||||
MCU[MCU: RP2040 or nRF52840<br/>protocol + UI + transport<br/>PQ crypto in software]
|
||||
FPGA[FPGA: iCE40-UP5K<br/>secp256k1 signing core<br/>ed25519 signing core<br/>SHA-256/512 cores<br/>key in BRAM]
|
||||
Display[OLED / e-paper display]
|
||||
Buttons[approve / deny buttons]
|
||||
end
|
||||
Host -->|USB / IR / NFC / BLE| MCU
|
||||
MCU -->|SPI: msg_hash + key_index| FPGA
|
||||
FPGA -->|SPI: 64-byte signature| MCU
|
||||
MCU --> Display
|
||||
Buttons --> MCU
|
||||
MCU -->|response| Host
|
||||
```
|
||||
|
||||
### Division of labor
|
||||
|
||||
| Function | Chip | Notes |
|
||||
|---|---|---|
|
||||
| Transport (USB/IR/NFC/BLE) | MCU | Ported from CYD/Teensy firmware |
|
||||
| JSON-RPC dispatch | MCU | Ported from `handle_request()` |
|
||||
| Auth envelope verify | MCU | secp256k1 schnorr verify (software) |
|
||||
| Approval UI (display + buttons) | MCU | Ported from CYD UI |
|
||||
| Mnemonic entry | MCU | BIP-39 wordlist + entry UI |
|
||||
| BIP-32 / SLIP-0010 key derivation | **FPGA** | SHA-512 HMAC core + derivation FSM |
|
||||
| secp256k1 schnorr sign | **FPGA** | Constant-time scalar multiply + schnorr |
|
||||
| secp256k1 ECDSA sign | **FPGA** | Same scalar multiply + RFC 6979 nonce |
|
||||
| ed25519 sign | **FPGA** | Curve25519 arithmetic core |
|
||||
| x25519 ECDH | **FPGA** | Same curve as ed25519 |
|
||||
| SHA-256 | **FPGA** | Hardware core (~1000 LUTs) |
|
||||
| SHA-512 | **FPGA** | Hardware core (~2000 LUTs) — needed for BIP-32 |
|
||||
| HMAC-SHA-256 | **FPGA** | SHA-256 core + FSM — for the `derive` verb |
|
||||
| NIP-04 / NIP-44 encryption | MCU | AES + ChaCha20 in software |
|
||||
| ML-DSA-65 / SLH-DSA-128s / ML-KEM-768 | MCU | PQClean in software (too complex for FPGA) |
|
||||
| nostr_mine_event (PoW) | MCU | SHA-256 hash loop in software (or offload to FPGA) |
|
||||
|
||||
### Key storage in the FPGA
|
||||
|
||||
The mnemonic seed (64 bytes) is loaded into the FPGA's BRAM at boot (sent by
|
||||
the MCU after the user enters the mnemonic). The FPGA derives secp256k1/ed25519/
|
||||
x25519 keys on demand using its SHA-512 + modular arithmetic cores. The derived
|
||||
private keys live in FPGA registers/BRAM and are **never readable from the SPI
|
||||
interface** — the SPI interface only accepts "sign this hash with key index N"
|
||||
commands and returns signatures. There is no "read key" command.
|
||||
|
||||
This is the key security property: **the private key is physically unreachable
|
||||
from any external interface.** On an MCU, the key is in RAM and can be read via
|
||||
JTAG/SWD or a firmware exploit. On the FPGA, the key is in fabric and there is
|
||||
no path to it.
|
||||
|
||||
## FPGA board options
|
||||
|
||||
| Board | FPGA | LUTs | Toolchain | Price | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| **iCE40-UP5K** (e.g. iCEBreaker, Fomu) | iCE40UP5K | 5,300 | **Yosys + nextpnr** (open-source) | ~$15-20 | Best DIY choice. Open-source toolchain, DSP blocks, 128 KB BRAM, SPI flash. |
|
||||
| **Gowin Tang Nano 9K** | GW1NR-9 | 8,640 | Yosys + nextpnr (open-source) | ~$8 | Cheapest. Newer open-source support. |
|
||||
| **Lattice ECP5** (OrangeCrab, ULX3S) | LFE5U-12F / 25F / 45F | 12K-45K | Yosys + nextpnr (open-source) | ~$30-50 | More room. Supports **bitstream encryption** (important for production). |
|
||||
| **Xilinx Artix-7** (Arty A7) | XC7A35T | 33,280 | Vivado (proprietary) | ~$100 | Professional. Overkill for a signer. |
|
||||
|
||||
**Recommendation: Lattice iCE40-UP5K for prototyping, ECP5 for production.**
|
||||
The iCE40 has the most mature open-source toolchain (Yosys + nextpnr) and is
|
||||
cheap. The ECP5 adds bitstream encryption (prevents bitstream cloning) for a
|
||||
production device.
|
||||
|
||||
## The secp256k1 core (the hard part)
|
||||
|
||||
The secp256k1 signing core is the main development effort. It needs:
|
||||
|
||||
1. **256-bit modular arithmetic** over the secp256k1 prime field (p = 2²⁵⁶ - 2³² - 977):
|
||||
- Modular add, subtract, multiply (Montgomery multiplication for performance)
|
||||
- Modular inversion (Fermat's little theorem: a^(p-2) mod p, or extended Euclidean)
|
||||
2. **Point operations** on the secp256k1 curve (y² = x³ + 7):
|
||||
- Point addition (Jacobian coordinates)
|
||||
- Point doubling
|
||||
- Scalar multiplication (constant-time double-and-add, no conditional branches)
|
||||
3. **Schnorr sign** (BIP-340):
|
||||
- Deterministic nonce: k = HMAC-SHA256(d, x) where d is the key, x is the message hash
|
||||
- R = k·G (point multiplication)
|
||||
- e = tagged hash(R.x || P || m) (SHA-256)
|
||||
- s = (k + e·x) mod n (scalar multiply + modular add)
|
||||
- Signature = (R.x, s)
|
||||
4. **ECDSA sign** (for the `scheme:"ecdsa"` option):
|
||||
- RFC 6979 deterministic nonce: k = HMAC-SHA256(x, m) with rejection sampling
|
||||
- R = k·G
|
||||
- r = R.x mod n
|
||||
- s = k⁻¹ · (m + r·x) mod n
|
||||
- Signature = (r, s)
|
||||
|
||||
**Estimated size:** ~2000-5000 LUTs for the modular arithmetic + point
|
||||
multiplication + schnorr/ECDSA FSM. Fits comfortably in an iCE40-UP5K (5,300
|
||||
LUTs) alongside the SHA-256/512 cores and the SPI interface.
|
||||
|
||||
**Estimated sign time:** ~1-5 ms at 12 MHz (iCE40-UP5K typical clock). The
|
||||
bottleneck is the 256-bit modular multiplication (~0.5-2 ms per multiply, ~256
|
||||
multiplies per scalar multiplication). This is **much faster than software**
|
||||
(the CYD's software schnorr sign takes ~10-50 ms).
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Pure FPGA vs hybrid?** A pure-FPGA signer (no MCU) would implement the
|
||||
entire dispatch + transport + UI in Verilog. This is extremely secure but
|
||||
very hard to build (JSON parsing in Verilog is painful). The hybrid (FPGA
|
||||
signing oracle + MCU protocol) is practical and still gives the key-isolation
|
||||
benefit. **Lean toward hybrid.**
|
||||
- **Which MCU?** RP2040 (cheap, no WiFi) or nRF52840 (low power, NFC, BLE)?
|
||||
This determines the transport options (USB, IR, NFC, BLE).
|
||||
- **Bitstream security:** iCE40 doesn't support bitstream encryption. If an
|
||||
attacker reads the SPI flash, they get the bitstream (but not the key — the
|
||||
key is loaded at runtime by the MCU, not stored in the bitstream). For
|
||||
production, use ECP5 with bitstream encryption.
|
||||
- **Key loading:** the MCU sends the mnemonic seed to the FPGA at boot over
|
||||
SPI. Is this SPI transfer vulnerable to sniffing? It happens once, at boot,
|
||||
inside the device. If the device is physically sealed, the SPI bus is not
|
||||
accessible. For higher security, the FPGA could derive the key from the
|
||||
mnemonic internally (the MCU sends the mnemonic string, the FPGA does
|
||||
PBKDF2-HMAC-SHA512 + BIP-32 derivation in hardware).
|
||||
- **ed25519 core:** worth implementing, or secp256k1-only? ed25519 is the same
|
||||
field size (256-bit) but a different curve (Curve25519 vs secp256k1). The
|
||||
modular arithmetic is similar but the curve operations differ. Adding
|
||||
ed25519 roughly doubles the core size.
|
||||
- **Open-source secp256k1 FPGA cores:** are there existing Verilog secp256k1
|
||||
cores we can reuse or adapt? (There are Bitcoin mining cores, but those do
|
||||
double-SHA256, not ECDSA. Academic ECDSA-on-FPGA papers exist but the code
|
||||
is rarely open-sourced. We'd likely write the core from scratch.)
|
||||
|
||||
## Comparison to the other concepts
|
||||
|
||||
| | FPGA signer (hybrid) | MCU-only (CYD/Teensy) | Secure element |
|
||||
|---|---|---|---|
|
||||
| secp256k1 side-channel resistance | **Best** (constant-time, key in fabric) | Medium (software, timing leakage) | N/A (no secp256k1 support) |
|
||||
| Key isolation | **Best** (no bus access to key) | Low (RAM, JTAG/SWD accessible) | Best (tamper-resistant) |
|
||||
| Firmware attack surface | **Minimal** (no firmware on FPGA) | Large (OS, USB, BT stacks) | Minimal (fixed function) |
|
||||
| PQ crypto | On MCU (software) | On MCU (software) | N/A |
|
||||
| Development effort | **High** (Verilog secp256k1 core) | Low (port existing C code) | High (NDA + Java Card) |
|
||||
| Cost | ~$15 FPGA + ~$4 MCU = ~$20 | ~$4-27 (MCU only) | ~$3-5 (chip only) |
|
||||
| Auditability | **High** (small Verilog core, open toolchain) | Medium (large C codebase) | Low (proprietary, NDA) |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide: hybrid (FPGA + MCU) vs pure FPGA
|
||||
- Decide: iCE40-UP5K (prototype) vs ECP5 (production)
|
||||
- Decide: secp256k1-only vs secp256k1 + ed25519
|
||||
- Survey existing open-source secp256k1 / ECDSA FPGA cores
|
||||
- Write a Verilog secp256k1 modular arithmetic core (the foundation)
|
||||
- Write a plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))
|
||||
149
firmware/ir_airgap_signer/README.md
Normal file
149
firmware/ir_airgap_signer/README.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# n_signer IR Air-Gap Signer
|
||||
|
||||
**Status:** Concept — brainstorming. No plan yet.
|
||||
|
||||
A hardware signer that communicates with the host via **infrared light** —
|
||||
line-of-sight, short-range, physically directional. The signer never touches
|
||||
the host electrically: no wire, no radio, no shared ground. The only channel
|
||||
is modulated light through air. A small **USB receiver dongle** on the host
|
||||
decodes the IR signal and presents it as a CDC-ACM serial port.
|
||||
|
||||
This is the strongest air-gap model in the n_signer family: the signer is
|
||||
electrically isolated from the host, and the receiver dongle is a dumb
|
||||
IR-to-serial bridge with no crypto, no keys, and ~200 lines of auditable
|
||||
firmware.
|
||||
|
||||
## Concept
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Host[Host: laptop<br/>n_signer client] -->|USB CDC| Dongle[USB IR receiver dongle<br/>RP2040 + IR receiver]
|
||||
Dongle -->|IR light<br/>line-of-sight| Signer[IR signer<br/>RP2040 + OLED + buttons]
|
||||
Signer -->|approve/deny button| User[User]
|
||||
Signer -->|IR light response| Dongle
|
||||
Dongle -->|USB CDC| Host
|
||||
```
|
||||
|
||||
The signer speaks the same algorithm-based API as the host and the CYD/Teensy
|
||||
firmware ([`README.md`](../../README.md) §4). The auth envelope (kind 27235)
|
||||
protects the IR wire. The receiver dongle is a transparent byte pipe — it has
|
||||
no knowledge of the protocol, no keys, and no state beyond the IR-to-USB
|
||||
bridge.
|
||||
|
||||
## Why IR
|
||||
|
||||
- **True air-gap** — the signer is electrically isolated from the host. No wire,
|
||||
no radio, no shared ground. Host-side malware cannot reach the signer's
|
||||
firmware through the communication channel.
|
||||
- **Line-of-sight required** — you point the signer at the receiver. An attacker
|
||||
would need to be in the same room, in the line of sight, with their own IR
|
||||
transmitter. Much smaller attack surface than BT (which broadcasts
|
||||
omnidirectionally to ~10 m).
|
||||
- **Dumb dongle** — the USB receiver is a simple IR-to-serial bridge. ~200
|
||||
lines of firmware, no crypto, no keys, fully auditable in an afternoon. If
|
||||
compromised, it can only MITM the IR stream (which is already protected by the
|
||||
auth envelope).
|
||||
- **No BT stack** — much smaller firmware attack surface on the signer. No
|
||||
pairing, no GATT, no L2CAP, no SMP.
|
||||
- **Novel** — no hardware wallet uses IR for host communication. It's a
|
||||
creative solution to the air-gap problem that avoids both the wire (USB) and
|
||||
the radio (BT/NFC) attack surfaces.
|
||||
|
||||
## Hardware (preliminary)
|
||||
|
||||
### Signer
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **RP2040** (Raspberry Pi Pico) | $4, Cortex-M0+ @ 133 MHz, 264 KB SRAM, no WiFi/BT (perfect for air-gap). Enough RAM for secp256k1 + ed25519. ML-DSA-65 fits (~6 KB heap). |
|
||||
| | or **nRF52840** | If you want NFC for mnemonic loading + lower power. |
|
||||
| IR transceiver | **38 kHz IR LED + TSOP38238** (raw async, 115200 baud, ~$1) | Simplest. ~11 KB/s. Fine for Nostr events (~500 bytes). Slow for PQ sigs (3-8 KB → 0.3-0.7 s). |
|
||||
| | or **TFBS4711 IrDA module** (~$2, up to 4 Mbps) | Faster (~400 KB/s) but harder to source + more complex protocol. |
|
||||
| Display | 0.96" SSD1306 OLED (I2C, ~$2) or 1.54" e-paper | Small is fine — shows "approve kind 1 from <caller>?" |
|
||||
| Input | 2-3 tactile buttons (approve/deny/back) | |
|
||||
| Power | Coin cell or small LiPo | RP2040 + OLED + IR = very low power |
|
||||
|
||||
### USB receiver dongle
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **RP2040** (Pico) or **ATmega32U4** (Arduino Micro) | $4-8. Native USB device. |
|
||||
| IR receiver | Matching TSOP38238 or IrDA module | Must match the signer's IR modulation. |
|
||||
| USB | Native USB CDC-ACM | Presents as `/dev/ttyACM0` to the host. |
|
||||
| Firmware | ~200 lines | Read IR → write USB CDC; read USB CDC → transmit IR. A dumb pipe. No crypto, no keys, no state. |
|
||||
|
||||
## Throughput
|
||||
|
||||
| IR mode | Baud | Throughput | sign_event (500 B req + 600 B resp) | ML-DSA-65 sign (3.3 KB sig) |
|
||||
|---|---|---|---|---|
|
||||
| Raw 38 kHz async | 115200 | ~11 KB/s | ~100 ms | ~300 ms |
|
||||
| Raw 38 kHz async | 230400 | ~23 KB/s | ~50 ms | ~150 ms |
|
||||
| IrDA | 4 Mbps | ~400 KB/s | ~3 ms | ~8 ms |
|
||||
|
||||
**Recommendation:** start with raw 38 kHz IR at 115200 baud (simplest, cheapest,
|
||||
works with any IR LED + TSOP receiver). Upgrade to 230400 or IrDA if PQ
|
||||
signature throughput is a bottleneck.
|
||||
|
||||
## Protocol
|
||||
|
||||
The IR link is **half-duplex** — the signer and receiver take turns
|
||||
transmitting. The protocol is simple:
|
||||
|
||||
1. Host sends JSON-RPC request → USB CDC → dongle transmits IR.
|
||||
2. Signer receives IR, parses the request, shows approval prompt.
|
||||
3. User approves/denies.
|
||||
4. Signer transmits IR response → dongle → USB CDC → host.
|
||||
|
||||
The 4-byte big-endian length-prefix framing (same as the CYD/feather) works
|
||||
over IR as-is. The auth envelope protects against MITM on the IR stream.
|
||||
|
||||
## Security model
|
||||
|
||||
- **Electrical isolation:** the signer has no electrical connection to the host.
|
||||
The IR link is a one-way-at-a-time optical channel.
|
||||
- **Line-of-sight:** an attacker must be in the same room, in the line of sight,
|
||||
with their own IR transmitter. The auth envelope + approval prompt protect
|
||||
against a MITM even if the attacker intercepts the IR stream.
|
||||
- **Dumb dongle:** the USB receiver has no crypto, no keys, no protocol
|
||||
knowledge. It's a byte pipe. If compromised, it can only MITM the IR stream
|
||||
(already protected by the auth envelope). The dongle's firmware is small
|
||||
enough to audit in an afternoon.
|
||||
- **No radio:** no BT, no WiFi, no NFC (unless you add NFC for mnemonic loading).
|
||||
The signer emits no RF — only modulated IR light when actively transmitting.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **IR modulation:** raw 38 kHz async (simplest) vs IrDA (faster, more complex)?
|
||||
- **Mnemonic entry:** buttons (scroll BIP-39 words) vs NFC from phone vs
|
||||
generate-on-device? On a 0.96" OLED, scrolling 2048 words is tedious but
|
||||
secure.
|
||||
- **PQ crypto on RP2040:** 264 KB SRAM is enough for ML-DSA-65 but SLH-DSA-128s
|
||||
is tight. May need to limit the PQ algorithm set.
|
||||
- **Dongle design:** separate RP2040 Pico, or integrate the IR receiver into a
|
||||
custom PCB with a USB-A plug for a compact dongle?
|
||||
- **Range:** raw IR with an IR LED + TSOP38238 reaches ~1-2 m line-of-sight.
|
||||
Enough for "point at the dongle on your desk" but not across a room.
|
||||
- **Bidirectional IR:** the signer needs both an IR LED (transmit) and a TSOP
|
||||
receiver (receive). Two modules, or an IrDA transceiver module that does both?
|
||||
|
||||
## Comparison to the BLE wearable signer
|
||||
|
||||
| | IR air-gap | BLE wearable |
|
||||
|---|---|---|
|
||||
| Air-gap | **High (light, line-of-sight, ~1 m)** | Medium (radio, ~10 m, omnidirectional) |
|
||||
| Attack surface | **Small (no BT, dumb dongle)** | Large (BT stack) |
|
||||
| Host compatibility | Requires USB dongle | Universal (phones, laptops) |
|
||||
| Form factor | Handheld (point at dongle) | Wearable |
|
||||
| Throughput | ~11 KB/s (raw IR) or ~400 KB/s (IrDA) | ~250 KB/s (BLE 5) |
|
||||
| Novelty | **Novel (no hardware wallet uses IR)** | Conventional |
|
||||
| Cost | ~$10 signer + ~$8 dongle | ~$10-15 (nRF52840 + OLED) |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide on IR modulation (raw 38 kHz vs IrDA)
|
||||
- Decide on MCU (RP2040 vs nRF52840)
|
||||
- Decide on mnemonic entry method
|
||||
- Decide on display (OLED vs e-paper)
|
||||
- Prototype the IR link: two RP2040 Picos + IR LEDs + TSOP38238, bidirectional
|
||||
byte pipe at 115200 baud
|
||||
- Write a port plan (similar to [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md))
|
||||
192
firmware/nfc_card_signer/README.md
Normal file
192
firmware/nfc_card_signer/README.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# n_signer NFC Card / Ring Signer
|
||||
|
||||
**Status:** Concept — open-ended brainstorming. No plan yet.
|
||||
|
||||
A contactless signer in a **card or ring form factor** that is powered and
|
||||
communicates via **NFC (13.56 MHz)**. You place it on a reader (USB NFC reader
|
||||
or a phone); the reader's RF field powers the device and exchanges data. No
|
||||
battery, no wire, no radio beyond the 4 cm NFC zone.
|
||||
|
||||
This directory also explores **passive RFID/NFC tag ideas** that don't do
|
||||
signing on-device — they store keys or seed material that a host reads and uses.
|
||||
|
||||
---
|
||||
|
||||
## Concept A: NFC-powered active signer (card with display + button)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Host[Host: laptop/phone<br/>n_signer client] -->|USB or built-in NFC| Reader[NFC reader<br/>ACR122U or phone]
|
||||
Reader -->|13.56 MHz RF field<br/>powers + communicates| Card[Signer card<br/>nRF52840 + e-paper + button]
|
||||
Card -->|NFC response| Reader
|
||||
Reader -->|USB| Host
|
||||
```
|
||||
|
||||
The card has a tiny e-paper display + one button. The reader powers the card;
|
||||
the card shows the approval prompt on its own display; the user presses the
|
||||
button to approve; the card signs and sends the response over NFC. The card
|
||||
does not trust the reader for display — it shows what it's signing.
|
||||
|
||||
### Hardware (preliminary)
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | **nRF52840** (WLCSP) | Cortex-M4, NFC-A tag mode built in, secp256k1/ed25519 in software. Needs a thin-film battery (not fully passive). |
|
||||
| | or **NXP JCOP 4** (Java Card) | Fully passive, hardware secp256k1, tamper-resistant. Requires NDA + Java Card applet. Not DIY. |
|
||||
| Display | 1.1" e-paper segment display | Shows "approve kind 1? caller: <hex>". Zero power when static. |
|
||||
| Input | One capacitive touch button | Press to approve, timeout = deny. |
|
||||
| Power | Thin-film battery (like payment cards with displays) + NFC harvesting | |
|
||||
| Antenna | Etched into flex PCB around card perimeter | Standard smart card manufacturing. |
|
||||
|
||||
### Security
|
||||
|
||||
- **Attack radius ~4 cm** — an attacker must touch your card with their reader.
|
||||
- **No emissions when not on a reader** — the card is invisible to remote attackers.
|
||||
- **Self-contained approval** — the card's display shows what it's signing. The reader can't lie.
|
||||
- **Physical possession = authorization** — same model as a payment card.
|
||||
|
||||
### Build difficulty
|
||||
|
||||
- **DIY prototype:** nRF52840 dev board + wire-wound NFC antenna + ACR122U reader + small OLED. Prove NFC-powered signing works.
|
||||
- **Production:** custom flex PCB + etched antenna + thin battery + e-paper segment. Standard smart card manufacturing, but not DIY.
|
||||
|
||||
---
|
||||
|
||||
## Concept B: NFC ring (tap-to-sign, no display)
|
||||
|
||||
A ring with an NFC tag + MCU inside. No display, no button. You tap it on a
|
||||
reader; the reader displays the approval prompt; you tap again to confirm
|
||||
(two-tap protocol) or the ring signs immediately (single-tap, trusts the reader).
|
||||
|
||||
### Hardware
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| MCU | Secure element (JCOP / Infineon) or nRF52840 (WLCSP) | Must be tiny (2×3 mm package). |
|
||||
| Power | **Fully passive** (harvested from reader) if using a secure element. nRF52840 needs a battery. |
|
||||
| Antenna | Coil wound into the ring body | Custom manufacturing. |
|
||||
| Display | **None** | No room. |
|
||||
| Input | **None** | Pure tap-to-sign. |
|
||||
|
||||
### Security
|
||||
|
||||
- **Two-tap protocol:** tap to receive the request, reader displays it, tap again to sign. Forces deliberate action but still trusts the reader's display.
|
||||
- **Single-tap:** anyone who taps your ring with a reader can sign. Only safe if the ring is always in your physical possession and the reader is trusted.
|
||||
- **No display = reader-trusted approval.** Weaker than Concept A but much more portable.
|
||||
|
||||
### Build difficulty
|
||||
|
||||
- **Very hard** — custom antenna winding, tiny chip placement, ring-form-factor PCB. Not DIY without specialized equipment.
|
||||
|
||||
---
|
||||
|
||||
## Concept C: Passive NFC tag that stores keys (no on-device signing)
|
||||
|
||||
This is a fundamentally different idea: the tag **does not sign anything**. It
|
||||
stores key material (a mnemonic seed, a private key, or a derived key) that a
|
||||
host reads over NFC and uses to sign. The tag is a **portable key storage
|
||||
device**, not a signer.
|
||||
|
||||
### How it would work
|
||||
|
||||
1. The user taps the tag on a phone or USB NFC reader.
|
||||
2. The host reads the stored key material over NFC (ISO 14443 / NDEF).
|
||||
3. The host uses the key material to sign (the host runs the n_signer crypto).
|
||||
4. The tag is just storage — it has no MCU, no crypto, no battery.
|
||||
|
||||
### What the tag stores (options)
|
||||
|
||||
| Storage model | What's on the tag | Security | Notes |
|
||||
|---|---|---|---|
|
||||
| **Encrypted seed** | The mnemonic seed, encrypted with a passphrase (BIP-39 password). The host decrypts after the user types the passphrase. | Medium — if the tag is stolen, the attacker needs the passphrase. | Like an encrypted paper backup, but in NFC form. |
|
||||
| **Raw private key** | The secp256k1 private key (32 bytes), stored in the tag's EEPROM. | **Low** — anyone who reads the tag has the key. Only safe if the tag is PIN-protected (needs a secure element, not a dumb tag). | Like storing a private key on a USB stick. |
|
||||
| **NDEF URI** | A URI like `nostr:npub1...` (just the public key). The host uses it to identify which key to use (the actual private key is elsewhere). | High (it's just a pubkey) | Not a signer — just an identity token. |
|
||||
| **Shamir shard** | One share of a Shamir's Secret Sharing split of the seed. The tag holds 1 of N shares; you need M tags to reconstruct. | **High** — a single tag is useless. | Like a metal seed backup but in NFC form. Multiple tags = multiple shares. |
|
||||
| **HD wallet derivation path** | Just the derivation path + a reference to a master seed stored elsewhere. The tag tells the host *which* key to derive. | Medium | The tag is a pointer, not the key itself. |
|
||||
|
||||
### Hardware
|
||||
|
||||
| Component | Candidate | Notes |
|
||||
|---|---|---|
|
||||
| **NTAG215** (NXP) | 504 bytes user memory, no crypto, ~$0.10 | The cheapest option. Stores an encrypted seed or NDEF URI. No MCU. |
|
||||
| **NTAG424 DNA** (NXP) | 4 KB, AES-128, tamper detection, ~$0.50 | Has crypto — can do authenticated read (the host must present a key to read the data). Better security. |
|
||||
| **MIFARE DESFire EV3** | 32 KB, AES, secure applets, ~$1 | Smart card chip. Can store encrypted key material with PIN/mutual auth. |
|
||||
| **Java Card (JCOP)** | Full smart card, runs applets, ~$3-5 | Could run a "key storage" applet that only releases the seed after a PIN is verified on the host. |
|
||||
|
||||
### Security analysis
|
||||
|
||||
The **passive tag as key storage** is the weakest signer model (the host does
|
||||
the signing, so a compromised host can steal the key), but it has interesting
|
||||
niche uses:
|
||||
|
||||
- **Encrypted seed backup:** an NTAG215 storing an encrypted seed is a
|
||||
convenient portable backup — tap your phone to read it, type the passphrase
|
||||
to decrypt. More convenient than a metal plate, less secure than a hardware
|
||||
signer.
|
||||
- **Shamir shard carrier:** each tag holds one SSS share. You need M of N tags
|
||||
to reconstruct the seed. Distribute the tags to different locations/people.
|
||||
A single stolen tag is useless. This is a **key-recovery** tool, not a signer.
|
||||
- **Identity token:** an NDEF URI tag with your npub. Tap to share your Nostr
|
||||
identity with a phone. Not a signer — just a business card for Nostr.
|
||||
- **PIN-protected key release:** a DESFire or JCOP tag that only releases the
|
||||
seed after the host verifies a PIN. The host never sees the key until the PIN
|
||||
is correct. Better than a raw tag, but the host still gets the key after the
|
||||
PIN — so a compromised host can still steal it.
|
||||
|
||||
### The fundamental limitation
|
||||
|
||||
A passive tag that stores keys **cannot protect the key from a compromised
|
||||
host**. Once the host reads the key, the host has it. This is the same problem
|
||||
as storing a private key in a file — the OS can steal it. The only way to
|
||||
protect the key from the host is to **never release the raw key** — which means
|
||||
the tag must do the signing itself (Concept A or B), or the tag must participate
|
||||
in a protocol where the host sends a hash to sign and the tag returns a
|
||||
signature (which requires an MCU + crypto = not a passive tag).
|
||||
|
||||
**The one exception:** a **secure element** (JCOP / Infineon) can do
|
||||
"sign inside, never release the key." The host sends the message hash; the
|
||||
secure element signs it internally and returns the signature. The private key
|
||||
never leaves the chip. This is how smart card signing works (e.g. FIDO2 keys,
|
||||
PIV cards). But this is Concept A (active signer), not a passive tag.
|
||||
|
||||
---
|
||||
|
||||
## Open questions (all concepts)
|
||||
|
||||
- **Is the goal a signer (does crypto on-device) or a key carrier (stores keys for a host to use)?**
|
||||
- Signer → Concept A (card with display) or B (ring). The key never leaves the device.
|
||||
- Key carrier → Concept C (passive tag). The host gets the key. Simpler but less secure.
|
||||
- **Form factor:** card (credit card size, room for display) vs ring (tiny, no display)?
|
||||
- **Power:** passive (secure element, no battery) vs semi-passive (nRF52840 + thin battery)?
|
||||
- **Approval model:** on-device display (secure, needs a screen) vs reader display (trusts the reader, no screen needed) vs two-tap (medium security, no screen)?
|
||||
- **Host reader:** USB NFC reader (ACR122U, ~$15) vs phone NFC (universal, no dongle)?
|
||||
- **MCU/toolchain:** nRF52840 + C (DIY-friendly) vs JCOP + Java Card (production, NDA)?
|
||||
- **Could a passive tag + a host-side n_signer be a useful "portable encrypted seed backup" even if it's not a signer?** (Yes — for key recovery / Shamir shard distribution.)
|
||||
|
||||
---
|
||||
|
||||
## Comparison across all three concepts (NFC, BLE, IR)
|
||||
|
||||
| | NFC card (A) | NFC ring (B) | NFC tag (C) | BLE wearable | IR air-gap |
|
||||
|---|---|---|---|---|---|
|
||||
| Does signing on-device? | **Yes** | **Yes** | No (host signs) | Yes | Yes |
|
||||
| Key leaves device? | **No** | **No** | Yes (host reads it) | No | No |
|
||||
| Power | Reader + thin battery | Reader (passive) or battery | **Reader (passive)** | Battery | Battery |
|
||||
| Display | Tiny e-paper | None | None | Tiny OLED | Tiny OLED/e-paper |
|
||||
| Approval | On-card display + button | Reader display (two-tap) | N/A (host decides) | On-device display + buttons | On-device display + buttons |
|
||||
| Attack radius | ~4 cm | ~4 cm | ~4 cm | ~10 m | ~1 m (line-of-sight) |
|
||||
| Host needs | NFC reader or phone NFC | NFC reader or phone NFC | NFC reader or phone NFC | BT (universal) | USB IR dongle |
|
||||
| Form factor | Card | Ring | Tag/card/sticker | Wristband/pendant | Handheld |
|
||||
| Build difficulty | Hard (flex PCB) | Very hard (custom ring) | **Easy (off-the-shelf tag)** | Moderate | Moderate |
|
||||
| Best for | Daily signing | Quick tap-to-sign | Key backup / recovery | Wearable daily use | High-security air-gap |
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
- Decide: signer (A/B) vs key carrier (C) vs both
|
||||
- Decide: card vs ring vs tag
|
||||
- Decide: DIY prototype (nRF52840 + ACR122U) vs production (secure element)
|
||||
- Explore the Shamir-shard-on-NFC-tags idea as a key-recovery tool
|
||||
- Explore the encrypted-seed-on-NTAG215 idea as a portable backup
|
||||
- Write a plan for the chosen direction
|
||||
60
firmware/teensy41/README.md
Normal file
60
firmware/teensy41/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# n_signer Teensy 4.1 Firmware
|
||||
|
||||
**Status:** Planned — not yet implemented. See
|
||||
[`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md).
|
||||
|
||||
The **Teensy 4.1** (NXP i.MX RT1062, Cortex-M7 @ 600 MHz) is the high-capacity
|
||||
OTP pad target. Its built-in SD slot supports **1 TB SDXC cards (exFAT)** via
|
||||
PJRC's SdFat library, making it the ideal hardware signer for large one-time-pad
|
||||
storage.
|
||||
|
||||
## Why the Teensy 4.1
|
||||
|
||||
| Concern | Teensy 4.1 | CYD (ESP32) | Feather S3 TFT (ESP32-S3) |
|
||||
|---|---|---|---|
|
||||
| MCU | 600 MHz Cortex-M7 | 240 MHz Xtensa LX6 | 240 MHz Xtensa LX7 |
|
||||
| SRAM | 1 MB + 16 MB PSRAM | 512 KB (no PSRAM) | 512 KB + quad PSRAM |
|
||||
| SD slot | **4-bit SDMMC, exFAT, up to 2 TB** | 1-bit SDSPI, FAT32, up to 32 GB | — |
|
||||
| USB | Hi-Speed (480 Mbps) device + host | CH340 UART only | Full-Speed USB |
|
||||
| WiFi | **None** | Yes (unused) | Yes (unused) |
|
||||
| Ethernet | 10/100 PHY (optional) | — | — |
|
||||
| PQ crypto speed | ~1-2 s SLH-DSA-128s | 5-30 s SLH-DSA-128s | 5-30 s SLH-DSA-128s |
|
||||
|
||||
## SD card size support
|
||||
|
||||
| Card type | Size | Works? |
|
||||
|---|---|---|
|
||||
| SDSC | ≤ 2 GB | Yes (FAT16/32) |
|
||||
| SDHC | 2 – 32 GB | Yes (FAT32) |
|
||||
| **SDXC** | **32 GB – 2 TB** | **Yes (exFAT via SdFat)** |
|
||||
| SDUC | 2 – 128 TB | No |
|
||||
|
||||
**1 TB SDXC cards work** — SdFat has native exFAT support, and the Teensy's
|
||||
4-bit SDMMC bus runs at ~20-40 MB/s. No reformatting needed.
|
||||
|
||||
## Display + touch
|
||||
|
||||
**4.0" ST7796S 480×320 SPI TFT with XPT2046 resistive touch** (Hosyond or
|
||||
equivalent, ~$12-15). Specs: 4-wire SPI, RGB 65K, 3.3V~5V (works at the Teensy's
|
||||
3.3V logic), XPT2046 resistive touch, includes touch pen + on-module SD slot.
|
||||
|
||||
Resistive touch is the right choice for a hardware signer (deliberate physical
|
||||
activation, stylus-compatible, simple driver). The XPT2046 driver ports from
|
||||
the CYD's [`touch.c`](../cyd_esp32_2432s028/main/touch.c) with 480×320
|
||||
resolution constants. The ST7796S display driver is new code (different init
|
||||
sequence than the CYD's ILI9341).
|
||||
|
||||
Wiring: TFT SPI on pins 11/12/13, CS=10, DC=9, RESET=8, BL=22; touch shares
|
||||
the SPI bus with T_CS=7, T_IRQ=6. See
|
||||
[`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md) for the
|
||||
full pin table.
|
||||
|
||||
## Build (planned)
|
||||
|
||||
```bash
|
||||
# Arduino CLI / Teensyduino
|
||||
arduino-cli compile --fqbn teensy:avr:teensy41 firmware/teensy41
|
||||
arduino-cli upload -p /dev/ttyACM0 --fqbn teensy:avr:teensy41 firmware/teensy41
|
||||
```
|
||||
|
||||
See the port plan: [`plans/teensy41_signer_port.md`](../../plans/teensy41_signer_port.md).
|
||||
306
plans/cyd_algorithm_api_upgrade.md
Normal file
306
plans/cyd_algorithm_api_upgrade.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Plan: Bring `firmware/cyd_esp32_2432s028` Up to Date with the Algorithm-Based API
|
||||
|
||||
## Context
|
||||
|
||||
The main project ([`src/dispatcher.c`](../src/dispatcher.c)) has fully migrated to the
|
||||
algorithm-based API documented in [`README.md`](../README.md) §4. The migration is marked
|
||||
COMPLETED in [`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md): legacy verb
|
||||
names are gone from the wire protocol, implementation, tests, clients, and docs.
|
||||
|
||||
The CYD firmware at [`firmware/cyd_esp32_2432s028/main/main.c`](../firmware/cyd_esp32_2432s028/main/main.c)
|
||||
was **not** updated and still speaks the **legacy verb API**:
|
||||
|
||||
| Firmware verb (current) | Main-project verb (target) |
|
||||
|--------------------------|----------------------------|
|
||||
| `get_public_key` (+ `nostr_index`) | split → `get_public_key` (+ `algorithm`) **and** `nostr_get_public_key` (+ `nostr_index`) |
|
||||
| `sign_event` | `nostr_sign_event` |
|
||||
| `nip04_encrypt` / `nip04_decrypt` | `nostr_nip04_encrypt` / `nostr_nip04_decrypt` |
|
||||
| `nip44_encrypt` / `nip44_decrypt` | `nostr_nip44_encrypt` / `nostr_nip44_decrypt` |
|
||||
| _(missing)_ | `sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`, `derive`, `encrypt`/`decrypt` (otp), `nostr_mine_event` |
|
||||
|
||||
### Good news: the crypto primitives already exist
|
||||
|
||||
The firmware already has every underlying primitive needed — only the **dispatch layer**
|
||||
in [`main.c`](../firmware/cyd_esp32_2432s028/main/main.c) is stale:
|
||||
|
||||
- [`key_derivation.h`](../firmware/cyd_esp32_2432s028/main/key_derivation.h): `derive_nostr_key_index`, `derive_ed25519_key`, `derive_x25519_key`, `derive_ml_dsa_65_key`, `derive_slh_dsa_128s_key`, `derive_ml_kem_768_key`, `schnorr_sign32`, `ed25519_sign32`
|
||||
- [`pq_crypto_firmware.h`](../firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.h): `fw_pq_ml_dsa_65_sign/verify`, `fw_pq_slh_dsa_128s_sign/verify`, `fw_pq_ml_kem_768_encaps/decaps`
|
||||
- [`nostr_core_lib`](../resources/nostr_core_lib) nip004/nip044 already linked for the Nostr verbs
|
||||
|
||||
### `key_id` convention
|
||||
|
||||
The main project defines `key_id` as the **first 16 hex characters of the public key**
|
||||
(see [`src/dispatcher.c`](../src/dispatcher.c) ~line 1788). The firmware must match this
|
||||
so clients can correlate keys across targets.
|
||||
|
||||
## Scope
|
||||
|
||||
**Target:** [`firmware/cyd_esp32_2432s028`](../firmware/cyd_esp32_2432s028) only.
|
||||
The feather_s3_tft firmware is explicitly out of scope for this pass (it has the same gap
|
||||
but will be handled separately).
|
||||
|
||||
## Architecture: dispatch flow after upgrade
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[recv frame] --> B[parse JSON-RPC]
|
||||
B --> C{auth envelope}
|
||||
C -->|fail| Z[auth error]
|
||||
C -->|ok| D{method}
|
||||
D -->|nostr_*| E[Nostr verb branch<br/>secp256k1 NIP-06<br/>nostr_index selector]
|
||||
D -->|alg verb| F[Algorithm verb branch<br/>algorithm + index selector]
|
||||
E --> G[derive_request_key<br/>nostr_index]
|
||||
F --> H{algorithm}
|
||||
H -->|secp256k1| H1[derive_nostr_key_index]
|
||||
H -->|ed25519| H2[derive_ed25519_key]
|
||||
H -->|x25519| H3[derive_x25519_key]
|
||||
H -->|ml-dsa-65| H4[derive_ml_dsa_65_key]
|
||||
H -->|slh-dsa-128s| H5[derive_slh_dsa_128s_key]
|
||||
H -->|ml-kem-768| H6[derive_ml_kem_768_key]
|
||||
H -->|otp| H7[bound pad]
|
||||
G --> I[enforcement matrix check]
|
||||
H1 --> I
|
||||
H2 --> I
|
||||
H3 --> I
|
||||
H4 --> I
|
||||
H5 --> I
|
||||
H6 --> I
|
||||
H7 --> I
|
||||
I -->|reject 1010| Z
|
||||
I -->|ok| J[approval prompt]
|
||||
J --> K[execute verb]
|
||||
K --> L[structured result JSON<br/>algorithm + key_id + field]
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Add algorithm-name parsing helpers (`main.c`)
|
||||
|
||||
Add a small enum + parser mirroring the main project's algorithm set:
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
FW_ALG_SECP256K1 = 0,
|
||||
FW_ALG_ED25519,
|
||||
FW_ALG_X25519,
|
||||
FW_ALG_ML_DSA_65,
|
||||
FW_ALG_SLH_DSA_128S,
|
||||
FW_ALG_ML_KEM_768,
|
||||
FW_ALG_OTP,
|
||||
FW_ALG_UNKNOWN
|
||||
} fw_alg_t;
|
||||
```
|
||||
|
||||
- `parse_algorithm_from_options(cJSON *options, fw_alg_t *out_alg, uint32_t *out_index)`
|
||||
— reads `algorithm` (string) and `index` (number, default 0) from the trailing options
|
||||
object in `params`.
|
||||
- `fw_alg_name(fw_alg_t)` → canonical string (`"secp256k1"`, `"ed25519"`, …) for response
|
||||
JSON.
|
||||
- Keep the existing `parse_nostr_index_from_params()` for the `nostr_*` verbs.
|
||||
|
||||
### 2. Add a unified algorithm-key derivation + `key_id` helper
|
||||
|
||||
Add `derive_alg_key(fw_alg_t alg, uint32_t index, ...)` that dispatches to the right
|
||||
`derive_*_key` function and produces:
|
||||
- the raw private key bytes (when applicable),
|
||||
- the public key hex,
|
||||
- the `key_id` (first 16 hex chars of the public key).
|
||||
|
||||
PQ algorithms have large key buffers (ml-dsa-65 sk = 4032 B, ml-kem-768 sk = 2400 B).
|
||||
Allocate these as **static** buffers (not on the stack) and `secure_memzero` after use,
|
||||
matching the existing `s_privkey`/`s_pubkey` pattern. SLH-DSA-128s keygen is slow
|
||||
(5–30 s) — log a warning and show a "deriving key…" UI screen before calling it.
|
||||
|
||||
### 3. Add a structured-result builder
|
||||
|
||||
Add `build_alg_result_json(const char *alg_name, const char *key_id_16hex, const char *field_name, const char *field_value)` → returns a JSON string like
|
||||
`{"algorithm":"ed25519","key_id":"<16hex>","public_key":"<hex>"}`. This mirrors
|
||||
[`build_alg_result_json`](../src/dispatcher.c:861) in the main dispatcher.
|
||||
|
||||
### 4. Add the enforcement matrix
|
||||
|
||||
Add `enforce_alg_verb(fw_alg_t alg, const char *verb)` returning 0 / `1010`, matching
|
||||
[`README.md`](../README.md) §4.3 enforcement matrix:
|
||||
|
||||
| Verb | Valid algorithms |
|
||||
|------|------------------|
|
||||
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `derive` | secp256k1 |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `get_public_key` | all key-deriving algorithms |
|
||||
|
||||
Reject any unlisted pair with
|
||||
`{"error":{"code":1010,"message":"algorithm_not_supported_for_verb"}}`.
|
||||
|
||||
### 5. Rename the Nostr verbs (in-place, no compat shim)
|
||||
|
||||
In the dispatch `if/else if` chain in [`main.c`](../firmware/cyd_esp32_2432s028/main/main.c)
|
||||
~lines 836–1230:
|
||||
|
||||
- `get_public_key` (nostr_index branch) → `nostr_get_public_key`
|
||||
- `sign_event` → `nostr_sign_event`
|
||||
- `nip04_encrypt` → `nostr_nip04_encrypt`
|
||||
- `nip04_decrypt` → `nostr_nip04_decrypt`
|
||||
- `nip44_encrypt` → `nostr_nip44_encrypt`
|
||||
- `nip44_decrypt` → `nostr_nip44_decrypt`
|
||||
|
||||
The `nostr_get_public_key` verb should also honor the `format` option
|
||||
(`"structured"` → `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16hex>"}`,
|
||||
default → bare 64-hex pubkey string), matching the main project.
|
||||
|
||||
### 6. Add the algorithm-based `get_public_key` verb
|
||||
|
||||
`get_public_key` with `algorithm` + `index` → derive the key, return structured JSON:
|
||||
`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16hex>"}`. For PQ algorithms the
|
||||
`public_key` is the full PQClean pubkey hex (1952 B for ml-dsa-65, 1184 B for ml-kem-768,
|
||||
32 B for slh-dsa-128s) — ensure `s_response_buf` (currently 2048 B) is large enough, or
|
||||
emit via `cJSON_PrintUnformatted` into a larger static buffer.
|
||||
|
||||
### 7. Add `sign` and `verify` (algorithm-based)
|
||||
|
||||
- `sign`: params `[<message_hex>, {algorithm, index, scheme?}]`. `scheme` is
|
||||
secp256k1-only (`"schnorr"` default / `"ecdsa"`). For ed25519 use `ed25519_sign32`
|
||||
(note: ed25519 signs the raw 32-byte message, not a pre-hash — match main project
|
||||
behavior). For ml-dsa-65 / slh-dsa-128s use `fw_pq_*_sign`. Response:
|
||||
`{"algorithm":"<alg>","key_id":"<16hex>","signature":"<hex>"}`.
|
||||
- `verify`: params `[<message_hex>, <signature_hex>, {algorithm, index, scheme?}]`.
|
||||
Derive the signer's own pubkey and verify against it. Response:
|
||||
`{"valid":true,"algorithm":"<alg>"}`.
|
||||
|
||||
### 8. Add `encapsulate` / `decapsulate` (ml-kem-768)
|
||||
|
||||
- `encapsulate`: params `[<peer_pubkey_hex>, {algorithm:"ml-kem-768"}]` →
|
||||
`fw_pq_ml_kem_768_encaps` →
|
||||
`{"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"}`.
|
||||
- `decapsulate`: params `[<ciphertext_hex>, {algorithm:"ml-kem-768", index}]` → derive
|
||||
kem keypair, `fw_pq_ml_kem_768_decaps` →
|
||||
`{"shared_secret":"<hex>","algorithm":"ml-kem-768"}`.
|
||||
|
||||
### 9. Add `derive_shared_secret` (x25519)
|
||||
|
||||
params `[<peer_pubkey_hex>, {algorithm:"x25519", index}]` → derive x25519 keypair,
|
||||
compute X25519 ECDH via mbedtls →
|
||||
`{"shared_secret":"<hex>","algorithm":"x25519"}`.
|
||||
|
||||
### 10. Add `derive` (secp256k1 HMAC-SHA256)
|
||||
|
||||
params `[<data>, {algorithm:"secp256k1", index}]` (`index` **required**) → derive
|
||||
secp256k1 privkey, compute `HMAC-SHA256(privkey, data)` via mbedtls →
|
||||
`{"algorithm":"secp256k1","key_id":"<16hex>","digest":"<64hex>"}`. See
|
||||
[`plans/derive_hmac.md`](../plans/derive_hmac.md) for the spec.
|
||||
|
||||
### 11. Add `nostr_mine_event` (PoW)
|
||||
|
||||
params `[<event_json>, {nostr_index, difficulty, timeout_sec, threads?}]`. Reuse
|
||||
`build_signed_event_json` but iterate nonce in the event's `tags` until the leading-zero
|
||||
bits of the event id meet `difficulty`. ESP32 is slow — cap `threads` at 1 and enforce a
|
||||
firm `timeout_sec` (default 30). Show a "mining…" UI screen. If timeout, return error
|
||||
`1008 mining_failed`. This mirrors [`src/miner.c`](../src/miner.c).
|
||||
|
||||
### 12. Add `encrypt` / `decrypt` (otp)
|
||||
|
||||
The main project binds a pad from `--otp-pad-dir` + `--otp-pad` (a USB file). The CYD has
|
||||
no filesystem pad source. **Decision: derive the OTP pad from the mnemonic seed** via a
|
||||
SHAKE-256 / HKDF expansion keyed on `algorithm:"otp"` so the pad is deterministic per
|
||||
mnemonic and advances monotonically across requests (offset stored in a static variable,
|
||||
reported in every response). This keeps the wire contract identical (`encrypt`/`decrypt`
|
||||
with `algorithm:"otp"`, `encoding:"ascii"|""binary""`) while fitting the embedded
|
||||
constraint. Document this divergence in [`firmware/README.md`](../firmware/README.md).
|
||||
|
||||
### 13. Bump `FIRMWARE_VERSION` and update `firmware/README.md`
|
||||
|
||||
- `FIRMWARE_VERSION` "0.0.1" → "0.0.2" (algorithm-based API).
|
||||
- Document the new verb table, the OTP pad-derivation divergence, and the SLH-DSA-128s
|
||||
latency warning.
|
||||
|
||||
### 14. Update CYD-targeting examples / clients
|
||||
|
||||
Audit and update any example or client that speaks to the CYD over UART/Web-Serial and
|
||||
uses legacy verb names:
|
||||
- [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) and
|
||||
[`examples/feather_sign_event.py`](../examples/feather_sign_event.py) (feather-targeting
|
||||
but the wire protocol is shared — note in README they need `nostr_` prefixes for CYD
|
||||
after this change; leave feather examples alone since feather is out of scope, but add a
|
||||
CYD-specific example pair if none exists).
|
||||
- [`client/`](../client/) demos already use the new verbs (per
|
||||
[`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md)) — verify no CYD-specific
|
||||
legacy calls remain.
|
||||
|
||||
### 15. Add a CYD Web Serial test page covering all algorithms
|
||||
|
||||
The existing [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html) is
|
||||
**WebUSB-only** (feather's native USB) and uses the **legacy verbs**. The CYD's CH340
|
||||
bridge (`1a86:7523`) is not a WebUSB device — it exposes a serial port, so the browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only.
|
||||
|
||||
Create [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) — a
|
||||
single-file, dependency-light test page that:
|
||||
|
||||
**Transport:**
|
||||
- `navigator.serial.requestPort()` → `port.open({ baudRate: 115200 })` (matches
|
||||
[`uart_transport.c`](../firmware/cyd_esp32_2432s028/main/uart_transport.c) UART_BAUD_RATE).
|
||||
- Same 4-byte big-endian length-prefix frame format as the feather demo
|
||||
([`be32()`](../examples/feather_webusb_demo.html:256), read loop reassembling frames).
|
||||
- Read via a `ReadableStream` reader + length-prefix reassembly (Web Serial is stream-based,
|
||||
not packet-based like WebUSB `transferIn`).
|
||||
- Same auth-envelope construction (kind 27235, `nsigner_method` / `nsigner_body_hash` tags,
|
||||
schnorr sign with a demo caller key) — reuse the
|
||||
[`buildAuth()`](../examples/feather_webusb_demo.html:279) logic verbatim.
|
||||
|
||||
**UI sections (one card per verb family, all algorithms):**
|
||||
1. **Connect** — Connect Web Serial button + status.
|
||||
2. **Get Public Key** — algorithm dropdown (`secp256k1`, `ed25519`, `x25519`, `ml-dsa-65`,
|
||||
`slh-dsa-128s`, `ml-kem-768`) + index → `get_public_key`. Also a `nostr_get_public_key`
|
||||
card with `nostr_index` + `format` (bare / structured) toggle.
|
||||
3. **Sign / Verify** — algorithm dropdown (sig algs only) + index + `scheme` (schnorr/ecdsa,
|
||||
secp256k1-only) + message hex → `sign`; then `verify` with the returned signature.
|
||||
4. **KEM (ml-kem-768)** — `encapsulate` with a peer pubkey (or self-pubkey from
|
||||
`get_public_key`) → ciphertext + shared secret; `decapsulate` with that ciphertext →
|
||||
shared secret (confirm match).
|
||||
5. **X25519** — `derive_shared_secret` with peer pubkey.
|
||||
6. **Derive (HMAC)** — `derive` with data string + index → 64-hex digest.
|
||||
7. **Nostr Sign Event** — `nostr_sign_event` (kind 1) with `nostr_index`.
|
||||
8. **Nostr Mine Event** — `nostr_mine_event` with difficulty + timeout (low default, e.g.
|
||||
difficulty 4) — warn it's slow on ESP32.
|
||||
9. **NIP-04 / NIP-44** — `nostr_nip04_encrypt`/`decrypt`, `nostr_nip44_encrypt`/`decrypt`
|
||||
with `nostr_index`.
|
||||
10. **OTP** — `encrypt` / `decrypt` with `algorithm:"otp"`, `encoding` toggle
|
||||
(ascii/binary), base64 plaintext.
|
||||
|
||||
Each card shows the raw JSON-RPC request and response in a `<pre>` so the wire format is
|
||||
visible. Reuse the feather demo's CSS (dark theme, cards, `.mono` log) for consistency.
|
||||
|
||||
**Link it from [`firmware/README.md`](../firmware/README.md)** in the CYD section (the
|
||||
"Quick validation" / Web Serial path), since the current README only points at the
|
||||
feather WebUSB demo.
|
||||
|
||||
### 16. Verification
|
||||
|
||||
- `idf.py build` in [`firmware/cyd_esp32_2432s028`](../firmware/cyd_esp32_2432s028) compiles
|
||||
clean.
|
||||
- Flash to the connected CYD board (CH340 on `/dev/ttyUSB0`) and smoke-test each verb:
|
||||
- Primary path: open [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html)
|
||||
in Chrome/Edge, connect via Web Serial, exercise every card.
|
||||
- Secondary path: a small Python script over `/dev/ttyUSB0` for headless confirmation.
|
||||
- Cover: `get_public_key` (each algorithm), `sign`/`verify` (each sig alg),
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_encrypt`/`decrypt`,
|
||||
`nostr_nip44_encrypt`/`decrypt`, `nostr_mine_event` (low difficulty),
|
||||
`encrypt`/`decrypt` (otp).
|
||||
- Confirm `key_id` matches the first 16 hex of the returned pubkey for every alg.
|
||||
- Confirm an invalid `(verb, algorithm)` pair returns code 1010.
|
||||
- `grep -rn "sign_event\|nip04_encrypt\|nip04_decrypt\|nip44_encrypt\|nip44_decrypt"
|
||||
firmware/cyd_esp32_2432s028/` returns no matches (legacy names gone).
|
||||
|
||||
## Open questions / decisions baked in
|
||||
|
||||
- **OTP pad source:** derived from mnemonic (no USB pad on CYD). Documented divergence.
|
||||
- **`nostr_mine_event`:** implemented, single-threaded, hard 30 s default timeout, with a
|
||||
"mining…" UI screen. Not stubbed — the main project has it and the user asked to bring
|
||||
the firmware up to date.
|
||||
- **No legacy-verb compat shim:** matches the main project's policy
|
||||
([`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md) — "No backward-
|
||||
compatibility shim is needed").
|
||||
- **feather_s3_tft not touched** in this pass.
|
||||
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.
|
||||
@@ -1,4 +1,6 @@
|
||||
# Plan: Legacy Verb Aliases (Migration Path)
|
||||
# Plan: Legacy Verb Aliases (Migration Path) — COMPLETED
|
||||
|
||||
> **Status: Done.** All steps below have been executed. The legacy verb names are gone from the wire protocol, the implementation, the tests, the clients, and the documentation. `make dev` builds clean and `make test` passes (all 10 test binaries, 230+ assertions).
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
248
plans/teensy41_signer_port.md
Normal file
248
plans/teensy41_signer_port.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Plan: Port n_signer to Teensy 4.1 with SDXC 1 TB exFAT OTP Pad
|
||||
|
||||
## Goal
|
||||
|
||||
Port the n_signer hardware signer to the **Teensy 4.1** (NXP i.MX RT1062,
|
||||
Cortex-M7 @ 600 MHz), using its built-in SD slot to hold a **1 TB SDXC exFAT
|
||||
one-time-pad file**. The Teensy 4.1 is the high-capacity-OTP target; the CYD
|
||||
(classic ESP32) remains the cheap/low-power option with the mnemonic-derived
|
||||
stream pad.
|
||||
|
||||
The on-the-wire protocol is identical to the host and the CYD/feather firmware
|
||||
([`README.md`](../README.md) §4 — the algorithm-based API). The auth envelope,
|
||||
verb set, enforcement matrix, `key_id` convention, and structured-result JSON
|
||||
are all unchanged.
|
||||
|
||||
## Why the Teensy 4.1
|
||||
|
||||
| Concern | Teensy 4.1 | CYD (ESP32) |
|
||||
|---|---|---|
|
||||
| MCU | 600 MHz Cortex-M7 | 240 MHz Xtensa LX6 |
|
||||
| SRAM | 1 MB + 16 MB PSRAM (on-board) | 512 KB, no PSRAM |
|
||||
| SD slot | **4-bit SDMMC, exFAT via SdFat, up to 2 TB** | 1-bit SDSPI, FAT32, up to 32 GB |
|
||||
| SD speed | ~20-40 MB/s | ~2 MB/s |
|
||||
| USB | Hi-Speed (480 Mbps) device + host | CH340 UART only |
|
||||
| WiFi | **None** | Yes (unused) |
|
||||
| PQ crypto | ~1-2 s SLH-DSA-128s | 5-30 s SLH-DSA-128s |
|
||||
| Display | Add SPI ILI9341 (same panel as CYD) | Built-in 2.8" ILI9341 + touch |
|
||||
|
||||
The decisive factor is the **SD slot**: PJRC's [SdFat](https://github.com/greiman/SdFat)
|
||||
library has native exFAT support, so a 1 TB SDXC card (which ships formatted
|
||||
exFAT) mounts and reads/writes directly — no reformatting, no exFAT driver
|
||||
work. The 4-bit SDMMC bus is fast enough (~20-40 MB/s) for pad reads with
|
||||
arbitrary seeks.
|
||||
|
||||
## Hardware
|
||||
|
||||
### Board
|
||||
- **Teensy 4.1** (PJRC) — $27. Has 8 MB flash, 16 MB external QSPI, 16 MB PSRAM
|
||||
(soldered), 1 MB internal SRAM, native USB Hi-Speed, built-in SD slot, 10/100
|
||||
Ethernet PHY, no WiFi/BT.
|
||||
|
||||
### Display + touch (add-on)
|
||||
- **4.0" ST7796S 480×320 with XPT2046 resistive touch** (Hosyond or equivalent,
|
||||
~$12-15). Specs: 4-wire SPI, RGB 65K, 3.3V~5V (works at Teensy's 3.3V logic),
|
||||
XPT2046 resistive touch, includes touch pen + SD card slot on the module.
|
||||
SPI wiring to the Teensy 4.1:
|
||||
- TFT: MOSI=pin 11, SCK=pin 13, MISO=pin 12, CS=pin 10, DC=pin 9,
|
||||
RESET=pin 8, BL=pin 22 (PWM via analogWrite)
|
||||
- Touch (XPT2046, shared SPI bus): T_CS=pin 7, T_IRQ=pin 6,
|
||||
T_CLK=pin 13, T_MOSI=pin 11, T_MISO=pin 12
|
||||
- Power: VCC=3.3V, GND=GND
|
||||
- The ST7796S controller needs a different init sequence than the CYD's
|
||||
ILI9341, and the resolution is 480×320 (not 320×240). The XPT2046 touch
|
||||
driver ports from [`firmware/cyd_esp32_2432s028/main/touch.c`](../firmware/cyd_esp32_2432s028/main/touch.c)
|
||||
with new resolution constants.
|
||||
- The module's on-board SD card slot is a bonus (backup pad / offset file),
|
||||
but the 1 TB pad uses the Teensy's built-in SD slot (4-bit SDMMC, faster).
|
||||
|
||||
### SD card
|
||||
- **1 TB microSDXC** (exFAT, ~$60-80) in the Teensy's built-in slot.
|
||||
- The pad file (`/pad.bin`) + offset file (`/pad.offset`) live on this card.
|
||||
|
||||
### USB transport
|
||||
- The Teensy's native USB port (device mode) exposes a **CDC-ACM serial** +
|
||||
optional **WebUSB vendor** interface (TinyUSB composite), same framing as the
|
||||
feather/CYD (4-byte big-endian length prefix + JSON-RPC payload).
|
||||
- The host sees `/dev/ttyACM0` (Linux) or `COMx` (Windows).
|
||||
|
||||
## Target directory layout
|
||||
|
||||
```
|
||||
firmware/teensy41/
|
||||
├── README.md (created)
|
||||
├── teensy41_signer.ino (Arduino entry, or main.cpp for PlatformIO)
|
||||
├── src/
|
||||
│ ├── main.cpp (app loop: UI → transport → dispatch)
|
||||
│ ├── dispatch.cpp (verb dispatch — ported from cyd main.c handle_request)
|
||||
│ ├── dispatch.h
|
||||
│ ├── key_derivation.cpp (BIP-39 → seed → secp256k1/ed25519/x25519/PQ keys)
|
||||
│ ├── key_derivation.h
|
||||
│ ├── pq_crypto.cpp (PQClean wrappers: ml-dsa-65, slh-dsa-128s, ml-kem-768)
|
||||
│ ├── pq_crypto.h
|
||||
│ ├── otp_pad_sd.cpp (SDXC exFAT pad: mount, read, offset persistence)
|
||||
│ ├── otp_pad_sd.h
|
||||
│ ├── transport.cpp (USB CDC + length-prefix framing)
|
||||
│ ├── transport.h
|
||||
│ ├── display.cpp (ILI9341 driver — ported from cyd ili9341.c)
|
||||
│ ├── display.h
|
||||
│ ├── touch.cpp (XPT2046 driver — ported from cyd touch.c)
|
||||
│ ├── touch.h
|
||||
│ ├── ui.cpp (LVGL or hand-rolled UI — ported from cyd ui.c)
|
||||
│ ├── ui.h
|
||||
│ ├── secure_mem.cpp (zeroize helpers)
|
||||
│ ├── secure_mem.h
|
||||
│ ├── bech32.cpp (npub encoding)
|
||||
│ ├── bech32.h
|
||||
│ ├── mnemonic.cpp (BIP-39 wordlist + validation)
|
||||
│ ├── mnemonic.h
|
||||
│ └── mnemonic_wordlist.h
|
||||
├── lib/
|
||||
│ ├── secp256k1/ (libsecp256k1, built for ARM Cortex-M7)
|
||||
│ ├── pqclean/ (PQClean ML-DSA-65, SLH-DSA-128s, ML-KEM-768)
|
||||
│ ├── nostr_core_lib/ (symlink/copy of resources/nostr_core_lib)
|
||||
│ └── SdFat/ (PJRC SdFat with exFAT — via Arduino Library Manager)
|
||||
└── platformio.ini (or Arduino project config)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
USB[Host USB CDC] --> Frame[transport.cpp<br/>length-prefix framing]
|
||||
Frame --> Auth[auth envelope verify<br/>secp256k1 schnorr]
|
||||
Auth -->|ok| Disp[dispatch.cpp<br/>handle_request]
|
||||
Auth -->|fail| Err[auth error]
|
||||
Disp -->|nostr_*| NIP[Nostr verbs<br/>secp256k1 NIP-06]
|
||||
Disp -->|alg verb| Alg[Algorithm verbs<br/>algorithm + index]
|
||||
Alg -->|otp| OTP[otp_pad_sd.cpp<br/>SDXC exFAT pad]
|
||||
Alg -->|secp256k1/ed25519/x25519/PQ| KD[key_derivation.cpp]
|
||||
OTP --> SD[(1 TB SDXC<br/>exFAT<br/>/pad.bin + /pad.offset)]
|
||||
KD --> UI[ui.cpp<br/>approval prompt<br/>ILI9341 + XPT2046]
|
||||
UI -->|approve| Exec[execute verb]
|
||||
Exec --> Resp[structured JSON result]
|
||||
Resp --> Frame
|
||||
```
|
||||
|
||||
## Implementation phases
|
||||
|
||||
### Phase 1: Board bring-up (display + touch + SD)
|
||||
|
||||
1. **Toolchain:** Arduino CLI + Teensyduino (simplest), or PlatformIO with the
|
||||
`teensy` platform. Verify `Blink` + `HelloSerial` compile and flash.
|
||||
2. **ILI9341 display:** port [`firmware/cyd_esp32_2432s028/main/ili9341.c`](../firmware/cyd_esp32_2432s028/main/ili9341.c)
|
||||
to Teensy GPIO + SPI (use `SPI.beginTransaction` for 40 MHz HSPI). Exit
|
||||
criterion: fill screen + draw text.
|
||||
3. **XPT2046 touch:** port [`firmware/cyd_esp32_2432s028/main/touch.c`](../firmware/cyd_esp32_2432s028/main/touch.c).
|
||||
Exit criterion: read touch coordinates, map to 320×240.
|
||||
4. **SD card + exFAT:** install SdFat via Arduino Library Manager. Mount a 1 TB
|
||||
SDXC card (exFAT), write + read a test file. Exit criterion:
|
||||
`sd.begin(SdioConfig(FIFO_SDIO))` succeeds on a 1 TB card, `file.open`
|
||||
+ `file.write` + `file.read` round-trips.
|
||||
|
||||
### Phase 2: Crypto stack port
|
||||
|
||||
1. **secp256k1:** build `libsecp256k1` for ARM Cortex-M7 (no ASM, pure C with
|
||||
`USE_NUM_NONE` / `USE_FIELD_INV_BUILTIN`). Verify schnorr sign/verify +
|
||||
ECDSA sign/verify against known test vectors.
|
||||
2. **ed25519 / x25519:** use a portable ed25519 (e.g. the ref10 impl or
|
||||
`crypto_mbedtls` if mbedtls is available for Teensy — alternatively
|
||||
[`micro-ecc`](https://github.com/kmackay/micro-ecc) + a portable ed25519).
|
||||
Verify against the host's test vectors.
|
||||
3. **PQClean:** compile `resources/pqclean/` (ML-DSA-65, SLH-DSA-128s,
|
||||
ML-KEM-768) for Cortex-M7. The `crypto_backend` abstraction needs a Teensy
|
||||
backend (`crypto_backend_sdfat.c` or reuse the vendored Keccak from
|
||||
[`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c)
|
||||
— the Keccak core is portable C). SHA-256/512 via SdFat's built-in or a
|
||||
portable impl. Verify keygen + sign + verify against the host.
|
||||
4. **nostr_core_lib:** compile [`resources/nostr_core_lib/`](../resources/nostr_core_lib/)
|
||||
(nip004, nip044, nostr_common, utils, crypto) for Cortex-M7. This is
|
||||
portable C and should compile as-is.
|
||||
|
||||
### Phase 3: Key derivation + mnemonic
|
||||
|
||||
1. Port [`firmware/cyd_esp32_2432s028/main/key_derivation.c`](../firmware/cyd_esp32_2432s028/main/key_derivation.c)
|
||||
(BIP-32/SLIP-0010 derivation for all 6 algorithms). Replace mbedtls/PSA
|
||||
calls with the portable crypto from Phase 2.
|
||||
2. Port [`firmware/cyd_esp32_2432s028/main/mnemonic.c`](../firmware/cyd_esp32_2432s028/main/mnemonic.c)
|
||||
(BIP-39 wordlist + validation + mnemonic_to_seed via PBKDF2-HMAC-SHA512).
|
||||
3. Port [`firmware/cyd_esp32_2432s028/main/bech32.c`](../firmware/cyd_esp32_2432s028/main/bech32.c)
|
||||
(npub encoding).
|
||||
|
||||
### Phase 4: OTP pad from SDXC (the key feature)
|
||||
|
||||
1. **`otp_pad_sd.cpp`:**
|
||||
- `otp_pad_init()`: mount the SD card via `sd.begin(SdioConfig(FIFO_SDIO))`,
|
||||
open `/pad.bin` for reading, open `/pad.offset` for the persistent offset.
|
||||
Read the offset file on boot; if absent, start at 0.
|
||||
- `otp_pad_read(buf, len)`: seek to the current offset in `/pad.bin`, read
|
||||
`len` bytes, advance the offset. If the offset + len exceeds the file
|
||||
size, return an error (pad exhausted).
|
||||
- `otp_pad_persist_offset()`: write the current offset to `/pad.offset`
|
||||
after each `encrypt`/`decrypt` call (or batch: persist every N calls to
|
||||
reduce SD wear).
|
||||
- `otp_pad_zeroize()`: close files, zeroize in-RAM state.
|
||||
2. **Wire into the `encrypt`/`decrypt` verbs:** replace the CYD's
|
||||
HKDF-derived in-RAM pad with `otp_pad_read()`. The wire contract is
|
||||
unchanged (`encrypt`/`decrypt` with `algorithm:"otp"`, base64 payload,
|
||||
`pad_offset` in the response).
|
||||
3. **UI:** show "reading pad from SD…" during the read (the SD read is fast
|
||||
but the user should see activity). Show the pad offset + remaining bytes on
|
||||
the idle screen.
|
||||
|
||||
### Phase 5: Transport + dispatch + UI
|
||||
|
||||
1. **Transport (`transport.cpp`):** TinyUSB CDC-ACM + 4-byte length-prefix
|
||||
framing (same as [`firmware/cyd_esp32_2432s028/main/uart_transport.c`](../firmware/cyd_esp32_2432s028/main/uart_transport.c)).
|
||||
Optionally add a WebUSB vendor interface for browser transport (the Teensy's
|
||||
Hi-Speed USB makes this fast).
|
||||
2. **Dispatch (`dispatch.cpp`):** port `handle_request()` from
|
||||
[`firmware/cyd_esp32_2432s028/main/main.c`](../firmware/cyd_esp32_2432s028/main/main.c)
|
||||
— the entire verb dispatch (all nostr_* + algorithm-based verbs, enforcement
|
||||
matrix, structured results, auth envelope verify). This is the bulk of the
|
||||
logic and ports nearly verbatim (only the crypto backend calls change).
|
||||
3. **UI (`ui.cpp`):** port [`firmware/cyd_esp32_2432s028/main/ui.c`](../firmware/cyd_esp32_2432s028/main/ui.c)
|
||||
(LVGL 8.3 or hand-rolled). The UI screens are: startup menu → generate
|
||||
mnemonic → confirm mnemonic → enter mnemonic → idle (show npub + pad offset)
|
||||
→ approval prompt. The Teensy's 600 MHz M7 makes LVGL snappy.
|
||||
|
||||
### Phase 6: Integration + testing
|
||||
|
||||
1. **End-to-end smoke test:** load a mnemonic, exercise every verb over USB
|
||||
CDC with a Python script or the Web Serial test page
|
||||
([`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html)
|
||||
works for any CDC device).
|
||||
2. **OTP pad test:** place a known pad file on the 1 TB SDXC card, run
|
||||
`encrypt` + `decrypt` round-trips, verify the XOR is correct and the offset
|
||||
advances + persists across power cycles.
|
||||
3. **Cross-board parity:** same mnemonic on the Teensy 4.1 and the CYD → same
|
||||
npub, same secp256k1/ed25519/x25519/PQ public keys, same signatures.
|
||||
4. **Performance:** measure SLH-DSA-128s sign time (expect ~1-2 s vs 5-30 s on
|
||||
ESP32), ML-DSA-65 sign time (expect ~50 ms), SD pad read throughput.
|
||||
|
||||
## Open questions / decisions
|
||||
|
||||
- **UI framework:** LVGL 8.3 (heavier, proven on CYD) vs hand-rolled (lighter,
|
||||
faster to port, no external dep). The Teensy has enough RAM for LVGL. **Lean
|
||||
toward LVGL** for consistency with the CYD.
|
||||
- **ed25519/x25519 impl:** mbedtls is available for Teensy via the
|
||||
`mbedtls` Arduino library, but PSA crypto is not. Options: (a) mbedtls
|
||||
ed25519 (if the Arduino mbedtls has it), (b) a portable ed25519 like
|
||||
[`orlp/ed25519`](https://github.com/orlp/ed25519) + micro-ecc for x25519,
|
||||
(c) libsodium for Teensy. **Investigate (a) first, fall back to (b).**
|
||||
- **Toolchain:** Arduino CLI + Teensyduino (simplest, SdFat + USB stack
|
||||
included) vs PlatformIO (better dependency management, CI-friendly). **Lean
|
||||
toward Arduino CLI** for the initial port, migrate to PlatformIO later.
|
||||
- **Offset persistence frequency:** writing `/pad.offset` to SD after every
|
||||
`encrypt`/`decrypt` is safe but wears the SD. **Batch: persist every 16
|
||||
calls, and also on idle timeout.** If power is lost, at most 16 pad bytes are
|
||||
reused (acceptable for a signing device, not for a high-volume OTP channel).
|
||||
|
||||
## Verification
|
||||
|
||||
- `arduino-cli compile` (or `pio run`) builds clean for `teensy:avr:teensy41`.
|
||||
- Flash to the Teensy 4.1, load a mnemonic, exercise every verb over USB CDC.
|
||||
- 1 TB SDXC card mounts, `/pad.bin` reads at >20 MB/s, offset persists across
|
||||
power cycles.
|
||||
- Same mnemonic → same keys as the CYD and the host n_signer.
|
||||
- SLH-DSA-128s signs in <3 s (vs 5-30 s on ESP32).
|
||||
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.
|
||||
822
src/dispatcher.c
822
src/dispatcher.c
File diff suppressed because it is too large
Load Diff
@@ -202,36 +202,26 @@ 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"
|
||||
|
||||
@@ -240,7 +230,7 @@ const char *selector_strerror(int err);
|
||||
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
|
||||
*
|
||||
* The enforcement rules are:
|
||||
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
|
||||
* - All nostr verbs (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_nip04_*) require:
|
||||
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
|
||||
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
|
||||
*/
|
||||
@@ -712,69 +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) {
|
||||
@@ -782,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;
|
||||
}
|
||||
|
||||
@@ -804,56 +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);
|
||||
}
|
||||
|
||||
/* encrypt / decrypt: allowed for secp256k1 (NIP-04/44), x25519 (age),
|
||||
* and ml-kem-768 (pq-kem). OTP is handled before role resolution. */
|
||||
if (strcmp(verb, VERB_ENCRYPT) == 0 || strcmp(verb, VERB_DECRYPT) == 0) {
|
||||
if (role->curve == CURVE_SECP256K1 && role->purpose == PURPOSE_NOSTR) {
|
||||
return ENFORCE_OK;
|
||||
}
|
||||
if (role->curve == CURVE_X25519 && role->purpose == PURPOSE_AGE) {
|
||||
return ENFORCE_OK;
|
||||
}
|
||||
if (role->curve == CURVE_ML_KEM_768 && role->purpose == PURPOSE_PQ_KEM) {
|
||||
return ENFORCE_OK;
|
||||
}
|
||||
return ENFORCE_ERR_CURVE;
|
||||
}
|
||||
|
||||
/* All other verbs (sign, verify, encapsulate, decapsulate,
|
||||
* derive_shared_secret, get_public_key, encrypt, decrypt) are
|
||||
* algorithm-based and handled by enforce_verb_algorithm() before
|
||||
* role resolution. Anything reaching here is unrecognized. */
|
||||
return ENFORCE_ERR_UNKNOWN_VERB;
|
||||
}
|
||||
|
||||
/* ---- Algorithm-based enforcement (new) ----
|
||||
/* ---- Algorithm-based enforcement ----
|
||||
*
|
||||
* enforce_verb_algorithm() checks whether a verb is valid for a given
|
||||
* algorithm, WITHOUT consulting the role table or purpose field. This is
|
||||
* the enforcement path for the new algorithm-based verbs (sign, verify,
|
||||
* the enforcement path for the algorithm-based verbs (sign, verify,
|
||||
* encapsulate, decapsulate, derive_shared_secret, get_public_key).
|
||||
*
|
||||
* The Nostr-specific verbs (sign_event, nip44_*, nip04_*, mine_event) are
|
||||
* always secp256k1 — that's a Nostr protocol requirement, not a policy
|
||||
* choice. They are accepted here only when alg == CRYPTO_ALG_SECP256K1
|
||||
* so that algorithm-based policy entries can approve them too.
|
||||
* The Nostr-specific verbs (nostr_sign_event, nostr_nip44_*, nostr_nip04_*,
|
||||
* nostr_mine_event) are always secp256k1 — that's a Nostr protocol
|
||||
* requirement, not a policy choice. They are accepted here only when
|
||||
* alg == CRYPTO_ALG_SECP256K1 so that algorithm-based policy entries can
|
||||
* approve them too.
|
||||
*/
|
||||
int enforce_verb_algorithm(const char *verb, crypto_alg_t alg) {
|
||||
if (verb == NULL) {
|
||||
@@ -895,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`.
|
||||
|
||||
323
src/main.c
323
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).
|
||||
*/
|
||||
@@ -758,9 +761,9 @@ 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 52
|
||||
#define NSIGNER_VERSION "v0.0.52"
|
||||
#define NSIGNER_VERSION_MINOR 1
|
||||
#define NSIGNER_VERSION_PATCH 0
|
||||
#define NSIGNER_VERSION "v0.1.0"
|
||||
|
||||
|
||||
/* 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,76 +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) {
|
||||
const char *tcp_addr = servers[tcp_server_idx].socket_name;
|
||||
connection_info_add("Server: FIPS/TCP %s", tcp_addr + 4); /* skip "tcp:" */
|
||||
connection_info_add(" Client: nsigner --listen %s client '<json>'", tcp_addr);
|
||||
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) {
|
||||
const char *http_addr = servers[http_server_idx].socket_name;
|
||||
connection_info_add("Server: HTTP %s", http_addr + 5); /* skip "http:" */
|
||||
connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d '<json>'",
|
||||
http_addr + 5);
|
||||
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) {
|
||||
const char *actual_addr = server.socket_name;
|
||||
connection_info_add("Server: %s", actual_addr);
|
||||
connection_info_add(" Client: nsigner --listen %s client '<json>'", actual_addr);
|
||||
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) {
|
||||
const char *actual_addr = server.socket_name;
|
||||
connection_info_add("Server: %s", actual_addr);
|
||||
connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d '<json>'",
|
||||
actual_addr + 5); /* skip "http:" prefix */
|
||||
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);
|
||||
@@ -3077,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).
|
||||
*/
|
||||
|
||||
23
src/server.c
23
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).
|
||||
*/
|
||||
@@ -1760,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;
|
||||
@@ -2132,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