v0.0.57 - Migrated to unified nostr_ prefixed verb names; removed legacy verb aliases (sign_data, ssh_sign, verify_signature, kem_encapsulate, kem_decapsulate, otp_encrypt, otp_decrypt); split get_public_key into algorithm-based get_public_key and role-based nostr_get_public_key; OTP now selected via algorithm:otp instead of curve:otp; consolidated API docs from api.md into README.md

This commit is contained in:
Laan Tungir
2026-07-20 17:29:45 -04:00
parent 96ab9741ef
commit b3421c3e40
51 changed files with 1105 additions and 2282 deletions

View File

@@ -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,46 @@ 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 |
| `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

View File

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

View File

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

View File

@@ -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.")