From a0a5987ffac33dcfad74e671cbae737bcb96ed0f Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Mon, 20 Jul 2026 09:49:20 -0400 Subject: [PATCH] v0.0.54 - TUI: show full derivation path in Roles table, move connection instructions to on-demand 'd' hotkey display with spaced transport blocks --- README.md | 73 ++++- plans/tui_display_connections_on_demand.md | 151 +++++++++++ src/main.c | 295 +++++++++++++++++---- 3 files changed, 449 insertions(+), 70 deletions(-) create mode 100644 plans/tui_display_connections_on_demand.md diff --git a/README.md b/README.md index 3987c39..9353817 100644 --- a/README.md +++ b/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) -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 -n_signer v0.0.2 > Main Menu +n_signer v0.0.53 > Main Menu -Connections: -Server: unix @nsigner_hairy_dog - Client: nsigner --socket-name nsigner_hairy_dog client '' -OTP pad: 333e9902db839d9d... (offset 288 / 1048576 bytes) +Press d for connection instructions Roles: -Role Purpose Curve Selector -main nostr secp256k1 role:main -ops nostr secp256k1 nostr_index:7 -backup bitcoin secp256k1 role_path:m/84'/0'/0'/0/5 +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 Activity (latest first): 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 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'/'/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 '' + + Qrexec (bridge-source-trusted): + qrexec-client-vm qubes.NsignerRpc + + +FIPS: + + http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111 + + Example: + curl -X POST http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111/ + -H 'Content-Type: application/json' + -d '' + + +HTTP: + + http://127.0.0.1:11112 + + Example: + curl -X POST http://127.0.0.1:11112/ + -H 'Content-Type: application/json' + -d '' + +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 diff --git a/plans/tui_display_connections_on_demand.md b/plans/tui_display_connections_on_demand.md new file mode 100644 index 0000000..4655152 --- /dev/null +++ b/plans/tui_display_connections_on_demand.md @@ -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 '' + + Qrexec (bridge-source-trusted): + qrexec-client-vm qubes.NsignerRpc + + +FIPS + + http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111 + + Example: + curl -X POST http://npub10vt4scusw6lq27qw83nfwp5sqer492h0tnwa8ugqjvp6l4xuz2qsdycrwd.fips:11111/ \ + -H 'Content-Type: application/json' \ + -d '' + + +HTTP + + http://127.0.0.1:11112 + + Example: + curl -X POST http://127.0.0.1:11112/ \ + -H 'Content-Type: application/json' \ + -d '' + + +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 ''"`, 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://.fips:`), 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. diff --git a/src/main.c b/src/main.c index 799ec59..b566e82 100644 --- a/src/main.c +++ b/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 53 -#define NSIGNER_VERSION "v0.0.53" +#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; @@ -2886,76 +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 ''", 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 ''", socket_name); if (transport_mask & TRANSPORT_QREXEC_BRIDGE) { - connection_info_add(" Qrexec: qrexec-client-vm %s", - NSIGNER_QREXEC_SERVICE_NAME); + snprintf(unix_extra, sizeof(unix_extra), + "Qrexec (bridge-source-trusted):\n" + " qrexec-client-vm %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) { const char *tcp_addr = servers[tcp_server_idx].socket_name; - connection_info_add("Server: FIPS/TCP %s", tcp_addr + 4); /* skip "tcp:" */ - connection_info_add(" Client: nsigner --listen %s client ''", tcp_addr); + 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 ''", 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 ''", 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) { const char *http_addr = servers[http_server_idx].socket_name; - connection_info_add("Server: HTTP %s", http_addr + 5); /* skip "http:" */ - connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d ''", - http_addr + 5); + 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 ''", 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 ''", 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 ''", socket_name); if (bridge_source_trusted) { - connection_info_add(" Qrexec: qrexec-client-vm %s", - NSIGNER_QREXEC_SERVICE_NAME); + snprintf(unix_extra, sizeof(unix_extra), + "Qrexec (bridge-source-trusted):\n" + " qrexec-client-vm %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) { const char *actual_addr = server.socket_name; - connection_info_add("Server: %s", actual_addr); - connection_info_add(" Client: nsigner --listen %s client ''", actual_addr); + 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 ''", 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 ''", 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) { const char *actual_addr = server.socket_name; - connection_info_add("Server: %s", actual_addr); - connection_info_add(" Client: curl -X POST http://%s/ -H 'Content-Type: application/json' -d ''", - actual_addr + 5); /* skip "http:" prefix */ + 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 ''", 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); @@ -3077,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;