Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd6630311 | ||
|
|
252d026991 | ||
|
|
f9d7b94962 |
@@ -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:<source-vm>`).
|
||||
- `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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
195
install_qube_fips_nsigner.sh
Executable file
195
install_qube_fips_nsigner.sh
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/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
|
||||
# - startup helper -> ~/start_nsigner.sh
|
||||
#
|
||||
# Usage:
|
||||
# bash install_qube_fips_nsigner.sh
|
||||
# bash install_qube_fips_nsigner.sh --help
|
||||
#
|
||||
# Optional env vars:
|
||||
# NSIGNER_VERSION=v0.0.7
|
||||
# NSIGNER_GITEA_TOKEN=<token> # if n_signer release assets are private
|
||||
# NSIGNER_BINARY_URL=<direct url to nsigner_static_x86_64>
|
||||
|
||||
NSIGNER_VERSION="${NSIGNER_VERSION:-v0.0.7}"
|
||||
|
||||
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}"
|
||||
|
||||
PREFIX_BIN="${HOME}/.local/bin"
|
||||
|
||||
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 <<EOF
|
||||
Usage: bash install_qube_fips_nsigner.sh [options]
|
||||
|
||||
User-only install (Qubes AppVM friendly):
|
||||
- n_signer ${NSIGNER_VERSION}
|
||||
- signer startup helper script
|
||||
|
||||
Options:
|
||||
-h, --help Show this help and exit
|
||||
|
||||
Optional env vars:
|
||||
NSIGNER_VERSION=v0.0.7
|
||||
NSIGNER_GITEA_TOKEN=<token> # required if n_signer release assets are private
|
||||
NSIGNER_BINARY_URL=<direct url to nsigner_static_x86_64>
|
||||
|
||||
Install paths:
|
||||
~/.local/bin/nsigner
|
||||
~/start_nsigner.sh
|
||||
EOF
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
err "Missing command: $1"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
install_runtime_deps() {
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
log "Installing runtime dependencies via apt"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ca-certificates curl jq
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
log "Installing runtime dependencies via dnf"
|
||||
sudo dnf install -y ca-certificates curl jq
|
||||
else
|
||||
err "Unsupported distro: need apt-get or dnf to install runtime dependencies"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_dirs() {
|
||||
mkdir -p "${PREFIX_BIN}"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
write_signer_start_script() {
|
||||
local script_path="${HOME}/start_nsigner.sh"
|
||||
|
||||
cat >"${script_path}" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
LISTEN_TARGET="${NSIGNER_LISTEN_TARGET:-tcp:[::]:8080}"
|
||||
|
||||
echo "=== n_signer startup ==="
|
||||
echo "listen target: ${LISTEN_TARGET}"
|
||||
|
||||
# Optional: print current FIPS identity info if fipsctl is available.
|
||||
if command -v fipsctl >/dev/null 2>&1; then
|
||||
if fipsctl show status >/dev/null 2>&1; then
|
||||
STATUS_JSON="$(fipsctl show status)"
|
||||
elif sudo -n fipsctl show status >/dev/null 2>&1; then
|
||||
STATUS_JSON="$(sudo -n fipsctl show status)"
|
||||
else
|
||||
STATUS_JSON=""
|
||||
fi
|
||||
|
||||
if [[ -n "${STATUS_JSON}" ]]; then
|
||||
FIPS_IPV6="$(printf '%s\n' "${STATUS_JSON}" | sed -n 's/.*"ipv6_addr": "\([^"]*\)".*/\1/p')"
|
||||
FIPS_NPUB="$(printf '%s\n' "${STATUS_JSON}" | sed -n 's/.*"npub": "\([^"]*\)".*/\1/p')"
|
||||
LISTEN_PORT="$(printf '%s\n' "${LISTEN_TARGET}" | sed -n 's/.*:\([0-9][0-9]*\)$/\1/p')"
|
||||
[[ -n "${FIPS_IPV6}" ]] && echo "fips ipv6: ${FIPS_IPV6}"
|
||||
[[ -n "${FIPS_NPUB}" ]] && echo "fips npub: ${FIPS_NPUB}"
|
||||
if [[ -n "${FIPS_NPUB}" && -n "${LISTEN_PORT}" ]]; then
|
||||
echo "fips address: http://${FIPS_NPUB}.fips:${LISTEN_PORT}"
|
||||
fi
|
||||
else
|
||||
echo "fips status: unavailable (run as user in fips group or with sudo)"
|
||||
fi
|
||||
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}"
|
||||
EOF
|
||||
|
||||
chmod 0755 "${script_path}"
|
||||
log "Wrote ${script_path}"
|
||||
}
|
||||
|
||||
post_checks() {
|
||||
export PATH="${PREFIX_BIN}:${PATH}"
|
||||
|
||||
log "Running post-install checks"
|
||||
require_cmd nsigner
|
||||
nsigner --version || true
|
||||
|
||||
log "User binaries installed in: ${PREFIX_BIN}"
|
||||
log "If needed, add to shell PATH: export PATH=\"${PREFIX_BIN}:\$PATH\""
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
err "Unknown option: $1"
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install_runtime_deps
|
||||
prepare_dirs
|
||||
install_nsigner
|
||||
write_signer_start_script
|
||||
post_checks
|
||||
|
||||
log "Completed user-only install of n_signer"
|
||||
log "Start signer with: ~/start_nsigner.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -214,22 +214,22 @@ Use Qubes' native inter-qube primitive instead of inventing one.
|
||||
- Tests: a unit test that injects fake qrexec env vars and a stdio framing harness; an integration script that documents end-to-end install in a Qubes VM (manual, not in CI).
|
||||
- Docs: add a "Qubes deployment" section to [`README.md`](../README.md) and to [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).
|
||||
|
||||
### 7.2 Phase T2 — TCP loopback transport
|
||||
### 7.2 Phase T2 — TCP transport
|
||||
|
||||
Smallest IP-based step; on-ramp for non-Linux clients and for FIPS later.
|
||||
|
||||
- New CLI: `nsigner --listen tcp:127.0.0.1:PORT`.
|
||||
- Default-deny non-loopback binds (reject `0.0.0.0` / non-`127.x` / non-`::1` unless `--allow-remote`, see T3).
|
||||
- Caller identity for loopback: `tcp_local { addr }`. Approval prompt still mandatory.
|
||||
- New CLI: `nsigner --listen tcp:HOST:PORT` (IPv4 literal) or `nsigner --listen tcp:[IPv6]:PORT`.
|
||||
- Current behavior: accepts operator-selected local/remote bind addresses (including `[::]` and `fd..`), pending later transport hardening.
|
||||
- Caller identity for TCP: endpoint address/port in caller descriptor. Approval prompt still mandatory.
|
||||
- `nsigner list` extended to enumerate active TCP listeners (from internal registry; not from `/proc/net/tcp`).
|
||||
- Same framing as AF_UNIX path; no protocol changes.
|
||||
- Tests: integration coverage that spawns a child signer with `--listen tcp:127.0.0.1:0`, captures the bound port, runs the same NIP-04/NIP-44/sign_event matrix as AF_UNIX.
|
||||
- Tests: integration coverage that spawns a child signer with `--listen tcp:127.0.0.1:0` (and one IPv6 case), captures the bound port, runs the same NIP-04/NIP-44/sign_event matrix as AF_UNIX.
|
||||
- Docs: extend [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) section 2 with `tcp:` discovery rules and section 3 confirming framing parity.
|
||||
|
||||
Implementation checklist (Tier-1 delivery):
|
||||
|
||||
- [x] Parse `--listen tcp:HOST:PORT` in [`src/main.c`](../src/main.c).
|
||||
- [x] Reject invalid/non-loopback listen targets in [`src/server.c`](../src/server.c).
|
||||
- [x] Parse and validate literal IPv4/IPv6 listen targets in [`src/server.c`](../src/server.c).
|
||||
- [x] Bind/listen non-blocking TCP sockets and run server loop without TUI dependence.
|
||||
- [x] Keep existing framed JSON-RPC protocol unchanged via shared [`src/transport_frame.c`](../src/transport_frame.c).
|
||||
- [ ] Add integration test coverage for `tcp:127.0.0.1:PORT` request flow.
|
||||
@@ -250,7 +250,7 @@ Only after T2 is solid.
|
||||
|
||||
FIPS is a *substrate* for an existing TCP listener, not a new transport in nsigner code.
|
||||
|
||||
- Deployment topology: nsigner binds TCP loopback inside the FIPS network namespace (or on a host where `fips0` is up); peers reach it via `fd00::/8` IPv6 derived from the signer's npub.
|
||||
- Deployment topology: nsigner binds a chosen TCP endpoint inside the FIPS network namespace (or on a host where `fips0` is up); peers reach it via `fd00::/8` IPv6 derived from the signer's npub.
|
||||
- Optional `caller_kind=fips` enrichment: a small sidecar query (`fipsctl show sessions` style) maps the connecting IPv6 address to a peer npub and feeds it into `caller_identity_t.fips { peer_npub }`. If unavailable, fall back to `tcp_remote` identity.
|
||||
- nsigner does not embed FIPS, does not depend on libfips, and does not require Rust.
|
||||
- New optional flag: `--peer-id-source fips:/var/run/fips/fips.sock` (path/method TBD per FIPS API).
|
||||
@@ -259,7 +259,7 @@ FIPS is a *substrate* for an existing TCP listener, not a new transport in nsign
|
||||
|
||||
Execution tasks for initial FIPS trial:
|
||||
|
||||
- [x] Deliver T2 TCP loopback listener as FIPS substrate prerequisite.
|
||||
- [x] Deliver T2 TCP listener as FIPS substrate prerequisite.
|
||||
- [x] Document signer/caller qube deployment flow in [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md).
|
||||
- [ ] Add two-node operator validation script (manual) using `fipsctl` + framed JSON-RPC client.
|
||||
- [ ] Evaluate optional caller identity enrichment from FIPS session metadata.
|
||||
|
||||
28
src/main.c
28
src/main.c
@@ -451,8 +451,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 6
|
||||
#define NSIGNER_VERSION "v0.0.6"
|
||||
#define NSIGNER_VERSION_PATCH 9
|
||||
#define NSIGNER_VERSION "v0.0.9"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
@@ -746,6 +746,28 @@ static void activity_log_cb(const char *message, void *user_data) {
|
||||
g_activity_log.count++;
|
||||
}
|
||||
|
||||
static void tcp_activity_stdout_cb(const char *message, void *user_data) {
|
||||
time_t now;
|
||||
struct tm tm_now;
|
||||
char ts[32];
|
||||
|
||||
(void)user_data;
|
||||
if (message == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
now = time(NULL);
|
||||
if (localtime_r(&now, &tm_now) != NULL) {
|
||||
(void)strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm_now);
|
||||
} else {
|
||||
strncpy(ts, "0000-00-00 00:00:00", sizeof(ts) - 1);
|
||||
ts[sizeof(ts) - 1] = '\0';
|
||||
}
|
||||
|
||||
printf("[%s] %s\n", ts, message);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void render_status(const role_table_t *role_table,
|
||||
const mnemonic_state_t *mnemonic,
|
||||
int derived_count,
|
||||
@@ -1141,7 +1163,7 @@ int main(int argc, char *argv[]) {
|
||||
break;
|
||||
}
|
||||
if (prc > 0 && (pfds[0].revents & POLLIN)) {
|
||||
if (server_handle_one(&server, NULL, NULL) < 0) {
|
||||
if (server_handle_one(&server, tcp_activity_stdout_cb, NULL) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
190
src/server.c
190
src/server.c
@@ -469,6 +469,157 @@ int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
||||
static int g_prompt_always_allow = 0;
|
||||
static int g_noninteractive_prompt_default = -1;
|
||||
|
||||
static int caller_id_extract_ipv6(const char *caller_id, char *out_ipv6, size_t out_sz) {
|
||||
const char *start;
|
||||
const char *end;
|
||||
size_t len;
|
||||
|
||||
if (caller_id == NULL || out_ipv6 == NULL || out_sz == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strncmp(caller_id, "tcp:[", 5) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
start = caller_id + 5;
|
||||
end = strchr(start, ']');
|
||||
if (end == NULL || end[1] != ':') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = (size_t)(end - start);
|
||||
if (len == 0 || len >= out_sz) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(out_ipv6, start, len);
|
||||
out_ipv6[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_cmd_output(const char *cmd, char **out_buf) {
|
||||
FILE *fp;
|
||||
char chunk[512];
|
||||
char *buf = NULL;
|
||||
size_t used = 0;
|
||||
size_t cap = 0;
|
||||
|
||||
if (cmd == NULL || out_buf == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_buf = NULL;
|
||||
fp = popen(cmd, "r");
|
||||
if (fp == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (fgets(chunk, sizeof(chunk), fp) != NULL) {
|
||||
size_t n = strlen(chunk);
|
||||
if (used + n + 1 > cap) {
|
||||
size_t new_cap = (cap == 0) ? 2048 : cap * 2;
|
||||
while (new_cap < used + n + 1) {
|
||||
new_cap *= 2;
|
||||
}
|
||||
{
|
||||
char *tmp = (char *)realloc(buf, new_cap);
|
||||
if (tmp == NULL) {
|
||||
free(buf);
|
||||
(void)pclose(fp);
|
||||
return -1;
|
||||
}
|
||||
buf = tmp;
|
||||
cap = new_cap;
|
||||
}
|
||||
}
|
||||
memcpy(buf + used, chunk, n);
|
||||
used += n;
|
||||
}
|
||||
|
||||
(void)pclose(fp);
|
||||
|
||||
if (buf == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
buf[used] = '\0';
|
||||
*out_buf = buf;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lookup_fips_peer_for_ipv6(const char *ipv6,
|
||||
char *out_npub,
|
||||
size_t out_npub_sz,
|
||||
char *out_name,
|
||||
size_t out_name_sz) {
|
||||
char *json = NULL;
|
||||
cJSON *root = NULL;
|
||||
cJSON *peers = NULL;
|
||||
int i;
|
||||
|
||||
if (ipv6 == NULL || out_npub == NULL || out_npub_sz == 0 || out_name == NULL || out_name_sz == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_npub[0] = '\0';
|
||||
out_name[0] = '\0';
|
||||
|
||||
if (read_cmd_output("fipsctl show peers 2>/dev/null", &json) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
root = cJSON_Parse(json);
|
||||
free(json);
|
||||
if (root == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
peers = cJSON_GetObjectItemCaseSensitive(root, "peers");
|
||||
if (!cJSON_IsArray(peers)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < cJSON_GetArraySize(peers); ++i) {
|
||||
cJSON *peer = cJSON_GetArrayItem(peers, i);
|
||||
cJSON *peer_ip;
|
||||
cJSON *peer_npub;
|
||||
cJSON *peer_name;
|
||||
|
||||
if (!cJSON_IsObject(peer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
peer_ip = cJSON_GetObjectItemCaseSensitive(peer, "ipv6_addr");
|
||||
if (!cJSON_IsString(peer_ip) || peer_ip->valuestring == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(peer_ip->valuestring, ipv6) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
peer_npub = cJSON_GetObjectItemCaseSensitive(peer, "npub");
|
||||
peer_name = cJSON_GetObjectItemCaseSensitive(peer, "display_name");
|
||||
|
||||
if (cJSON_IsString(peer_npub) && peer_npub->valuestring != NULL) {
|
||||
strncpy(out_npub, peer_npub->valuestring, out_npub_sz - 1);
|
||||
out_npub[out_npub_sz - 1] = '\0';
|
||||
}
|
||||
if (cJSON_IsString(peer_name) && peer_name->valuestring != NULL) {
|
||||
strncpy(out_name, peer_name->valuestring, out_name_sz - 1);
|
||||
out_name[out_name_sz - 1] = '\0';
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return (out_npub[0] != '\0') ? 0 : -1;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void server_set_prompt_always_allow(int enabled) {
|
||||
g_prompt_always_allow = enabled ? 1 : 0;
|
||||
}
|
||||
@@ -501,6 +652,20 @@ static int prompt_for_policy_decision(const caller_identity_t *caller,
|
||||
|
||||
printf("\nApproval required\n");
|
||||
printf("caller: %s\n", (caller != NULL) ? caller->caller_id : "unknown");
|
||||
if (caller != NULL && caller->kind == NSIGNER_LISTEN_TCP) {
|
||||
char ipv6[INET6_ADDRSTRLEN];
|
||||
char npub[128];
|
||||
char display_name[128];
|
||||
|
||||
if (caller_id_extract_ipv6(caller->caller_id, ipv6, sizeof(ipv6)) == 0 &&
|
||||
lookup_fips_peer_for_ipv6(ipv6, npub, sizeof(npub), display_name, sizeof(display_name)) == 0) {
|
||||
if (display_name[0] != '\0') {
|
||||
printf("fips peer: %s (%s)\n", npub, display_name);
|
||||
} else {
|
||||
printf("fips peer: %s\n", npub);
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("method: %s\n", (method != NULL) ? method : "unknown");
|
||||
printf("role: %s\n", (role_name != NULL) ? role_name : "unknown");
|
||||
printf("purpose: %s\n", (purpose != NULL) ? purpose : "unknown");
|
||||
@@ -601,17 +766,17 @@ static int parse_tcp_target(const char *target,
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strcmp(out_host, "::1") == 0) {
|
||||
*out_family = AF_INET6;
|
||||
} else {
|
||||
{
|
||||
struct in6_addr addr6;
|
||||
struct in_addr addr4;
|
||||
if (inet_pton(AF_INET, out_host, &addr4) != 1) {
|
||||
|
||||
if (inet_pton(AF_INET6, out_host, &addr6) == 1) {
|
||||
*out_family = AF_INET6;
|
||||
} else if (inet_pton(AF_INET, out_host, &addr4) == 1) {
|
||||
*out_family = AF_INET;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
if ((ntohl(addr4.s_addr) & 0xff000000U) != 0x7f000000U) {
|
||||
return -2;
|
||||
}
|
||||
*out_family = AF_INET;
|
||||
}
|
||||
|
||||
*out_port = (uint16_t)port_long;
|
||||
@@ -746,17 +911,10 @@ int server_start(server_ctx_t *ctx) {
|
||||
int one = 1;
|
||||
|
||||
int prc = parse_tcp_target(ctx->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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user