Compare commits

...

2 Commits

4 changed files with 151 additions and 58 deletions

View File

@@ -236,7 +236,7 @@ The `key_id` is a short display identifier (first 16 hex chars of the public key
In addition to the role-based API, n_signer supports an **algorithm-based API** where the caller specifies the algorithm and derivation index directly, without needing to know role names. This is the preferred API for new clients.
### 4c.1 New verbs
### 4c.1 Verbs
| Verb | Description | Algorithm parameter | Key parameter |
|---|---|---|---|
@@ -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)
@@ -769,48 +795,3 @@ Static build:
./build/nsigner_static_x86_64 --version
```
## 11. Implemented adjuncts and future work
### Implemented features
- **HTTP listener mode (`--listen http:HOST:PORT`).** Curl-friendly HTTP POST with JSON body. CORS headers included. See [§7.1](#71-linux-desktop-abstract-namespace-unix-socket) for curl examples.
- **OTP one-time pad encryption (`otp_encrypt` / `otp_decrypt` verbs).** Information-theoretically secure encryption using one-time pads from the [`otp`](../otp) project. Pad material on USB drive, auto-detected at startup. See [`plans/otp_nostr_integration.md`](plans/otp_nostr_integration.md).
- **Algorithm-based API (`sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`).** Caller specifies algorithm + index, no role needed. See [§4c](#4c-algorithm-based-api).
- **MCU / USB signer (Feather ESP32-S3 Reverse TFT).** Working PoC in [`firmware/feather_s3_tft`](firmware/feather_s3_tft). Single TinyUSB composite USB device exposes both CDC-ACM and WebUSB Vendor interfaces. Same dispatcher serves both transports with auth envelope verification, on-device TFT prompts, and physical button approval. See [`firmware/README.md`](firmware/README.md) and [`plans/feather_tinyusb_composite.md`](plans/feather_tinyusb_composite.md).
### Future work (deferred)
These items are designed and worth doing, but are not in the current implementation scope. They are listed here so they are not lost.
- **HTTPS / WSS (TLS) listener modes.** `--listen https:HOST:PORT` and `--listen wss:HOST:PORT` with `--tls-cert` / `--tls-key`. See [`plans/http_wss_listener.md`](plans/http_wss_listener.md).
- **WebSocket listener mode (`--listen ws:HOST:PORT`).** For browser access with JSON text frames. See [`plans/http_wss_listener.md`](plans/http_wss_listener.md).
- **NIP-46 bunker / relay transport.** Tracked in [`plans/nip46_bunker_mode.md`](plans/nip46_bunker_mode.md).
- **Browser extension.** Tracked in [`plans/nsigner_browser_extension.md`](plans/nsigner_browser_extension.md). NIP-07 surface forwarding to a running `nsigner` instance.
## 12. Document map
- [`README.md`](README.md): authoritative behavior specification for the foreground single-program model
- [`documents/CLIENT_IMPLEMENTATION.md`](documents/CLIENT_IMPLEMENTATION.md): client integration contract and framing behavior
- [`documents/QUBES_OS.md`](documents/QUBES_OS.md): Qubes OS deployment/integration checklist for dedicated signer qubes
- [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md): Tier-1 FIPS deployment runbook using loopback TCP listener
- [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md): design for the qrexec → unix-socket bridge transport (persistent signer, no mnemonic on disk)
- [`packaging/qubes/`](packaging/qubes/): qrexec service script, dom0 policy, and setup scripts for Qubes deployment
- [`examples/n_signer_qube_example_qrexec.js`](examples/n_signer_qube_example_qrexec.js): JavaScript caller example using qrexec (no network, no auth envelope)
- [`examples/n_signer_qube_example_fips.js`](examples/n_signer_qube_example_fips.js): JavaScript caller example using FIPS/TCP with auth envelope
- [`examples/get_pubkey_tcp.c`](examples/get_pubkey_tcp.c): C caller example using TCP transport with auth envelope
- [`plans/nsigner.md`](plans/nsigner.md): implementation plan and sequencing
- [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md): seed phrase domain/use catalog and caveats
- [`plans/post_quantum_crypto.md`](plans/post_quantum_crypto.md): post-quantum and multi-algorithm crypto expansion plan (all 8 phases)
- [`firmware/feather_s3_tft`](firmware/feather_s3_tft): Feather ESP32-S3 Reverse TFT firmware (TinyUSB composite CDC + WebUSB signer PoC)
- [`plans/feather_tinyusb_composite.md`](plans/feather_tinyusb_composite.md): firmware Phase 7b plan and outcome (TinyUSB composite USB transport)
- [`plans/nsigner_browser_extension.md`](plans/nsigner_browser_extension.md): browser extension exposing NIP-07 over `nsigner`
- [`plans/nip46_bunker_mode.md`](plans/nip46_bunker_mode.md): deferred NIP-46 relay-mode signer transport
- [`plans/otp_nostr_integration.md`](plans/otp_nostr_integration.md): OTP one-time pad integration plan and design
- [`plans/http_wss_listener.md`](plans/http_wss_listener.md): HTTP/WS/WSS listener modes plan
- [`libotppad/`](libotppad/libotppad.h): shared OTP pad format library (XOR, ASCII armor, binary .otp, Padmé padding, state files, checksum)
- [`src/otp_pad.c`](src/otp_pad.c) / [`src/otp_pad.h`](src/otp_pad.h): OTP pad state management for n_signer
- [`src/http_listener.c`](src/http_listener.c) / [`src/http_listener.h`](src/http_listener.h): minimal HTTP/1.1 parser for HTTP listener mode
- [`tools/make_test_pad.c`](tools/make_test_pad.c): generate a test OTP pad on a USB drive
- [`tools/otp_roundtrip_test.py`](tools/otp_roundtrip_test.py): end-to-end OTP encrypt/decrypt round-trip test
- [`examples/otp_nostr_30078.py`](examples/otp_nostr_30078.py): example — encrypt with OTP, wrap in Nostr kind 30078 event, sign
- [`firmware/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration

View File

@@ -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]

View File

@@ -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;
}

View File

@@ -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 50
#define NSIGNER_VERSION "v0.0.50"
/* NSIGNER_HEADERLESS_DECLS_END */