Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a8657f663 | ||
|
|
478c3a569e | ||
|
|
10208e5fac | ||
|
|
922a45ce3a | ||
|
|
d28f691aae | ||
|
|
2e8ce777d8 | ||
|
|
8ebdb50789 | ||
|
|
a39baed82b |
7
Makefile
7
Makefile
@@ -43,6 +43,7 @@ TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input
|
||||
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
|
||||
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
|
||||
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
|
||||
EXAMPLE_GET_PUBKEY_QREXEC_TARGET := $(BUILD_DIR)/example_get_pubkey_qrexec
|
||||
|
||||
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth examples test-client clean
|
||||
|
||||
@@ -109,7 +110,7 @@ test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
|
||||
|
||||
test-client: examples
|
||||
|
||||
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET)
|
||||
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET)
|
||||
|
||||
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@@ -167,5 +168,9 @@ $(EXAMPLE_GET_PUBKEY_TCP_TARGET): $(EXAMPLES_DIR)/get_pubkey_tcp.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_tcp.c -o $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_GET_PUBKEY_QREXEC_TARGET): $(EXAMPLES_DIR)/get_pubkey_qrexec.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_qrexec.c -o $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
57
README.md
57
README.md
@@ -247,6 +247,8 @@ Discovery:
|
||||
- `nsigner --listen stdio` runs one framed JSON-RPC request/response over stdin/stdout.
|
||||
- `nsigner --listen qrexec` is the same stdio framing mode, but caller identity can be derived from `QREXEC_REMOTE_DOMAIN` (displayed as `qubes:<source-vm>`).
|
||||
- `nsigner --listen tcp:IPv4:PORT` or `tcp:[IPv6]:PORT` enables TCP listening for non-AF_UNIX clients (for example `tcp:127.0.0.1:8080`, `tcp:[::]:8080`, or `tcp:[fd00::1234]:8080`).
|
||||
- `nsigner bridge --to <socket-name>` is a stateless relay for Qubes qrexec: reads one framed request from stdin, forwards it to a persistent signer's abstract unix socket, and relays the response to stdout. Used as the `qubes.NsignerRpc` service entrypoint. See [§8.3](#83-qubes-os-qube) and [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md).
|
||||
- `--bridge-source-trusted` (unix listener only): marks the socket as a trusted bridge endpoint. Each connection sends a framed `{"qrexec_source":"<vm>"}` preamble before the request, and the caller identity is composed as `qubes:<vm>` — matching the native qrexec identity path. This enables a persistent signer (mnemonic in mlock'd RAM) to receive qrexec-routed requests without spawning a fresh process per call.
|
||||
|
||||
### 7.2 ESP32 MCU: TinyUSB composite (CDC + WebUSB)
|
||||
|
||||
@@ -283,7 +285,43 @@ MCU target reuses mnemonic/role/selector/enforcement/dispatcher core and swaps t
|
||||
|
||||
### 8.3 Qubes OS qube
|
||||
|
||||
Qubes deployment runs `n_signer` in a dedicated signer qube as a foreground process under explicit user session control. Transport binding is platform-specific, but lifecycle and memory-only state model are unchanged.
|
||||
Qubes deployment runs `n_signer` in a dedicated signer qube (e.g. `nostr_signer`) as a foreground process under explicit user session control. The mnemonic lives only in mlock'd RAM in that qube — a compromised agent in a caller qube cannot read it (hypervisor-enforced memory isolation).
|
||||
|
||||
Two transport paths are supported:
|
||||
|
||||
**FIPS/TCP** — the signer listens on `tcp:[::]:8080` and FIPS carries traffic between qubes as an IPv6 mesh substrate. See [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md).
|
||||
|
||||
**Qubes qrexec bridge** (recommended for no-network deployments) — a persistent signer listens on an abstract unix socket, and a stateless `nsigner bridge` relay (the `qubes.NsignerRpc` qrexec service) forwards one request per qrexec invocation. No network, no FIPS — pure intra-host IPC. Caller identity is `qubes:<source-vm>` (from `QREXEC_REMOTE_DOMAIN`), relayed via a trusted preamble. See [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md) for the full design.
|
||||
|
||||
#### Qrexec bridge setup
|
||||
|
||||
**In the signer qube** (`nostr_signer`):
|
||||
|
||||
```bash
|
||||
# Install nsigner and the qrexec service
|
||||
bash setup_signer_qube.sh # from packaging/qubes/
|
||||
|
||||
# Start the persistent signer (mnemonic entered at terminal, in mlock'd RAM)
|
||||
~/.local/bin/nsigner --listen unix --socket-name nsigner --bridge-source-trusted
|
||||
```
|
||||
|
||||
**In dom0**:
|
||||
|
||||
```bash
|
||||
# Install policy and tag the signer qube
|
||||
bash setup_dom0.sh nostr_signer # from packaging/qubes/
|
||||
```
|
||||
|
||||
The dom0 policy allows trusted caller qubes without a popup (memory isolation is the real security boundary) and asks for confirmation from any other qube. The signer's own approval prompt at the `nostr_signer` terminal is the operation-level gate.
|
||||
|
||||
**From a caller qube**:
|
||||
|
||||
```bash
|
||||
# JavaScript example (uses qrexec-client-vm, no auth envelope needed)
|
||||
node examples/n_signer_qube_example_qrexec.js nostr_signer
|
||||
```
|
||||
|
||||
Setup scripts and policy are in [`packaging/qubes/`](packaging/qubes/). See also [`documents/QUBES_OS.md`](documents/QUBES_OS.md) and [`documents/qubes_client_examples.md`](documents/qubes_client_examples.md).
|
||||
|
||||
## 9. Usage
|
||||
|
||||
@@ -319,6 +357,18 @@ TCP transport mode (no TUI; serves requests until terminated):
|
||||
nsigner --listen tcp:[::]:8080
|
||||
```
|
||||
|
||||
Qrexec bridge mode (stateless relay to a persistent signer's unix socket; used as the `qubes.NsignerRpc` service):
|
||||
|
||||
```bash
|
||||
nsigner bridge --to nsigner
|
||||
```
|
||||
|
||||
Persistent signer for qrexec bridge (unix listener with trusted source-qube preamble):
|
||||
|
||||
```bash
|
||||
nsigner --listen unix --socket-name nsigner --bridge-source-trusted
|
||||
```
|
||||
|
||||
### 9.2 Send a request (client mode)
|
||||
|
||||
From another terminal, target the signer by its socket name:
|
||||
@@ -414,6 +464,11 @@ These items are designed and worth doing, but are not in the current implementat
|
||||
- [`documents/CLIENT_IMPLEMENTATION.md`](documents/CLIENT_IMPLEMENTATION.md): client integration contract and framing behavior
|
||||
- [`documents/QUBES_OS.md`](documents/QUBES_OS.md): Qubes OS deployment/integration checklist for dedicated signer qubes
|
||||
- [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md): Tier-1 FIPS deployment runbook using loopback TCP listener
|
||||
- [`plans/qrexec_persistent_bridge.md`](plans/qrexec_persistent_bridge.md): design for the qrexec → unix-socket bridge transport (persistent signer, no mnemonic on disk)
|
||||
- [`packaging/qubes/`](packaging/qubes/): qrexec service script, dom0 policy, and setup scripts for Qubes deployment
|
||||
- [`examples/n_signer_qube_example_qrexec.js`](examples/n_signer_qube_example_qrexec.js): JavaScript caller example using qrexec (no network, no auth envelope)
|
||||
- [`examples/n_signer_qube_example_fips.js`](examples/n_signer_qube_example_fips.js): JavaScript caller example using FIPS/TCP with auth envelope
|
||||
- [`examples/get_pubkey_tcp.c`](examples/get_pubkey_tcp.c): C caller example using TCP transport with auth envelope
|
||||
- [`plans/nsigner.md`](plans/nsigner.md): implementation plan and sequencing
|
||||
- [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md): seed phrase domain/use catalog and caveats
|
||||
- [`firmware/feather_s3_tft`](firmware/feather_s3_tft): Feather ESP32-S3 Reverse TFT firmware (TinyUSB composite CDC + WebUSB signer PoC)
|
||||
|
||||
110
examples/get_pubkey_qrexec.c
Normal file
110
examples/get_pubkey_qrexec.c
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* get_pubkey_qrexec.c — connect to a running n_signer in another Qubes qube
|
||||
* via qrexec, using the high-level nostr_signer API from nostr_core_lib.
|
||||
*
|
||||
* This demonstrates the new nostr_core_lib client features:
|
||||
* - nostr_signer_nsigner_qrexec() — qrexec transport
|
||||
* - nostr_signer_nsigner_set_nostr_index() — index-based key selection
|
||||
*
|
||||
* Usage:
|
||||
* ./get_pubkey_qrexec <target_qube> [nostr_index]
|
||||
* ./get_pubkey_qrexec nostr_signer 0
|
||||
* ./get_pubkey_qrexec nostr_signer 1
|
||||
*
|
||||
* No auth envelope needed — qrexec identity comes from QREXEC_REMOTE_DOMAIN
|
||||
* on the server side.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "nip019.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *target_qube;
|
||||
const char *service_name = "qubes.NsignerRpc";
|
||||
int nostr_index = 0;
|
||||
nostr_signer_t *signer = NULL;
|
||||
char pubkey_hex[65];
|
||||
unsigned char pubkey_bytes[32];
|
||||
char npub[128];
|
||||
int rc;
|
||||
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <target_qube> [nostr_index]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
target_qube = argv[1];
|
||||
if (argc > 2) {
|
||||
nostr_index = atoi(argv[2]);
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to n_signer in qube \"%s\" via qrexec (index %d)...\n",
|
||||
target_qube, nostr_index);
|
||||
|
||||
/* Create a high-level signer backed by qrexec transport */
|
||||
signer = nostr_signer_nsigner_qrexec(target_qube, service_name, NULL, 30000);
|
||||
if (signer == NULL) {
|
||||
fprintf(stderr, "failed to create qrexec signer (is qrexec-client-vm available?)\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Select key by nostr_index (NIP-06 m/44'/1237'/N'/0/0) */
|
||||
if (nostr_signer_nsigner_set_nostr_index(signer, nostr_index) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to set nostr_index\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Request the public key */
|
||||
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
if (rc == NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED) {
|
||||
fprintf(stderr, "DENIED: index %d is not in the signer's whitelist\n", nostr_index);
|
||||
} else if (rc == NOSTR_ERROR_NSIGNER_POLICY_DENIED) {
|
||||
fprintf(stderr, "DENIED: policy denied (caller not approved at signer terminal)\n");
|
||||
} else {
|
||||
fprintf(stderr, "get_public_key failed: error code %d\n", rc);
|
||||
}
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Convert hex pubkey to npub (bech32) */
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 32; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(pubkey_hex + 2 * i, "%2x", &byte) != 1) {
|
||||
fprintf(stderr, "failed to parse hex pubkey\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
pubkey_bytes[i] = (unsigned char)byte;
|
||||
}
|
||||
}
|
||||
if (nostr_key_to_bech32(pubkey_bytes, "npub", npub) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to convert to npub\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("index %d: hex=%s npub=%s\n", nostr_index, pubkey_hex, npub);
|
||||
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
@@ -178,6 +178,16 @@ set -euo pipefail
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
LISTEN_TARGET="${NSIGNER_LISTEN_TARGET:-tcp:[::]:8080}"
|
||||
|
||||
# If first arg is "qrexec", start in unix bridge mode for Qubes qrexec.
|
||||
# Otherwise, pass any extra args through to nsigner (e.g. --allow-all).
|
||||
if [[ "${1:-}" == "qrexec" ]]; then
|
||||
LISTEN_TARGET="unix"
|
||||
EXTRA_ARGS="--socket-name nsigner --bridge-source-trusted"
|
||||
shift
|
||||
else
|
||||
EXTRA_ARGS="$*"
|
||||
fi
|
||||
|
||||
echo "=== n_signer startup ==="
|
||||
echo "listen target: ${LISTEN_TARGET}"
|
||||
|
||||
@@ -208,7 +218,11 @@ fi
|
||||
echo
|
||||
echo "Starting signer..."
|
||||
echo "On first remote request, approve in prompt with [y] or [a]."
|
||||
exec "$HOME/.local/bin/nsigner" --listen "${LISTEN_TARGET}"
|
||||
if [[ -n "${EXTRA_ARGS}" ]]; then
|
||||
exec "$HOME/.local/bin/nsigner" --listen "${LISTEN_TARGET}" ${EXTRA_ARGS}
|
||||
else
|
||||
exec "$HOME/.local/bin/nsigner" --listen "${LISTEN_TARGET}"
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod 0755 "${script_path}"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Qubes OS qrexec policy for nsigner
|
||||
# Syntax: service +argument source target action
|
||||
# Allow specific qubes to reach the signer qube with user confirmation
|
||||
#
|
||||
# The ai qube is allowed without a dom0 popup (memory isolation is the
|
||||
# real security boundary — the signer's own approval prompt in the
|
||||
# nostr_signer terminal is the operation-level gate).
|
||||
qubes.NsignerRpc * ai @tag:nsigner-signer allow target=nostr_signer
|
||||
# Any other qube must get a dom0 confirmation popup.
|
||||
qubes.NsignerRpc * @anyvm @tag:nsigner-signer ask default_target=nostr_signer
|
||||
qubes.NsignerRpc * @anyvm @anyvm deny
|
||||
|
||||
58
packaging/qubes/setup_dom0.sh
Executable file
58
packaging/qubes/setup_dom0.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# setup_dom0.sh — install qrexec policy and tag the signer qube in dom0.
|
||||
#
|
||||
# Run this in dom0. It creates /etc/qubes/policy.d/40-nsigner.policy
|
||||
# and tags the nostr_signer qube with 'nsigner-signer'.
|
||||
#
|
||||
# Usage:
|
||||
# bash setup_dom0.sh [signer_qube_name]
|
||||
#
|
||||
# Default signer qube name is "nostr_signer".
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SIGNER_QUBE="${1:-nostr_signer}"
|
||||
|
||||
echo "=== n_signer dom0 setup ==="
|
||||
echo "Signer qube: ${SIGNER_QUBE}"
|
||||
echo
|
||||
|
||||
# Create policy directory if it doesn't exist
|
||||
sudo mkdir -p /etc/qubes/policy.d
|
||||
|
||||
# Write the qrexec policy
|
||||
# The 'ai' qube is allowed without a dom0 popup (memory isolation is the
|
||||
# real security boundary; the signer's own approval prompt is the gate).
|
||||
# Any other qube gets a dom0 confirmation popup.
|
||||
echo "Installing qrexec policy to /etc/qubes/policy.d/40-nsigner.policy..."
|
||||
echo "# Qubes OS qrexec policy for nsigner" | sudo tee /etc/qubes/policy.d/40-nsigner.policy > /dev/null
|
||||
echo "qubes.NsignerRpc * ai @tag:nsigner-signer allow target=${SIGNER_QUBE}" | sudo tee -a /etc/qubes/policy.d/40-nsigner.policy > /dev/null
|
||||
echo "qubes.NsignerRpc * @anyvm @tag:nsigner-signer ask default_target=${SIGNER_QUBE}" | sudo tee -a /etc/qubes/policy.d/40-nsigner.policy > /dev/null
|
||||
echo "qubes.NsignerRpc * @anyvm @anyvm deny" | sudo tee -a /etc/qubes/policy.d/40-nsigner.policy > /dev/null
|
||||
|
||||
echo "Policy installed. Contents:"
|
||||
cat /etc/qubes/policy.d/40-nsigner.policy
|
||||
echo
|
||||
|
||||
# Tag the signer qube
|
||||
echo "Tagging ${SIGNER_QUBE} with 'nsigner-signer'..."
|
||||
qvm-tags "${SIGNER_QUBE}" add nsigner-signer
|
||||
echo "Tag added."
|
||||
echo
|
||||
|
||||
# Verify
|
||||
echo "=== Verification ==="
|
||||
echo "Policy file:"
|
||||
ls -l /etc/qubes/policy.d/40-nsigner.policy
|
||||
echo
|
||||
echo "Tags on ${SIGNER_QUBE}:"
|
||||
qvm-tags "${SIGNER_QUBE}" list 2>/dev/null | grep nsigner || echo "(tag not found — check qube name)"
|
||||
echo
|
||||
echo "=== dom0 setup complete ==="
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo " 1. In ${SIGNER_QUBE}: run setup_signer_qube.sh"
|
||||
echo " 2. In ${SIGNER_QUBE}: start the persistent signer:"
|
||||
echo " ~/.local/bin/nsigner --listen unix --socket-name nsigner --bridge-source-trusted"
|
||||
echo " 3. In caller qube: node examples/n_signer_qube_example_qrexec.js ${SIGNER_QUBE}"
|
||||
68
packaging/qubes/setup_signer_qube.sh
Executable file
68
packaging/qubes/setup_signer_qube.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# setup_signer_qube.sh — install nsigner v0.0.34+ and the qrexec bridge service
|
||||
# in the nostr_signer qube.
|
||||
#
|
||||
# Run this in the nostr_signer qube (as the regular user, not root).
|
||||
# It will:
|
||||
# 1. Run install_nsigner.sh to download the latest nsigner release (v0.0.34+)
|
||||
# 2. Install the qubes.NsignerRpc qrexec service (bridge mode)
|
||||
# 3. Print instructions for starting the persistent signer
|
||||
#
|
||||
# Usage:
|
||||
# bash setup_signer_qube.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NSIGNER_BIN="${HOME}/.local/bin/nsigner"
|
||||
SERVICE_DST="/etc/qubes-rpc/qubes.NsignerRpc"
|
||||
|
||||
echo "=== n_signer qube setup ==="
|
||||
echo
|
||||
|
||||
# Step 1: Update nsigner binary
|
||||
if [ -f "${HOME}/install_nsigner.sh" ]; then
|
||||
echo "Step 1: Running install_nsigner.sh to update nsigner..."
|
||||
bash "${HOME}/install_nsigner.sh"
|
||||
else
|
||||
echo "Step 1: install_nsigner.sh not found in ${HOME}."
|
||||
echo " Make sure install_nsigner.sh is in your home directory first."
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
|
||||
# Verify the bridge subcommand exists
|
||||
echo "Verifying bridge subcommand..."
|
||||
if ! "${NSIGNER_BIN}" bridge --help > /dev/null 2>&1; then
|
||||
echo "ERROR: nsigner does not have the 'bridge' subcommand."
|
||||
echo " The installed version may be too old. Expected v0.0.34 or later."
|
||||
"${NSIGNER_BIN}" --version
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: bridge subcommand available."
|
||||
"${NSIGNER_BIN}" --version
|
||||
echo
|
||||
|
||||
# Step 2: Install the qrexec service
|
||||
echo "Step 2: Installing qrexec service to ${SERVICE_DST}..."
|
||||
echo '#!/bin/sh' | sudo tee "${SERVICE_DST}" > /dev/null
|
||||
echo '# qubes.NsignerRpc — stateless bridge to persistent nsigner unix socket' | sudo tee -a "${SERVICE_DST}" > /dev/null
|
||||
echo '# No mnemonic, no secrets. Relays QREXEC_REMOTE_DOMAIN as source-qube preamble.' | sudo tee -a "${SERVICE_DST}" > /dev/null
|
||||
echo "exec ${NSIGNER_BIN} bridge --to nsigner" | sudo tee -a "${SERVICE_DST}" > /dev/null
|
||||
sudo chmod 0755 "${SERVICE_DST}"
|
||||
|
||||
echo "Service installed. Contents:"
|
||||
cat "${SERVICE_DST}"
|
||||
echo
|
||||
|
||||
echo "=== Signer qube setup complete ==="
|
||||
echo
|
||||
echo "To start the persistent signer (mnemonic entered at terminal, in mlock'd RAM):"
|
||||
echo
|
||||
echo " ${NSIGNER_BIN} --listen unix --socket-name nsigner --bridge-source-trusted"
|
||||
echo
|
||||
echo "The signer will prompt you for the mnemonic. It stays running with the"
|
||||
echo "mnemonic in RAM only — nothing on disk. Leave it running in a terminal."
|
||||
echo
|
||||
echo "Then from a caller qube (e.g. 'ai'):"
|
||||
echo " node examples/n_signer_qube_example_qrexec.js nostr_signer"
|
||||
143
plans/interactive_transport_selection.md
Normal file
143
plans/interactive_transport_selection.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Plan: Interactive multi-transport selection at startup
|
||||
|
||||
Status: design / ready for review.
|
||||
|
||||
Related:
|
||||
- [`README.md`](../README.md) §7 Transport, §8.3 Qubes OS qube, §9 Usage
|
||||
- [`src/main.c`](../src/main.c) — argument parsing, main loop, TUI
|
||||
- [`src/server.c`](../src/server.c) — `server_ctx_t`, `server_start`, `server_handle_one`
|
||||
- [`plans/qrexec_persistent_bridge.md`](qrexec_persistent_bridge.md) — bridge design
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
The current CLI has many transport flags (`--listen`, `--socket-name`, `--bridge-source-trusted`, `--auth`, `--allow-all`) that the user must know in advance. For interactive use, this is unfriendly — the user just wants to answer "how should other programs reach this signer?"
|
||||
|
||||
Additionally, the user may want **multiple transports active simultaneously** (e.g. unix socket for local clients + TCP for FIPS mesh clients + qrexec bridge for other qubes). Today only one `--listen` mode is supported at a time.
|
||||
|
||||
---
|
||||
|
||||
## 2. Design
|
||||
|
||||
### 2.1 Interactive transport menu at startup
|
||||
|
||||
After mnemonic entry and before the running phase, if no `--listen` flag was given (i.e. interactive mode), present a multi-select menu:
|
||||
|
||||
```text
|
||||
Transport — how should other programs reach this signer?
|
||||
Select one or more (space to toggle, enter to confirm):
|
||||
|
||||
[x] 1. Local Unix socket (same machine/qube)
|
||||
[ ] 2. Qubes qrexec bridge (other qubes via qrexec, no network)
|
||||
[ ] 3. TCP listener (FIPS mesh or local network)
|
||||
[ ] 4. Qrexec one-shot (legacy, one request per invocation)
|
||||
|
||||
[a] select all
|
||||
|
||||
(at least one must be selected)
|
||||
```
|
||||
|
||||
Each selected transport gets configured with sensible defaults:
|
||||
|
||||
| Choice | What it starts | Defaults |
|
||||
|---|---|---|
|
||||
| 1. Unix socket | `--listen unix` | socket name `nsigner` (or random if collision) |
|
||||
| 2. Qrexec bridge | `--listen unix --bridge-source-trusted` | socket name `nsigner`, accepts qrexec preamble |
|
||||
| 3. TCP | `--listen tcp:[::]:8080` | bind all interfaces, port 8080 |
|
||||
| 4. Qrexec one-shot | `--listen qrexec` | one request via stdin, then exit |
|
||||
|
||||
**Choices 1 and 2 can coexist** — they're both unix listeners, just with different identity handling. Choice 2 implies choice 1's socket. If both are selected, the bridge-source-trusted flag is set on the single unix listener (it handles both local and bridge connections).
|
||||
|
||||
**Choice 3 (TCP) can coexist with 1+2** — it's a separate listener on a different fd.
|
||||
|
||||
**Choice 4 (qrexec one-shot) is mutually exclusive** with all others — it uses stdin/stdout and exits after one request. If selected alone, it runs the one-shot path. If selected with others, it's ignored with a warning (or: the user is told it can't combine).
|
||||
|
||||
### 2.2 Multi-listener architecture
|
||||
|
||||
Currently `main()` creates one `server_ctx_t` and polls `server.listen_fd` + `STDIN_FILENO`. To support multiple listeners:
|
||||
|
||||
- Create an array of `server_ctx_t` (up to 3: unix, tcp, and optionally a second unix for bridge — though 1+2 collapse into one).
|
||||
- Each `server_ctx_t` gets its own `server_start()`.
|
||||
- The poll loop expands to poll all listener fds + STDIN_FILENO.
|
||||
- When a listener fd has activity, call `server_handle_one()` on that context.
|
||||
- The TUI status display shows all active transports.
|
||||
|
||||
```text
|
||||
Connections
|
||||
listen: unix @nsigner (bridge-source-trusted)
|
||||
listen: tcp [::]:8080
|
||||
client: nsigner --socket-name nsigner client '<json>'
|
||||
```
|
||||
|
||||
### 2.3 Coexistence with CLI flags
|
||||
|
||||
- If `--listen` is given on the command line, **skip the interactive menu** (automation/scripting path unchanged).
|
||||
- If no `--listen` is given and stdin is a TTY, **show the interactive menu**.
|
||||
- If no `--listen` is given and stdin is NOT a TTY, **default to unix socket** (current behavior, for `--mnemonic-stdin` / `--mnemonic-fd` supervised launches).
|
||||
|
||||
This preserves backward compatibility: all existing flags and scripts work unchanged. The menu is purely an interactive convenience.
|
||||
|
||||
### 2.4 TUI implementation
|
||||
|
||||
The menu uses the existing TUI rendering helpers (`tui_render_content_screen`, `tui_print`). It appears after mnemonic acceptance and before the status display. Navigation:
|
||||
|
||||
- `1`-`4` or space: toggle a choice
|
||||
- `a`: select all (except 4, which is mutually exclusive)
|
||||
- `a`: select all (except 4, which is mutually exclusive)
|
||||
- `enter`: confirm and proceed to running phase (at least one must be selected)
|
||||
|
||||
After confirmation, the selected listeners are started and the status display renders.
|
||||
|
||||
### 2.5 Security considerations
|
||||
|
||||
- The menu is purely a UX layer — it doesn't change the security model. Each transport still goes through the same policy/approval pipeline.
|
||||
- `--bridge-source-trusted` is only set if the user explicitly selects the qrexec bridge option. It's never silently enabled.
|
||||
- TCP listener still requires auth envelopes (existing behavior).
|
||||
- The menu doesn't offer `--allow-all`; that remains a CLI flag for users who want it.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation checklist
|
||||
|
||||
### 3.1 Multi-listener support in `main.c`
|
||||
- [ ] Replace single `server_ctx_t server` with an array (e.g. `server_ctx_t servers[3]`).
|
||||
- [ ] Add a `server_count` variable.
|
||||
- [ ] Expand the poll loop to poll all `servers[i].listen_fd` + STDIN_FILENO.
|
||||
- [ ] On activity, call `server_handle_one(&servers[i], ...)` for the matching fd.
|
||||
- [ ] `server_stop()` all on shutdown.
|
||||
|
||||
### 3.2 Interactive menu
|
||||
- [ ] Add `prompt_transport_selection()` function in `main.c` (after mnemonic load, before server start).
|
||||
- [ ] Returns a bitmask of selected transports.
|
||||
- [ ] Only called when `listen_mode` was not set by CLI and stdin is a TTY.
|
||||
- [ ] Uses `tui_render_content_screen` + `read_line_stdin` for input.
|
||||
|
||||
### 3.3 Transport setup from menu selection
|
||||
- [ ] Unix: `server_init` + `server_start` with `NSIGNER_LISTEN_UNIX`, socket name `nsigner`.
|
||||
- [ ] Qrexec bridge: same as unix + `server_set_bridge_source_trusted(&server, 1)`.
|
||||
- [ ] TCP: `server_init` + `server_start` with `NSIGNER_LISTEN_TCP`, target `tcp:[::]:8080`.
|
||||
- [ ] Qrexec one-shot: existing `NSIGNER_LISTEN_QREXEC` path (stdin, one request, exit).
|
||||
|
||||
### 3.4 TUI status display
|
||||
- [ ] Update `render_status()` to show multiple active listeners.
|
||||
- [ ] Show `(bridge-source-trusted)` annotation on unix listeners that have it.
|
||||
|
||||
### 3.5 Testing
|
||||
- [ ] Interactive: start nsigner with no `--listen`, select unix+TCP, verify both listeners work.
|
||||
- [ ] Interactive: select qrexec bridge, verify bridge connections get `qubes:<vm>` identity.
|
||||
- [ ] CLI: `--listen tcp:[::]:8080` still works (skips menu).
|
||||
- [ ] CLI: `--listen unix --bridge-source-trusted` still works (skips menu).
|
||||
- [ ] Non-TTY: `--mnemonic-stdin` without `--listen` defaults to unix (no menu).
|
||||
|
||||
---
|
||||
|
||||
## 4. Open questions
|
||||
|
||||
1. **Should the menu remember the last selection?** n_signer has no config files (by design). We could store the preference in an env var or a `/tmp` file, but that violates the zero-filesystem-footprint principle. **Decision: no persistence — the menu appears fresh each time.**
|
||||
|
||||
2. **Should there be a hotkey to add/remove transports at runtime?** E.g. press `t` in the TUI to bring up the transport menu again. This is a nice-to-have but adds complexity. **Decision: defer — the menu is startup-only for now.**
|
||||
|
||||
3. **TCP port selection in the menu?** The menu could ask for a port number if TCP is selected. **Decision: default to 8080, let the user override with `--listen tcp:...` if they need a different port. Keep the menu simple.**
|
||||
|
||||
4. **Socket name selection?** Same — default to `nsigner`, override with `--socket-name` if needed. **Decision: default, no prompt.**
|
||||
123
plans/nostr_core_lib_client_updates.md
Normal file
123
plans/nostr_core_lib_client_updates.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Plan: nostr_core_lib client updates for new n_signer features
|
||||
|
||||
Status: design / ready for review.
|
||||
|
||||
Related:
|
||||
- [`../nostr_core_lib/nostr_core/nsigner_transport.h`](../../nostr_core_lib/nostr_core/nsigner_transport.h) — transport vtable
|
||||
- [`../nostr_core_lib/nostr_core/nsigner_client.h`](../../nostr_core_lib/nostr_core/nsigner_client.h) — low-level client
|
||||
- [`../nostr_core_lib/nostr_core/nostr_signer.h`](../../nostr_core_lib/nostr_core/nostr_signer.h) — high-level signer
|
||||
- [`../nostr_core_lib/nostr_core/nostr_signer.c`](../../nostr_core_lib/nostr_core/nostr_signer.c) — `signer_remote_params_with_selector`
|
||||
- [`plans/qrexec_persistent_bridge.md`](qrexec_persistent_bridge.md) — qrexec bridge design
|
||||
- [`plans/interactive_transport_selection.md`](interactive_transport_selection.md) — index whitelist
|
||||
|
||||
---
|
||||
|
||||
## 1. Gaps identified
|
||||
|
||||
The `nostr_core_lib` client stack was built before the recent n_signer features. Three gaps need filling:
|
||||
|
||||
### 1.1 No `nostr_index` selector in the high-level API
|
||||
|
||||
`signer_remote_params_with_selector()` ([`nostr_signer.c:287`](../../nostr_core_lib/nostr_core/nostr_signer.c:287)) only emits `{"role":"..."}`. It does not support `{"nostr_index":N}`, which is n_signer's primary key-selection mechanism for Nostr identities (NIP-06 `m/44'/1237'/N'/0/0`).
|
||||
|
||||
The `nostr_signer_nsigner_*` constructors take a `const char* role` parameter. There's no way to say "use nostr_index 3" through the high-level API. Callers who want a specific Nostr identity by index must drop down to the low-level `nsigner_client_call` and build params manually — which is exactly what [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) and [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) do.
|
||||
|
||||
### 1.2 No qrexec transport
|
||||
|
||||
`nsigner_transport_open_*` supports unix, tcp, serial, fds — but not qrexec. On Qubes OS, the primary inter-qube path is `qrexec-client-vm <target> qubes.NsignerRpc`, which pipes framed I/O through stdin/stdout of a subprocess. There's no transport that wraps this.
|
||||
|
||||
### 1.3 No error code mapping for `index_not_allowed` (2002)
|
||||
|
||||
n_signer now returns `{"error":{"code":2002,"message":"index_not_allowed"}}` when the index whitelist denies a request. The client should surface this distinctly from `policy_denied` (2001) so callers can tell "this index isn't whitelisted" vs "this caller isn't approved".
|
||||
|
||||
---
|
||||
|
||||
## 2. Proposed changes
|
||||
|
||||
### 2.1 Add `nostr_index` selector to the high-level API
|
||||
|
||||
**Option A (minimal):** Add a new constructor variant that takes `nostr_index` instead of `role`:
|
||||
|
||||
```c
|
||||
nostr_signer_t* nostr_signer_nsigner_unix_index(const char* socket_name, int nostr_index, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp_index(const char* host, int port, int nostr_index, int timeout_ms);
|
||||
/* etc. */
|
||||
```
|
||||
|
||||
**Option B (cleaner):** Replace the `role` string parameter with a selector struct:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
const char* role; /* NULL = use default */
|
||||
int nostr_index; /* -1 = not set */
|
||||
const char* role_path; /* NULL = not set */
|
||||
} nsigner_selector_t;
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const nsigner_selector_t* selector, int timeout_ms);
|
||||
```
|
||||
|
||||
**Option C (additive, chosen):** Keep existing constructors, add a setter for nostr_index:
|
||||
|
||||
```c
|
||||
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index);
|
||||
```
|
||||
|
||||
This sets the index on the remote backend, and `signer_remote_params_with_selector` emits `{"nostr_index":N}` instead of `{"role":"..."}` when set. Existing callers using `role` are unaffected.
|
||||
|
||||
**Decision: Option C** — additive, no API break, smallest change.
|
||||
|
||||
### 2.2 Add qrexec transport
|
||||
|
||||
New transport constructor:
|
||||
|
||||
```c
|
||||
nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms);
|
||||
```
|
||||
|
||||
Implementation: `fork()` + `execvp("qrexec-client-vm", [target_qube, service_name])` with `stdin`/`stdout` pipes. The `send_framed`/`recv_framed` vtable methods write/read through the pipes. `reconnect` re-spawns the subprocess (qrexec handles one request per invocation, so reconnect is mandatory before each call — same as the existing per-request reconnect pattern).
|
||||
|
||||
The transport is stateless per call: each `reconnect` spawns a fresh `qrexec-client-vm` process, sends one framed request, reads one framed response, and the process exits. This matches the qrexec execution model.
|
||||
|
||||
### 2.3 Add `nostr_signer_nsigner_qrexec` high-level constructor
|
||||
|
||||
```c
|
||||
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms);
|
||||
```
|
||||
|
||||
Wraps `nsigner_transport_open_qrexec` + `nostr_signer_nsigner_from_transport`. No auth envelope needed — qrexec identity comes from `QREXEC_REMOTE_DOMAIN` on the server side.
|
||||
|
||||
### 2.4 Surface `index_not_allowed` error distinctly
|
||||
|
||||
Add to `nostr_common.h`:
|
||||
|
||||
```c
|
||||
#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002
|
||||
```
|
||||
|
||||
In `nsigner_client_call` (or `nostr_signer` remote backend), when the response error code is 2002, map it to `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` so callers can distinguish it from generic policy denial.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation checklist
|
||||
|
||||
In `nostr_core_lib`:
|
||||
|
||||
- [ ] `nsigner_transport.h` / `nsigner_transport.c`: add `nsigner_transport_open_qrexec(target_qube, service_name, timeout_ms)` — fork+exec `qrexec-client-vm`, pipe-based framed I/O, reconnect re-spawns.
|
||||
- [ ] `nostr_signer.h` / `nostr_signer.c`: add `nostr_signer_nsigner_qrexec(target_qube, service_name, role, timeout_ms)`.
|
||||
- [ ] `nostr_signer.c`: add `nostr_signer_nsigner_set_nostr_index(signer, nostr_index)`; update `signer_remote_params_with_selector` to emit `{"nostr_index":N}` when index is set.
|
||||
- [ ] `nostr_common.h`: add `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` error code.
|
||||
- [ ] `nsigner_client.c`: map error code 2002 to the new constant.
|
||||
- [ ] `NSIGNER_INTEGRATION.md`: document the qrexec transport, nostr_index selector, and index_not_allowed error.
|
||||
|
||||
In `n_signer` (this repo):
|
||||
|
||||
- [ ] Update [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) to optionally use the high-level `nostr_signer` API with `nostr_index` selector (demonstrates the new client API).
|
||||
- [ ] Add a C example for the qrexec path using the new `nostr_signer_nsigner_qrexec` constructor.
|
||||
|
||||
---
|
||||
|
||||
## 4. Open questions
|
||||
|
||||
1. **Should the qrexec transport live in `nostr_core_lib` or in a Qubes-specific addon?** The transport is only useful on Qubes OS, but `nostr_core_lib` is meant to be cross-platform. **Decision: put it in `nostr_core_lib` behind `NOSTR_ENABLE_NSIGNER_CLIENT` — it just won't work on non-Qubes hosts (execvp fails). No new feature flag needed.**
|
||||
|
||||
2. **Should `nostr_signer_nsigner_set_nostr_index` override `role` or coexist?** n_signer rejects ambiguous selectors (both `role` and `nostr_index` set = `ambiguous_role_selector`). **Decision: setting nostr_index clears role, and vice versa. The setter is exclusive.**
|
||||
318
src/main.c
318
src/main.c
@@ -412,6 +412,9 @@ typedef struct {
|
||||
} caller_identity_t;
|
||||
|
||||
/* Server context */
|
||||
#define INDEX_WHITELIST_MAX 256
|
||||
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8)
|
||||
|
||||
typedef struct {
|
||||
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
|
||||
char last_error[256];
|
||||
@@ -424,6 +427,9 @@ typedef struct {
|
||||
int socket_name_explicit;
|
||||
int auth_mode;
|
||||
int auth_skew_seconds;
|
||||
int bridge_source_trusted;
|
||||
int index_whitelist_active;
|
||||
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE];
|
||||
} server_ctx_t;
|
||||
|
||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
||||
@@ -457,6 +463,9 @@ void server_set_prompt_always_allow(int enabled);
|
||||
/* Mark the unix listener as a trusted bridge socket (qrexec source preamble) */
|
||||
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
|
||||
|
||||
/* Set the nostr_index whitelist from a spec string ("all", "1,3,4", "0-3", "0-3,7,9") */
|
||||
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec);
|
||||
|
||||
/* Configure non-interactive prompt fallback: -1 disabled, POLICY_ALLOW, or POLICY_DENY */
|
||||
void server_set_noninteractive_prompt_default(int decision);
|
||||
|
||||
@@ -494,8 +503,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 34
|
||||
#define NSIGNER_VERSION "v0.0.34"
|
||||
#define NSIGNER_VERSION_PATCH 42
|
||||
#define NSIGNER_VERSION "v0.0.42"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
@@ -799,6 +808,8 @@ static void print_usage(const char *program_name) {
|
||||
tui_print(" --mnemonic-fd N Read mnemonic from inherited fd N (one line) at startup");
|
||||
tui_print(" --allow-all, -A Allow all policy prompts for this server session");
|
||||
tui_print(" --bridge-source-trusted Accept qrexec_source preamble on unix connections (bridge mode)");
|
||||
tui_print(" --allow-index SPEC Restrict which nostr_index values this session can access");
|
||||
tui_print(" SPEC: 'all' (default), '1,3,4', '0-3', or '0-3,7,9'");
|
||||
}
|
||||
|
||||
static int extract_nsigner_socket_from_proc_line(const char *line,
|
||||
@@ -1591,6 +1602,128 @@ static void apply_test_overrides(policy_table_t *policy) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Transport selection bitmask for interactive menu */
|
||||
#define TRANSPORT_UNIX 0x01
|
||||
#define TRANSPORT_QREXEC_BRIDGE 0x02
|
||||
#define TRANSPORT_TCP 0x04
|
||||
#define TRANSPORT_QREXEC_ONESHOT 0x08
|
||||
|
||||
/*
|
||||
* Interactive transport selection menu.
|
||||
* Shown after mnemonic entry when no --listen flag was given and stdin is a TTY.
|
||||
* Returns a bitmask of selected transports, or 0 on error/no selection.
|
||||
*/
|
||||
static int prompt_transport_selection(void) {
|
||||
int selected = TRANSPORT_UNIX; /* default: local unix socket */
|
||||
char input[32];
|
||||
|
||||
for (;;) {
|
||||
tui_render_content_screen(NULL, "Transport — how should other programs reach this signer?");
|
||||
printf("Select one or more (type a number to toggle, 'a' for all, Enter to confirm):\n\n");
|
||||
printf(" [%s] 1. Local Unix socket (same machine/qube)\n",
|
||||
(selected & TRANSPORT_UNIX) ? "x" : " ");
|
||||
printf(" [%s] 2. Qubes qrexec bridge (other qubes via qrexec, no network)\n",
|
||||
(selected & TRANSPORT_QREXEC_BRIDGE) ? "x" : " ");
|
||||
printf(" [%s] 3. TCP listener (FIPS mesh or local network)\n",
|
||||
(selected & TRANSPORT_TCP) ? "x" : " ");
|
||||
printf("\n [a] select all Enter = confirm\n");
|
||||
printf("> ");
|
||||
fflush(stdout);
|
||||
|
||||
if (read_line_stdin(input, sizeof(input)) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Trim trailing whitespace */
|
||||
{
|
||||
size_t len = strlen(input);
|
||||
while (len > 0 && (input[len-1] == '\n' || input[len-1] == '\r' ||
|
||||
input[len-1] == ' ' || input[len-1] == '\t')) {
|
||||
input[--len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
if (input[0] == '\0') {
|
||||
/* Enter pressed — confirm current selection */
|
||||
if (selected == 0) {
|
||||
printf("At least one transport must be selected.\n");
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'a' || input[0] == 'A') {
|
||||
selected = TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE | TRANSPORT_TCP;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (input[0] == '1') {
|
||||
selected ^= TRANSPORT_UNIX;
|
||||
} else if (input[0] == '2') {
|
||||
selected ^= TRANSPORT_QREXEC_BRIDGE;
|
||||
} else if (input[0] == '3') {
|
||||
selected ^= TRANSPORT_TCP;
|
||||
} else {
|
||||
printf("Invalid input. Type 1-3, 'a', or Enter to confirm.\n");
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
/*
|
||||
* Interactive index whitelist prompt.
|
||||
* Shown after transport selection when no --allow-index flag was given.
|
||||
* Returns a malloc'd spec string (caller must free), or NULL for "all".
|
||||
*/
|
||||
static char *prompt_index_whitelist(void) {
|
||||
char input[256];
|
||||
|
||||
for (;;) {
|
||||
tui_render_content_screen(NULL, "Index whitelist — restrict which nostr_index values this session can access");
|
||||
printf("Enter allowed indices, or press Enter for 'all' (no restriction):\n\n");
|
||||
printf(" Examples:\n");
|
||||
printf(" all (default — allow all indices)\n");
|
||||
printf(" 0 (only index 0)\n");
|
||||
printf(" 0,1,3 (specific indices)\n");
|
||||
printf(" 0-3 (range 0 through 3)\n");
|
||||
printf(" 0,2-3,7 (mixed list and ranges)\n");
|
||||
printf("\n Enter = all\n");
|
||||
printf("> ");
|
||||
fflush(stdout);
|
||||
|
||||
if (read_line_stdin(input, sizeof(input)) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Trim trailing whitespace */
|
||||
{
|
||||
size_t len = strlen(input);
|
||||
while (len > 0 && (input[len-1] == '\n' || input[len-1] == '\r' ||
|
||||
input[len-1] == ' ' || input[len-1] == '\t')) {
|
||||
input[--len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
if (input[0] == '\0') {
|
||||
/* Enter pressed — default to "all" */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Validate by parsing into a temp whitelist */
|
||||
{
|
||||
server_ctx_t tmp;
|
||||
memset(&tmp, 0, sizeof(tmp));
|
||||
if (server_set_index_whitelist(&tmp, input) != 0) {
|
||||
printf("Invalid spec: '%s'. Try again or press Enter for 'all'.\n", input);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return strdup(input);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
mnemonic_state_t mnemonic;
|
||||
role_table_t role_table;
|
||||
@@ -1598,13 +1731,14 @@ int main(int argc, char *argv[]) {
|
||||
policy_table_t policy;
|
||||
server_ctx_t server;
|
||||
key_store_t key_store;
|
||||
struct pollfd pfds[2];
|
||||
struct pollfd pfds[3]; /* max: 2 listeners (unix+tcp) + stdin */
|
||||
uid_t owner_uid;
|
||||
int derived_count;
|
||||
const char *socket_name = NSIGNER_DEFAULT_SOCKET_NAME;
|
||||
char generated_socket_name[SERVER_SOCKET_NAME_MAX];
|
||||
int socket_name_explicit = 0;
|
||||
int listen_mode = NSIGNER_LISTEN_UNIX;
|
||||
int listen_mode_explicit = 0;
|
||||
const char *listen_target = NSIGNER_DEFAULT_SOCKET_NAME;
|
||||
int argi = 1;
|
||||
const char *preapprove_specs[POLICY_MAX_ENTRIES];
|
||||
@@ -1613,6 +1747,7 @@ int main(int argc, char *argv[]) {
|
||||
int auth_skew_seconds = AUTH_DEFAULT_SKEW_SECONDS;
|
||||
int allow_all = 0;
|
||||
int bridge_source_trusted = 0;
|
||||
const char *allow_index_spec = NULL;
|
||||
mnemonic_source_t mnemonic_source = { MNEMONIC_SOURCE_TUI, -1 };
|
||||
|
||||
while (argi < argc) {
|
||||
@@ -1633,6 +1768,7 @@ int main(int argc, char *argv[]) {
|
||||
fprintf(stderr, "Missing value for %s\n", argv[argi]);
|
||||
return 1;
|
||||
}
|
||||
listen_mode_explicit = 1;
|
||||
if (strcmp(argv[argi + 1], "unix") == 0) {
|
||||
listen_mode = NSIGNER_LISTEN_UNIX;
|
||||
} else if (strcmp(argv[argi + 1], "stdio") == 0) {
|
||||
@@ -1722,6 +1858,15 @@ int main(int argc, char *argv[]) {
|
||||
argi += 1;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(argv[argi], "--allow-index") == 0) {
|
||||
if (argi + 1 >= argc) {
|
||||
fprintf(stderr, "Missing value for %s\n", argv[argi]);
|
||||
return 1;
|
||||
}
|
||||
allow_index_spec = argv[argi + 1];
|
||||
argi += 2;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1853,6 +1998,59 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store);
|
||||
|
||||
/*
|
||||
* Interactive transport selection: if no --listen flag was given and
|
||||
* stdin is a TTY, show the multi-select transport menu. This lets the
|
||||
* user choose how other programs reach the signer without memorizing
|
||||
* CLI flags. CLI flags skip the menu (backward compatible).
|
||||
*/
|
||||
int transport_mask = 0;
|
||||
int multi_listen = 0; /* set when multiple persistent listeners are active */
|
||||
server_ctx_t servers[2]; /* max: unix + tcp */
|
||||
int server_count = 0;
|
||||
int unix_server_idx = -1;
|
||||
int tcp_server_idx = -1;
|
||||
|
||||
if (!listen_mode_explicit && isatty(STDIN_FILENO) &&
|
||||
mnemonic_source.kind == MNEMONIC_SOURCE_TUI) {
|
||||
transport_mask = prompt_transport_selection();
|
||||
if (transport_mask == 0) {
|
||||
fprintf(stderr, "No transport selected. Exiting.\n");
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Index whitelist prompt (only if --allow-index wasn't given on CLI) */
|
||||
if (allow_index_spec == NULL) {
|
||||
char *wl_spec = prompt_index_whitelist();
|
||||
if (wl_spec != NULL) {
|
||||
allow_index_spec = wl_spec; /* will be freed at program exit */
|
||||
}
|
||||
}
|
||||
|
||||
/* Persistent listeners — configure from menu selection */
|
||||
multi_listen = 1;
|
||||
|
||||
/* Unix socket (with or without bridge-source-trusted) */
|
||||
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
|
||||
/* Use fixed name "nsigner" for scripted access */
|
||||
socket_name = "nsigner";
|
||||
socket_name_explicit = 1;
|
||||
listen_mode = NSIGNER_LISTEN_UNIX;
|
||||
listen_target = socket_name;
|
||||
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
|
||||
bridge_source_trusted = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* TCP listener */
|
||||
if (transport_mask & TRANSPORT_TCP) {
|
||||
/* Will be started as a second listener */
|
||||
}
|
||||
}
|
||||
|
||||
if (listen_mode == NSIGNER_LISTEN_UNIX && !socket_name_explicit) {
|
||||
if (socket_name_random(generated_socket_name, sizeof(generated_socket_name)) != 0) {
|
||||
fprintf(stderr, "Failed to generate random socket name\n");
|
||||
@@ -1898,6 +2096,16 @@ int main(int argc, char *argv[]) {
|
||||
if (bridge_source_trusted) {
|
||||
server_set_bridge_source_trusted(&server, 1);
|
||||
}
|
||||
if (allow_index_spec != NULL) {
|
||||
if (server_set_index_whitelist(&server, allow_index_spec) != 0) {
|
||||
fprintf(stderr, "Invalid --allow-index spec: %s\n", allow_index_spec);
|
||||
fprintf(stderr, "Expected: 'all', '1,3,4', '0-3', or '0-3,7,9'\n");
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (server_start(&server) != 0) {
|
||||
if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server));
|
||||
@@ -1914,6 +2122,44 @@ int main(int argc, char *argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Multi-listener: if interactive menu selected TCP in addition to unix,
|
||||
* start a second server context for TCP. The primary server (unix) is
|
||||
* already running. Both share the same dispatcher and policy.
|
||||
*/
|
||||
if (multi_listen && (transport_mask & TRANSPORT_TCP)) {
|
||||
const char *tcp_target = "tcp:[::]:8080";
|
||||
tcp_server_idx = server_count;
|
||||
server_init(&servers[tcp_server_idx],
|
||||
tcp_target,
|
||||
0, /* socket_name_explicit = 0 for TCP */
|
||||
NSIGNER_LISTEN_TCP,
|
||||
NSIGNER_AUTH_REQUIRED,
|
||||
auth_skew_seconds,
|
||||
&dispatcher,
|
||||
&policy);
|
||||
if (allow_index_spec != NULL) {
|
||||
server_set_index_whitelist(&servers[tcp_server_idx], allow_index_spec);
|
||||
}
|
||||
if (server_start(&servers[tcp_server_idx]) != 0) {
|
||||
fprintf(stderr, "Failed to start TCP server on %s: %s\n",
|
||||
tcp_target, server_last_error(&servers[tcp_server_idx]));
|
||||
server_stop(&server);
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 1;
|
||||
}
|
||||
server_count++;
|
||||
}
|
||||
|
||||
/* Copy the primary (unix) server into the array for unified polling */
|
||||
if (multi_listen) {
|
||||
unix_server_idx = server_count;
|
||||
servers[unix_server_idx] = server; /* struct copy */
|
||||
server_count++;
|
||||
}
|
||||
|
||||
(void)signal(SIGINT, handle_signal);
|
||||
(void)signal(SIGTERM, handle_signal);
|
||||
/*
|
||||
@@ -1935,7 +2181,22 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
connection_info_clear();
|
||||
|
||||
if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
if (multi_listen) {
|
||||
/* Multi-listener mode: show all active transports */
|
||||
if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) {
|
||||
if (bridge_source_trusted) {
|
||||
connection_info_add("listen: unix @%s (bridge-source-trusted)", socket_name);
|
||||
} else {
|
||||
connection_info_add("listen: unix @%s", socket_name);
|
||||
}
|
||||
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
}
|
||||
if (transport_mask & TRANSPORT_TCP) {
|
||||
connection_info_add("listen: tcp [::]:8080");
|
||||
printf("System is ready and waiting for connections on tcp:[::]:8080.\n");
|
||||
}
|
||||
} else if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
connection_info_add("listen: unix @%s", socket_name);
|
||||
connection_info_add("client: nsigner --socket-name %s client '<json>'", socket_name);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
@@ -2013,13 +2274,26 @@ int main(int argc, char *argv[]) {
|
||||
tui_install_resize_handler();
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
|
||||
pfds[0].fd = server.listen_fd;
|
||||
pfds[0].events = POLLIN;
|
||||
pfds[1].fd = STDIN_FILENO;
|
||||
pfds[1].events = POLLIN;
|
||||
/* Set up poll fds: listener(s) + stdin */
|
||||
int npfds = 0;
|
||||
if (multi_listen) {
|
||||
for (int si = 0; si < server_count; si++) {
|
||||
pfds[npfds].fd = servers[si].listen_fd;
|
||||
pfds[npfds].events = POLLIN;
|
||||
npfds++;
|
||||
}
|
||||
} else {
|
||||
pfds[npfds].fd = server.listen_fd;
|
||||
pfds[npfds].events = POLLIN;
|
||||
npfds++;
|
||||
}
|
||||
pfds[npfds].fd = STDIN_FILENO;
|
||||
pfds[npfds].events = POLLIN;
|
||||
npfds++;
|
||||
int stdin_pfd_idx = npfds - 1;
|
||||
|
||||
while (g_running && server.running) {
|
||||
int prc = poll(pfds, 2, 200);
|
||||
int prc = poll(pfds, npfds, 200);
|
||||
if (prc < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
@@ -2031,15 +2305,31 @@ int main(int argc, char *argv[]) {
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
}
|
||||
|
||||
if (prc > 0 && (pfds[0].revents & POLLIN)) {
|
||||
/* Check all listener fds for activity */
|
||||
if (prc > 0) {
|
||||
if (multi_listen) {
|
||||
for (int si = 0; si < server_count; si++) {
|
||||
if (pfds[si].revents & POLLIN) {
|
||||
int hrc = server_handle_one(&servers[si], activity_log_cb, NULL);
|
||||
if (hrc < 0) {
|
||||
g_running = 0;
|
||||
break;
|
||||
}
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pfds[0].revents & POLLIN) {
|
||||
int hrc = server_handle_one(&server, activity_log_cb, NULL);
|
||||
if (hrc < 0) {
|
||||
break;
|
||||
}
|
||||
render_status(&role_table, &mnemonic, derived_count, socket_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prc > 0 && (pfds[1].revents & POLLIN)) {
|
||||
if (prc > 0 && (pfds[stdin_pfd_idx].revents & POLLIN)) {
|
||||
char ch = '\0';
|
||||
if (read(STDIN_FILENO, &ch, 1) > 0) {
|
||||
char lower = (char)tolower((unsigned char)ch);
|
||||
@@ -2083,7 +2373,13 @@ int main(int argc, char *argv[]) {
|
||||
server_set_approval_cb(NULL, NULL);
|
||||
}
|
||||
|
||||
if (multi_listen) {
|
||||
for (int si = 0; si < server_count; si++) {
|
||||
server_stop(&servers[si]);
|
||||
}
|
||||
} else {
|
||||
server_stop(&server);
|
||||
}
|
||||
crypto_wipe(&key_store);
|
||||
nostr_cleanup();
|
||||
mnemonic_unload(&mnemonic);
|
||||
|
||||
122
src/server.c
122
src/server.c
@@ -415,6 +415,9 @@ typedef struct {
|
||||
} caller_identity_t;
|
||||
|
||||
/* Server context */
|
||||
#define INDEX_WHITELIST_MAX 256 /* nostr_index range 0-255 */
|
||||
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) /* 32 bytes */
|
||||
|
||||
typedef struct {
|
||||
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
|
||||
char last_error[256];
|
||||
@@ -428,6 +431,8 @@ typedef struct {
|
||||
int auth_mode;
|
||||
int auth_skew_seconds;
|
||||
int bridge_source_trusted; /* when set, unix connections send a qrexec_source preamble */
|
||||
int index_whitelist_active; /* 1 if index whitelist is set (not "all") */
|
||||
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE]; /* bitmap of allowed nostr_index values */
|
||||
} server_ctx_t;
|
||||
|
||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
||||
@@ -444,6 +449,15 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp
|
||||
* identity is composed as qubes:<vm>. */
|
||||
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
|
||||
|
||||
/* Set the nostr_index whitelist from a parsed spec string.
|
||||
* Spec format: "all" (no restriction), or comma-separated list of
|
||||
* indices and ranges, e.g. "1,3,4" or "0-3" or "0-3,7,9".
|
||||
* Returns 0 on success, -1 on parse error. */
|
||||
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec);
|
||||
|
||||
/* Check if a nostr_index is in the whitelist. Returns 1 if allowed, 0 if not. */
|
||||
int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index);
|
||||
|
||||
/* Start listening. Returns 0 on success, -1 on error. */
|
||||
int server_start(server_ctx_t *ctx);
|
||||
|
||||
@@ -1058,6 +1072,8 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp
|
||||
ctx->auth_mode = auth_mode;
|
||||
ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS;
|
||||
ctx->bridge_source_trusted = 0;
|
||||
ctx->index_whitelist_active = 0;
|
||||
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
|
||||
if (!g_auth_nonce_cache_inited) {
|
||||
auth_nonce_cache_init(&g_auth_nonce_cache);
|
||||
g_auth_nonce_cache_inited = 1;
|
||||
@@ -1071,6 +1087,87 @@ void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled) {
|
||||
ctx->bridge_source_trusted = enabled ? 1 : 0;
|
||||
}
|
||||
|
||||
static void whitelist_set_bit(unsigned char *bitmap, int index) {
|
||||
if (index >= 0 && index < INDEX_WHITELIST_MAX) {
|
||||
bitmap[index / 8] |= (1 << (index % 8));
|
||||
}
|
||||
}
|
||||
|
||||
static int whitelist_get_bit(const unsigned char *bitmap, int index) {
|
||||
if (index < 0 || index >= INDEX_WHITELIST_MAX) {
|
||||
return 0;
|
||||
}
|
||||
return (bitmap[index / 8] >> (index % 8)) & 1;
|
||||
}
|
||||
|
||||
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec) {
|
||||
char buf[256];
|
||||
char *p;
|
||||
char *token;
|
||||
|
||||
if (ctx == NULL || spec == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* "all" means no restriction */
|
||||
if (strcmp(spec, "all") == 0) {
|
||||
ctx->index_whitelist_active = 0;
|
||||
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
|
||||
return 0;
|
||||
}
|
||||
|
||||
strncpy(buf, spec, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
|
||||
/* Parse comma-separated entries: "1,3,4" or "0-3" or "0-3,7,9" */
|
||||
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
|
||||
p = buf;
|
||||
while (p != NULL && *p != '\0') {
|
||||
char *comma = strchr(p, ',');
|
||||
if (comma != NULL) {
|
||||
*comma = '\0';
|
||||
}
|
||||
|
||||
/* Parse token: either "N" or "N-M" (range) */
|
||||
char *dash = strchr(p, '-');
|
||||
if (dash != NULL) {
|
||||
*dash = '\0';
|
||||
char *endptr1 = NULL, *endptr2 = NULL;
|
||||
long lo = strtol(p, &endptr1, 10);
|
||||
long hi = strtol(dash + 1, &endptr2, 10);
|
||||
if (*endptr1 != '\0' || *endptr2 != '\0' || lo < 0 || hi < 0 ||
|
||||
lo >= INDEX_WHITELIST_MAX || hi >= INDEX_WHITELIST_MAX || lo > hi) {
|
||||
return -1;
|
||||
}
|
||||
for (long i = lo; i <= hi; i++) {
|
||||
whitelist_set_bit(ctx->index_whitelist, (int)i);
|
||||
}
|
||||
} else {
|
||||
char *endptr = NULL;
|
||||
long idx = strtol(p, &endptr, 10);
|
||||
if (*endptr != '\0' || idx < 0 || idx >= INDEX_WHITELIST_MAX) {
|
||||
return -1;
|
||||
}
|
||||
whitelist_set_bit(ctx->index_whitelist, (int)idx);
|
||||
}
|
||||
|
||||
p = (comma != NULL) ? comma + 1 : NULL;
|
||||
}
|
||||
|
||||
ctx->index_whitelist_active = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index) {
|
||||
if (ctx == NULL) {
|
||||
return 1; /* no ctx = allow (shouldn't happen) */
|
||||
}
|
||||
if (!ctx->index_whitelist_active) {
|
||||
return 1; /* "all" mode = no restriction */
|
||||
}
|
||||
return whitelist_get_bit(ctx->index_whitelist, nostr_index);
|
||||
}
|
||||
|
||||
int server_start(server_ctx_t *ctx) {
|
||||
int fd;
|
||||
struct sockaddr_un addr;
|
||||
@@ -1569,6 +1666,24 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Index whitelist enforcement: if the request specifies a nostr_index
|
||||
* and the whitelist is active (not "all"), check that the index is
|
||||
* allowed. This is a hard deny — it happens before policy/approval,
|
||||
* so even an approved caller cannot access a non-whitelisted index.
|
||||
* We set hard_selector_error to a sentinel so the policy_check branch
|
||||
* is skipped.
|
||||
*/
|
||||
if (hard_selector_error == 0 && selector_req.has_nostr_index &&
|
||||
!server_index_whitelist_allows(ctx, selector_req.nostr_index)) {
|
||||
(void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index);
|
||||
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2002,\"message\":\"index_not_allowed\"}}");
|
||||
pchk = POLICY_DENY;
|
||||
verdict = "DENIED";
|
||||
source_label = "index-not-approved";
|
||||
hard_selector_error = -100; /* sentinel: skip policy_check */
|
||||
}
|
||||
|
||||
if (hard_selector_error == SELECTOR_ERR_AMBIGUOUS) {
|
||||
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}");
|
||||
pchk = POLICY_DENY;
|
||||
@@ -1578,9 +1693,11 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||
} else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) {
|
||||
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}");
|
||||
pchk = POLICY_DENY;
|
||||
} else {
|
||||
} else if (hard_selector_error == 0) {
|
||||
/* Normal path: run policy_check (skip if whitelist already denied) */
|
||||
pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose, &policy_src);
|
||||
}
|
||||
/* else: hard_selector_error == -100 (whitelist deny) — keep pchk=POLICY_DENY */
|
||||
|
||||
if (pchk == POLICY_PROMPT) {
|
||||
pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);
|
||||
@@ -1661,6 +1778,9 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
||||
source_label = "session-grant";
|
||||
} else if (pchk == POLICY_ALLOW) {
|
||||
source_label = "prompt";
|
||||
} else if (hard_selector_error == -100) {
|
||||
/* Index whitelist denial — keep the label set by the whitelist check */
|
||||
/* source_label already set to "index-not-approved" above */
|
||||
} else {
|
||||
source_label = "no-match";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user