v0.0.49 - Added general encrypt/decrypt verbs with curve-based routing (otp, secp256k1 NIP-04/44, x25519, ml-kem-768) and updated README documentation

This commit is contained in:
Laan Tungir
2026-07-19 11:38:44 -04:00
parent 05c055503d
commit a017dc40e0
4 changed files with 150 additions and 12 deletions

View File

@@ -357,7 +357,18 @@ Implemented signer verbs in this build:
| `derive_shared_secret` | x25519 | ECDH key agreement. Returns `{"shared_secret":"<hex>","algorithm":"x25519"}`. | | `derive_shared_secret` | x25519 | ECDH key agreement. Returns `{"shared_secret":"<hex>","algorithm":"x25519"}`. |
| `get_public_key` | all algorithms | Returns the public key for the specified algorithm+index. | | `get_public_key` | all algorithms | Returns the public key for the specified algorithm+index. |
**OTP verbs** (one-time pad encryption, requires `--otp-pad-dir` + `--otp-pad`): **General encryption verbs** (use `curve` parameter to select encryption method):
| Verb | `curve` value | Description |
|---|---|---|
| `encrypt` | `otp` | One-time pad encryption (requires `--otp-pad-dir` + `--otp-pad`). Returns ASCII-armored or binary ciphertext. |
| `decrypt` | `otp` | One-time pad decryption. Returns plaintext (base64). |
| `encrypt` | `secp256k1` | NIP-44 (default) or NIP-04 encryption. Params: `[peer_pubkey, message, {nip_version: 4\|44}]`. |
| `decrypt` | `secp256k1` | NIP-44 (default) or NIP-04 decryption. Params: `[peer_pubkey, ciphertext, {nip_version: 4\|44}]`. |
| `encrypt` | `x25519` | ECDH + symmetric encryption (not yet implemented — use `derive_shared_secret` + your own cipher). |
| `encrypt` | `ml-kem-768` | KEM-based hybrid encryption (not yet implemented — use `encapsulate`/`decapsulate` + your own cipher). |
**OTP verb aliases** (also available, same as `encrypt`/`decrypt` with `curve: "otp"`):
| Verb | Description | | Verb | Description |
|---|---| |---|---|
@@ -536,18 +547,30 @@ curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"}]}' -d '{"id":"1","method":"sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"}]}'
``` ```
OTP encrypt (requires `--otp-pad-dir` and `--otp-pad`): General encrypt (OTP, requires `--otp-pad-dir` and `--otp-pad`):
```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"curve":"otp","encoding":"ascii"}]}'
```
General encrypt (NIP-44, secp256k1):
```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"encrypt","params":["<peer_pubkey_hex>","Hello!",{"role":"main"}]}'
```
General decrypt (OTP):
```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"decrypt","params":["-----BEGIN OTP MESSAGE-----...",{"curve":"otp","encoding":"ascii"}]}'
```
OTP verb aliases (same as encrypt/decrypt with curve=otp):
```bash ```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \ curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"otp_encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"encoding":"ascii"}]}' -d '{"id":"1","method":"otp_encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"encoding":"ascii"}]}'
``` ```
OTP decrypt:
```bash
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
-d '{"id":"1","method":"otp_decrypt","params":["-----BEGIN OTP MESSAGE-----...","{"encoding":"ascii"}]}'
```
#### Unix socket examples (framed mode) #### Unix socket examples (framed mode)
```bash ```bash
@@ -557,8 +580,11 @@ nsigner --socket-name nsigner client '{"id":"1","method":"get_public_key","param
# Sign event # Sign event
nsigner --socket-name nsigner client '{"id":"1","method":"sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"}]}' nsigner --socket-name nsigner client '{"id":"1","method":"sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"}]}'
# OTP encrypt # Encrypt (OTP)
nsigner --socket-name nsigner client '{"id":"1","method":"otp_encrypt","params":["SGVsbG8=",{"encoding":"ascii"}]}' nsigner --socket-name nsigner client '{"id":"1","method":"encrypt","params":["SGVsbG8=",{"curve":"otp","encoding":"ascii"}]}'
# Encrypt (NIP-44)
nsigner --socket-name nsigner client '{"id":"1","method":"encrypt","params":["<peer_pubkey>","Hello!",{"role":"main"}]}'
``` ```
### 7.2 ESP32 MCU: TinyUSB composite (CDC + WebUSB) ### 7.2 ESP32 MCU: TinyUSB composite (CDC + WebUSB)

View File

