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:
46
README.md
46
README.md
@@ -357,7 +357,18 @@ Implemented signer verbs in this build:
|
||||
| `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. |
|
||||
|
||||
**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 |
|
||||
|---|---|
|
||||
@@ -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"}]}'
|
||||
```
|
||||
|
||||
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
|
||||
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"}]}'
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
```bash
|
||||
@@ -557,8 +580,11 @@ nsigner --socket-name nsigner client '{"id":"1","method":"get_public_key","param
|
||||
# Sign event
|
||||
nsigner --socket-name nsigner client '{"id":"1","method":"sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"}]}'
|
||||
|
||||
# OTP encrypt
|
||||
nsigner --socket-name nsigner client '{"id":"1","method":"otp_encrypt","params":["SGVsbG8=",{"encoding":"ascii"}]}'
|
||||
# Encrypt (OTP)
|
||||
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)
|
||||
|
||||
@@ -236,6 +236,8 @@ const char *selector_strerror(int err);
|
||||
#define VERB_KEM_DECAPS "kem_decapsulate"
|
||||
#define VERB_OTP_ENCRYPT "otp_encrypt"
|
||||
#define VERB_OTP_DECRYPT "otp_decrypt"
|
||||
#define VERB_ENCRYPT "encrypt"
|
||||
#define VERB_DECRYPT "decrypt"
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* 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)) {
|
||||
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role");
|
||||
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");
|
||||
}
|
||||
result = owned_result;
|
||||
} else if (strcmp(method, VERB_ENCRYPT) == 0 ||
|
||||
strcmp(method, VERB_DECRYPT) == 0) {
|
||||
/* General encrypt/decrypt verbs (role-based path for non-OTP curves).
|
||||
* The curve is determined by the role's curve. For secp256k1, the
|
||||
* nip_version option (4 or 44, default 44) selects NIP-04 or NIP-44.
|
||||
* For x25519 and ml-kem-768, the key agreement is available but
|
||||
* direct encrypt/decrypt is not yet implemented. */
|
||||
const char *curve_str = role->curve_str;
|
||||
int nip_version = 44; /* default to NIP-44 */
|
||||
|
||||
if (cJSON_IsObject(options_item)) {
|
||||
cJSON *nv = cJSON_GetObjectItemCaseSensitive(options_item, "nip_version");
|
||||
if (cJSON_IsNumber(nv)) {
|
||||
nip_version = nv->valueint;
|
||||
}
|
||||
}
|
||||
|
||||
if (role->curve == CURVE_SECP256K1) {
|
||||
/* secp256k1: route to NIP-04 or NIP-44 */
|
||||
cJSON *peer_item = cJSON_GetArrayItem(params_item, 0);
|
||||
cJSON *msg_item = cJSON_GetArrayItem(params_item, 1);
|
||||
if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL ||
|
||||
!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) {
|
||||
cJSON_Delete(root);
|
||||
return make_error_response(id_str, -32602, "invalid_params");
|
||||
}
|
||||
if (strcmp(method, VERB_ENCRYPT) == 0) {
|
||||
if (nip_version == 4) {
|
||||
owned_result = crypto_nip04_encrypt(ctx->key_store, role_index,
|
||||
peer_item->valuestring,
|
||||
msg_item->valuestring);
|
||||
} else {
|
||||
owned_result = crypto_nip44_encrypt(ctx->key_store, role_index,
|
||||
peer_item->valuestring,
|
||||
msg_item->valuestring);
|
||||
}
|
||||
} else { /* decrypt */
|
||||
if (nip_version == 4) {
|
||||
owned_result = crypto_nip04_decrypt(ctx->key_store, role_index,
|
||||
peer_item->valuestring,
|
||||
msg_item->valuestring);
|
||||
} else {
|
||||
owned_result = crypto_nip44_decrypt(ctx->key_store, role_index,
|
||||
peer_item->valuestring,
|
||||
msg_item->valuestring);
|
||||
}
|
||||
}
|
||||
if (owned_result == NULL) {
|
||||
cJSON_Delete(root);
|
||||
return make_error_response(id_str, -32602, "invalid_params");
|
||||
}
|
||||
result = owned_result;
|
||||
} else if (role->curve == CURVE_X25519) {
|
||||
/* x25519: ECDH + symmetric encryption — not yet implemented.
|
||||
* The derive_shared_secret verb is available for key agreement;
|
||||
* the caller can use the shared secret with their own symmetric
|
||||
* cipher. Direct encrypt/decrypt will be added in a future release. */
|
||||
cJSON_Delete(root);
|
||||
return make_error_response(id_str, -32601,
|
||||
"encrypt_decrypt_not_implemented_for_x25519");
|
||||
} else if (role->curve == CURVE_ML_KEM_768) {
|
||||
/* ml-kem-768: KEM-based hybrid encryption — not yet implemented.
|
||||
* The encapsulate/decapsulate verbs are available for key agreement;
|
||||
* direct encrypt/decrypt will be added in a future release. */
|
||||
cJSON_Delete(root);
|
||||
return make_error_response(id_str, -32601,
|
||||
"encrypt_decrypt_not_implemented_for_ml_kem_768");
|
||||
} else {
|
||||
cJSON_Delete(root);
|
||||
return make_error_response(id_str, -32602,
|
||||
"encrypt_decrypt_not_supported_for_curve");
|
||||
}
|
||||
} else if (strcmp(method, VERB_SIGN_DATA) == 0 ||
|
||||
strcmp(method, VERB_SSH_SIGN) == 0) {
|
||||
/* sign_data / ssh_sign: params = [message_bytes_hex, options]
|
||||
|
||||
@@ -232,6 +232,8 @@ const char *selector_strerror(int err);
|
||||
#define VERB_SSH_SIGN "ssh_sign"
|
||||
#define VERB_KEM_ENCAPS "kem_encapsulate"
|
||||
#define VERB_KEM_DECAPS "kem_decapsulate"
|
||||
#define VERB_ENCRYPT "encrypt"
|
||||
#define VERB_DECRYPT "decrypt"
|
||||
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -759,8 +759,8 @@ int socket_name_random(char *out, size_t out_len);
|
||||
/* Version information (auto-updated by build/version tooling) */
|
||||
#define NSIGNER_VERSION_MAJOR 0
|
||||
#define NSIGNER_VERSION_MINOR 0
|
||||
#define NSIGNER_VERSION_PATCH 48
|
||||
#define NSIGNER_VERSION "v0.0.48"
|
||||
#define NSIGNER_VERSION_PATCH 49
|
||||
#define NSIGNER_VERSION "v0.0.49"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
Reference in New Issue
Block a user