Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56f37e092d | ||
|
|
a017dc40e0 |
93
README.md
93
README.md
@@ -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
|
||||
|
||||
@@ -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 50
|
||||
#define NSIGNER_VERSION "v0.0.50"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
Reference in New Issue
Block a user