diff --git a/README.md b/README.md index 8c1c4c4..bd2d47b 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ Discovery: - `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`. - `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:127.0.0.1:PORT` (or `tcp:[::1]:PORT`) enables loopback-only TCP listening for non-AF_UNIX clients. +- `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`). ### 7.2 ESP32 MCU: USB-CDC serial @@ -303,10 +303,10 @@ Generic stdio transport mode (single framed request over stdin/stdout): nsigner --listen stdio ``` -TCP loopback transport mode (no TUI; serves requests until terminated): +TCP transport mode (no TUI; serves requests until terminated): ```bash -nsigner --listen tcp:127.0.0.1:8080 +nsigner --listen tcp:[::]:8080 ``` ### 9.2 Send a request (client mode) diff --git a/documents/FIPS_DEPLOYMENT.md b/documents/FIPS_DEPLOYMENT.md index 1c46640..2373cfe 100644 --- a/documents/FIPS_DEPLOYMENT.md +++ b/documents/FIPS_DEPLOYMENT.md @@ -2,17 +2,17 @@ ## 1. Scope -This runbook covers a practical Tier-1 deployment of `nsigner` over a loopback TCP listener, with connectivity provided by FIPS as the network substrate. +This runbook covers a practical Tier-1 deployment of `nsigner` over a TCP listener, with connectivity provided by FIPS as the network substrate. Tier-1 objective: -- Keep `nsigner` transport simple (`--listen tcp:127.0.0.1:PORT` or `--listen tcp:[::1]:PORT`). +- Keep `nsigner` transport simple (`--listen tcp:IPv4:PORT` or `--listen tcp:[IPv6]:PORT`). - Use FIPS to carry traffic between peers. - Do not add FIPS runtime dependencies into `nsigner`. Out of scope in this document: -- Remote non-loopback TCP exposure (`--allow-remote`, TLS) planned for later phase. +- Mandatory transport-level TLS/authentication hardening (still planned for a later phase). - Automatic caller->npub enrichment from FIPS session metadata. --- @@ -22,17 +22,17 @@ Out of scope in this document: Two cooperating layers: 1. **Signer process layer** (`nsigner`) - - Listens on loopback TCP only. + - Listens on operator-selected TCP endpoint (IPv4 or IPv6). - Uses existing 4-byte big-endian framed JSON-RPC protocol. - Keeps existing policy/prompt behavior. 2. **Network substrate layer** (FIPS) - Establishes peer connectivity between nodes/qubes. - - Carries application traffic to a local loopback endpoint on each side. + - Carries application traffic to the configured signer TCP endpoint. Conceptually: -`client app -> local FIPS endpoint -> FIPS mesh -> remote FIPS endpoint -> 127.0.0.1:PORT -> nsigner` +`client app -> local FIPS endpoint -> FIPS mesh -> remote FIPS endpoint -> signer TCP endpoint -> nsigner` --- @@ -58,21 +58,21 @@ Operational assumptions: ## 4. Start signer in Tier-1 TCP mode -Run `nsigner` in loopback TCP listen mode: +Run `nsigner` in TCP listen mode: ```bash -./build/nsigner --listen tcp:127.0.0.1:8080 +./build/nsigner --listen tcp:[::]:8080 ``` -Or IPv6 loopback: +Or bind to a specific FIPS ULA address: ```bash -./build/nsigner --listen tcp:[::1]:8080 +./build/nsigner --listen tcp:[fd00::1234]:8080 ``` Behavior notes: -- Non-loopback values are rejected by design. +- Bind target is operator-controlled; pick the narrowest reachable address that satisfies your topology. - No TUI hotkey loop is required in TCP mode; process serves requests until terminated. - Caller identity is shown as a TCP endpoint descriptor in activity/prompt context. @@ -82,14 +82,14 @@ Behavior notes: Because FIPS deployment topologies vary, use this generic pattern: -1. Bind `nsigner` on loopback in signer environment. -2. Configure FIPS service/forwarding so remote authenticated peer traffic is delivered to that loopback endpoint. +1. Bind `nsigner` on a deliberate signer endpoint (`[::]:PORT` for broad reach, or specific `fd..` for tighter scope). +2. Configure FIPS service/forwarding so remote authenticated peer traffic is delivered to that signer endpoint. 3. On caller side, direct client traffic to the local FIPS ingress endpoint for that remote service. Validation checklist: - FIPS session is established between caller and signer nodes. -- Transport path from caller -> signer loopback endpoint succeeds. +- Transport path from caller -> configured signer endpoint succeeds. - `nsigner` receives framed request and returns framed response. --- @@ -134,7 +134,7 @@ Trigger a request path that requires prompt/denial and confirm client handles po ## 7. Security guardrails -- Keep listener loopback-only in Tier-1. +- Prefer binding to a specific FIPS IPv6 ULA (`fd..`) rather than wildcard (`[::]`) when possible. - Do not expose signer port directly on LAN/WAN. - Keep FIPS peer allowlist tight; avoid broad trust domains. - Treat FIPS connectivity as transport, not authorization bypass. @@ -147,20 +147,12 @@ Trigger a request path that requires prompt/denial and confirm client handles po ### 8.1 `invalid tcp listen target` Cause: -- `--listen` argument does not match `tcp:HOST:PORT` or `tcp:[::1]:PORT`. +- `--listen` argument does not match `tcp:HOST:PORT` or `tcp:[IPv6]:PORT`. Fix: -- Use explicit loopback host and valid numeric port. +- Use valid numeric port and valid IPv4/IPv6 literal host. -### 8.2 `non-loopback TCP bind denied` - -Cause: -- Attempt to bind non-loopback target in Tier-1 mode. - -Fix: -- Switch to `127.x.x.x` or `::1` target. - -### 8.3 Framing parse failures (`parse_error`) +### 8.2 Framing parse failures (`parse_error`) Cause: - Client sent line-delimited/raw JSON instead of framed JSON. @@ -168,7 +160,7 @@ Cause: Fix: - Send 4-byte big-endian length prefix followed by exact UTF-8 JSON payload bytes. -### 8.4 FIPS path up, signer path down +### 8.3 FIPS path up, signer path down Cause: - FIPS session exists but forwarding/service mapping to signer loopback endpoint is missing. diff --git a/install_qube_fips_nsigner.sh b/install_qube_fips_nsigner.sh new file mode 100755 index 0000000..7c848de --- /dev/null +++ b/install_qube_fips_nsigner.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +set -euo pipefail + +# User-only installer for Qubes AppVM persistence model. +# Nothing is written to /usr, /etc, or other root-owned paths. +# +# Installs into $HOME: +# - nsigner -> ~/.local/bin/nsigner +# - fips/fipsctl/fipstop -> ~/.local/bin/ +# - fips config -> ~/.config/fips/fips.yaml +# - user service -> ~/.config/systemd/user/fips.service +# +# Usage: +# bash install_qube_fips_nsigner.sh +# bash install_qube_fips_nsigner.sh --help +# +# Optional env vars: +# NSIGNER_VERSION=v0.0.6 +# FIPS_VERSION=v0.2.0-rel +# NSIGNER_GITEA_TOKEN= # if n_signer release assets are private +# NSIGNER_BINARY_URL= + +NSIGNER_VERSION="${NSIGNER_VERSION:-v0.0.6}" +FIPS_VERSION="${FIPS_VERSION:-v0.2.0-rel}" + +NSIGNER_RELEASE_PAGE="https://git.laantungir.net/laantungir/n_signer/releases/tag/${NSIGNER_VERSION}" +NSIGNER_API_TAG_URL="https://git.laantungir.net/api/v1/repos/laantungir/n_signer/releases/tags/${NSIGNER_VERSION}" +NSIGNER_GIT_URL="https://git.laantungir.net/laantungir/n_signer.git" + +FIPS_RELEASE_PAGE="https://github.com/jmcorgan/fips/releases/tag/${FIPS_VERSION}" +FIPS_ARCHIVE_URL="https://github.com/jmcorgan/fips/archive/refs/tags/${FIPS_VERSION}.tar.gz" + +PREFIX_BIN="${HOME}/.local/bin" +PREFIX_CFG="${HOME}/.config" +PREFIX_DATA="${HOME}/.local/share/nsigner-fips-install" +SYSTEMD_USER_DIR="${HOME}/.config/systemd/user" + +log() { printf "\033[1;34m[INFO]\033[0m %s\n" "$*"; } +warn() { printf "\033[1;33m[WARN]\033[0m %s\n" "$*"; } +err() { printf "\033[1;31m[ERR ]\033[0m %s\n" "$*"; } + +show_help() { + cat < # required if n_signer release assets are private + NSIGNER_BINARY_URL= + +Install paths: + ~/.local/bin/nsigner + ~/.local/bin/fips + ~/.local/bin/fipsctl + ~/.local/bin/fipstop + ~/.config/fips/fips.yaml + ~/.config/systemd/user/fips.service +EOF +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + err "Missing command: $1" + exit 1 + } +} + +install_build_deps() { + if command -v apt-get >/dev/null 2>&1; then + log "Installing build/runtime dependencies via apt" + sudo apt-get update + sudo apt-get install -y \ + ca-certificates curl jq tar gzip git python3 \ + build-essential pkg-config libssl-dev zlib1g-dev \ + rustc cargo + elif command -v dnf >/dev/null 2>&1; then + log "Installing build/runtime dependencies via dnf" + sudo dnf install -y \ + ca-certificates curl jq tar gzip git python3 \ + gcc make pkgconf-pkg-config openssl-devel zlib-devel \ + rust cargo + else + err "Unsupported distro: need apt-get or dnf to install build dependencies" + exit 1 + fi +} + +prepare_dirs() { + mkdir -p "${PREFIX_BIN}" "${PREFIX_CFG}/fips" "${PREFIX_DATA}" "${SYSTEMD_USER_DIR}" +} + +download_nsigner_asset_url() { + local headers=() + if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then + headers=(-H "Authorization: token ${NSIGNER_GITEA_TOKEN}") + fi + + curl -fsSL "${headers[@]}" "${NSIGNER_API_TAG_URL}" \ + | jq -r '.assets[]?.browser_download_url // empty' \ + | grep -E 'nsigner_static_x86_64$' \ + | head -n1 || true +} + +install_nsigner() { + log "Installing n_signer ${NSIGNER_VERSION}" + log "Release page: ${NSIGNER_RELEASE_PAGE}" + + local asset_url="${NSIGNER_BINARY_URL:-}" + if [[ -z "${asset_url}" ]]; then + asset_url="$(download_nsigner_asset_url)" + fi + + if [[ -z "${asset_url}" ]]; then + err "Could not find downloadable n_signer x86_64 release binary for ${NSIGNER_VERSION}." + err "Provide NSIGNER_BINARY_URL or NSIGNER_GITEA_TOKEN so the release asset can be resolved." + exit 1 + fi + + log "Using n_signer binary URL: ${asset_url}" + if [[ -n "${NSIGNER_GITEA_TOKEN:-}" ]]; then + curl -fL -H "Authorization: token ${NSIGNER_GITEA_TOKEN}" -o "${PREFIX_BIN}/nsigner" "${asset_url}" + else + curl -fL -o "${PREFIX_BIN}/nsigner" "${asset_url}" + fi + chmod 0755 "${PREFIX_BIN}/nsigner" + log "Installed ${PREFIX_BIN}/nsigner from release binary" +} + +install_fips() { + log "Installing FIPS ${FIPS_VERSION}" + log "Release page: ${FIPS_RELEASE_PAGE}" + + rm -rf "${PREFIX_DATA}/fips-src" + mkdir -p "${PREFIX_DATA}/fips-src" + + curl -fL -o "${PREFIX_DATA}/fips.tar.gz" "${FIPS_ARCHIVE_URL}" + tar -xzf "${PREFIX_DATA}/fips.tar.gz" -C "${PREFIX_DATA}/fips-src" --strip-components=1 + + ( + cd "${PREFIX_DATA}/fips-src" + cargo build --release + + install -m 0755 ./target/release/fips "${PREFIX_BIN}/fips" + install -m 0755 ./target/release/fipsctl "${PREFIX_BIN}/fipsctl" + + if [[ -x ./target/release/fipstop ]]; then + install -m 0755 ./target/release/fipstop "${PREFIX_BIN}/fipstop" + fi + ) + + log "Installed FIPS binaries into ${PREFIX_BIN}" +} + +write_fips_user_config() { + local cfg="${PREFIX_CFG}/fips/fips.yaml" + if [[ -f "${cfg}" ]]; then + warn "Keeping existing ${cfg}" + return + fi + + cat >"${cfg}" <<'EOF' +# User-local FIPS config for AppVM persistence +# Edit peers to join your mesh. + +node: + identity: + persistent: true + +# Try userspace tun name; adjust if your environment requires a different setting. +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 5354 + +transports: + udp: + bind_addr: "0.0.0.0:2121" + tcp: + bind_addr: "0.0.0.0:8443" + +peers: [] +EOF + + log "Wrote ${cfg}" +} + +write_fips_user_service() { + local svc="${SYSTEMD_USER_DIR}/fips.service" + cat >"${svc}" <socket_name, &family, host, sizeof(host), &port); - if (prc == -2) { - (void)snprintf(ctx->last_error, - sizeof(ctx->last_error), - "non-loopback TCP bind denied: %s (use 127.x.x.x or ::1)", - ctx->socket_name); - return -1; - } if (prc != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), - "invalid tcp listen target: %s (expected tcp:127.0.0.1:PORT or tcp:[::1]:PORT)", + "invalid tcp listen target: %s (expected tcp:IPv4:PORT or tcp:[IPv6]:PORT)", ctx->socket_name); return -1; }