@@ -236,6 +236,8 @@ const char *selector_strerror(int err);
#define VERB_KEM_DECAPS "kem_decapsulate" #define VERB_KEM_DECAPS "kem_decapsulate"
#define VERB_OTP_ENCRYPT "otp_encrypt" #define VERB_OTP_ENCRYPT "otp_encrypt"
#define VERB_OTP_DECRYPT "otp_decrypt" #define VERB_OTP_DECRYPT "otp_decrypt"
#define VERB_ENCRYPT "encrypt"
#define VERB_DECRYPT "decrypt"
/* /*
* Check whether `verb` is allowed to execute against `role`. * Check whether `verb` is allowed to execute against `role`.
@@ -1675,6 +1677,27 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return otp_response; return otp_response;
} }
/* General encrypt/decrypt verbs: route to OTP if curve is "otp",
* otherwise fall through to the role-based path (for secp256k1 NIP-04/44,
* x25519, ml-kem-768). */
if (strcmp(method, VERB_ENCRYPT) == 0 ||
strcmp(method, VERB_DECRYPT) == 0) {
if (cJSON_IsObject(options_item)) {
cJSON *curve_item = cJSON_GetObjectItemCaseSensitive(options_item, "curve");
if (cJSON_IsString(curve_item) && curve_item->valuestring != NULL &&
strcmp(curve_item->valuestring, "otp") == 0) {
/* Route to OTP handler — map encrypt→otp_encrypt, decrypt→otp_decrypt */
const char *otp_method = (strcmp(method, VERB_ENCRYPT) == 0)
? VERB_OTP_ENCRYPT : VERB_OTP_DECRYPT;
char *otp_response = handle_otp_verb(id_str, otp_method,
params_item, options_item);
cJSON_Delete(root);
return otp_response;
}
}
/* For other curves, fall through to role-based path below. */
}
if (cJSON_IsObject(options_item)) { if (cJSON_IsObject(options_item)) {
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role"); tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role");
if (tmp != NULL) { if (tmp != NULL) {
@@ -1978,6 +2001,78 @@ char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request)
return make_error_response(id_str, -32602, "invalid_params"); return make_error_response(id_str, -32602, "invalid_params");
} }
result = owned_result; result = owned_result;
} else if (strcmp(method, VERB_ENCRYPT) == 0 ||
strcmp(method, VERB_DECRYPT) == 0) {
/* General encrypt/decrypt verbs (role-based path for non-OTP curves).
* The curve is determined by the role's curve. For secp256k1, the
* nip_version option (4 or 44, default 44) selects NIP-04 or NIP-44.
* For x25519 and ml-kem-768, the key agreement is available but
* direct encrypt/decrypt is not yet implemented. */
const char *curve_str = role->curve_str;
int nip_version = 44; /* default to NIP-44 */
if (cJSON_IsObject(options_item)) {
cJSON *nv = cJSON_GetObjectItemCaseSensitive(options_item, "nip_version");
if (cJSON_IsNumber(nv)) {
nip_version = nv->valueint;
}
}
if (role->curve == CURVE_SECP256K1) {
/* secp256k1: route to NIP-04 or NIP-44 */
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
if (strcmp(method, VERB_ENCRYPT) == 0) {
if (nip_version == 4) {
owned_result = crypto_nip04_encrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
} else {
owned_result = crypto_nip44_encrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
}
} else { /* decrypt */
if (nip_version == 4) {
owned_result = crypto_nip04_decrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
} else {
owned_result = crypto_nip44_decrypt(ctx->key_store, role_index,
peer_item->valuestring,
msg_item->valuestring);
}
}
if (owned_result == NULL) {
cJSON_Delete(root);
return make_error_response(id_str, -32602, "invalid_params");
}
result = owned_result;
} else if (role->curve == CURVE_X25519) {
/* x25519: ECDH + symmetric encryption — not yet implemented.
* The derive_shared_secret verb is available for key agreement;
* the caller can use the shared secret with their own symmetric
* cipher. Direct encrypt/decrypt will be added in a future release. */
cJSON_Delete(root);
return make_error_response(id_str, -32601,
"encrypt_decrypt_not_implemented_for_x25519");
} else if (role->curve == CURVE_ML_KEM_768) {
/* ml-kem-768: KEM-based hybrid encryption — not yet implemented.
* The encapsulate/decapsulate verbs are available for key agreement;
* direct encrypt/decrypt will be added in a future release. */
cJSON_Delete(root);
return make_error_response(id_str, -32601,
"encrypt_decrypt_not_implemented_for_ml_kem_768");
} else {
cJSON_Delete(root);
return make_error_response(id_str, -32602,
"encrypt_decrypt_not_supported_for_curve");
}
} else if (strcmp(method, VERB_SIGN_DATA) == 0 || } else if (strcmp(method, VERB_SIGN_DATA) == 0 ||
strcmp(method, VERB_SSH_SIGN) == 0) { strcmp(method, VERB_SSH_SIGN) == 0) {
/* sign_data / ssh_sign: params = [message_bytes_hex, options] /* sign_data / ssh_sign: params = [message_bytes_hex, options]

View File

@@ -232,6 +232,8 @@ const char *selector_strerror(int err);
#define VERB_SSH_SIGN "ssh_sign" #define VERB_SSH_SIGN "ssh_sign"
#define VERB_KEM_ENCAPS "kem_encapsulate" #define VERB_KEM_ENCAPS "kem_encapsulate"
#define VERB_KEM_DECAPS "kem_decapsulate" #define VERB_KEM_DECAPS "kem_decapsulate"
#define VERB_ENCRYPT "encrypt"
#define VERB_DECRYPT "decrypt"
/* /*
* Check whether `verb` is allowed to execute against `role`. * Check whether `verb` is allowed to execute against `role`.
@@ -823,6 +825,21 @@ int enforce_verb_role(const char *verb, const role_entry_t *role) {
return enforce_kem_role(role); 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;
}
return ENFORCE_ERR_UNKNOWN_VERB; return ENFORCE_ERR_UNKNOWN_VERB;
} }

View File

@@ -759,8 +759,8 @@ int socket_name_random(char *out, size_t out_len);
/* Version information (auto-updated by build/version tooling) */ /* Version information (auto-updated by build/version tooling) */
#define NSIGNER_VERSION_MAJOR 0 #define NSIGNER_VERSION_MAJOR 0
#define NSIGNER_VERSION_MINOR 0 #define NSIGNER_VERSION_MINOR 0
#define NSIGNER_VERSION_PATCH 48 #define NSIGNER_VERSION_PATCH 49
#define NSIGNER_VERSION "v0.0.48" #define NSIGNER_VERSION "v0.0.49"
/* NSIGNER_HEADERLESS_DECLS_END */ /* NSIGNER_HEADERLESS_DECLS_END */