6.4 KiB
n_signer C Client — migrated to nostr_core_lib
The hand-rolled nsigner_client.{c,h} that previously lived in this directory
has been removed. n_signer now uses the shared, transport-pluggable client
stack that lives in nostr_core_lib:
nostr_core/nsigner_transport.{h,c}— pluggable transport vtable (unix-abstract / tcp / usb-cdc serial / fds) + discovery.nostr_core/nsigner_client.{h,c}— heap-allocated client: 4-byte length-prefixed framing, kind-27235 auth envelope (self-contained), JSON-RPC verbs, cJSON result parsing, RPC error mapping.nostr_core/nostr_signer.{h,c}— high-levelnostr_signer_tabstraction (local + remote backends, 6 verbs).
This is the single source of truth for the n_signer wire contract. See
nostr_core/NSIGNER_INTEGRATION.md
for the full integration contract.
What moved where
Old (client/) |
New (nostr_core_lib) |
|---|---|
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_set_auth |
nsigner_client_set_auth or nostr_signer_nsigner_set_auth |
nsigner_client_request / request_raw |
nsigner_client_call (returns parsed cJSON result) |
Consumers (updated)
examples/get_public_key_client.c— usesnsigner_transport_open_unix+nsigner_client_new+nsigner_client_call.examples/sign_event_client.c— same pattern.tests/test_integration.c— same pattern; cJSON-based assertions.
The nsigner ... client '<json>' subcommand in src/main.c is
unaffected — it has its own raw framing pass-through and never used this directory.
Multi-Algorithm and Post-Quantum Verbs
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:
| Verb | Algorithms | Description |
|---|---|---|
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 |
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):
resultis a plain hex string (cJSON_IsString(result)is true, 64 hex chars). - secp256k1 with
{"format":"structured"}option:resultis a JSON string containing{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}. - All other algorithms:
resultis always a JSON string containing{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16 hex>"}.
Clients should parse the result string with cJSON_Parse to extract the
algorithm, public_key, and key_id fields for non-secp256k1 algorithms.
Key sizes
| Algorithm | Pub key | Priv key | Signature | Ciphertext | Shared secret |
|---|---|---|---|---|---|
| secp256k1 | 32 B | 32 B | 64 B | — | — |
| ed25519 | 32 B | 32 B | 64 B | — | — |
| x25519 | 32 B | 32 B | — | — | 32 B |
| ML-DSA-65 | 1952 B | 4032 B | 3309 B | — | — |
| SLH-DSA-128s | 32 B | 64 B | 7856 B | — | — |
| ML-KEM-768 | 1184 B | 2400 B | — | 1088 B | 32 B |
Example clients
examples/pq_sign_example.c— ML-DSA-65 signexamples/pq_kem_example.c— ML-KEM-768 encaps/decapsexamples/ssh_sign_example.c— ed25519 SSH sign
See documents/CLIENT_IMPLEMENTATION.md
section 11 for the full multi-algorithm specification, derivation paths, and
example request/response transcripts.
Why
Per plans/nsigner_integration_plan.md
(Phase 7): retire per-project hand-rolled clients in favor of the shared module
in nostr_core_lib, so the wire contract has one implementation and downstream
projects get unix/tcp/serial/fds transports for free.