v0.0.54 - TUI: show full derivation path in Roles table, move connection instructions to on-demand 'd' hotkey display with spaced transport blocks
This commit is contained in:
73
README.md
73
README.md
@@ -105,21 +105,18 @@ These modes avoid putting mnemonic material in argv/environment and are designed
|
|||||||
|
|
||||||
### 3.2 Running phase (status display + signer)
|
### 3.2 Running phase (status display + signer)
|
||||||
|
|
||||||
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 Connections, Roles, and Activity sections, followed by a single status line. Example layout (single Unix listener, one role derived):
|
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
|
```text
|
||||||
n_signer v0.0.2 > Main Menu
|
n_signer v0.0.53 > Main Menu
|
||||||
|
|
||||||
Connections:
|
Press d for connection instructions
|
||||||
Server: unix @nsigner_hairy_dog
|
|
||||||
Client: nsigner --socket-name nsigner_hairy_dog client '<json>'
|
|
||||||
OTP pad: 333e9902db839d9d... (offset 288 / 1048576 bytes)
|
|
||||||
|
|
||||||
Roles:
|
Roles:
|
||||||
Role Purpose Curve Selector
|
Role Purpose Curve Derivation path
|
||||||
main nostr secp256k1 role:main
|
main nostr secp256k1 m/44'/1237'/0'/0/0
|
||||||
ops nostr secp256k1 nostr_index:7
|
nostr_idx_1 nostr secp256k1 m/44'/1237'/1'/0/0
|
||||||
backup bitcoin secp256k1 role_path:m/84'/0'/0'/0/5
|
backup bitcoin secp256k1 m/84'/0'/0'/0/5
|
||||||
|
|
||||||
Activity (latest first):
|
Activity (latest first):
|
||||||
16:03:11 allow caller=uid:1000 method=get_public_key role=main
|
16:03:11 allow caller=uid:1000 method=get_public_key role=main
|
||||||
@@ -128,16 +125,68 @@ Activity (latest first):
|
|||||||
15:59:10 deny caller=uid:1001 method=sign_event error=unauthorized
|
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
|
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 signer's name (`nsigner_hairy_dog` in this example) appears in two places: the **Connections** section as the abstract socket address (`Server: unix @nsigner_hairy_dog`), and 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.
|
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.
|
||||||
|
|
||||||
When multiple transports are active (interactive transport selection), the Connections section lists each one with its client command. HTTP and FIPS/TCP entries show `curl` / `nsigner --listen ... client` examples respectively.
|
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):
|
Hotkeys (active while the status display is shown):
|
||||||
|
|
||||||
- `a` — toggle auto-approve (prompt) for this session
|
- `a` — toggle auto-approve (prompt) for this session
|
||||||
- `r` — refresh the display
|
- `r` — refresh the display
|
||||||
|
- `d` — display connection instructions (press any key to return)
|
||||||
- `l` — lock / re-unlock the session
|
- `l` — lock / re-unlock the session
|
||||||
- `q` — quit
|
- `q` — quit
|
||||||
|
|
||||||
|
|||||||
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.
|
||||||
295
src/main.c
295
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) */
|
/* Version information (auto-updated by build/version tooling) */
|
||||||
#define NSIGNER_VERSION_MAJOR 0
|
#define NSIGNER_VERSION_MAJOR 0
|
||||||
#define NSIGNER_VERSION_MINOR 0
|
#define NSIGNER_VERSION_MINOR 0
|
||||||
#define NSIGNER_VERSION_PATCH 53
|
#define NSIGNER_VERSION_PATCH 54
|
||||||
#define NSIGNER_VERSION "v0.0.53"
|
#define NSIGNER_VERSION "v0.0.54"
|
||||||
|
|
||||||
|
|
||||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
/* 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 NSIGNER_QREXEC_SERVICE_NAME "qubes.NsignerRpc"
|
||||||
#define ACTIVITY_LOG_CAP 16
|
#define ACTIVITY_LOG_CAP 16
|
||||||
#define CONNECTION_INFO_CAP 8
|
#define CONNECTION_INFO_CAP 8
|
||||||
|
#define CONNECTION_INFO_STR_MAX 256
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char lines[ACTIVITY_LOG_CAP][256];
|
char lines[ACTIVITY_LOG_CAP][256];
|
||||||
int count;
|
int count;
|
||||||
} activity_log_t;
|
} 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 volatile sig_atomic_t g_running = 1;
|
||||||
static activity_log_t g_activity_log;
|
static activity_log_t g_activity_log;
|
||||||
static int g_auto_approve = 0;
|
static int g_auto_approve = 0;
|
||||||
@@ -819,13 +831,16 @@ typedef struct {
|
|||||||
} mnemonic_source_t;
|
} mnemonic_source_t;
|
||||||
|
|
||||||
static mnemonic_source_kind_t g_startup_mnemonic_source_kind = MNEMONIC_SOURCE_TUI;
|
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;
|
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[] = {
|
static const TuiMenuItem g_main_menu_items[] = {
|
||||||
{"^_l^: lock/reunlock", 'l'},
|
{"^_l^: lock/reunlock", 'l'},
|
||||||
{"^_r^: refresh", 'r'},
|
{"^_r^: refresh", 'r'},
|
||||||
{"^_a^: toggle auto-approve", 'a'},
|
{"^_a^: toggle auto-approve", 'a'},
|
||||||
|
{"^_d^: display connections", 'd'},
|
||||||
{"^_q^:/x quit", 'q'}
|
{"^_q^:/x quit", 'q'}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -845,28 +860,49 @@ static void render_status(const role_table_t *role_table,
|
|||||||
int derived_count,
|
int derived_count,
|
||||||
const char *socket_name);
|
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) {
|
static void connection_info_clear(void) {
|
||||||
g_connection_info_count = 0;
|
g_connection_info_count = 0;
|
||||||
memset(g_connection_info, 0, sizeof(g_connection_info));
|
memset(g_connection_info, 0, sizeof(g_connection_info));
|
||||||
|
g_otp_pad_status[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
static void connection_info_add(const char *fmt, ...) {
|
/* Add a transport block to the connection info.
|
||||||
va_list ap;
|
* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
e = &g_connection_info[g_connection_info_count];
|
||||||
va_start(ap, fmt);
|
memset(e, 0, sizeof(*e));
|
||||||
(void)vsnprintf(g_connection_info[g_connection_info_count],
|
if (title) { strncpy(e->title, title, sizeof(e->title) - 1); }
|
||||||
sizeof(g_connection_info[g_connection_info_count]),
|
if (connection_str) { strncpy(e->connection_string, connection_str, sizeof(e->connection_string) - 1); }
|
||||||
fmt,
|
if (example) { strncpy(e->example, example, sizeof(e->example) - 1); }
|
||||||
ap);
|
if (extra) { strncpy(e->extra, extra, sizeof(e->extra) - 1); }
|
||||||
va_end(ap);
|
|
||||||
|
|
||||||
g_connection_info_count++;
|
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) {
|
static void handle_signal(int sig) {
|
||||||
(void)sig;
|
(void)sig;
|
||||||
g_running = 0;
|
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));
|
(void)snprintf(out, out_size, "%s", role_curve_to_str(r->curve));
|
||||||
break;
|
break;
|
||||||
case 3:
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
out[0] = '\0';
|
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,
|
static void render_status(const role_table_t *role_table,
|
||||||
const mnemonic_state_t *mnemonic,
|
const mnemonic_state_t *mnemonic,
|
||||||
int derived_count,
|
int derived_count,
|
||||||
@@ -1461,15 +1592,7 @@ static void render_status(const role_table_t *role_table,
|
|||||||
tui_clear_continuous(size.height);
|
tui_clear_continuous(size.height);
|
||||||
tui_render_top_frame(&frame, size.width);
|
tui_render_top_frame(&frame, size.width);
|
||||||
|
|
||||||
tui_print("^*Connections^:");
|
tui_print("Press ^_d^ for connection instructions");
|
||||||
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("");
|
tui_print("");
|
||||||
tui_print("^*Roles^:");
|
tui_print("^*Roles^:");
|
||||||
if (role_table == NULL || role_table->count == 0) {
|
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},
|
{"Role", 20, 0},
|
||||||
{"Purpose", 12, 0},
|
{"Purpose", 12, 0},
|
||||||
{"Curve", 12, 0},
|
{"Curve", 12, 0},
|
||||||
{"Selector", 12, 0}
|
{"Derivation path", 24, 0}
|
||||||
};
|
};
|
||||||
role_table_view_data_t view;
|
role_table_view_data_t view;
|
||||||
TuiTable table;
|
TuiTable table;
|
||||||
@@ -2886,76 +3009,122 @@ int main(int argc, char *argv[]) {
|
|||||||
if (multi_listen) {
|
if (multi_listen) {
|
||||||
/* Multi-listener mode: show all active transports */
|
/* Multi-listener mode: show all active transports */
|
||||||
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
|
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
|
||||||
connection_info_add("Server: unix @%s%s", socket_name,
|
char unix_conn[128];
|
||||||
bridge_source_trusted ? " (bridge-source-trusted)" : "");
|
char unix_example[256];
|
||||||
connection_info_add(" Client: nsigner --socket-name %s client '<json>'", socket_name);
|
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) {
|
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
|
||||||
connection_info_add(" Qrexec: qrexec-client-vm <target_qube> %s",
|
snprintf(unix_extra, sizeof(unix_extra),
|
||||||
NSIGNER_QREXEC_SERVICE_NAME);
|
"Qrexec (bridge-source-trusted):\n"
|
||||||
|
" qrexec-client-vm <target_qube> %s",
|
||||||
|
NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
printf("qrexec service: %s\n", 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);
|
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||||
}
|
}
|
||||||
if (transport_mask & TRANSPORT_TCP) {
|
if (transport_mask & TRANSPORT_TCP) {
|
||||||
const char *tcp_addr = servers[tcp_server_idx].socket_name;
|
const char *tcp_addr = servers[tcp_server_idx].socket_name;
|
||||||
connection_info_add("Server: FIPS/TCP %s", tcp_addr + 4); /* skip "tcp:" */
|
char fips_conn[256] = "";
|
||||||
connection_info_add(" Client: nsigner --listen %s client '<json>'", tcp_addr);
|
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);
|
printf("System is ready and waiting for connections on %s.\n", tcp_addr);
|
||||||
}
|
}
|
||||||
if (transport_mask & TRANSPORT_HTTP) {
|
if (transport_mask & TRANSPORT_HTTP) {
|
||||||
const char *http_addr = servers[http_server_idx].socket_name;
|
const char *http_addr = servers[http_server_idx].socket_name;
|
||||||
connection_info_add("Server: HTTP %s", http_addr + 5); /* skip "http:" */
|
char http_conn[256];
|
||||||
connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d '<json>'",
|
char http_example[256];
|
||||||
http_addr + 5);
|
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);
|
printf("System is ready and waiting for connections on http://%s.\n", http_addr + 5);
|
||||||
}
|
}
|
||||||
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||||
connection_info_add("Server: unix @%s", socket_name);
|
char unix_conn[128];
|
||||||
connection_info_add(" Client: nsigner --socket-name %s client '<json>'", socket_name);
|
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) {
|
if (bridge_source_trusted) {
|
||||||
connection_info_add(" Qrexec: qrexec-client-vm <target_qube> %s",
|
snprintf(unix_extra, sizeof(unix_extra),
|
||||||
NSIGNER_QREXEC_SERVICE_NAME);
|
"Qrexec (bridge-source-trusted):\n"
|
||||||
|
" qrexec-client-vm <target_qube> %s",
|
||||||
|
NSIGNER_QREXEC_SERVICE_NAME);
|
||||||
printf("qrexec service: %s\n", 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);
|
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||||
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||||
const char *actual_addr = server.socket_name;
|
const char *actual_addr = server.socket_name;
|
||||||
connection_info_add("Server: %s", actual_addr);
|
char fips_conn[256] = "";
|
||||||
connection_info_add(" Client: nsigner --listen %s client '<json>'", actual_addr);
|
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);
|
printf("System is ready and waiting for connections on %s.\n", actual_addr);
|
||||||
} else if (listen_mode == NSIGNER_LISTEN_HTTP) {
|
} else if (listen_mode == NSIGNER_LISTEN_HTTP) {
|
||||||
const char *actual_addr = server.socket_name;
|
const char *actual_addr = server.socket_name;
|
||||||
connection_info_add("Server: %s", actual_addr);
|
char http_conn[256];
|
||||||
connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d '<json>'",
|
char http_example[256];
|
||||||
actual_addr + 5); /* skip "http:" prefix */
|
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);
|
printf("System is ready and waiting for connections on %s.\n", actual_addr);
|
||||||
} else if (listen_mode == NSIGNER_LISTEN_QREXEC) {
|
} 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");
|
printf("System is ready and waiting for a qrexec request.\n");
|
||||||
} else {
|
} 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");
|
printf("System is ready and waiting for a stdio request.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show OTP pad status if bound. */
|
/* Show OTP pad status if bound. */
|
||||||
if (otp_pad_is_bound()) {
|
if (otp_pad_is_bound()) {
|
||||||
connection_info_add("OTP pad: %s (offset %llu / %llu bytes)",
|
connection_info_set_otp("OTP pad: %s (offset %llu / %llu bytes)",
|
||||||
otp_pad_chksum(),
|
otp_pad_chksum(),
|
||||||
(unsigned long long)otp_pad_current_offset(),
|
(unsigned long long)otp_pad_current_offset(),
|
||||||
(unsigned long long)otp_pad_size());
|
(unsigned long long)otp_pad_size());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (have_fips_identity) {
|
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 ipv6: %s\n", fips_ipv6);
|
||||||
printf("fips npub: %s\n", fips_npub);
|
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);
|
fflush(stdout);
|
||||||
@@ -3077,6 +3246,16 @@ int main(int argc, char *argv[]) {
|
|||||||
g_auto_approve = g_auto_approve ? 0 : 1;
|
g_auto_approve = g_auto_approve ? 0 : 1;
|
||||||
server_set_prompt_always_allow(g_auto_approve);
|
server_set_prompt_always_allow(g_auto_approve);
|
||||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
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') {
|
} else if (lower == 'l') {
|
||||||
mnemonic_source_t relock_source;
|
mnemonic_source_t relock_source;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user