diff --git a/README.md b/README.md index 8e9a074..e52f60c 100644 --- a/README.md +++ b/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:`). - `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 ` 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":""}` preamble before the request, and the caller identity is composed as `qubes:` — 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:` (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) diff --git a/install_qube_fips_nsigner.sh b/install_qube_fips_nsigner.sh index 5215d2f..85c4f29 100755 --- a/install_qube_fips_nsigner.sh +++ b/install_qube_fips_nsigner.sh @@ -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}" diff --git a/packaging/qubes/policy.d/40-nsigner.policy b/packaging/qubes/policy.d/40-nsigner.policy index 0ecd991..60ab582 100644 --- a/packaging/qubes/policy.d/40-nsigner.policy +++ b/packaging/qubes/policy.d/40-nsigner.policy @@ -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 diff --git a/packaging/qubes/setup_dom0.sh b/packaging/qubes/setup_dom0.sh new file mode 100755 index 0000000..6e4895e --- /dev/null +++ b/packaging/qubes/setup_dom0.sh @@ -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}" diff --git a/packaging/qubes/setup_signer_qube.sh b/packaging/qubes/setup_signer_qube.sh new file mode 100755 index 0000000..4f77903 --- /dev/null +++ b/packaging/qubes/setup_signer_qube.sh @@ -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" diff --git a/src/main.c b/src/main.c index 00e69b2..a96d924 100644 --- a/src/main.c +++ b/src/main.c @@ -494,8 +494,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 35 +#define NSIGNER_VERSION "v0.0.35" /* NSIGNER_HEADERLESS_DECLS_END */