Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0a5987ffa | ||
|
|
0b0ec5eb1a | ||
|
|
0355744103 |
114
README.md
114
README.md
@@ -92,7 +92,7 @@ When started, `n_signer` immediately enters terminal input mode:
|
||||
5. **OTP pad selection** (optional): auto-scans attached USB drives for OTP pads and offers to bind one. See [`plans/otp_nostr_integration.md`](plans/otp_nostr_integration.md).
|
||||
6. Pick the abstract socket name (random BIP-39 pair, or `--socket-name` / `--name` / `-n` override).
|
||||
7. Initialize transport endpoints and bind the socket.
|
||||
8. Switch to running status display, with the signer name, socket address, and OTP pad status shown in the banner.
|
||||
8. Switch to running status display, with the signer name and socket address shown in the Connections section and status line (see [§3.2](#32-running-phase-status-display--signer)).
|
||||
|
||||
No startup files are read or written. The mnemonic — typed or generated — lives only in `mlock`'d memory and is zeroized on shutdown or crash.
|
||||
|
||||
@@ -105,46 +105,91 @@ These modes avoid putting mnemonic material in argv/environment and are designed
|
||||
|
||||
### 3.2 Running phase (status display + signer)
|
||||
|
||||
After unlock, terminal becomes a live status and control console. Example layout:
|
||||
After unlock, the terminal becomes a live status and control console rendered by [`render_status()`](src/main.c). The top frame shows the program name and version; below it are the Roles and Activity sections, followed by a single status line. Connection instructions are not shown by default — press `d` to display them on demand (see below). Example layout:
|
||||
|
||||
```text
|
||||
n_signer v0.x | foreground session active
|
||||
n_signer v0.0.53 > Main Menu
|
||||
|
||||
Connections
|
||||
Server: unix @nsigner (bridge-source-trusted)
|
||||
Client: nsigner --socket-name nsigner client '<json>'
|
||||
Qrexec: qrexec-client-vm <target_qube> qubes.NsignerRpc
|
||||
Server: HTTP 127.0.0.1:11111
|
||||
Client: curl -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' -d '<json>'
|
||||
OTP pad: 333e9902db839d9d... (offset 288 / 1048576 bytes)
|
||||
Press d for connection instructions
|
||||
|
||||
session: unlocked (RAM-only)
|
||||
Roles:
|
||||
Role Purpose Curve Derivation path
|
||||
main nostr secp256k1 m/44'/1237'/0'/0/0
|
||||
nostr_idx_1 nostr secp256k1 m/44'/1237'/1'/0/0
|
||||
backup bitcoin secp256k1 m/84'/0'/0'/0/5
|
||||
|
||||
Roles
|
||||
-----
|
||||
main purpose=nostr curve=secp256k1 selector=role:main
|
||||
ops purpose=nostr curve=secp256k1 selector=nostr_index:7
|
||||
backup purpose=bitcoin curve=secp256k1 selector=role_path:m/84'/0'/0'/0/5
|
||||
|
||||
Pending approvals
|
||||
-----------------
|
||||
(none)
|
||||
|
||||
Activity (latest first)
|
||||
-----------------------
|
||||
Activity (latest first):
|
||||
16:03:11 allow caller=uid:1000 method=get_public_key role=main
|
||||
16:02:44 prompt caller=uid:1000 method=sign_event role=ops
|
||||
16:02:46 allow caller=uid:1000 method=sign_event role=ops
|
||||
15:59:10 deny caller=uid:1001 method=sign_event error=unauthorized
|
||||
|
||||
Hotkeys
|
||||
-------
|
||||
a toggle auto-approve(prompt) for this session
|
||||
r refresh
|
||||
l lock/reunlock session
|
||||
q quit
|
||||
session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
|
||||
|
||||
l lock/reunlock
|
||||
r refresh
|
||||
a toggle auto-approve
|
||||
d display connections
|
||||
q/x quit
|
||||
```
|
||||
|
||||
The **Derivation path** column shows the full BIP-44 path for each role's key. For `nostr_index` roles this is `m/44'/1237'/<n>'/0/0` (NIP-06); for `role_path` roles it's the explicit path.
|
||||
|
||||
The signer's name (`nsigner_hairy_dog` in this example) appears in the **status line** at the bottom (`signer=nsigner_hairy_dog`). See [§7.1](#71-linux-desktop-abstract-namespace-unix-socket) for how the name is generated.
|
||||
|
||||
### Connection instructions (press `d`)
|
||||
|
||||
Pressing `d` clears the screen and shows each active transport as a titled block with the connection string and an example client command. Press any key to return to the status display.
|
||||
|
||||
```text
|
||||
n_signer v0.0.53 > Connection Instructions
|
||||
|
||||
|
||||
Unix socket:
|
||||
|
||||
@nsigner_hairy_dog
|
||||
|
||||
Example:
|
||||
nsigner --socket-name nsigner_hairy_dog client '<json>'
|
||||
|
||||
Qrexec (bridge-source-trusted):
|
||||
qrexec-client-vm <target_qube> qubes.NsignerRpc
|
||||
|
||||
|
||||
FIPS:
|
||||
|
||||
http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111
|
||||
|
||||
Example:
|
||||
curl -X POST http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111/
|
||||
-H 'Content-Type: application/json'
|
||||
-d '<json>'
|
||||
|
||||
|
||||
HTTP:
|
||||
|
||||
http://127.0.0.1:11112
|
||||
|
||||
Example:
|
||||
curl -X POST http://127.0.0.1:11112/
|
||||
-H 'Content-Type: application/json'
|
||||
-d '<json>'
|
||||
|
||||
OTP pad: 333e9902db839d9d... (offset 288 / 1048576 bytes)
|
||||
|
||||
session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
|
||||
|
||||
Press any key to return
|
||||
```
|
||||
|
||||
Hotkeys (active while the status display is shown):
|
||||
|
||||
- `a` — toggle auto-approve (prompt) for this session
|
||||
- `r` — refresh the display
|
||||
- `d` — display connection instructions (press any key to return)
|
||||
- `l` — lock / re-unlock the session
|
||||
- `q` — quit
|
||||
|
||||
### 3.3 Approval prompts
|
||||
|
||||
When a request needs confirmation, `n_signer` interrupts the status view with a prompt and waits for a local keystroke.
|
||||
@@ -334,6 +379,8 @@ nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsula
|
||||
|
||||
## 5. Wire contract (JSON-RPC)
|
||||
|
||||
> **The full, authoritative API reference now lives in [`api.md`](api.md).** The summary below is kept for orientation; for request/response shapes, error codes, and worked examples, consult [`api.md`](api.md).
|
||||
|
||||
Request shape is JSON-RPC with NIP-46-style methods and optional trailing selector options.
|
||||
|
||||
```jsonc
|
||||
@@ -522,7 +569,7 @@ Properties:
|
||||
|
||||
Naming rules:
|
||||
|
||||
- Default: random pick at startup, displayed in the TUI banner.
|
||||
- Default: random pick at startup, displayed in the Connections section and status line of the running display.
|
||||
- Override: `--socket-name <name>` (alias: `--name <name>` / `-n <name>`) forces a specific name (useful for scripts and tests).
|
||||
- Collision: if the chosen random name is already bound by another process, `nsigner` retries with a fresh pair up to 8 times before erroring out with a hint to use an explicit name override.
|
||||
|
||||
@@ -678,7 +725,7 @@ Setup scripts and policy are in [`packaging/qubes/`](packaging/qubes/). See also
|
||||
nsigner
|
||||
```
|
||||
|
||||
Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the TUI banner shows the randomly assigned signer name and its abstract socket address.
|
||||
Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the running status display shows the randomly assigned signer name and its abstract socket address in the Connections section and the bottom status line (see [§3.2](#32-running-phase-status-display--signer)).
|
||||
|
||||
To force a specific socket name (e.g. for scripted clients):
|
||||
|
||||
@@ -764,10 +811,7 @@ Terminal A:
|
||||
```text
|
||||
$ nsigner
|
||||
[unlock] enter mnemonic:
|
||||
[ok] session unlocked
|
||||
signer name : hairy dog
|
||||
socket : @nsigner_hairy_dog
|
||||
[listen] unix-abstract:@nsigner_hairy_dog
|
||||
System is ready and waiting for connections on @nsigner_hairy_dog.
|
||||
[prompt] caller=uid:1000 method=sign_event role=main -> allow? (y/n)
|
||||
```
|
||||
|
||||
|
||||
382
api.md
Normal file
382
api.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# n_signer API
|
||||
|
||||
`n_signer` exposes a JSON-RPC 2.0-style request/response protocol. Every request is a single JSON object; every response is a single JSON object. This document is the complete, authoritative description of the API.
|
||||
|
||||
For the security model, transports, and operational behavior, see [`README.md`](README.md). For the migration plan from the legacy verb names, see [`plans/legacy_verb_aliases.md`](plans/legacy_verb_aliases.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. API format
|
||||
|
||||
### 1.1 Request
|
||||
|
||||
```json
|
||||
{ "id": "<string>", "method": "<verb>", "params": [ <arg0>, <arg1>, ..., { <options> } ] }
|
||||
```
|
||||
|
||||
- `id` — caller-supplied string echoed verbatim in the response. Used to match requests to responses.
|
||||
- `method` — the verb name (see [§2](#2-verbs)).
|
||||
- `params` — a JSON array. Positional arguments come first; the **last array element** is conventionally an options object. The options object is optional for most verbs.
|
||||
|
||||
### 1.2 Response (success)
|
||||
|
||||
```json
|
||||
{ "id": "<string>", "result": <value> }
|
||||
```
|
||||
|
||||
`result` is a JSON string. For structured verbs the string is itself a serialized JSON object — clients should `JSON.parse` it.
|
||||
|
||||
### 1.3 Response (error)
|
||||
|
||||
```json
|
||||
{ "id": "<string>", "error": { "code": <int>, "message": "<string>" } }
|
||||
```
|
||||
|
||||
### 1.4 Error codes
|
||||
|
||||
| Code | Message | Meaning |
|
||||
|-------|-------------------------------|--------------------------------------------------------------------|
|
||||
| -32700| `parse_error` | Request was not valid JSON. |
|
||||
| -32600| `invalid_request` | Missing `id`, `method`, or `params`, or `params` is not an array. |
|
||||
| -32601| `method_not_found` | Unknown verb, or verb not valid for the selected algorithm. |
|
||||
| -32602| `invalid_params` | Malformed arguments (bad hex, wrong length, missing field, etc.). |
|
||||
| 1001 | `ambiguous_role_selector` | More than one role selector was supplied. |
|
||||
| 1002 | `unknown_role` | No role matched the selector. |
|
||||
| 1003 | `no_default_role` | No selector given and no `main` role exists. |
|
||||
| 1004 | `purpose_mismatch` | Role's purpose is not valid for this verb. |
|
||||
| 1005 | `curve_mismatch` | Role's curve is not valid for this verb. |
|
||||
| 1006 | `mnemonic_not_loaded` | No mnemonic is loaded in the signer. |
|
||||
| 1007 | `no_termination_condition` | `nostr_mine_event` called without `difficulty` or `timeout_sec`. |
|
||||
| 1008 | `mining_failed` | Internal error during proof-of-work mining. |
|
||||
| 1009 | `not_yet_implemented` | Verb+algorithm combination is reserved but not yet implemented. |
|
||||
| 1010 | `algorithm_not_supported_for_verb` | The `algorithm` value is not valid for this verb. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Verbs
|
||||
|
||||
All verbs take their arguments as positional `params` and their options in a trailing options object. Most verbs select a key via the `algorithm` + `index` options (see [§3](#3-algorithms)). The `nostr_*` verbs select a secp256k1 NIP-06 key via `nostr_index` and implement Nostr-protocol-specific serialization on top of the raw crypto.
|
||||
|
||||
| Verb | Algorithms | Positional params | Options |
|
||||
|-------------------------|-----------------------------------------------|----------------------------------|----------------------------------|
|
||||
| `get_public_key` | all key-deriving algorithms | — | `algorithm`, `index` |
|
||||
| `sign` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>` | `algorithm`, `index`, `scheme`* |
|
||||
| `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s | `<message_hex>`, `<signature_hex>` | `algorithm`, `index`, `scheme`* |
|
||||
| `encapsulate` | ml-kem-768 | `<peer_pubkey_hex>` | `algorithm` |
|
||||
| `decapsulate` | ml-kem-768 | `<ciphertext_hex>` | `algorithm`, `index` |
|
||||
| `derive_shared_secret` | x25519 | `<peer_pubkey_hex>` | `algorithm`, `index` |
|
||||
| `encrypt` | otp | `<plaintext_base64>` | `algorithm`, `encoding` |
|
||||
| `decrypt` | otp | `<ciphertext>` | `algorithm`, `encoding` |
|
||||
| `nostr_get_public_key` | secp256k1 (NIP-06) | — | `nostr_index`, `format` |
|
||||
| `nostr_sign_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index` |
|
||||
| `nostr_mine_event` | secp256k1 (NIP-06) | `<event_json>` | `nostr_index`, `difficulty`, `timeout_sec`, `threads` |
|
||||
| `nostr_nip04_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
|
||||
| `nostr_nip04_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
|
||||
| `nostr_nip44_encrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<plaintext>` | `nostr_index` |
|
||||
| `nostr_nip44_decrypt` | secp256k1 (NIP-06) | `<peer_pubkey_hex>`, `<ciphertext>` | `nostr_index` |
|
||||
|
||||
\* `scheme` is secp256k1-only: `"schnorr"` (default, BIP-340) or `"ecdsa"`.
|
||||
|
||||
### 2.1 Enforcement matrix
|
||||
|
||||
| Verb | Valid algorithms |
|
||||
|----------------------------|-----------------------------------------------|
|
||||
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `get_public_key` | all key-deriving algorithms |
|
||||
| `nostr_*` | secp256k1 (Nostr protocol) |
|
||||
|
||||
Any unlisted `(verb, algorithm)` pair is rejected with `algorithm_not_supported_for_verb` (1010).
|
||||
|
||||
---
|
||||
|
||||
## 3. Algorithms
|
||||
|
||||
All keys derive deterministically from the loaded BIP-39 mnemonic. The caller selects an algorithm by name and a derivation `index` (an integer `<n>` substituted into the algorithm's derivation path). OTP is the exception — it does not derive a key, it consumes a bound one-time pad (see [§3.3](#33-otp)).
|
||||
|
||||
### 3.1 Algorithm table
|
||||
|
||||
| Algorithm | Key type | FIPS standard | Derivation path | Key sizes (priv / pub, bytes) |
|
||||
|-----------------|-----------------|---------------|---------------------------------------|-------------------------------|
|
||||
| `secp256k1` | Signature | — | `m/44'/1237'/<n>'/0/0` (NIP-06) | 32 / 32 |
|
||||
| `ed25519` | Signature | — | `m/44'/102001'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
|
||||
| `x25519` | Key agreement | — | `m/44'/102002'/<n>'/0/0'` (SLIP-0010) | 32 / 32 |
|
||||
| `ml-dsa-65` | PQ signature | FIPS 204 | `m/44'/102003'/<n>'/0/0'` → DRBG | 4032 / 1952 |
|
||||
| `slh-dsa-128s` | PQ signature | FIPS 205 | `m/44'/102004'/<n>'/0/0'` → DRBG | 64 / 32 |
|
||||
| `ml-kem-768` | PQ KEM | FIPS 203 | `m/44'/102005'/<n>'/0/0'` → DRBG | 2400 / 1184 |
|
||||
| `otp` | One-time pad | — | (no key — bound USB pad) | n/a |
|
||||
|
||||
### 3.2 Key derivation
|
||||
|
||||
- **secp256k1** uses standard BIP-32/NIP-06 derivation. The 32-byte path output is the private key scalar.
|
||||
- **ed25519 / x25519** use SLIP-0010 HMAC-SHA512 derivation (all-hardened paths, as required by SLIP-0010 for ed25519). The 32-byte output is the private key.
|
||||
- **PQ algorithms** (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) use a two-stage approach: the mnemonic-derived 32-byte seed feeds a SHAKE-256 DRBG (NIST SP 800-90A style), which replaces PQClean's `randombytes()` callback during keygen. Same mnemonic, same index, same key pair every time.
|
||||
- **otp** does not derive a key. A pad is bound at signer startup (`--otp-pad-dir` + `--otp-pad`); the pad offset advances monotonically across requests.
|
||||
|
||||
### 3.3 OTP
|
||||
|
||||
The `otp` algorithm is a stream-style one-time pad, not a key-derivation scheme. It is selected like any other algorithm via `{"algorithm":"otp"}` and works with the `encrypt` / `decrypt` verbs. One pad per session; the pad offset advances monotonically across requests and is reported in every response.
|
||||
|
||||
---
|
||||
|
||||
## 4. Examples
|
||||
|
||||
### 4.1 `get_public_key`
|
||||
|
||||
```json
|
||||
{ "id": "1", "method": "get_public_key", "params": [ { "algorithm": "ml-dsa-65", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "1", "result": "{\"algorithm\":\"ml-dsa-65\",\"public_key\":\"<hex>\",\"key_id\":\"<16 hex>\"}" }
|
||||
```
|
||||
|
||||
`key_id` is the first 16 hex characters of the public key — a short display identifier.
|
||||
|
||||
### 4.2 `sign`
|
||||
|
||||
```json
|
||||
{ "id": "2", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "ed25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "2", "result": "{\"signature\":\"<hex>\",\"algorithm\":\"ed25519\",\"key_id\":\"<16 hex>\"}" }
|
||||
```
|
||||
|
||||
The first positional argument is the message as hex. For `secp256k1` the `scheme` option selects `"schnorr"` (default, BIP-340) or `"ecdsa"`:
|
||||
|
||||
```json
|
||||
{ "id": "3", "method": "sign", "params": [ "68656c6c6f", { "algorithm": "secp256k1", "index": 0, "scheme": "ecdsa" } ] }
|
||||
```
|
||||
|
||||
### 4.3 `verify`
|
||||
|
||||
```json
|
||||
{ "id": "4", "method": "verify", "params": [ "<message_hex>", "<signature_hex>", { "algorithm": "ed25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "4", "result": "{\"valid\":true,\"algorithm\":\"ed25519\"}" }
|
||||
```
|
||||
|
||||
The signer derives its own public key from `(algorithm, index)` and verifies against it. To verify an arbitrary third-party key, use a client-side library.
|
||||
|
||||
### 4.4 `encapsulate` (ML-KEM-768)
|
||||
|
||||
```json
|
||||
{ "id": "5", "method": "encapsulate", "params": [ "<peer_pubkey_hex>", { "algorithm": "ml-kem-768" } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "5", "result": "{\"ciphertext\":\"<hex>\",\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
|
||||
```
|
||||
|
||||
`peer_pubkey_hex` is the recipient's ML-KEM-768 public key (1184 bytes → 2368 hex chars). Send the returned `ciphertext` to the recipient; both sides end up with the same `shared_secret`.
|
||||
|
||||
### 4.5 `decapsulate` (ML-KEM-768)
|
||||
|
||||
```json
|
||||
{ "id": "6", "method": "decapsulate", "params": [ "<ciphertext_hex>", { "algorithm": "ml-kem-768", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "6", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"ml-kem-768\"}" }
|
||||
```
|
||||
|
||||
### 4.6 `derive_shared_secret` (X25519)
|
||||
|
||||
```json
|
||||
{ "id": "7", "method": "derive_shared_secret", "params": [ "<peer_pubkey_hex>", { "algorithm": "x25519", "index": 0 } ] }
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{ "id": "7", "result": "{\"shared_secret\":\"<hex>\",\"algorithm\":\"x25519\"}" }
|
||||
```
|
||||
|
||||
`peer_pubkey_hex` is the peer's 32-byte X25519 public key (64 hex chars). Feed the returned `shared_secret` into your own symmetric cipher (e.g. AES-GCM, ChaCha20-Poly1305).
|
||||
|
||||
### 4.7 `encrypt` / `decrypt` (OTP)
|
||||
|
||||
```json
|
||||
{ "id": "8", "method": "encrypt", "params": [ "<plaintext_base64>", { "algorithm": "otp", "encoding": "ascii" } ] }
|
||||
{ "id": "9", "method": "decrypt", "params": [ "<ciphertext>", { "algorithm": "otp", "encoding": "ascii" } ] }
|
||||
```
|
||||
|
||||
`encoding` is `"ascii"` (ASCII-armored, default) or `"binary"` (base64-encoded raw `.otp` blob). If omitted on `decrypt`, auto-detection by magic bytes is used.
|
||||
|
||||
`encrypt` response:
|
||||
```json
|
||||
{
|
||||
"id": "8",
|
||||
"result": "{\"ciphertext\":\"<ascii-armor-or-base64-blob>\",\"encoding\":\"ascii\",\"pad_chksum\":\"<64 hex>\",\"pad_offset_before\":288,\"pad_offset_after\":416}"
|
||||
}
|
||||
```
|
||||
|
||||
`decrypt` response:
|
||||
```json
|
||||
{ "id": "9", "result": "{\"plaintext\":\"<base64>\",\"pad_chksum\":\"<64 hex>\"}" }
|
||||
```
|
||||
|
||||
If no pad is bound at startup, the error is `-32601` `otp_pad_not_bound`.
|
||||
|
||||
### 4.8 `nostr_get_public_key`
|
||||
|
||||
```json
|
||||
{ "id": "10", "method": "nostr_get_public_key", "params": [ { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
Response (default): a plain 64-hex-char secp256k1 public key string.
|
||||
Response with `{"format":"structured"}` in options: `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16 hex>"}`.
|
||||
|
||||
### 4.9 `nostr_sign_event`
|
||||
|
||||
Serializes the event to canonical form (`[0, pubkey, created_at, kind, tags, content]`), SHA-256 hashes it to produce the event `id`, signs the hash with BIP-340 Schnorr, and returns the complete signed event.
|
||||
|
||||
```json
|
||||
{ "id": "11", "method": "nostr_sign_event", "params": [ "<event_json>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`<event_json>` is the unsigned event object:
|
||||
```json
|
||||
{ "pubkey": "...", "created_at": 1234567890, "kind": 1, "tags": [], "content": "hello" }
|
||||
```
|
||||
|
||||
Response: the signed event JSON string, with `id` and `sig` populated.
|
||||
|
||||
### 4.10 `nostr_mine_event`
|
||||
|
||||
Mines NIP-13 proof-of-work (adds a `nonce` tag) and signs the event in one step. Mining runs in a detached thread so the signer stays responsive.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"method": "nostr_mine_event",
|
||||
"params": [ "<event_json>", { "difficulty": 20, "threads": 4, "timeout_sec": 30, "nostr_index": 0 } ]
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Required | Default | Meaning |
|
||||
|---------------|-------------------------------|---------|---------------------------------------------------------------|
|
||||
| `difficulty` | one of `difficulty`/`timeout` | 0 | Target leading zero bits. Stops early if reached. |
|
||||
| `timeout_sec` | one of `difficulty`/`timeout` | 600 | Time budget in seconds. Always returns the best event found. |
|
||||
| `threads` | no | 1 | Mining threads (clamped to 1..32). |
|
||||
|
||||
At least one of `difficulty` or `timeout_sec` must be specified. If both are given, mining stops when **either** condition is met. Timeout is never an error — the best event found is always returned.
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"result": "{\"event\":\"<signed event JSON with nonce tag>\",\"achieved_difficulty\":18,\"target_difficulty\":20,\"target_reached\":false,\"elapsed_sec\":30,\"attempts\":4523456}"
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `1007` `no_termination_condition` — neither `difficulty` nor `timeout_sec` given.
|
||||
- `1008` `mining_failed` — internal mining error.
|
||||
|
||||
### 4.11 `nostr_nip04_encrypt` / `nostr_nip04_decrypt`
|
||||
|
||||
NIP-04 encryption (deprecated in Nostr but still widely used): ECDH + AES-256-CBC, base64 payload.
|
||||
|
||||
```json
|
||||
{ "id": "13", "method": "nostr_nip04_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
|
||||
{ "id": "14", "method": "nostr_nip04_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`encrypt` returns the NIP-04 ciphertext string; `decrypt` returns the plaintext string.
|
||||
|
||||
### 4.12 `nostr_nip44_encrypt` / `nostr_nip44_decrypt`
|
||||
|
||||
NIP-44 encryption (current Nostr standard): ECDH + HKDF + ChaCha20-Poly1305 + specific payload format.
|
||||
|
||||
```json
|
||||
{ "id": "15", "method": "nostr_nip44_encrypt", "params": [ "<peer_pubkey_hex>", "<plaintext>", { "nostr_index": 0 } ] }
|
||||
{ "id": "16", "method": "nostr_nip44_decrypt", "params": [ "<peer_pubkey_hex>", "<ciphertext>", { "nostr_index": 0 } ] }
|
||||
```
|
||||
|
||||
`encrypt` returns the NIP-44 ciphertext string; `decrypt` returns the plaintext string.
|
||||
|
||||
---
|
||||
|
||||
## 5. Transports
|
||||
|
||||
The API is transport-independent. The same JSON request works over every transport; only the framing differs.
|
||||
|
||||
| Transport | `--listen` flag | Framing | Caller identity |
|
||||
|-----------|--------------------------------|------------------------------------------|----------------------------|
|
||||
| Unix socket (abstract) | `unix` (default on desktop) | Length-prefixed framed JSON | `SO_PEERCRED` → `uid:<n>` |
|
||||
| stdio | `stdio` | One framed request/response over stdin/stdout | inherited uid |
|
||||
| qrexec | `qrexec` | Same as stdio; caller from `QREXEC_REMOTE_DOMAIN` | `qubes:<vm>` |
|
||||
| FIPS/TCP | `tcp:[host]:port` | Length-prefixed framed JSON | (transport-defined) |
|
||||
| HTTP | `http:host:port` | Standard HTTP POST, JSON body, no custom framing. CORS enabled. | (transport-defined) |
|
||||
|
||||
### 5.1 HTTP examples
|
||||
|
||||
Start the signer:
|
||||
```bash
|
||||
nsigner --listen http:127.0.0.1:11111 --allow-all
|
||||
```
|
||||
|
||||
Get a public key:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
|
||||
```
|
||||
|
||||
Sign a Nostr event:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
|
||||
```
|
||||
|
||||
OTP encrypt:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' \
|
||||
-d '{"id":"1","method":"encrypt","params":["SGVsbG8sIE9UUCB3b3JsZCE=",{"algorithm":"otp","encoding":"ascii"}]}'
|
||||
```
|
||||
|
||||
### 5.2 Unix socket examples (framed mode)
|
||||
|
||||
```bash
|
||||
# get_public_key
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"1","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}'
|
||||
|
||||
# Sign a Nostr event
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"2","method":"nostr_sign_event","params":[{"pubkey":"...","created_at":1234567890,"kind":1,"tags":[],"content":"hello"},{"nostr_index":0}]}'
|
||||
|
||||
# ed25519 sign
|
||||
nsigner --socket-name nsigner client \
|
||||
'{"id":"3","method":"sign","params":["68656c6c6f",{"algorithm":"ed25519","index":0}]}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Pre-approval
|
||||
|
||||
Pre-approval entries skip the interactive prompt for matching requests. They are configured at startup with `--preapprove`.
|
||||
|
||||
### 6.1 Algorithm-based
|
||||
|
||||
```bash
|
||||
nsigner --preapprove caller=uid:1000,algorithm=ed25519,index=0-4,verb=sign,verify
|
||||
nsigner --preapprove caller=uid:1000,algorithm=ml-kem-768,index=0,verb=decapsulate
|
||||
```
|
||||
|
||||
### 6.2 Nostr
|
||||
|
||||
```bash
|
||||
nsigner --preapprove caller=uid:1000,nostr_index=0,verb=nostr_sign_event,nostr_get_public_key
|
||||
```
|
||||
|
||||
A `*` wildcard matches any caller, role, or verb. Index ranges use `min-max` syntax. Unmatched requests fall through to the default policy (prompt for same-uid, deny for others).
|
||||
78
plans/legacy_verb_aliases.md
Normal file
78
plans/legacy_verb_aliases.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Plan: Legacy Verb Aliases (Migration Path)
|
||||
|
||||
## Context
|
||||
|
||||
`n_signer` is being moved to a clean, unified API (see [`api.md`](../api.md)). The current implementation in [`src/dispatcher.c`](../src/dispatcher.c) still uses the legacy unprefixed verb names. This document captures the old → new mapping so the implementation can be updated in one pass. Since this is a new project, there are no external clients to migrate — the legacy names are an internal cleanup item, not a long-term compatibility surface.
|
||||
|
||||
## Goal
|
||||
|
||||
Rename the verbs in [`src/dispatcher.c`](../src/dispatcher.c) to match [`api.md`](../api.md), remove the legacy aliases, and delete the alias-routing logic. No backward-compatibility shim is needed.
|
||||
|
||||
## Old → New Verb Mapping
|
||||
|
||||
### Nostr protocol verbs (add `nostr_` prefix)
|
||||
|
||||
| Legacy verb | New verb |
|
||||
|---------------------|---------------------------|
|
||||
| `sign_event` | `nostr_sign_event` |
|
||||
| `mine_event` | `nostr_mine_event` |
|
||||
| `nip04_encrypt` | `nostr_nip04_encrypt` |
|
||||
| `nip04_decrypt` | `nostr_nip04_decrypt` |
|
||||
| `nip44_encrypt` | `nostr_nip44_encrypt` |
|
||||
| `nip44_decrypt` | `nostr_nip44_decrypt` |
|
||||
|
||||
The role-based `get_public_key` (with `nostr_index`/`role`/`role_path` selector) becomes `nostr_get_public_key`. The algorithm-based `get_public_key` (with `algorithm` option) stays `get_public_key`.
|
||||
|
||||
### Algorithm-based verb aliases (collapse into canonical names)
|
||||
|
||||
| Legacy verb | Canonical verb | Default algorithm (when `algorithm` omitted) |
|
||||
|---------------------|--------------------|----------------------------------------------|
|
||||
| `sign_data` | `sign` | (from role) |
|
||||
| `ssh_sign` | `sign` | `ed25519` |
|
||||
| `verify_signature` | `verify` | (from role) |
|
||||
| `kem_encapsulate` | `encapsulate` | `ml-kem-768` |
|
||||
| `kem_decapsulate` | `decapsulate` | `ml-kem-768` |
|
||||
|
||||
After the rename, callers must always supply `algorithm` explicitly — the "default algorithm" fallbacks are removed. This makes the algorithm-based verbs uniform: every call specifies its algorithm.
|
||||
|
||||
### OTP verb aliases (collapse into `encrypt`/`decrypt`)
|
||||
|
||||
| Legacy verb | Canonical verb | Required option |
|
||||
|-----------------|----------------|--------------------------|
|
||||
| `otp_encrypt` | `encrypt` | `{"algorithm":"otp"}` |
|
||||
| `otp_decrypt` | `decrypt` | `{"algorithm":"otp"}` |
|
||||
|
||||
The general `encrypt`/`decrypt` with `curve:"otp"` routing is replaced by `algorithm:"otp"`.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **[`src/dispatcher.c`](../src/dispatcher.c)** — rename the `VERB_*` string constants:
|
||||
- `VERB_SIGN_EVENT` → `"nostr_sign_event"`
|
||||
- `VERB_MINE_EVENT` → `"nostr_mine_event"`
|
||||
- `VERB_NIP04_ENCRYPT` → `"nostr_nip04_encrypt"`
|
||||
- `VERB_NIP04_DECRYPT` → `"nostr_nip04_decrypt"`
|
||||
- `VERB_NIP44_ENCRYPT` → `"nostr_nip44_encrypt"`
|
||||
- `VERB_NIP44_DECRYPT` → `"nostr_nip44_decrypt"`
|
||||
- Add `VERB_NOSTR_GET_PUBLIC_KEY` = `"nostr_get_public_key"`; split the current `get_public_key` handler into two branches based on whether `algorithm` is present (algorithm-based) or `nostr_index`/`role`/`role_path` is present (Nostr).
|
||||
|
||||
2. **Remove alias routing** — delete [`is_algorithm_verb()`](../src/dispatcher.c:829), [`canonical_alg_verb()`](../src/dispatcher.c:846), and the alias-fallthrough logic in [`dispatcher_handle_request()`](../src/dispatcher.c:1559). The verbs `sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`, `otp_encrypt`, `otp_decrypt` are no longer recognized.
|
||||
|
||||
3. **Remove `curve:"otp"` routing** — the `encrypt`/`decrypt` handler no longer special-cases `curve:"otp"`; OTP is selected via `algorithm:"otp"` like every other algorithm.
|
||||
|
||||
4. **Update tests** — [`tests/test_dispatcher.c`](../tests/test_dispatcher.c), [`tests/test_integration.c`](../tests/test_integration.c), [`tests/test_algorithm_api.c`](../tests/test_algorithm_api.c), and any other test that uses the legacy verb names. Rename all call sites to the new verbs and add `algorithm` explicitly where it was previously defaulted.
|
||||
|
||||
5. **Update examples and clients**:
|
||||
- [`client/demo_c99.c`](../client/demo_c99.c)
|
||||
- [`client/demo_javascript.js`](../client/demo_javascript.js)
|
||||
- [`client/demo_python.py`](../client/demo_python.py)
|
||||
- [`examples/`](../examples/) — all example files using legacy verb names
|
||||
- [`README.md`](../README.md) — the §5 summary and any inline examples
|
||||
|
||||
6. **Update policy/preapprove** — [`src/policy.c`](../src/policy.c) and the `--preapprove` CLI parsing in [`src/main.c`](../src/main.c) should accept the new verb names. The role-based preapprove examples in [`api.md`](../api.md) §6 already use the `nostr_` prefix.
|
||||
|
||||
## Verification
|
||||
|
||||
- `make dev && ./build/nsigner --version` builds clean.
|
||||
- `make test` — all tests pass with the new verb names.
|
||||
- Manual smoke test over HTTP: each verb in [`api.md`](../api.md) §3 responds as documented.
|
||||
- `grep -rn "sign_event\|mine_event\|nip04_\|nip44_\|sign_data\|ssh_sign\|verify_signature\|kem_encapsulate\|kem_decapsulate\|otp_encrypt\|otp_decrypt" src/ tests/ client/ examples/` returns no matches (all legacy names gone).
|
||||
151
plans/tui_display_connections_on_demand.md
Normal file
151
plans/tui_display_connections_on_demand.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Plan: Move Connection Instructions to On-Demand Display
|
||||
|
||||
## Problem
|
||||
|
||||
The Connections section in the running TUI is verbose and confusing. It mixes server addresses with client command examples, and when multiple transports are active (Unix + TCP + HTTP) it takes up a large chunk of the status screen, pushing the Roles and Activity sections down. The connection instructions are only needed once when a user wants to know how to reach the signer — they don't need to be permanently visible.
|
||||
|
||||
## Solution
|
||||
|
||||
Remove the Connections section from the default `render_status()` display. Add a new hotkey **`d`** (display connection instructions) that prints the connection info on demand, then returns to the normal status view on the next refresh/keystroke.
|
||||
|
||||
## Design
|
||||
|
||||
### Default status display (after change)
|
||||
|
||||
The Connections section is removed. The display shows: Roles, Activity, status line, hotkey menu. A one-line hint at the top reminds the user that `d` shows connection instructions.
|
||||
|
||||
```text
|
||||
================================================================================
|
||||
n_signer v0.0.53
|
||||
================================================================================
|
||||
> Main Menu
|
||||
|
||||
Press d for connection instructions
|
||||
|
||||
Roles
|
||||
Role Purpose Curve Selector
|
||||
-------------------- ------------ ------------ ------------
|
||||
main nostr secp256k1 nostr_index
|
||||
ops nostr secp256k1 nostr_index
|
||||
backup bitcoin secp256k1 role_path
|
||||
|
||||
Activity (latest first)
|
||||
16:03:11 allow caller=uid:1000 method=get_public_key role=main
|
||||
16:02:44 prompt caller=uid:1000 method=sign_event role=ops
|
||||
16:02:46 allow caller=uid:1000 method=sign_event role=ops
|
||||
15:59:10 deny caller=uid:1001 method=sign_event error=unauthorized
|
||||
|
||||
session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
|
||||
|
||||
l lock/reunlock
|
||||
r refresh
|
||||
a toggle auto-approve
|
||||
d display connections
|
||||
q/x quit
|
||||
```
|
||||
|
||||
### On-demand connection display (press `d`)
|
||||
|
||||
When the user presses `d`, the screen clears and shows **only** the connection instructions, with clear spacing between transports. Each transport block shows the **connection string** (the address/endpoint to reach), a blank line, then an **Example:** with the client command indented beneath. No "Server:" / "Client:" labels — just the address and an example. A footer tells the user how to return.
|
||||
|
||||
```text
|
||||
================================================================================
|
||||
n_signer v0.0.53
|
||||
================================================================================
|
||||
> Connection Instructions
|
||||
|
||||
|
||||
Unix socket
|
||||
|
||||
@nsigner_hairy_dog
|
||||
|
||||
Example:
|
||||
nsigner --socket-name nsigner_hairy_dog client '<json>'
|
||||
|
||||
Qrexec (bridge-source-trusted):
|
||||
qrexec-client-vm <target_qube> qubes.NsignerRpc
|
||||
|
||||
|
||||
FIPS
|
||||
|
||||
http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111
|
||||
|
||||
Example:
|
||||
curl -X POST http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111/ \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '<json>'
|
||||
|
||||
|
||||
HTTP
|
||||
|
||||
http://127.0.0.1:11112
|
||||
|
||||
Example:
|
||||
curl -X POST http://127.0.0.1:11112/ \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '<json>'
|
||||
|
||||
|
||||
OTP pad: 333e9902db839d9d... (offset 288 / 1048576 bytes)
|
||||
|
||||
session=unlocked (12 words) signer=nsigner_hairy_dog derived=3 auto-approve=OFF
|
||||
|
||||
Press any key to return
|
||||
```
|
||||
|
||||
### Key differences from current display
|
||||
|
||||
1. **Spacing between transports** — each transport gets its own titled block with blank lines separating it from the next, instead of a flat list of `Server:` / `Client:` lines.
|
||||
2. **Connection string + example, no labels** — each block shows the bare connection string (e.g. `http://127.0.0.1:11112`), then an `Example:` with the client command indented beneath. No confusing "Server:" / "Client:" labels.
|
||||
3. **On-demand only** — the default running display no longer shows connections at all, just a one-line hint. The user presses `d` when they need the instructions, reads them, then presses any key to return.
|
||||
4. **OTP pad status** — shown at the bottom of the connection display (it's connection-related: which pad is bound and how much has been consumed).
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **[`src/main.c`](src/main.c) — `render_status()`** (line 1451):
|
||||
- Remove the Connections section (lines 1464-1471).
|
||||
- Add a one-line hint after the top frame: `tui_print("Press d for connection instructions");`
|
||||
- Add a blank line after the hint.
|
||||
|
||||
2. **[`src/main.c`](src/main.c) — new function `render_connections()`**:
|
||||
- Clears the screen and renders the top frame.
|
||||
- Iterates `g_connection_info` but formats it with the spaced layout shown above. Since `g_connection_info` already stores formatted strings like `"Server: unix @nsigner_hairy_dog"` and `" Client: nsigner --socket-name ... client '<json>'"`, either:
|
||||
- **Option A**: Reformat the stored strings into blocks by detecting `Server:` lines as transport boundaries and printing blank lines + indentation.
|
||||
- **Option B**: Store connection info in a structured form (transport type, server address, client command(s)) and render the block layout from the structured data.
|
||||
- Option B is cleaner but requires changing `connection_info_add()` and all its call sites. Option A is a smaller change. **Recommend Option A** for now — parse the existing flat list into blocks.
|
||||
- **FIPS section**: The connection string is the npub-based FIPS URL (`http://<npub>.fips:<port>`), which FIPS resolves to the signer's TCP endpoint. The example uses `curl` since FIPS makes the endpoint HTTP-reachable for clients with FIPS installed. The raw TCP bind address (`tcp:[::]:11111`) is not shown — it's an implementation detail; the npub URL is what clients use.
|
||||
- Print OTP pad status if bound.
|
||||
- Print the status line at the bottom.
|
||||
- Print "Press any key to return" footer.
|
||||
|
||||
3. **[`src/main.c`](src/main.c) — `g_main_menu_items`** (line 825):
|
||||
- Add `{"^_d^: display connections", 'd'}` to the menu array.
|
||||
|
||||
4. **[`src/main.c`](src/main.c) — event loop** (line ~3068, the stdin keystroke handler):
|
||||
- Add a case for `'d'`:
|
||||
- Call `render_connections()`.
|
||||
- Read one keystroke from stdin (blocking `read()`).
|
||||
- Call `render_status()` to return to the normal display.
|
||||
|
||||
5. **[`README.md`](README.md) §3.2**:
|
||||
- Update the example TUI layout to match the new default display (no Connections section, `d` in hotkeys).
|
||||
- Add a note that pressing `d` shows the connection instructions.
|
||||
|
||||
6. **[`api.md`](api.md) §5 Transports**:
|
||||
- No changes needed — the transport documentation is already separate from the TUI display.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`src/main.c`](src/main.c) | Remove Connections from `render_status()`, add `render_connections()`, add `d` hotkey + menu item + event loop case |
|
||||
| [`README.md`](README.md) | Update §3.2 example display to match new layout |
|
||||
| [`api.md`](api.md) | No changes |
|
||||
|
||||
## Verification
|
||||
|
||||
- `make dev` builds clean.
|
||||
- Start nsigner with all transports selected. Default display shows no Connections section, just the "Press d for connection instructions" hint.
|
||||
- Press `d` — connection instructions appear with clear spacing between transports.
|
||||
- Press any key — returns to normal status display.
|
||||
- All other hotkeys (`l`, `r`, `a`, `q`) still work.
|
||||
308
src/main.c
308
src/main.c
@@ -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 51
|
||||
#define NSIGNER_VERSION "v0.0.51"
|
||||
#define NSIGNER_VERSION_PATCH 54
|
||||
#define NSIGNER_VERSION "v0.0.54"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
@@ -797,12 +797,24 @@ int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
||||
#define NSIGNER_QREXEC_SERVICE_NAME "qubes.NsignerRpc"
|
||||
#define ACTIVITY_LOG_CAP 16
|
||||
#define CONNECTION_INFO_CAP 8
|
||||
#define CONNECTION_INFO_STR_MAX 256
|
||||
|
||||
typedef struct {
|
||||
char lines[ACTIVITY_LOG_CAP][256];
|
||||
int count;
|
||||
} activity_log_t;
|
||||
|
||||
/* Structured connection info for the on-demand connection display.
|
||||
* Each entry represents one transport block: a title (e.g. "Unix socket"),
|
||||
* a connection string (the address/endpoint), an optional example command,
|
||||
* and an optional extra line (e.g. qrexec bridge info under Unix). */
|
||||
typedef struct {
|
||||
char title[64];
|
||||
char connection_string[CONNECTION_INFO_STR_MAX];
|
||||
char example[CONNECTION_INFO_STR_MAX]; /* empty if none */
|
||||
char extra[CONNECTION_INFO_STR_MAX]; /* empty if none */
|
||||
} connection_info_entry_t;
|
||||
|
||||
static volatile sig_atomic_t g_running = 1;
|
||||
static activity_log_t g_activity_log;
|
||||
static int g_auto_approve = 0;
|
||||
@@ -819,13 +831,16 @@ typedef struct {
|
||||
} mnemonic_source_t;
|
||||
|
||||
static mnemonic_source_kind_t g_startup_mnemonic_source_kind = MNEMONIC_SOURCE_TUI;
|
||||
static char g_connection_info[CONNECTION_INFO_CAP][192];
|
||||
static connection_info_entry_t g_connection_info[CONNECTION_INFO_CAP];
|
||||
static int g_connection_info_count = 0;
|
||||
/* OTP pad status line (shown at bottom of connection display) */
|
||||
static char g_otp_pad_status[CONNECTION_INFO_STR_MAX];
|
||||
|
||||
static const TuiMenuItem g_main_menu_items[] = {
|
||||
{"^_l^: lock/reunlock", 'l'},
|
||||
{"^_r^: refresh", 'r'},
|
||||
{"^_a^: toggle auto-approve", 'a'},
|
||||
{"^_d^: display connections", 'd'},
|
||||
{"^_q^:/x quit", 'q'}
|
||||
};
|
||||
|
||||
@@ -845,28 +860,49 @@ static void render_status(const role_table_t *role_table,
|
||||
int derived_count,
|
||||
const char *socket_name);
|
||||
|
||||
static void render_connections(const role_table_t *role_table,
|
||||
const mnemonic_state_t *mnemonic,
|
||||
int derived_count,
|
||||
const char *socket_name);
|
||||
|
||||
static void connection_info_clear(void) {
|
||||
g_connection_info_count = 0;
|
||||
memset(g_connection_info, 0, sizeof(g_connection_info));
|
||||
g_otp_pad_status[0] = '\0';
|
||||
}
|
||||
|
||||
static void connection_info_add(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
/* Add a transport block to the connection info.
|
||||
* title: transport name (e.g. "Unix socket", "FIPS", "HTTP")
|
||||
* connection_str: the address/endpoint clients use to reach the signer
|
||||
* example: example client command (empty string "" if none)
|
||||
* extra: optional extra line under the block (e.g. qrexec info; "" if none) */
|
||||
static void connection_info_add_transport(const char *title,
|
||||
const char *connection_str,
|
||||
const char *example,
|
||||
const char *extra) {
|
||||
connection_info_entry_t *e;
|
||||
|
||||
if (fmt == NULL || g_connection_info_count >= CONNECTION_INFO_CAP) {
|
||||
if (g_connection_info_count >= CONNECTION_INFO_CAP) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
(void)vsnprintf(g_connection_info[g_connection_info_count],
|
||||
sizeof(g_connection_info[g_connection_info_count]),
|
||||
fmt,
|
||||
ap);
|
||||
va_end(ap);
|
||||
|
||||
e = &g_connection_info[g_connection_info_count];
|
||||
memset(e, 0, sizeof(*e));
|
||||
if (title) { strncpy(e->title, title, sizeof(e->title) - 1); }
|
||||
if (connection_str) { strncpy(e->connection_string, connection_str, sizeof(e->connection_string) - 1); }
|
||||
if (example) { strncpy(e->example, example, sizeof(e->example) - 1); }
|
||||
if (extra) { strncpy(e->extra, extra, sizeof(e->extra) - 1); }
|
||||
g_connection_info_count++;
|
||||
}
|
||||
|
||||
/* Set the OTP pad status line (shown at bottom of connection display). */
|
||||
static void connection_info_set_otp(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
if (fmt == NULL) { g_otp_pad_status[0] = '\0'; return; }
|
||||
va_start(ap, fmt);
|
||||
(void)vsnprintf(g_otp_pad_status, sizeof(g_otp_pad_status), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static void handle_signal(int sig) {
|
||||
(void)sig;
|
||||
g_running = 0;
|
||||
@@ -1440,7 +1476,11 @@ static void role_table_get_cell(int row, int col, char *out, size_t out_size, vo
|
||||
(void)snprintf(out, out_size, "%s", role_curve_to_str(r->curve));
|
||||
break;
|
||||
case 3:
|
||||
(void)snprintf(out, out_size, "%s", (r->selector_type == SELECTOR_NOSTR_INDEX) ? "nostr_index" : "role_path");
|
||||
if (r->selector_type == SELECTOR_NOSTR_INDEX) {
|
||||
(void)snprintf(out, out_size, "m/44'/1237'/%d'/0/0", r->nostr_index);
|
||||
} else {
|
||||
(void)snprintf(out, out_size, "%s", r->role_path);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
out[0] = '\0';
|
||||
@@ -1448,6 +1488,97 @@ static void role_table_get_cell(int row, int col, char *out, size_t out_size, vo
|
||||
}
|
||||
}
|
||||
|
||||
/* On-demand connection instructions display (press 'd').
|
||||
* Shows each transport as a titled block with the connection string
|
||||
* and an example client command, with blank lines between transports. */
|
||||
static void render_connections(const role_table_t *role_table,
|
||||
const mnemonic_state_t *mnemonic,
|
||||
int derived_count,
|
||||
const char *socket_name) {
|
||||
TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Connection Instructions" };
|
||||
TuiSize size = tui_terminal_size();
|
||||
char status_buf[256];
|
||||
int i;
|
||||
|
||||
tui_clear_continuous(size.height);
|
||||
tui_render_top_frame(&frame, size.width);
|
||||
tui_print("");
|
||||
|
||||
for (i = 0; i < g_connection_info_count; i++) {
|
||||
connection_info_entry_t *e = &g_connection_info[i];
|
||||
|
||||
/* Transport title */
|
||||
tui_print("^*%s^:", e->title);
|
||||
tui_print("");
|
||||
|
||||
/* Connection string (indented) */
|
||||
tui_print(" %s", e->connection_string);
|
||||
tui_print("");
|
||||
|
||||
/* Example command (if present) */
|
||||
if (e->example[0] != '\0') {
|
||||
tui_print(" Example:");
|
||||
/* The example may contain embedded newlines for multi-line commands */
|
||||
{
|
||||
const char *p = e->example;
|
||||
const char *nl;
|
||||
while (*p != '\0') {
|
||||
nl = strchr(p, '\n');
|
||||
if (nl != NULL) {
|
||||
tui_print(" %.*s", (int)(nl - p), p);
|
||||
p = nl + 1;
|
||||
} else {
|
||||
tui_print(" %s", p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
/* Extra info (if present, e.g. qrexec bridge) */
|
||||
if (e->extra[0] != '\0') {
|
||||
const char *p = e->extra;
|
||||
const char *nl;
|
||||
while (*p != '\0') {
|
||||
nl = strchr(p, '\n');
|
||||
if (nl != NULL) {
|
||||
tui_print(" %.*s", (int)(nl - p), p);
|
||||
p = nl + 1;
|
||||
} else {
|
||||
tui_print(" %s", p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
/* OTP pad status */
|
||||
if (g_otp_pad_status[0] != '\0') {
|
||||
tui_print("%s", g_otp_pad_status);
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
/* Status line */
|
||||
(void)snprintf(status_buf,
|
||||
sizeof(status_buf),
|
||||
"session=%s (%d words) signer=%s derived=%d auto-approve=%s",
|
||||
mnemonic_is_loaded(mnemonic) ? "unlocked" : "locked",
|
||||
(mnemonic != NULL) ? mnemonic->word_count : 0,
|
||||
(socket_name != NULL) ? socket_name : "(none)",
|
||||
derived_count,
|
||||
g_auto_approve ? "ON" : "OFF");
|
||||
tui_print("%s", status_buf);
|
||||
tui_print("");
|
||||
tui_print("Press any key to return");
|
||||
|
||||
tui_anchor_prompt(0, 0);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void render_status(const role_table_t *role_table,
|
||||
const mnemonic_state_t *mnemonic,
|
||||
int derived_count,
|
||||
@@ -1461,15 +1592,7 @@ static void render_status(const role_table_t *role_table,
|
||||
tui_clear_continuous(size.height);
|
||||
tui_render_top_frame(&frame, size.width);
|
||||
|
||||
tui_print("^*Connections^:");
|
||||
if (g_connection_info_count == 0) {
|
||||
tui_print("(none)");
|
||||
} else {
|
||||
for (int i = 0; i < g_connection_info_count; ++i) {
|
||||
tui_print("%s", g_connection_info[i]);
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("Press ^_d^ for connection instructions");
|
||||
tui_print("");
|
||||
tui_print("^*Roles^:");
|
||||
if (role_table == NULL || role_table->count == 0) {
|
||||
@@ -1479,7 +1602,7 @@ static void render_status(const role_table_t *role_table,
|
||||
{"Role", 20, 0},
|
||||
{"Purpose", 12, 0},
|
||||
{"Curve", 12, 0},
|
||||
{"Selector", 12, 0}
|
||||
{"Derivation path", 24, 0}
|
||||
};
|
||||
role_table_view_data_t view;
|
||||
TuiTable table;
|
||||
@@ -2325,7 +2448,7 @@ int main(int argc, char *argv[]) {
|
||||
server_ctx_t server;
|
||||
key_store_t key_store;
|
||||
static algorithm_key_cache_t alg_key_cache;
|
||||
struct pollfd pfds[3]; /* max: 2 listeners (unix+tcp) + stdin */
|
||||
struct pollfd pfds[4]; /* max: 3 listeners (unix+tcp+http) + stdin */
|
||||
uid_t owner_uid;
|
||||
int derived_count;
|
||||
const char *socket_name = NSIGNER_DEFAULT_SOCKET_NAME;
|
||||
@@ -2886,71 +3009,122 @@ int main(int argc, char *argv[]) {
|
||||
if (multi_listen) {
|
||||
/* Multi-listener mode: show all active transports */
|
||||
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
|
||||
connection_info_add("Server: unix @%s%s", socket_name,
|
||||
bridge_source_trusted ? " (bridge-source-trusted)" : "");
|
||||
connection_info_add(" Client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||
char unix_conn[128];
|
||||
char unix_example[256];
|
||||
char unix_extra[256] = "";
|
||||
snprintf(unix_conn, sizeof(unix_conn), "@%s", socket_name);
|
||||
snprintf(unix_example, sizeof(unix_example),
|
||||
"nsigner --socket-name %s client '<json>'", socket_name);
|
||||
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
|
||||
connection_info_add(" Qrexec: qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
snprintf(unix_extra, sizeof(unix_extra),
|
||||
"Qrexec (bridge-source-trusted):\n"
|
||||
" qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||
}
|
||||
connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
}
|
||||
if (transport_mask & TRANSPORT_TCP) {
|
||||
connection_info_add("Server: FIPS/TCP [::]:11111");
|
||||
connection_info_add(" Client: nsigner --listen tcp:[::]:11111 client '<json>'");
|
||||
printf("System is ready and waiting for connections on tcp:[::]:11111.\n");
|
||||
const char *tcp_addr = servers[tcp_server_idx].socket_name;
|
||||
char fips_conn[256] = "";
|
||||
char fips_example[256] = "";
|
||||
/* If we have a FIPS identity, show the npub-based URL */
|
||||
if (have_fips_identity &&
|
||||
extract_listen_port(tcp_addr, listen_port, sizeof(listen_port)) == 0) {
|
||||
snprintf(fips_conn, sizeof(fips_conn),
|
||||
"http://%s.fips:%s", fips_npub, listen_port);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"curl -X POST http://%s.fips:%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", fips_npub, listen_port);
|
||||
printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port);
|
||||
} else {
|
||||
snprintf(fips_conn, sizeof(fips_conn), "%s", tcp_addr);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"nsigner --listen %s client '<json>'", tcp_addr);
|
||||
}
|
||||
connection_info_add_transport("FIPS", fips_conn, fips_example, "");
|
||||
printf("System is ready and waiting for connections on %s.\n", tcp_addr);
|
||||
}
|
||||
if (transport_mask & TRANSPORT_HTTP) {
|
||||
connection_info_add("Server: HTTP 127.0.0.1:11111");
|
||||
connection_info_add(" Client: curl -X POST http://127.0.0.1:11111/ -H 'Content-Type: application/json' -d '<json>'");
|
||||
printf("System is ready and waiting for connections on http://127.0.0.1:11111.\n");
|
||||
const char *http_addr = servers[http_server_idx].socket_name;
|
||||
char http_conn[256];
|
||||
char http_example[256];
|
||||
snprintf(http_conn, sizeof(http_conn), "http://%s", http_addr + 5);
|
||||
snprintf(http_example, sizeof(http_example),
|
||||
"curl -X POST http://%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", http_addr + 5);
|
||||
connection_info_add_transport("HTTP", http_conn, http_example, "");
|
||||
printf("System is ready and waiting for connections on http://%s.\n", http_addr + 5);
|
||||
}
|
||||
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
connection_info_add("Server: unix @%s", socket_name);
|
||||
connection_info_add(" Client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||
char unix_conn[128];
|
||||
char unix_example[256];
|
||||
char unix_extra[256] = "";
|
||||
snprintf(unix_conn, sizeof(unix_conn), "@%s", socket_name);
|
||||
snprintf(unix_example, sizeof(unix_example),
|
||||
"nsigner --socket-name %s client '<json>'", socket_name);
|
||||
if (bridge_source_trusted) {
|
||||
connection_info_add(" Qrexec: qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
snprintf(unix_extra, sizeof(unix_extra),
|
||||
"Qrexec (bridge-source-trusted):\n"
|
||||
" qrexec-client-vm <target_qube> %s",
|
||||
NSIGNER_QREXEC_SERVICE_NAME);
|
||||
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||
}
|
||||
connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
connection_info_add("Server: %s", listen_target);
|
||||
connection_info_add(" Client: nsigner --listen %s client '<json>'", listen_target);
|
||||
printf("System is ready and waiting for connections on %s.\n", listen_target);
|
||||
const char *actual_addr = server.socket_name;
|
||||
char fips_conn[256] = "";
|
||||
char fips_example[256] = "";
|
||||
if (have_fips_identity &&
|
||||
extract_listen_port(actual_addr, listen_port, sizeof(listen_port)) == 0) {
|
||||
snprintf(fips_conn, sizeof(fips_conn),
|
||||
"http://%s.fips:%s", fips_npub, listen_port);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"curl -X POST http://%s.fips:%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", fips_npub, listen_port);
|
||||
printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port);
|
||||
} else {
|
||||
snprintf(fips_conn, sizeof(fips_conn), "%s", actual_addr);
|
||||
snprintf(fips_example, sizeof(fips_example),
|
||||
"nsigner --listen %s client '<json>'", actual_addr);
|
||||
}
|
||||
connection_info_add_transport("FIPS", fips_conn, fips_example, "");
|
||||
printf("System is ready and waiting for connections on %s.\n", actual_addr);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_HTTP) {
|
||||
connection_info_add("Server: %s", listen_target);
|
||||
connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d '<json>'",
|
||||
listen_target + 5); /* skip "http:" prefix */
|
||||
printf("System is ready and waiting for connections on %s.\n", listen_target);
|
||||
const char *actual_addr = server.socket_name;
|
||||
char http_conn[256];
|
||||
char http_example[256];
|
||||
snprintf(http_conn, sizeof(http_conn), "http://%s", actual_addr + 5);
|
||||
snprintf(http_example, sizeof(http_example),
|
||||
"curl -X POST http://%s/ \\\n"
|
||||
" -H 'Content-Type: application/json' \\\n"
|
||||
" -d '<json>'", actual_addr + 5);
|
||||
connection_info_add_transport("HTTP", http_conn, http_example, "");
|
||||
printf("System is ready and waiting for connections on %s.\n", actual_addr);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_QREXEC) {
|
||||
connection_info_add("Server: qrexec (one request per invocation)");
|
||||
connection_info_add_transport("Qrexec", "qrexec (one request per invocation)", "", "");
|
||||
printf("System is ready and waiting for a qrexec request.\n");
|
||||
} else {
|
||||
connection_info_add("Server: stdio (one request per invocation)");
|
||||
connection_info_add_transport("Stdio", "stdio (one request per invocation)", "", "");
|
||||
printf("System is ready and waiting for a stdio request.\n");
|
||||
}
|
||||
|
||||
/* Show OTP pad status if bound. */
|
||||
if (otp_pad_is_bound()) {
|
||||
connection_info_add("OTP pad: %s (offset %llu / %llu bytes)",
|
||||
otp_pad_chksum(),
|
||||
(unsigned long long)otp_pad_current_offset(),
|
||||
(unsigned long long)otp_pad_size());
|
||||
connection_info_set_otp("OTP pad: %s (offset %llu / %llu bytes)",
|
||||
otp_pad_chksum(),
|
||||
(unsigned long long)otp_pad_current_offset(),
|
||||
(unsigned long long)otp_pad_size());
|
||||
}
|
||||
|
||||
if (have_fips_identity) {
|
||||
connection_info_add("fips ipv6: %s", fips_ipv6);
|
||||
connection_info_add("fips npub: %s", fips_npub);
|
||||
printf("fips ipv6: %s\n", fips_ipv6);
|
||||
printf("fips npub: %s\n", fips_npub);
|
||||
|
||||
if (listen_mode == NSIGNER_LISTEN_TCP &&
|
||||
extract_listen_port(listen_target, listen_port, sizeof(listen_port)) == 0) {
|
||||
connection_info_add("fips address: http://%s.fips:%s", fips_npub, listen_port);
|
||||
printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port);
|
||||
}
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
@@ -3072,6 +3246,16 @@ int main(int argc, char *argv[]) {
|
||||
g_auto_approve = g_auto_approve ? 0 : 1;
|
||||
server_set_prompt_always_allow(g_auto_approve);
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
} else if (lower == 'd') {
|
||||
/* Display connection instructions on demand.
|
||||
* Show the connections screen, wait for any key, then
|
||||
* return to the normal status display. */
|
||||
render_connections(&role_table, &mnemonic, derived_count, socket_name);
|
||||
{
|
||||
char dch = '\0';
|
||||
(void)read(STDIN_FILENO, &dch, 1);
|
||||
}
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
} else if (lower == 'l') {
|
||||
mnemonic_source_t relock_source;
|
||||
|
||||
|
||||
133
src/server.c
133
src/server.c
@@ -1457,6 +1457,8 @@ int server_start(server_ctx_t *ctx) {
|
||||
uint16_t port;
|
||||
char host[64];
|
||||
int one = 1;
|
||||
int bind_attempt;
|
||||
int bound = 0;
|
||||
|
||||
/* HTTP mode uses the same TCP socket setup, just with an "http:" prefix
|
||||
* instead of "tcp:". Convert to tcp: for parse_tcp_target. */
|
||||
@@ -1476,54 +1478,95 @@ int server_start(server_ctx_t *ctx) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = socket(family, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"socket(tcp) failed: %s",
|
||||
strerror(errno));
|
||||
/* Try the requested port, then increment up to 5 times if it's in use.
|
||||
* This handles both real conflicts and false positives (e.g. when TCP
|
||||
* [::]:11111 and HTTP 127.0.0.1:11111 are started together in
|
||||
* multi-listen mode, the second bind can fail with EADDRINUSE even
|
||||
* though no other process holds the port). */
|
||||
for (bind_attempt = 0; bind_attempt < 5 && !bound; ++bind_attempt) {
|
||||
uint16_t try_port = (uint16_t)(port + bind_attempt);
|
||||
if (try_port < port) {
|
||||
/* port wrapped past 65535 — stop retrying */
|
||||
break;
|
||||
}
|
||||
|
||||
fd = socket(family, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"socket(tcp) failed: %s",
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
|
||||
if (family == AF_INET) {
|
||||
struct sockaddr_in addr4;
|
||||
memset(&addr4, 0, sizeof(addr4));
|
||||
addr4.sin_family = AF_INET;
|
||||
addr4.sin_port = htons(try_port);
|
||||
if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
if (errno == EADDRINUSE) {
|
||||
continue; /* try next port */
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
struct sockaddr_in6 addr6;
|
||||
memset(&addr6, 0, sizeof(addr6));
|
||||
addr6.sin6_family = AF_INET6;
|
||||
addr6.sin6_port = htons(try_port);
|
||||
if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
if (errno == EADDRINUSE) {
|
||||
continue; /* try next port */
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
bound = 1;
|
||||
port = try_port;
|
||||
}
|
||||
|
||||
if (!bound) {
|
||||
/* All 5 attempts failed with EADDRINUSE. ctx->last_error already
|
||||
* holds the most recent bind failure message. */
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
|
||||
if (family == AF_INET) {
|
||||
struct sockaddr_in addr4;
|
||||
memset(&addr4, 0, sizeof(addr4));
|
||||
addr4.sin_family = AF_INET;
|
||||
addr4.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
struct sockaddr_in6 addr6;
|
||||
memset(&addr6, 0, sizeof(addr6));
|
||||
addr6.sin6_family = AF_INET6;
|
||||
addr6.sin6_port = htons(port);
|
||||
if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
|
||||
close(fd);
|
||||
server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
|
||||
return -1;
|
||||
}
|
||||
if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
/* Update ctx->socket_name to reflect the actual bound port, so the
|
||||
* status display and error messages show the real address. */
|
||||
{
|
||||
const char *prefix = (ctx->listen_mode == NSIGNER_LISTEN_HTTP) ? "http:" : "tcp:";
|
||||
if (family == AF_INET6) {
|
||||
snprintf(ctx->socket_name, sizeof(ctx->socket_name),
|
||||
"%s[%s]:%u", prefix, host, (unsigned)port);
|
||||
} else {
|
||||
snprintf(ctx->socket_name, sizeof(ctx->socket_name),
|
||||
"%s%s:%u", prefix, host, (unsigned)port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user