118 lines
6.4 KiB
Markdown
118 lines
6.4 KiB
Markdown
# 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`](../resources/nostr_core_lib):
|
|
|
|
- [`nostr_core/nsigner_transport.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_transport.h) —
|
|
pluggable transport vtable (unix-abstract / tcp / usb-cdc serial / fds) + discovery.
|
|
- [`nostr_core/nsigner_client.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) —
|
|
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}`](../resources/nostr_core_lib/nostr_core/nostr_signer.h) —
|
|
high-level `nostr_signer_t` abstraction (local + remote backends, 6 verbs).
|
|
|
|
This is the single source of truth for the n_signer wire contract. See
|
|
[`nostr_core/NSIGNER_INTEGRATION.md`](../resources/nostr_core_lib/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`](../examples/get_public_key_client.c) — uses `nsigner_transport_open_unix` + `nsigner_client_new` + `nsigner_client_call`.
|
|
- [`examples/sign_event_client.c`](../examples/sign_event_client.c) — same pattern.
|
|
- [`tests/test_integration.c`](../tests/test_integration.c) — same pattern; cJSON-based assertions.
|
|
|
|
The `nsigner ... client '<json>'` subcommand in [`src/main.c`](../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):** `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>"}`.
|
|
|
|
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`](../examples/pq_sign_example.c) — ML-DSA-65 sign
|
|
- [`examples/pq_kem_example.c`](../examples/pq_kem_example.c) — ML-KEM-768 encaps/decaps
|
|
- [`examples/ssh_sign_example.c`](../examples/ssh_sign_example.c) — ed25519 SSH sign
|
|
|
|
See [`documents/CLIENT_IMPLEMENTATION.md`](../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`](../resources/nostr_core_lib/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.
|